whassan commited on
Commit
79ae023
·
verified ·
1 Parent(s): e8829d6

Deploy Sensorflow Studio (part 2)

Browse files
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