Spaces:
Paused
Paused
Deploy Sensorflow Studio (part 2)
Browse files- src/services/vitis.ts +122 -0
- src/types/vitis.ts +253 -0
- tests/test_vitis/__init__.py +0 -0
- tests/test_vitis/conftest.py +21 -0
- tests/test_vitis/test_api.py +106 -0
- tests/test_vitis/test_backend.py +156 -0
- tests/test_vitis/test_hil.py +74 -0
- tests/test_vitis/test_isp_augment.py +80 -0
- tests/test_vitis/test_temporal.py +130 -0
src/services/vitis.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** Typed fetch wrappers for the Vitis acceleration layer API (/api/vitis). */
|
| 2 |
+
import type {
|
| 3 |
+
AugmentBatch,
|
| 4 |
+
AugmentationSpec,
|
| 5 |
+
BackendStatus,
|
| 6 |
+
HilRun,
|
| 7 |
+
HilSweep,
|
| 8 |
+
IspRun,
|
| 9 |
+
PrdDoc,
|
| 10 |
+
PrdListEntry,
|
| 11 |
+
RunListEntry,
|
| 12 |
+
TemporalRun,
|
| 13 |
+
} from '../types/vitis';
|
| 14 |
+
|
| 15 |
+
async function handle<T>(res: Response): Promise<T> {
|
| 16 |
+
if (!res.ok) {
|
| 17 |
+
let detail = '';
|
| 18 |
+
try {
|
| 19 |
+
const body = await res.json();
|
| 20 |
+
detail = typeof body?.detail === 'string' ? body.detail : JSON.stringify(body);
|
| 21 |
+
} catch {
|
| 22 |
+
detail = await res.text().catch(() => '');
|
| 23 |
+
}
|
| 24 |
+
const err = new Error(detail || res.statusText) as Error & { status?: number };
|
| 25 |
+
err.status = res.status;
|
| 26 |
+
throw err;
|
| 27 |
+
}
|
| 28 |
+
return res.json() as Promise<T>;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const JSON_HEADERS = { 'Content-Type': 'application/json' };
|
| 32 |
+
|
| 33 |
+
const get = <T,>(url: string): Promise<T> => fetch(url).then((r) => handle<T>(r));
|
| 34 |
+
const post = <T,>(url: string, body?: unknown): Promise<T> =>
|
| 35 |
+
fetch(url, {
|
| 36 |
+
method: 'POST',
|
| 37 |
+
headers: JSON_HEADERS,
|
| 38 |
+
body: body === undefined ? undefined : JSON.stringify(body),
|
| 39 |
+
}).then((r) => handle<T>(r));
|
| 40 |
+
|
| 41 |
+
// ------------------------------------------------------------- backends
|
| 42 |
+
|
| 43 |
+
export const getBackendStatus = () => get<BackendStatus>('/api/vitis/backends/status');
|
| 44 |
+
|
| 45 |
+
// ------------------------------------------------------------- HIL
|
| 46 |
+
|
| 47 |
+
export interface HilRunRequest {
|
| 48 |
+
n_sequences?: number;
|
| 49 |
+
frames_per_sequence?: number;
|
| 50 |
+
seed?: number;
|
| 51 |
+
width_bits?: number;
|
| 52 |
+
int_bits?: number;
|
| 53 |
+
max_line_buffer_depth?: number;
|
| 54 |
+
use_lut_approx?: boolean;
|
| 55 |
+
lut_bits?: number;
|
| 56 |
+
device?: string;
|
| 57 |
+
regression_delta?: number;
|
| 58 |
+
alpha?: number;
|
| 59 |
+
run_ablation?: boolean;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
export const runHil = (body: HilRunRequest) => post<HilRun>('/api/vitis/hil/run', body);
|
| 63 |
+
|
| 64 |
+
export const runHilSweep = (body: HilRunRequest & { widths?: number[] }) =>
|
| 65 |
+
post<HilSweep>('/api/vitis/hil/sweep', body);
|
| 66 |
+
|
| 67 |
+
export const listHilRuns = () => get<{ runs: RunListEntry[] }>('/api/vitis/hil/runs');
|
| 68 |
+
|
| 69 |
+
// ------------------------------------------------------------- ISP
|
| 70 |
+
|
| 71 |
+
export interface IspRunRequest {
|
| 72 |
+
n_frames?: number;
|
| 73 |
+
seed?: number;
|
| 74 |
+
stages?: string[];
|
| 75 |
+
stage_params?: Record<string, unknown>;
|
| 76 |
+
width_bits?: number;
|
| 77 |
+
int_bits?: number;
|
| 78 |
+
max_line_buffer_depth?: number;
|
| 79 |
+
use_lut_approx?: boolean;
|
| 80 |
+
device?: string;
|
| 81 |
+
include_previews?: boolean;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
export const runIsp = (body: IspRunRequest) => post<IspRun>('/api/vitis/isp/run', body);
|
| 85 |
+
|
| 86 |
+
export const getIspStages = () => get<{ stages: string[] }>('/api/vitis/isp/stages');
|
| 87 |
+
|
| 88 |
+
// ------------------------------------------------------------- augment
|
| 89 |
+
|
| 90 |
+
export const getAugmentRecipes = () =>
|
| 91 |
+
get<{ augmentations: AugmentationSpec[] }>('/api/vitis/augment/recipes');
|
| 92 |
+
|
| 93 |
+
export const generateAugmentBatch = (body: {
|
| 94 |
+
recipes?: { aug: string; params?: Record<string, number> }[];
|
| 95 |
+
n_variants?: number;
|
| 96 |
+
seed?: number;
|
| 97 |
+
backend?: string;
|
| 98 |
+
width_bits?: number;
|
| 99 |
+
device?: string;
|
| 100 |
+
include_thumbnails?: boolean;
|
| 101 |
+
}) => post<AugmentBatch>('/api/vitis/augment/generate', body);
|
| 102 |
+
|
| 103 |
+
// ------------------------------------------------------------- temporal
|
| 104 |
+
|
| 105 |
+
export const getTemporalEngines = () =>
|
| 106 |
+
get<{ engines: string[] }>('/api/vitis/temporal/engines');
|
| 107 |
+
|
| 108 |
+
export const runTemporal = (body: {
|
| 109 |
+
engines?: string[];
|
| 110 |
+
n_sequences?: number;
|
| 111 |
+
frames_per_sequence?: number;
|
| 112 |
+
seed?: number;
|
| 113 |
+
width_bits?: number;
|
| 114 |
+
device?: string;
|
| 115 |
+
}) => post<TemporalRun>('/api/vitis/temporal/run', body);
|
| 116 |
+
|
| 117 |
+
// ------------------------------------------------------------- PRD
|
| 118 |
+
|
| 119 |
+
export const listPrds = () => get<{ prds: PrdListEntry[] }>('/api/vitis/prd');
|
| 120 |
+
|
| 121 |
+
export const getPrd = (id: string) =>
|
| 122 |
+
get<PrdDoc>(`/api/vitis/prd/${encodeURIComponent(id)}`);
|
src/types/vitis.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** Types mirroring the /api/vitis JSON payloads (snake_case from FastAPI). */
|
| 2 |
+
|
| 3 |
+
// ------------------------------------------------------------- backends
|
| 4 |
+
|
| 5 |
+
export interface BackendInfo {
|
| 6 |
+
name: string;
|
| 7 |
+
available: boolean;
|
| 8 |
+
emulated: boolean;
|
| 9 |
+
description: string;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export interface BackendStatus {
|
| 13 |
+
backends: BackendInfo[];
|
| 14 |
+
devices: Record<string, { clock_mhz: number; has_aie: boolean; description: string }>;
|
| 15 |
+
hardware_present: boolean;
|
| 16 |
+
note: string;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// ------------------------------------------------------------- HIL
|
| 20 |
+
|
| 21 |
+
export interface HilVerdict {
|
| 22 |
+
decision: 'REGRESSION' | 'PASS' | 'INSUFFICIENT_EVIDENCE';
|
| 23 |
+
mean_delta: number;
|
| 24 |
+
ci: [number, number];
|
| 25 |
+
n: number;
|
| 26 |
+
method: string;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export interface HilComparison {
|
| 30 |
+
config: Record<string, unknown>;
|
| 31 |
+
totals: {
|
| 32 |
+
gt_objects: number;
|
| 33 |
+
ref_detected: number;
|
| 34 |
+
vitis_detected: number;
|
| 35 |
+
paired_detections: number;
|
| 36 |
+
dropped_by_vitis: number;
|
| 37 |
+
spurious_in_vitis: number;
|
| 38 |
+
class_flips: number;
|
| 39 |
+
};
|
| 40 |
+
drift: {
|
| 41 |
+
mean_confidence_drift: number;
|
| 42 |
+
mean_abs_confidence_drift: number;
|
| 43 |
+
mean_position_drift_px: number;
|
| 44 |
+
mean_pair_iou: number;
|
| 45 |
+
};
|
| 46 |
+
gap_score: number;
|
| 47 |
+
cohort_delta: Record<string, number>;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
export interface HilAblation {
|
| 51 |
+
legs: Record<string, { gap_score: number }>;
|
| 52 |
+
attribution: Record<string, number>;
|
| 53 |
+
note: string;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
export interface HilRun {
|
| 57 |
+
run_id: string;
|
| 58 |
+
created_at: string;
|
| 59 |
+
params: Record<string, unknown>;
|
| 60 |
+
comparison: HilComparison;
|
| 61 |
+
verdict: HilVerdict;
|
| 62 |
+
ablation: HilAblation | null;
|
| 63 |
+
elapsed_s: number;
|
| 64 |
+
emulation_note: string;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
export interface HilSweepPoint {
|
| 68 |
+
width_bits: number;
|
| 69 |
+
int_bits: number;
|
| 70 |
+
gap_score: number;
|
| 71 |
+
dropped: number;
|
| 72 |
+
class_flips: number;
|
| 73 |
+
mean_abs_confidence_drift: number;
|
| 74 |
+
mean_position_drift_px: number;
|
| 75 |
+
mean_pair_iou: number;
|
| 76 |
+
decision: HilVerdict['decision'];
|
| 77 |
+
mean_delta: number;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
export interface HilSweep {
|
| 81 |
+
run_id: string;
|
| 82 |
+
created_at: string;
|
| 83 |
+
params: Record<string, unknown>;
|
| 84 |
+
points: HilSweepPoint[];
|
| 85 |
+
minimal_passing_config: {
|
| 86 |
+
width_bits: number;
|
| 87 |
+
int_bits: number;
|
| 88 |
+
decision: string;
|
| 89 |
+
note?: string;
|
| 90 |
+
} | null;
|
| 91 |
+
elapsed_s: number;
|
| 92 |
+
emulation_note: string;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// ------------------------------------------------------------- ISP
|
| 96 |
+
|
| 97 |
+
export interface IspStageEntry {
|
| 98 |
+
stage: string;
|
| 99 |
+
psnr_db: number;
|
| 100 |
+
ssim: number;
|
| 101 |
+
measured_cpu_ms: number;
|
| 102 |
+
modeled_fpga_ms?: number;
|
| 103 |
+
modeled_placement?: string;
|
| 104 |
+
modeled_speedup_x?: number;
|
| 105 |
+
modeled_not_measured?: boolean;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
export interface IspThroughput {
|
| 109 |
+
measured_cpu_ms_per_frame: number;
|
| 110 |
+
measured_cpu_fps: number;
|
| 111 |
+
modeled_fpga_ms_per_frame_serial: number;
|
| 112 |
+
modeled_fpga_fps_pipelined: number;
|
| 113 |
+
modeled_speedup_x_serial: number;
|
| 114 |
+
modeled_not_measured: boolean;
|
| 115 |
+
note: string;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
export interface IspPreviewStage {
|
| 119 |
+
stage: string;
|
| 120 |
+
reference_png: string;
|
| 121 |
+
vitis_png: string;
|
| 122 |
+
diff_png: string;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
export interface IspRun {
|
| 126 |
+
run_id: string;
|
| 127 |
+
created_at: string;
|
| 128 |
+
params: Record<string, unknown>;
|
| 129 |
+
stage_report: IspStageEntry[];
|
| 130 |
+
throughput: IspThroughput;
|
| 131 |
+
previews: { frame_id: string; cohort: string; input_png?: string; stages: IspPreviewStage[] }[];
|
| 132 |
+
elapsed_s: number;
|
| 133 |
+
emulation_note: string;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// ------------------------------------------------------------- augment
|
| 137 |
+
|
| 138 |
+
export interface AugmentationSpec {
|
| 139 |
+
name: string;
|
| 140 |
+
description: string;
|
| 141 |
+
defaults: Record<string, number>;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
export interface AugmentVariant {
|
| 145 |
+
variant_id: string;
|
| 146 |
+
frame_id: string;
|
| 147 |
+
dataset_id: string;
|
| 148 |
+
scene_id: string;
|
| 149 |
+
sequence_id: string;
|
| 150 |
+
weather: string;
|
| 151 |
+
time_of_day: string;
|
| 152 |
+
scenario_tags: string[];
|
| 153 |
+
evaluation_only: boolean;
|
| 154 |
+
training_eligible: boolean;
|
| 155 |
+
protected_evaluation: boolean;
|
| 156 |
+
recommended_dataset_destination: string;
|
| 157 |
+
lineage: {
|
| 158 |
+
batch_id: string;
|
| 159 |
+
source_frame_id: string;
|
| 160 |
+
source_sequence_id: string;
|
| 161 |
+
recipe: { aug: string; params: Record<string, number> }[];
|
| 162 |
+
seed: number;
|
| 163 |
+
backend: string;
|
| 164 |
+
backend_emulated: boolean;
|
| 165 |
+
};
|
| 166 |
+
recipe: { aug: string; params: Record<string, number> }[];
|
| 167 |
+
thumbnail_png?: string;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
export interface AugmentBatch {
|
| 171 |
+
run_id: string;
|
| 172 |
+
created_at: string;
|
| 173 |
+
params: Record<string, unknown>;
|
| 174 |
+
variants: AugmentVariant[];
|
| 175 |
+
raremine_hook: { available: boolean; routed_candidates: number; note?: string };
|
| 176 |
+
elapsed_s: number;
|
| 177 |
+
emulation_note: string;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
// ------------------------------------------------------------- temporal
|
| 181 |
+
|
| 182 |
+
export interface EngineStability {
|
| 183 |
+
flicker_rate: number;
|
| 184 |
+
mean_jitter: number;
|
| 185 |
+
fragmentation_per_track: number;
|
| 186 |
+
id_switches: number;
|
| 187 |
+
id_switches_at_flow_break: number;
|
| 188 |
+
unexcused_id_switch_fraction: number;
|
| 189 |
+
stability_score: number;
|
| 190 |
+
cohorts?: Record<string, EngineStability>;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
export interface StereoReport {
|
| 194 |
+
objects_checked: number;
|
| 195 |
+
median_abs_depth_error_m: number | null;
|
| 196 |
+
median_rel_depth_error: number | null;
|
| 197 |
+
median_abs_depth_error_near_m: number | null;
|
| 198 |
+
p90_abs_depth_error_m: number | null;
|
| 199 |
+
median_abs_disparity_error_px: number | null;
|
| 200 |
+
note?: string;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
export interface TimelineFrame {
|
| 204 |
+
frame_index: number;
|
| 205 |
+
detected: boolean;
|
| 206 |
+
track_id: string | number | null;
|
| 207 |
+
flow_continuous: boolean;
|
| 208 |
+
flow_residual_m: number | null;
|
| 209 |
+
occluded: boolean;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
export interface TemporalRun {
|
| 213 |
+
run_id: string;
|
| 214 |
+
created_at: string;
|
| 215 |
+
params: Record<string, unknown>;
|
| 216 |
+
results: Record<string, { engines: Record<string, EngineStability>; stereo: StereoReport }>;
|
| 217 |
+
backend_agreement: {
|
| 218 |
+
ranking_agrees: boolean;
|
| 219 |
+
ranking_reference: string[];
|
| 220 |
+
ranking_vitis_emulated: string[];
|
| 221 |
+
stability_score_delta_by_engine: Record<string, number>;
|
| 222 |
+
max_abs_score_delta: number;
|
| 223 |
+
note: string;
|
| 224 |
+
};
|
| 225 |
+
timeline_sample: {
|
| 226 |
+
instance_id: string;
|
| 227 |
+
engine: string;
|
| 228 |
+
sequence_id: string;
|
| 229 |
+
frames: TimelineFrame[];
|
| 230 |
+
} | null;
|
| 231 |
+
elapsed_s: number;
|
| 232 |
+
emulation_note: string;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
// ------------------------------------------------------------- PRD / listings
|
| 236 |
+
|
| 237 |
+
export interface PrdListEntry {
|
| 238 |
+
id: string;
|
| 239 |
+
file: string;
|
| 240 |
+
available: boolean;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
export interface PrdDoc {
|
| 244 |
+
id: string;
|
| 245 |
+
file: string;
|
| 246 |
+
markdown: string;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
export interface RunListEntry {
|
| 250 |
+
run_id: string;
|
| 251 |
+
created_at: string | null;
|
| 252 |
+
summary: Record<string, unknown>;
|
| 253 |
+
}
|
tests/test_vitis/__init__.py
ADDED
|
File without changes
|
tests/test_vitis/conftest.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from fastapi import FastAPI
|
| 3 |
+
from fastapi.testclient import TestClient
|
| 4 |
+
|
| 5 |
+
from sensorflow.vitis import store
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@pytest.fixture()
|
| 9 |
+
def vitis_root(tmp_path):
|
| 10 |
+
store.set_vitis_root(str(tmp_path / "vitis"))
|
| 11 |
+
yield tmp_path / "vitis"
|
| 12 |
+
store.set_vitis_root("runs/vitis")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@pytest.fixture()
|
| 16 |
+
def vitis_client(vitis_root):
|
| 17 |
+
from sensorflow.vitis import api as vitis_api
|
| 18 |
+
app = FastAPI()
|
| 19 |
+
app.include_router(vitis_api.router)
|
| 20 |
+
with TestClient(app) as client:
|
| 21 |
+
yield client
|
tests/test_vitis/test_api.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""API lifecycle across every endpoint group."""
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class TestBackendsApi:
|
| 5 |
+
def test_status(self, vitis_client):
|
| 6 |
+
r = vitis_client.get("/api/vitis/backends/status")
|
| 7 |
+
assert r.status_code == 200
|
| 8 |
+
body = r.json()
|
| 9 |
+
assert body["hardware_present"] is False
|
| 10 |
+
names = {b["name"]: b for b in body["backends"]}
|
| 11 |
+
assert names["vitis_emulated"]["emulated"] is True
|
| 12 |
+
assert names["vitis_hw"]["available"] is False
|
| 13 |
+
|
| 14 |
+
def test_devices(self, vitis_client):
|
| 15 |
+
r = vitis_client.get("/api/vitis/backends/devices")
|
| 16 |
+
assert {d["name"] for d in r.json()["devices"]} == \
|
| 17 |
+
{"versal-ai-edge", "zynq-ultrascale"}
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class TestHilApi:
|
| 21 |
+
def test_run_report_lifecycle(self, vitis_client):
|
| 22 |
+
r = vitis_client.post("/api/vitis/hil/run", json={
|
| 23 |
+
"n_sequences": 1, "frames_per_sequence": 6, "width_bits": 10,
|
| 24 |
+
"run_ablation": True})
|
| 25 |
+
assert r.status_code == 200
|
| 26 |
+
run = r.json()
|
| 27 |
+
assert run["verdict"]["decision"] in (
|
| 28 |
+
"REGRESSION", "PASS", "INSUFFICIENT_EVIDENCE")
|
| 29 |
+
assert run["ablation"]["attribution"]
|
| 30 |
+
|
| 31 |
+
rid = run["run_id"]
|
| 32 |
+
assert vitis_client.get(f"/api/vitis/hil/runs/{rid}").status_code == 200
|
| 33 |
+
listing = vitis_client.get("/api/vitis/hil/runs").json()["runs"]
|
| 34 |
+
assert any(x["run_id"] == rid for x in listing)
|
| 35 |
+
assert vitis_client.get("/api/vitis/hil/runs/nope").status_code == 404
|
| 36 |
+
|
| 37 |
+
def test_sweep(self, vitis_client):
|
| 38 |
+
r = vitis_client.post("/api/vitis/hil/sweep", json={
|
| 39 |
+
"n_sequences": 1, "frames_per_sequence": 6, "widths": [8, 12]})
|
| 40 |
+
assert r.status_code == 200
|
| 41 |
+
assert len(r.json()["points"]) == 2
|
| 42 |
+
|
| 43 |
+
def test_validation(self, vitis_client):
|
| 44 |
+
assert vitis_client.post("/api/vitis/hil/run", json={
|
| 45 |
+
"width_bits": 4, "int_bits": 4}).status_code == 400
|
| 46 |
+
assert vitis_client.post("/api/vitis/hil/run", json={
|
| 47 |
+
"device": "cray-1"}).status_code == 400
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class TestIspAugmentApi:
|
| 51 |
+
def test_isp_lifecycle(self, vitis_client):
|
| 52 |
+
r = vitis_client.post("/api/vitis/isp/run", json={
|
| 53 |
+
"n_frames": 1, "include_previews": False})
|
| 54 |
+
assert r.status_code == 200
|
| 55 |
+
run = r.json()
|
| 56 |
+
assert run["throughput"]["modeled_not_measured"] is True
|
| 57 |
+
rid = run["run_id"]
|
| 58 |
+
assert vitis_client.get(f"/api/vitis/isp/runs/{rid}").status_code == 200
|
| 59 |
+
assert vitis_client.post("/api/vitis/isp/run", json={
|
| 60 |
+
"stages": ["defrag"]}).status_code == 400
|
| 61 |
+
assert vitis_client.get("/api/vitis/isp/stages").json()["stages"]
|
| 62 |
+
|
| 63 |
+
def test_augment_lifecycle(self, vitis_client):
|
| 64 |
+
r = vitis_client.get("/api/vitis/augment/recipes")
|
| 65 |
+
assert len(r.json()["augmentations"]) == 7
|
| 66 |
+
r = vitis_client.post("/api/vitis/augment/generate", json={
|
| 67 |
+
"n_variants": 2, "include_thumbnails": False})
|
| 68 |
+
assert r.status_code == 200
|
| 69 |
+
batch = r.json()
|
| 70 |
+
assert all(v["evaluation_only"] for v in batch["variants"])
|
| 71 |
+
bid = batch["run_id"]
|
| 72 |
+
assert vitis_client.get(
|
| 73 |
+
f"/api/vitis/augment/batches/{bid}").status_code == 200
|
| 74 |
+
variants = vitis_client.get("/api/vitis/augment/variants").json()
|
| 75 |
+
assert len(variants["variants"]) >= 2
|
| 76 |
+
assert vitis_client.post("/api/vitis/augment/generate", json={
|
| 77 |
+
"backend": "vitis_hw"}).status_code == 400
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class TestTemporalApi:
|
| 81 |
+
def test_temporal_lifecycle(self, vitis_client):
|
| 82 |
+
engines = vitis_client.get("/api/vitis/temporal/engines").json()["engines"]
|
| 83 |
+
assert len(engines) == 2
|
| 84 |
+
r = vitis_client.post("/api/vitis/temporal/run", json={
|
| 85 |
+
"n_sequences": 1, "frames_per_sequence": 10})
|
| 86 |
+
assert r.status_code == 200
|
| 87 |
+
run = r.json()
|
| 88 |
+
assert set(run["results"]) == {"reference", "vitis_emulated"}
|
| 89 |
+
assert "ranking_agrees" in run["backend_agreement"]
|
| 90 |
+
rid = run["run_id"]
|
| 91 |
+
assert vitis_client.get(
|
| 92 |
+
f"/api/vitis/temporal/runs/{rid}").status_code == 200
|
| 93 |
+
assert vitis_client.post("/api/vitis/temporal/run", json={
|
| 94 |
+
"engines": ["skynet"]}).status_code == 400
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
class TestPrdApi:
|
| 98 |
+
def test_prd_listing_and_content(self, vitis_client):
|
| 99 |
+
listing = vitis_client.get("/api/vitis/prd").json()["prds"]
|
| 100 |
+
assert {p["id"] for p in listing} == {
|
| 101 |
+
"vitis-hil-regression", "vitis-isp-preprocessing",
|
| 102 |
+
"vitis-temporal-stability"}
|
| 103 |
+
assert all(p["available"] for p in listing)
|
| 104 |
+
doc = vitis_client.get("/api/vitis/prd/vitis-hil-regression").json()
|
| 105 |
+
assert "Value Proposition" in doc["markdown"]
|
| 106 |
+
assert vitis_client.get("/api/vitis/prd/nope").status_code == 404
|
tests/test_vitis/test_backend.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Backend abstraction: parity, quantization monotonicity, XFCVDEPTH,
|
| 2 |
+
LUT approximations, latency model, registry."""
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
import pytest
|
| 6 |
+
|
| 7 |
+
from sensorflow.vitis.backend import (
|
| 8 |
+
BACKENDS, DeviceConfig, PipelineConfig, backend_status, get_backend,
|
| 9 |
+
make_bayer_mosaic,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@pytest.fixture(scope="module")
|
| 14 |
+
def img():
|
| 15 |
+
rng = np.random.default_rng(3)
|
| 16 |
+
base = rng.random((96, 128, 3)).astype(np.float32)
|
| 17 |
+
return base
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@pytest.fixture(scope="module")
|
| 21 |
+
def gray(img):
|
| 22 |
+
return img.mean(axis=2).astype(np.float32)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _cfg(w, i=6, **kw):
|
| 26 |
+
return PipelineConfig(precision={"default": (w, i)}, **kw)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class TestParityAndMonotonicity:
|
| 30 |
+
def test_high_bitwidth_parity_within_quantization_bound(self, img):
|
| 31 |
+
ref = get_backend("reference")
|
| 32 |
+
vit = get_backend("vitis_emulated", _cfg(16))
|
| 33 |
+
r = ref.gaussian_filter(img, 1.5)
|
| 34 |
+
v = vit.gaussian_filter(img, 1.5)
|
| 35 |
+
# ap_fixed<16,6> has 10 fractional bits -> lsb ~ 1e-3; allow a few lsb
|
| 36 |
+
# of accumulated error through the filter.
|
| 37 |
+
assert float(np.abs(r - v).mean()) < 4 * 2.0 ** -10
|
| 38 |
+
|
| 39 |
+
def test_divergence_grows_monotonically_as_bits_shrink(self, img):
|
| 40 |
+
ref = get_backend("reference")
|
| 41 |
+
target = ref.gaussian_filter(img, 1.5)
|
| 42 |
+
errs = []
|
| 43 |
+
for w in (16, 12, 10, 8):
|
| 44 |
+
vit = get_backend("vitis_emulated", _cfg(w))
|
| 45 |
+
errs.append(float(np.abs(vit.gaussian_filter(img, 1.5) - target).mean()))
|
| 46 |
+
assert errs == sorted(errs), f"not monotone: {errs}"
|
| 47 |
+
assert errs[-1] > 10 * errs[0]
|
| 48 |
+
|
| 49 |
+
def test_ops_run_on_both_backends(self, img, gray):
|
| 50 |
+
raw = make_bayer_mosaic(img)
|
| 51 |
+
for name in ("reference", "vitis_emulated"):
|
| 52 |
+
be = get_backend(name, _cfg(16))
|
| 53 |
+
assert be.demosaic(raw).shape == img.shape
|
| 54 |
+
assert be.resize(img, 48, 64).shape == (48, 64, 3)
|
| 55 |
+
assert be.resize(gray, 48, 64, "area").shape == (48, 64)
|
| 56 |
+
assert be.crop(img, 10, 10, 20, 30).shape == (20, 30, 3)
|
| 57 |
+
yuv = be.rgb_to_yuv(img)
|
| 58 |
+
back = be.yuv_to_rgb(yuv)
|
| 59 |
+
assert float(np.abs(back - img).mean()) < 0.05
|
| 60 |
+
assert be.bad_pixel_correction(gray).shape == gray.shape
|
| 61 |
+
assert be.hdr_tone_map(img).max() <= 1.0
|
| 62 |
+
assert be.gain_exposure(img, 2.0).max() <= 1.0
|
| 63 |
+
assert be.lens_distortion(gray, k1=0.2).shape == gray.shape
|
| 64 |
+
assert be.median_filter(gray, 3).shape == gray.shape
|
| 65 |
+
merged = be.hdr_merge([img * 0.3, img], [0.5, 1.0])
|
| 66 |
+
assert merged.shape == img.shape
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class TestStreamingDepthConstraint:
|
| 70 |
+
def test_no_artifacts_when_depth_sufficient(self, gray):
|
| 71 |
+
wide = get_backend("vitis_emulated", _cfg(16, max_line_buffer_depth=4096))
|
| 72 |
+
exact = get_backend("vitis_emulated",
|
| 73 |
+
_cfg(16, max_line_buffer_depth=gray.shape[1]))
|
| 74 |
+
a = wide.gaussian_filter(gray, 2.0)
|
| 75 |
+
b = exact.gaussian_filter(gray, 2.0)
|
| 76 |
+
assert np.array_equal(a, b)
|
| 77 |
+
|
| 78 |
+
def test_localized_boundary_artifacts_only_when_exceeded(self, gray):
|
| 79 |
+
depth = 48
|
| 80 |
+
full = get_backend("vitis_emulated", _cfg(16, max_line_buffer_depth=4096))
|
| 81 |
+
narrow = get_backend("vitis_emulated",
|
| 82 |
+
_cfg(16, max_line_buffer_depth=depth))
|
| 83 |
+
a = full.gaussian_filter(gray, 2.0)
|
| 84 |
+
b = narrow.gaussian_filter(gray, 2.0)
|
| 85 |
+
col_err = np.abs(a - b).mean(axis=0)
|
| 86 |
+
radius = 8 # scipy gaussian kernel truncates at 4*sigma = 8 px
|
| 87 |
+
seam_cols = [c for s in range(depth, gray.shape[1], depth)
|
| 88 |
+
for c in range(max(0, s - radius),
|
| 89 |
+
min(gray.shape[1], s + radius))]
|
| 90 |
+
interior = [c for c in range(gray.shape[1]) if c not in set(seam_cols)]
|
| 91 |
+
assert col_err[seam_cols].max() > 1e-3, "expected seam artifacts"
|
| 92 |
+
assert col_err[interior].max() < 1e-6, "artifacts leaked beyond seams"
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class TestHlsApproximations:
|
| 96 |
+
def test_lut_recip_bounded_relative_error(self):
|
| 97 |
+
cfg = _cfg(24, i=8, lut_bits=8)
|
| 98 |
+
from sensorflow.vitis.backend import _FixedNumerics
|
| 99 |
+
num = _FixedNumerics(cfg)
|
| 100 |
+
x = np.linspace(0.01, 50.0, 5000).astype(np.float32)
|
| 101 |
+
rel = np.abs(num.recip(x) - 1.0 / x) * x
|
| 102 |
+
assert float(rel.max()) < 2.0 ** -7 # ~2x lsb of a 256-entry LUT
|
| 103 |
+
|
| 104 |
+
def test_lut_sqrt_bounded_relative_error(self):
|
| 105 |
+
cfg = _cfg(24, i=8, lut_bits=8)
|
| 106 |
+
from sensorflow.vitis.backend import _FixedNumerics
|
| 107 |
+
num = _FixedNumerics(cfg)
|
| 108 |
+
x = np.linspace(0.01, 50.0, 5000).astype(np.float32)
|
| 109 |
+
rel = np.abs(num.sqrt(x) - np.sqrt(x)) / np.sqrt(x)
|
| 110 |
+
assert float(rel.max()) < 2.0 ** -8
|
| 111 |
+
|
| 112 |
+
def test_lut_toggle_changes_output(self, img):
|
| 113 |
+
on = get_backend("vitis_emulated", _cfg(20, i=6, use_lut_approx=True))
|
| 114 |
+
off = get_backend("vitis_emulated", _cfg(20, i=6, use_lut_approx=False))
|
| 115 |
+
a = on.hdr_tone_map(img)
|
| 116 |
+
b = off.hdr_tone_map(img)
|
| 117 |
+
assert not np.array_equal(a, b)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class TestLatencyModelAndRegistry:
|
| 121 |
+
def test_modeled_latency_deterministic_and_labeled(self, gray):
|
| 122 |
+
be = get_backend("vitis_emulated", _cfg(12))
|
| 123 |
+
be.gaussian_filter(gray, 1.0)
|
| 124 |
+
rep = be.profile_report()[-1]
|
| 125 |
+
assert rep["modeled"]["modeled_not_measured"] is True
|
| 126 |
+
assert rep["modeled"]["latency_ms"] > 0
|
| 127 |
+
be2 = get_backend("vitis_emulated", _cfg(12))
|
| 128 |
+
be2.gaussian_filter(gray, 1.0)
|
| 129 |
+
assert be2.profile_report()[-1]["modeled"]["latency_ms"] == \
|
| 130 |
+
rep["modeled"]["latency_ms"]
|
| 131 |
+
|
| 132 |
+
def test_aie_placement_depends_on_device(self, gray):
|
| 133 |
+
versal = get_backend("vitis_emulated", PipelineConfig(
|
| 134 |
+
device=DeviceConfig(name="versal-ai-edge")))
|
| 135 |
+
zynq = get_backend("vitis_emulated", PipelineConfig(
|
| 136 |
+
device=DeviceConfig(name="zynq-ultrascale")))
|
| 137 |
+
versal.gaussian_filter(gray, 1.0)
|
| 138 |
+
zynq.gaussian_filter(gray, 1.0)
|
| 139 |
+
assert versal.profile_report()[-1]["modeled"]["placement"] == "AIE"
|
| 140 |
+
assert zynq.profile_report()[-1]["modeled"]["placement"] == "PL"
|
| 141 |
+
|
| 142 |
+
def test_reference_backend_reports_no_modeled_numbers(self, gray):
|
| 143 |
+
ref = get_backend("reference")
|
| 144 |
+
ref.gaussian_filter(gray, 1.0)
|
| 145 |
+
assert "modeled" not in ref.profile_report()[-1]
|
| 146 |
+
|
| 147 |
+
def test_registry_and_hw_stub(self):
|
| 148 |
+
assert set(BACKENDS) == {"reference", "vitis_emulated", "vitis_hw"}
|
| 149 |
+
with pytest.raises(NotImplementedError):
|
| 150 |
+
get_backend("vitis_hw")
|
| 151 |
+
with pytest.raises(ValueError):
|
| 152 |
+
get_backend("nonsense")
|
| 153 |
+
status = backend_status()
|
| 154 |
+
assert status["hardware_present"] is False
|
| 155 |
+
emu = next(b for b in status["backends"] if b["name"] == "vitis_emulated")
|
| 156 |
+
assert emu["emulated"] is True and "modeled" in emu["description"]
|
tests/test_vitis/test_hil.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HIL feature: ablation attribution, sequential/fallback verdicts, sweep."""
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
from sensorflow.vitis.hil import (
|
| 6 |
+
_fallback_verdict, run_bitwidth_sweep, run_hil, sequential_verdict,
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class TestAblationAttribution:
|
| 11 |
+
def test_planted_precision_regression_attributed_to_precision(self, vitis_root):
|
| 12 |
+
# W=8 with generous depth and LUTs on: precision is the only real
|
| 13 |
+
# constraint that bites at this image size.
|
| 14 |
+
run = run_hil(n_sequences=2, frames_per_sequence=8, seed=7,
|
| 15 |
+
width_bits=8, int_bits=4,
|
| 16 |
+
max_line_buffer_depth=2048, use_lut_approx=True)
|
| 17 |
+
attr = run["ablation"]["attribution"]
|
| 18 |
+
assert attr["precision_only"] > 0.8
|
| 19 |
+
assert attr["precision_only"] > attr["streaming_only"]
|
| 20 |
+
assert attr["precision_only"] > attr["hls_approx_only"]
|
| 21 |
+
|
| 22 |
+
def test_high_precision_run_has_negligible_gap(self, vitis_root):
|
| 23 |
+
run = run_hil(n_sequences=2, frames_per_sequence=8, seed=7,
|
| 24 |
+
width_bits=16, int_bits=4, run_ablation=False)
|
| 25 |
+
assert run["comparison"]["gap_score"] < 0.01
|
| 26 |
+
assert run["comparison"]["totals"]["dropped_by_vitis"] == 0
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class TestSequentialVerdicts:
|
| 30 |
+
def test_fallback_regression_on_planted_regression(self):
|
| 31 |
+
v = _fallback_verdict([-0.25 + 0.01 * (i % 3) for i in range(40)],
|
| 32 |
+
delta=0.02, alpha=0.05)
|
| 33 |
+
assert v["decision"] == "REGRESSION"
|
| 34 |
+
|
| 35 |
+
def test_fallback_pass_on_no_regression(self):
|
| 36 |
+
v = _fallback_verdict([0.002 - 0.001 * (i % 2) for i in range(40)],
|
| 37 |
+
delta=0.02, alpha=0.05)
|
| 38 |
+
assert v["decision"] == "PASS"
|
| 39 |
+
|
| 40 |
+
def test_fallback_insufficient_on_tiny_sample(self):
|
| 41 |
+
assert _fallback_verdict([0.0], 0.02, 0.05)["decision"] == \
|
| 42 |
+
"INSUFFICIENT_EVIDENCE"
|
| 43 |
+
|
| 44 |
+
def test_sequential_verdict_detects_planted_regression(self):
|
| 45 |
+
# Strong planted regression: vitis path loses ~half its objects.
|
| 46 |
+
rng = np.random.default_rng(5)
|
| 47 |
+
deltas = list(np.clip(rng.normal(-0.5, 0.05, 120), -1, 1))
|
| 48 |
+
pairs = [(True, bool(rng.random() > 0.5)) for _ in range(600)]
|
| 49 |
+
v = sequential_verdict(deltas, pairs, delta=0.02, alpha=0.05)
|
| 50 |
+
assert v["decision"] == "REGRESSION"
|
| 51 |
+
assert v["method"] in ("seqeval_anytime_valid", "paired_t_fallback")
|
| 52 |
+
|
| 53 |
+
def test_sequential_verdict_no_false_regression(self):
|
| 54 |
+
deltas = [0.0] * 120
|
| 55 |
+
pairs = [(True, True)] * 600
|
| 56 |
+
v = sequential_verdict(deltas, pairs, delta=0.02, alpha=0.05)
|
| 57 |
+
assert v["decision"] != "REGRESSION"
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class TestSweep:
|
| 61 |
+
def test_sweep_monotone_gap_and_minimal_config(self, vitis_root):
|
| 62 |
+
sw = run_bitwidth_sweep(n_sequences=2, frames_per_sequence=8, seed=7,
|
| 63 |
+
widths=[6, 8, 12, 16])
|
| 64 |
+
pts = sw["points"] # sorted ascending by width
|
| 65 |
+
gaps = [p["gap_score"] for p in pts]
|
| 66 |
+
assert gaps == sorted(gaps, reverse=True), f"gap not monotone: {gaps}"
|
| 67 |
+
assert pts[0]["decision"] == "REGRESSION" # W=6 is a planted disaster
|
| 68 |
+
mp = sw["minimal_passing_config"]
|
| 69 |
+
assert mp is not None and mp["width_bits"] > 6
|
| 70 |
+
|
| 71 |
+
def test_run_persisted(self, vitis_root):
|
| 72 |
+
run = run_hil(n_sequences=1, frames_per_sequence=6, width_bits=12,
|
| 73 |
+
run_ablation=False)
|
| 74 |
+
assert (vitis_root / "hil" / f"{run['run_id']}.json").exists()
|
tests/test_vitis/test_isp_augment.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ISP per-stage quality/throughput + augmentation lineage & leakage guard."""
|
| 2 |
+
|
| 3 |
+
from sensorflow.vitis.augment import generate_batch, list_augmentations
|
| 4 |
+
from sensorflow.vitis.isp import run_isp
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class TestIsp:
|
| 8 |
+
def test_per_stage_psnr_sanity(self, vitis_root):
|
| 9 |
+
run = run_isp(n_frames=2, width_bits=12, include_previews=False)
|
| 10 |
+
assert [e["stage"] for e in run["stage_report"]] == \
|
| 11 |
+
["bad_pixel_correction", "demosaic", "hdr_tone_map", "gain",
|
| 12 |
+
"denoise", "resize"]
|
| 13 |
+
for e in run["stage_report"]:
|
| 14 |
+
assert 20.0 < e["psnr_db"] <= 99.0, e
|
| 15 |
+
assert 0.5 < e["ssim"] <= 1.0, e
|
| 16 |
+
# Higher precision must not be worse than lower precision.
|
| 17 |
+
lo = run_isp(n_frames=2, width_bits=8, include_previews=False)
|
| 18 |
+
assert min(e["psnr_db"] for e in run["stage_report"]) > \
|
| 19 |
+
min(e["psnr_db"] for e in lo["stage_report"])
|
| 20 |
+
|
| 21 |
+
def test_throughput_labeled_modeled(self, vitis_root):
|
| 22 |
+
run = run_isp(n_frames=1, include_previews=False)
|
| 23 |
+
t = run["throughput"]
|
| 24 |
+
assert t["modeled_not_measured"] is True
|
| 25 |
+
assert t["measured_cpu_ms_per_frame"] > 0
|
| 26 |
+
assert t["modeled_fpga_fps_pipelined"] > 0
|
| 27 |
+
for e in run["stage_report"]:
|
| 28 |
+
assert e["modeled_not_measured"] is True
|
| 29 |
+
|
| 30 |
+
def test_previews_and_persistence(self, vitis_root):
|
| 31 |
+
run = run_isp(n_frames=1, include_previews=True)
|
| 32 |
+
assert run["previews"][0]["stages"][0]["reference_png"].startswith(
|
| 33 |
+
"data:image/png;base64,")
|
| 34 |
+
assert (vitis_root / "isp" / f"{run['run_id']}.json").exists()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class TestAugment:
|
| 38 |
+
def test_lineage_completeness_and_eval_only_flag(self, vitis_root):
|
| 39 |
+
batch = generate_batch(n_variants=5, include_thumbnails=False)
|
| 40 |
+
assert len(batch["variants"]) == 5
|
| 41 |
+
for v in batch["variants"]:
|
| 42 |
+
lin = v["lineage"]
|
| 43 |
+
for key in ("batch_id", "source_frame_id", "source_sequence_id",
|
| 44 |
+
"recipe", "seed", "backend", "backend_config"):
|
| 45 |
+
assert key in lin, f"lineage missing {key}"
|
| 46 |
+
assert lin["recipe"] and all("aug" in s and "params" in s
|
| 47 |
+
for s in lin["recipe"])
|
| 48 |
+
# Leakage guard: evaluation-only by default, never training data.
|
| 49 |
+
assert v["evaluation_only"] is True
|
| 50 |
+
assert v["training_eligible"] is False
|
| 51 |
+
assert v["recommended_dataset_destination"] == \
|
| 52 |
+
"REGRESSION_EVALUATION_SET"
|
| 53 |
+
assert v["gt_boxes"], "variants must inherit GT"
|
| 54 |
+
|
| 55 |
+
def test_deterministic_given_seed(self, vitis_root):
|
| 56 |
+
a = generate_batch(n_variants=3, seed=99, include_thumbnails=False)
|
| 57 |
+
b = generate_batch(n_variants=3, seed=99, include_thumbnails=False)
|
| 58 |
+
assert [v["lineage"]["seed"] for v in a["variants"]] == \
|
| 59 |
+
[v["lineage"]["seed"] for v in b["variants"]]
|
| 60 |
+
|
| 61 |
+
def test_unknown_recipe_rejected(self, vitis_root):
|
| 62 |
+
import pytest
|
| 63 |
+
with pytest.raises(ValueError):
|
| 64 |
+
generate_batch(recipes=[{"aug": "sharknado"}], n_variants=1)
|
| 65 |
+
|
| 66 |
+
def test_raremine_hook_skipped_for_test_batches(self, vitis_root):
|
| 67 |
+
# Test batches run under a redirected vitis root; routing into
|
| 68 |
+
# raremine's real candidate store must be skipped so tests never
|
| 69 |
+
# touch shared state. Real persisted batches route when raremine
|
| 70 |
+
# is importable (verified manually via the live API).
|
| 71 |
+
batch = generate_batch(n_variants=4, include_thumbnails=False)
|
| 72 |
+
hook = batch["raremine_hook"]
|
| 73 |
+
assert "available" in hook and "routed_candidates" in hook
|
| 74 |
+
assert hook["routed_candidates"] == 0
|
| 75 |
+
assert "ephemeral/test" in hook["note"]
|
| 76 |
+
|
| 77 |
+
def test_registry_listing(self):
|
| 78 |
+
names = {a["name"] for a in list_augmentations()}
|
| 79 |
+
assert {"sensor_noise", "low_light", "hdr_extreme", "lens_distortion",
|
| 80 |
+
"chromatic_aberration", "motion_blur", "glare"} <= names
|
tests/test_vitis/test_temporal.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Temporal profiling: flow recovery, engine ranking, stereo geometry."""
|
| 2 |
+
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
from sensorflow.bevfusion.scenes import generate_sequences
|
| 6 |
+
from sensorflow.vitis.backend import get_backend
|
| 7 |
+
from sensorflow.vitis.render import (
|
| 8 |
+
BASELINE_M, FOCAL_PX, render_stereo_pair,
|
| 9 |
+
)
|
| 10 |
+
from sensorflow.vitis.temporal import (
|
| 11 |
+
_aggregate_engine, _engine_track_metrics, _match_engine_to_gt,
|
| 12 |
+
compute_flow_baseline, run_temporal_profile,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class TestOpticalFlowRecovery:
|
| 17 |
+
def test_recovers_planted_constant_velocity(self):
|
| 18 |
+
"""A textured square moving at constant (dx, dy) px/frame must be
|
| 19 |
+
recovered by the pyramidal LK flow within tolerance."""
|
| 20 |
+
rng = np.random.default_rng(2)
|
| 21 |
+
h, w = 96, 128
|
| 22 |
+
tex = rng.uniform(0.4, 1.0, (18, 18)).astype(np.float32)
|
| 23 |
+
dx, dy = 3.0, 1.0
|
| 24 |
+
|
| 25 |
+
def frame(t):
|
| 26 |
+
img = np.full((h, w), 0.05, dtype=np.float32)
|
| 27 |
+
y0, x0 = int(30 + dy * t), int(20 + dx * t)
|
| 28 |
+
img[y0:y0 + 18, x0:x0 + 18] = tex
|
| 29 |
+
return img
|
| 30 |
+
|
| 31 |
+
for name in ("reference", "vitis_emulated"):
|
| 32 |
+
be = get_backend(name)
|
| 33 |
+
flow = be.optical_flow(frame(0), frame(1), levels=3, window=9,
|
| 34 |
+
iterations=3)
|
| 35 |
+
patch = flow[32:46, 22:36]
|
| 36 |
+
assert abs(float(np.median(patch[..., 0])) - dx) < 0.5, name
|
| 37 |
+
assert abs(float(np.median(patch[..., 1])) - dy) < 0.5, name
|
| 38 |
+
|
| 39 |
+
def test_flow_baseline_marks_tracks_continuous(self):
|
| 40 |
+
seqs = generate_sequences(1, 10, seed=7)
|
| 41 |
+
be = get_backend("reference")
|
| 42 |
+
records = compute_flow_baseline(seqs[0], be, seed=7)
|
| 43 |
+
continuous = [r["continuous"] for per in records.values()
|
| 44 |
+
for r in per.values()]
|
| 45 |
+
assert sum(continuous) > 0.5 * len(continuous)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class TestEngineRanking:
|
| 49 |
+
def test_unstable_engine_ranks_below_stable(self, vitis_root):
|
| 50 |
+
"""The naive camera-only engine (frame-to-frame NN tracker, misses
|
| 51 |
+
under occlusion/night) must score below the fused engine."""
|
| 52 |
+
run = run_temporal_profile(n_sequences=2, frames_per_sequence=12,
|
| 53 |
+
seed=7)
|
| 54 |
+
for be_name in ("reference", "vitis_emulated"):
|
| 55 |
+
eng = run["results"][be_name]["engines"]
|
| 56 |
+
cam = eng["perception-v1-camera"]
|
| 57 |
+
fused = eng["perception-v3-bevfusion"]
|
| 58 |
+
assert fused["stability_score"] > cam["stability_score"], be_name
|
| 59 |
+
assert cam["flicker_rate"] >= fused["flicker_rate"], be_name
|
| 60 |
+
assert cam["fragmentation_per_track"] > \
|
| 61 |
+
fused["fragmentation_per_track"], be_name
|
| 62 |
+
|
| 63 |
+
def test_synthetic_flicker_engine_penalized(self):
|
| 64 |
+
"""Deliberately dropping every third detection of a stable engine
|
| 65 |
+
must raise flicker and lower the aggregate score."""
|
| 66 |
+
seqs = generate_sequences(1, 14, seed=9)
|
| 67 |
+
seq = seqs[0]
|
| 68 |
+
be = get_backend("reference")
|
| 69 |
+
flow_records = compute_flow_baseline(seq, be, seed=9)
|
| 70 |
+
|
| 71 |
+
stable_out = {f.frame_id: [{"bbox_3d": g.bbox_3d,
|
| 72 |
+
"class_name": g.class_name,
|
| 73 |
+
"confidence": 0.9,
|
| 74 |
+
"track_id": g.instance_id}
|
| 75 |
+
for g in f.gt]
|
| 76 |
+
for f in seq.frames}
|
| 77 |
+
flicker_out = {fid: (dets if i % 3 else dets[:max(0, len(dets) - 3)])
|
| 78 |
+
for i, (fid, dets) in enumerate(stable_out.items())}
|
| 79 |
+
|
| 80 |
+
m_stable = _aggregate_engine([_engine_track_metrics(
|
| 81 |
+
seq, flow_records, _match_engine_to_gt(seq, stable_out))])
|
| 82 |
+
m_flicker = _aggregate_engine([_engine_track_metrics(
|
| 83 |
+
seq, flow_records, _match_engine_to_gt(seq, flicker_out))])
|
| 84 |
+
assert m_flicker["flicker_rate"] > m_stable["flicker_rate"]
|
| 85 |
+
assert m_flicker["stability_score"] < m_stable["stability_score"]
|
| 86 |
+
|
| 87 |
+
def test_backend_agreement_metacheck(self, vitis_root):
|
| 88 |
+
run = run_temporal_profile(n_sequences=2, frames_per_sequence=12,
|
| 89 |
+
seed=7, width_bits=12)
|
| 90 |
+
meta = run["backend_agreement"]
|
| 91 |
+
assert meta["ranking_agrees"] is True
|
| 92 |
+
assert meta["max_abs_score_delta"] < 5.0
|
| 93 |
+
|
| 94 |
+
def test_cohort_breakdown_present(self, vitis_root):
|
| 95 |
+
run = run_temporal_profile(n_sequences=3, frames_per_sequence=12,
|
| 96 |
+
seed=7)
|
| 97 |
+
cohorts = run["results"]["reference"]["engines"][
|
| 98 |
+
"perception-v3-bevfusion"]["cohorts"]
|
| 99 |
+
assert "night/clear" in cohorts and "occluded" in cohorts
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class TestStereoConsistency:
|
| 103 |
+
def test_disparity_depth_within_tolerance_of_geometry(self):
|
| 104 |
+
seqs = generate_sequences(1, 6, seed=7)
|
| 105 |
+
seq = seqs[0]
|
| 106 |
+
be = get_backend("reference")
|
| 107 |
+
rel_errors = []
|
| 108 |
+
for frame in seq.frames[:3]:
|
| 109 |
+
left, right, objs = render_stereo_pair(frame, seq, seed=7)
|
| 110 |
+
disp = be.stereo_block_match(left, right, max_disparity=48, block=9)
|
| 111 |
+
for o in objs:
|
| 112 |
+
u, v = int(round(o["u"])), int(round(o["v"]))
|
| 113 |
+
if not (4 <= v < disp.shape[0] - 4 and 4 <= u < disp.shape[1] - 4):
|
| 114 |
+
continue
|
| 115 |
+
d = float(np.median(disp[v - 2:v + 3, u - 2:u + 3]))
|
| 116 |
+
if d < 1.0 or o["depth_m"] > 30.0:
|
| 117 |
+
continue # near field is the geometry-limited regime
|
| 118 |
+
depth = FOCAL_PX * BASELINE_M / d
|
| 119 |
+
rel_errors.append(abs(depth - o["depth_m"]) / o["depth_m"])
|
| 120 |
+
# Median over objects: individual matches can be corrupted by
|
| 121 |
+
# object overlap, but the population must track scene geometry.
|
| 122 |
+
assert len(rel_errors) >= 3
|
| 123 |
+
assert float(np.median(rel_errors)) < 0.15, rel_errors
|
| 124 |
+
|
| 125 |
+
def test_stereo_report_in_profile(self, vitis_root):
|
| 126 |
+
run = run_temporal_profile(n_sequences=1, frames_per_sequence=10,
|
| 127 |
+
seed=7)
|
| 128 |
+
stereo = run["results"]["reference"]["stereo"]
|
| 129 |
+
assert stereo["objects_checked"] > 0
|
| 130 |
+
assert stereo["median_abs_disparity_error_px"] < 2.0
|