@trillboards/edge-sdk 0.2.92 → 0.2.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,618 @@
1
+ """`scripts/sensing/onnx_artifact_probe.py` — the DECLARED layout, VERIFIED.
2
+
3
+ WHY THIS SUITE LIVES HERE AND NOT NEXT TO THE SCRIPT
4
+ ----------------------------------------------------
5
+ The probe is Python, and the only lane in this repo that executes Python is
6
+ `detector-service`, which discovers `test_*.py` under `deploy/detector`. Putting
7
+ these cases anywhere else would make them the shape the probe itself is written
8
+ against: a check that exists and never runs. `test_session.py`'s
9
+ `TestEveryVocabularyConstructionDeclaresItsVariant` already reaches out of this
10
+ package into `scripts/sensing` for the same reason, and it did so because the
11
+ first version of that guard scanned only `deploy/detector` and passed while the
12
+ probe held a broken `ModelVocabulary` construction.
13
+
14
+ STDLIB ONLY, so it runs on that lane's bare interpreter — no onnxruntime, no
15
+ numpy, no Pillow, no ~12 MiB artifact. Everything under test here is arithmetic
16
+ over shapes an exporter wrote down, so a fake output descriptor with a `.name`
17
+ and a `.shape` is not a weaker input than a real `NodeArg`; it is the same two
18
+ fields. What it cannot reach — that a graph this accepts also RUNS — is covered
19
+ by the probe's own measurement child, which loads the artifact and infers.
20
+
21
+ WHAT IS BEING PROTECTED
22
+ -----------------------
23
+ Three failures, none of which raises anything at runtime:
24
+
25
+ * a DEFAULT that moved. The probe answers `--artifact` alone exactly as it did
26
+ before layouts were declarable, which is the only reason `rfdetr_medium`'s
27
+ published probe result is still reproducible. A new flag that quietly widened
28
+ that path would republish a different number under the same provenance.
29
+ * a layout DECLARED that the graph cannot speak. `[1, 300, 6]` and
30
+ `[1, 6, 8400]` are both rank-3 with a 6 in them, and the wrong one decodes
31
+ into a confident, meaningless table.
32
+ * a `4 + nc` check counting the wrong `nc`. Distinct NAMES and LOGIT SLOTS are
33
+ different numbers on real artifacts in both directions, and using the name
34
+ count rejects healthy graphs.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import contextlib
40
+ import importlib.util
41
+ import io
42
+ import json
43
+ import pathlib
44
+ import sys
45
+ import tempfile
46
+ import unittest
47
+
48
+ from trillboards_detector import labels as labels_mod
49
+
50
+
51
+ def _load_probe():
52
+ """Import the probe BY PATH, or return None on a checkout that has none.
53
+
54
+ `deploy/` ships inside the npm tarball, so on an installed connector
55
+ `scripts/sensing` is simply absent. The module is loaded rather than asserted
56
+ for exactly that reason — the same concession `test_session.py` makes about
57
+ the repo roots it scans.
58
+ """
59
+ here = pathlib.Path(__file__).resolve().parent.parent
60
+ for parent in here.parents:
61
+ # A git WORKTREE's `.git` is a file, not a directory.
62
+ if not (parent / ".git").exists():
63
+ continue
64
+ candidate = parent / "scripts" / "sensing" / "onnx_artifact_probe.py"
65
+ if not candidate.is_file():
66
+ return None
67
+ spec = importlib.util.spec_from_file_location("onnx_artifact_probe_under_test", candidate)
68
+ module = importlib.util.module_from_spec(spec)
69
+ spec.loader.exec_module(module)
70
+ return module
71
+ return None
72
+
73
+
74
+ PROBE = _load_probe()
75
+ NO_PROBE = "scripts/sensing/onnx_artifact_probe.py is not in this checkout"
76
+
77
+
78
+ class Out:
79
+ """An ORT output descriptor, which is a `.name` and a `.shape` and nothing else.
80
+
81
+ A dynamic axis arrives from onnxruntime as a symbolic STRING rather than an
82
+ int, so several cases here pass one — that is a real artifact property and
83
+ the probe's branches turn on it.
84
+ """
85
+
86
+ def __init__(self, name: str, shape: "list"):
87
+ self.name = name
88
+ self.shape = shape
89
+
90
+ def __repr__(self) -> str: # pragma: no cover - only reached in a failure message
91
+ return f"Out({self.name!r}, {self.shape!r})"
92
+
93
+
94
+ DETS_LABELS_91 = [Out("dets", [1, 300, 4]), Out("labels", [1, 300, 91])]
95
+ #: `ppe_vyra_14cls_v8m.onnx`, measured: 14 classes, so 4 + 14 = 18 channels.
96
+ ANCHOR_14 = [Out("output0", [1, 18, 8400])]
97
+ #: `gh_fall_tonlong.onnx`, measured: 11 channels over 8400 anchors.
98
+ ANCHOR_FALL = [Out("output0", [1, 11, 8400])]
99
+ END2END = [Out("output0", [1, 300, 6])]
100
+
101
+ #: `gh_fall_tonlong.onnx`'s own `names` metadata, byte for byte. SEVEN slots,
102
+ #: and `Violence`/`violence` fold to ONE canonical name.
103
+ FALL_NAMES = {0: "Violence", 1: "fall", 2: "fire", 3: "sit", 4: "sleep", 5: "standing", 6: "violence"}
104
+
105
+
106
+ @contextlib.contextmanager
107
+ def _captured_stdout():
108
+ buf = io.StringIO()
109
+ original = sys.stdout
110
+ sys.stdout = buf
111
+ try:
112
+ yield buf
113
+ finally:
114
+ sys.stdout = original
115
+
116
+
117
+ class ProbeCase(unittest.TestCase):
118
+ """Shared helpers: the probe refuses by writing JSON and exiting 1."""
119
+
120
+ def args(self, *argv):
121
+ """A parsed `Namespace` through the REAL CLI, never a hand-built stand-in.
122
+
123
+ Every case that pretends to be a command line has to go through the
124
+ parser the command line goes through, or the suite stops covering the
125
+ surface it claims to.
126
+ """
127
+ return PROBE.parse_args(["--artifact", "unused.onnx", *argv])
128
+
129
+ def assertRefuses(self, fn, *args, **kwargs):
130
+ """Run something expected to `_fail`, and return its refusal payload.
131
+
132
+ Asserts the WHOLE contract of a refusal, not merely that it happened:
133
+ `{"ok": false, "step": ..., "reason": ...}` on stdout and exit 1. The
134
+ publisher reads `reason` off the LAST stdout line of a non-zero exit, so
135
+ a refusal that raised instead — or exited 0, or wrote prose — would reach
136
+ it as "the probe failed" with a stack trace attached.
137
+ """
138
+ with _captured_stdout() as buf:
139
+ with self.assertRaises(SystemExit) as raised:
140
+ fn(*args, **kwargs)
141
+ self.assertEqual(raised.exception.code, 1)
142
+ lines = [line for line in buf.getvalue().splitlines() if line.strip()]
143
+ self.assertTrue(lines, "a refusal must write JSON on stdout")
144
+ payload = json.loads(lines[-1])
145
+ self.assertIs(payload["ok"], False)
146
+ self.assertTrue(payload["step"])
147
+ self.assertTrue(payload["reason"])
148
+ return payload
149
+
150
+
151
+ @unittest.skipIf(PROBE is None, NO_PROBE)
152
+ class TestTheDefaultDidNotMove(ProbeCase):
153
+ """`--artifact` alone must answer exactly as it did before layouts existed.
154
+
155
+ `rfdetr_medium` was published by a run that passed nothing else, and its
156
+ `coco_classes`, `resident_mb` and graph contract are committed against that
157
+ run. A widened default would republish different numbers under the same
158
+ provenance, which is worse than a refusal because nothing would notice.
159
+ """
160
+
161
+ def test_no_declaration_is_the_rfdetr_contract(self):
162
+ contract, source = PROBE._resolve_variant(labels_mod, self.args())
163
+ self.assertEqual(contract.output_layout, labels_mod.RFDETR_DETS_LABELS)
164
+ self.assertEqual(contract.box_units, labels_mod.BOX_UNITS_PIXELS)
165
+ self.assertEqual(contract.box_scale_to_pixels, 1.0)
166
+ self.assertIn("default", source)
167
+
168
+ def test_a_graph_with_no_labels_output_is_still_refused(self):
169
+ reason = self.assertRefuses(
170
+ PROBE._graph_class_axis, labels_mod, labels_mod.RFDETR_DETS_LABELS, ANCHOR_14,
171
+ )["reason"]
172
+ # The refusal that has always been here, and the fact that identifies it.
173
+ self.assertIn("`labels`", reason)
174
+ # It now also says what to do about it, because there IS something.
175
+ self.assertIn("--variant-from-card", reason)
176
+ self.assertIn("output0[1, 18, 8400]", reason)
177
+
178
+ def test_the_rfdetr_class_axis_is_still_read_off_labels(self):
179
+ self.assertEqual(
180
+ PROBE._graph_class_axis(labels_mod, labels_mod.RFDETR_DETS_LABELS, DETS_LABELS_91),
181
+ 91,
182
+ )
183
+
184
+ def test_a_dynamic_rfdetr_class_axis_is_still_refused(self):
185
+ outputs = [Out("dets", [1, 300, 4]), Out("labels", [1, 300, "num_classes"])]
186
+ reason = self.assertRefuses(
187
+ PROBE._graph_class_axis, labels_mod, labels_mod.RFDETR_DETS_LABELS, outputs,
188
+ )["reason"]
189
+ self.assertIn("fixed class axis", reason)
190
+
191
+
192
+ @unittest.skipIf(PROBE is None, NO_PROBE)
193
+ class TestDeclaredLayoutIsVerified(ProbeCase):
194
+ """Each layout accepted on a shape it can speak, refused on one it cannot.
195
+
196
+ The refusals name the DECLARED layout and the ACTUAL outputs, because those
197
+ two facts are what tells an operator whether the artifact is wrong or the
198
+ card is.
199
+ """
200
+
201
+ def test_anchor_accepted_and_its_class_axis_is_channels_minus_four(self):
202
+ self.assertEqual(
203
+ PROBE._graph_class_axis(labels_mod, labels_mod.ANCHOR_4_PLUS_NC, ANCHOR_14), 14,
204
+ )
205
+
206
+ def test_end2end_accepted_and_declares_no_class_axis(self):
207
+ # `None`, not 0 and not a guess: the class index rides in column 5, so
208
+ # the graph says nothing at all about how many classes this model has.
209
+ self.assertIsNone(
210
+ PROBE._graph_class_axis(labels_mod, labels_mod.END2END_N_6, END2END),
211
+ )
212
+
213
+ def test_anchor_refused_on_an_end2end_shape(self):
214
+ # THE DISAMBIGUATOR. `[1, 300, 6]` read as an anchor head is 296 classes
215
+ # over 6 anchors and decodes without raising.
216
+ reason = self.assertRefuses(
217
+ PROBE._graph_class_axis, labels_mod, labels_mod.ANCHOR_4_PLUS_NC, END2END,
218
+ )["reason"]
219
+ self.assertIn(labels_mod.ANCHOR_4_PLUS_NC, reason)
220
+ self.assertIn("output0[1, 300, 6]", reason)
221
+ self.assertIn("296 classes over 6 anchors", reason)
222
+
223
+ def test_end2end_refused_on_an_anchor_shape(self):
224
+ reason = self.assertRefuses(
225
+ PROBE._graph_class_axis, labels_mod, labels_mod.END2END_N_6, ANCHOR_FALL,
226
+ )["reason"]
227
+ self.assertIn(labels_mod.END2END_N_6, reason)
228
+ self.assertIn("output0[1, 11, 8400]", reason)
229
+
230
+ def test_rfdetr_refused_on_an_anchor_shape(self):
231
+ reason = self.assertRefuses(
232
+ PROBE._graph_class_axis, labels_mod, labels_mod.RFDETR_DETS_LABELS, ANCHOR_FALL,
233
+ )["reason"]
234
+ self.assertIn(labels_mod.RFDETR_DETS_LABELS, reason)
235
+
236
+ def test_a_single_output_layout_refuses_a_two_output_graph(self):
237
+ outputs = [Out("output0", [1, 18, 8400]), Out("proto", [1, 32, 160, 160])]
238
+ reason = self.assertRefuses(
239
+ PROBE._graph_class_axis, labels_mod, labels_mod.ANCHOR_4_PLUS_NC, outputs,
240
+ )["reason"]
241
+ self.assertIn("exactly ONE graph output", reason)
242
+ self.assertIn("proto", reason)
243
+
244
+ def test_anchor_refuses_a_rank_two_output(self):
245
+ reason = self.assertRefuses(
246
+ PROBE._graph_class_axis, labels_mod, labels_mod.ANCHOR_4_PLUS_NC, [Out("output0", [18, 8400])],
247
+ )["reason"]
248
+ self.assertIn("rank-3", reason)
249
+
250
+ def test_anchor_refuses_a_batch_that_is_not_one(self):
251
+ reason = self.assertRefuses(
252
+ PROBE._graph_class_axis, labels_mod, labels_mod.ANCHOR_4_PLUS_NC, [Out("output0", [4, 18, 8400])],
253
+ )["reason"]
254
+ self.assertIn("batch-1", reason)
255
+
256
+ def test_anchor_refuses_a_dynamic_class_axis(self):
257
+ # The class count is one of the two numbers a detector arm is meaningless
258
+ # without, and a symbolic axis does not carry it.
259
+ reason = self.assertRefuses(
260
+ PROBE._graph_class_axis, labels_mod, labels_mod.ANCHOR_4_PLUS_NC,
261
+ [Out("output0", [1, "channels", 8400])],
262
+ )["reason"]
263
+ self.assertIn("dynamic", reason)
264
+
265
+ def test_anchor_refuses_box_channels_with_no_class_left(self):
266
+ reason = self.assertRefuses(
267
+ PROBE._graph_class_axis, labels_mod, labels_mod.ANCHOR_4_PLUS_NC, [Out("output0", [1, 4, 8400])],
268
+ )["reason"]
269
+ self.assertIn("at least one class", reason)
270
+
271
+ def test_a_dynamic_anchor_count_does_not_block_a_real_head(self):
272
+ # `anchors > channels` is skipped rather than guessed when the axis is
273
+ # symbolic. The tensor ORT actually returns is still checked by
274
+ # `decode_anchor_4_plus_nc`'s own `expected_classes` refusal.
275
+ self.assertEqual(
276
+ PROBE._graph_class_axis(
277
+ labels_mod, labels_mod.ANCHOR_4_PLUS_NC, [Out("output0", [1, 18, "anchors"])],
278
+ ),
279
+ 14,
280
+ )
281
+
282
+
283
+ @unittest.skipIf(PROBE is None, NO_PROBE)
284
+ class TestFourPlusNcCountsLogitSlots(ProbeCase):
285
+ """`4 + nc` where nc is the LOGIT WIDTH — never the number of distinct names.
286
+
287
+ This is the case that would silently invert the check. Both directions are
288
+ live on real artifacts:
289
+
290
+ * COCO-91 names 80 classes on a 91-wide axis (gaps in the category ids);
291
+ * `gh_fall_tonlong` declares SEVEN slots of which `Violence` and `violence`
292
+ fold to one canonical name, so it has 7 logit slots and 6 names.
293
+
294
+ Its graph is `[1, 11, 8400]`. `4 + 7` is 11 and matches; `4 + 6` is 10 and
295
+ would reject a perfectly healthy head.
296
+
297
+ `_verify_class_axis_width` passes by construction on a real run — the width it
298
+ is handed was read off the same graph — so these cases are what EXERCISES it.
299
+ That is the point of it: it is a tripwire on the derivation, and a tripwire
300
+ nothing pulls is indistinguishable from one that is wired wrong.
301
+ """
302
+
303
+ def test_the_fall_artifact_has_seven_slots_and_six_names(self):
304
+ slot_map = PROBE._canonical_slot_map(labels_mod, FALL_NAMES)
305
+ self.assertEqual(len(slot_map), 7)
306
+ self.assertEqual(len(set(slot_map.values())), 6)
307
+ self.assertEqual(slot_map[0], slot_map[6])
308
+
309
+ def test_the_logit_width_matches_the_real_graph(self):
310
+ # No refusal: this is the healthy case, and it must stay healthy.
311
+ PROBE._verify_class_axis_width(
312
+ labels_mod, labels_mod.ANCHOR_4_PLUS_NC, ANCHOR_FALL, 7,
313
+ )
314
+
315
+ def test_the_distinct_name_count_would_reject_the_same_graph(self):
316
+ reason = self.assertRefuses(
317
+ PROBE._verify_class_axis_width,
318
+ labels_mod, labels_mod.ANCHOR_4_PLUS_NC, ANCHOR_FALL, 6,
319
+ )["reason"]
320
+ self.assertIn("11 wide", reason)
321
+ self.assertIn("needs 10", reason)
322
+
323
+ def test_a_committed_vocabulary_narrower_than_the_graph_is_refused(self):
324
+ reason = self.assertRefuses(
325
+ PROBE._verify_class_axis_width,
326
+ labels_mod, labels_mod.ANCHOR_4_PLUS_NC, ANCHOR_14, 13,
327
+ )["reason"]
328
+ self.assertIn("18 wide", reason)
329
+ self.assertIn("needs 17", reason)
330
+
331
+ def test_rfdetr_width_is_checked_against_the_labels_axis(self):
332
+ PROBE._verify_class_axis_width(
333
+ labels_mod, labels_mod.RFDETR_DETS_LABELS, DETS_LABELS_91, 91,
334
+ )
335
+ reason = self.assertRefuses(
336
+ PROBE._verify_class_axis_width,
337
+ labels_mod, labels_mod.RFDETR_DETS_LABELS, DETS_LABELS_91, 80,
338
+ )["reason"]
339
+ # COCO-91 names EIGHTY classes on a NINETY-ONE-wide axis, so the name
340
+ # count is the wrong number here too — in the other direction.
341
+ self.assertIn("91 wide", reason)
342
+
343
+ def test_end2end_has_no_width_to_check(self):
344
+ # Not an omission: the class index is a column, so any width is
345
+ # consistent with the graph and a check would be theatre.
346
+ PROBE._verify_class_axis_width(labels_mod, labels_mod.END2END_N_6, END2END, 3)
347
+ PROBE._verify_class_axis_width(labels_mod, labels_mod.END2END_N_6, END2END, 91)
348
+
349
+ def test_a_vocabulary_with_two_spellings_of_one_word_is_unpublishable(self):
350
+ # The other half of the same artifact: the SHAPE is fine, the LIST is
351
+ # not, and `ModelVocabulary` refuses it at construction on a device too.
352
+ with self.assertRaises(ValueError) as raised:
353
+ PROBE._build_vocabulary(
354
+ labels_mod,
355
+ "fall_specialist",
356
+ PROBE._canonical_slot_map(labels_mod, FALL_NAMES),
357
+ 7,
358
+ labels_mod.VariantContract(
359
+ labels_mod.ANCHOR_4_PLUS_NC, labels_mod.BOX_UNITS_PIXELS, 1.0,
360
+ ),
361
+ )
362
+ self.assertIn("two slots share one class name", str(raised.exception))
363
+
364
+
365
+ @unittest.skipIf(PROBE is None, NO_PROBE)
366
+ class TestBoxUnitsAreRecordedNeverInvented(ProbeCase):
367
+ """A coordinate space is declared or the run is refused. It is never guessed.
368
+
369
+ `session.py` multiplies by `box_scale_to_pixels` and draws, without ever
370
+ re-deriving from the tensor, so a defaulted value is a 640x error that raises
371
+ nothing on an unpadded frame. RF-DETR is the one exemption and it is
372
+ structural: its branch calls `postprocess`, which does its own
373
+ normalised-to-frame scaling and never reads the contract.
374
+ """
375
+
376
+ def test_a_non_rfdetr_layout_without_box_units_is_refused(self):
377
+ reason = self.assertRefuses(
378
+ PROBE._resolve_variant, labels_mod, self.args("--output-layout", labels_mod.ANCHOR_4_PLUS_NC),
379
+ )["reason"]
380
+ self.assertIn("--box-units", reason)
381
+ self.assertIn("640x too small", reason)
382
+
383
+ def test_end2end_without_box_units_is_refused_too(self):
384
+ self.assertRefuses(
385
+ PROBE._resolve_variant, labels_mod, self.args("--output-layout", labels_mod.END2END_N_6),
386
+ )
387
+
388
+ def test_normalized_without_a_scale_is_refused(self):
389
+ # 1.0 IS positive, so `VariantContract` would accept the default and the
390
+ # boxes would be silently wrong. The refusal has to happen before it.
391
+ reason = self.assertRefuses(
392
+ PROBE._resolve_variant, labels_mod,
393
+ self.args("--output-layout", labels_mod.ANCHOR_4_PLUS_NC, "--box-units", "normalized"),
394
+ )["reason"]
395
+ self.assertIn("--box-scale-to-pixels", reason)
396
+
397
+ def test_normalized_with_a_scale_is_carried_through(self):
398
+ contract, source = PROBE._resolve_variant(labels_mod, self.args(
399
+ "--output-layout", labels_mod.ANCHOR_4_PLUS_NC,
400
+ "--box-units", "normalized", "--box-scale-to-pixels", "640",
401
+ ))
402
+ self.assertEqual(contract.box_units, labels_mod.BOX_UNITS_NORMALIZED)
403
+ self.assertEqual(contract.box_scale_to_pixels, 640.0)
404
+ self.assertIn("--output-layout", source)
405
+
406
+ def test_pixels_with_a_scale_that_is_not_one_is_refused(self):
407
+ reason = self.assertRefuses(
408
+ PROBE._resolve_variant, labels_mod,
409
+ self.args("--output-layout", labels_mod.ANCHOR_4_PLUS_NC,
410
+ "--box-units", "pixels", "--box-scale-to-pixels", "640"),
411
+ )["reason"]
412
+ self.assertIn("already pixels", reason)
413
+
414
+ def test_a_scale_without_units_is_refused_rather_than_overwritten(self):
415
+ # RF-DETR is the one layout whose units may be omitted, so it is also the
416
+ # one place a declared scale could be silently replaced by the 1.0 that
417
+ # fallback supplies. A declaration accepted and discarded is worse than a
418
+ # refusal: nothing downstream can tell it happened.
419
+ reason = self.assertRefuses(
420
+ PROBE._resolve_variant, labels_mod,
421
+ self.args("--output-layout", labels_mod.RFDETR_DETS_LABELS, "--box-scale-to-pixels", "640"),
422
+ )["reason"]
423
+ self.assertIn("no box_units", reason)
424
+ self.assertIn("640", reason)
425
+
426
+ def test_units_without_a_layout_are_a_claim_about_nothing(self):
427
+ reason = self.assertRefuses(
428
+ PROBE._resolve_variant, labels_mod, self.args("--box-units", "pixels"),
429
+ )["reason"]
430
+ self.assertIn("--output-layout", reason)
431
+
432
+ def test_an_unknown_layout_is_refused_by_name(self):
433
+ reason = self.assertRefuses(
434
+ PROBE._resolve_variant, labels_mod, self.args("--output-layout", "yolo_v8_raw"),
435
+ )["reason"]
436
+ self.assertIn("yolo_v8_raw", reason)
437
+
438
+ def test_an_unknown_box_unit_is_refused_by_name(self):
439
+ reason = self.assertRefuses(
440
+ PROBE._resolve_variant, labels_mod,
441
+ self.args("--output-layout", labels_mod.ANCHOR_4_PLUS_NC, "--box-units", "pixel"),
442
+ )["reason"]
443
+ self.assertIn("'pixel'", reason)
444
+
445
+
446
+ @unittest.skipIf(PROBE is None, NO_PROBE)
447
+ class TestVariantFromCard(ProbeCase):
448
+ """The card is the primary path: a MEASUREMENT with provenance, not a claim."""
449
+
450
+ def _card(self, variant, **extra):
451
+ handle = tempfile.NamedTemporaryFile("w", suffix=".json", delete=False)
452
+ blob = {"schema": "detector-score-card/1", "verdict": {"publishable": True}}
453
+ blob.update(extra)
454
+ if variant is not None:
455
+ blob["variant"] = variant
456
+ json.dump(blob, handle)
457
+ handle.close()
458
+ self.addCleanup(lambda: pathlib.Path(handle.name).unlink(missing_ok=True))
459
+ return handle.name
460
+
461
+ #: The `variant` block `score-detector-candidate.js` wrote for every one of
462
+ #: the five staged specialists, verbatim in shape.
463
+ MEASURED = {
464
+ "format": "onnx",
465
+ "output_layout": "anchor_4_plus_nc",
466
+ "end2end": False,
467
+ "box_units": "pixels",
468
+ "box_scale_to_pixels": 1,
469
+ "box_units_evidence": "max|box channel| = 640.0000 over 110 real frames",
470
+ }
471
+
472
+ def test_the_three_values_are_read_off_the_card(self):
473
+ contract, source = PROBE._resolve_variant(
474
+ labels_mod, self.args("--variant-from-card", self._card(self.MEASURED)),
475
+ )
476
+ self.assertEqual(contract.output_layout, labels_mod.ANCHOR_4_PLUS_NC)
477
+ self.assertEqual(contract.box_units, labels_mod.BOX_UNITS_PIXELS)
478
+ # A JSON `1` is a scale of 1.0. `VariantContract` compares it against
479
+ # 1.0 exactly, so the coercion is load-bearing rather than cosmetic.
480
+ self.assertEqual(contract.box_scale_to_pixels, 1.0)
481
+ self.assertIn("--variant-from-card", source)
482
+
483
+ def test_a_card_with_no_variant_block_is_refused(self):
484
+ reason = self.assertRefuses(
485
+ PROBE._resolve_variant, labels_mod, self.args("--variant-from-card", self._card(None)),
486
+ )["reason"]
487
+ self.assertIn("no `variant` block", reason)
488
+
489
+ def test_a_partial_variant_block_is_refused(self):
490
+ partial = {k: v for k, v in self.MEASURED.items() if k != "box_units"}
491
+ reason = self.assertRefuses(
492
+ PROBE._resolve_variant, labels_mod, self.args("--variant-from-card", self._card(partial)),
493
+ )["reason"]
494
+ self.assertIn("variant.box_units", reason)
495
+
496
+ def test_unreadable_json_is_refused_rather_than_raised(self):
497
+ handle = tempfile.NamedTemporaryFile("w", suffix=".json", delete=False)
498
+ handle.write("{not json")
499
+ handle.close()
500
+ self.addCleanup(lambda: pathlib.Path(handle.name).unlink(missing_ok=True))
501
+ self.assertRefuses(
502
+ PROBE._resolve_variant, labels_mod, self.args("--variant-from-card", handle.name),
503
+ )
504
+
505
+ def test_a_card_and_a_flag_that_AGREE_are_fine(self):
506
+ contract, source = PROBE._resolve_variant(labels_mod, self.args(
507
+ "--variant-from-card", self._card(self.MEASURED),
508
+ "--output-layout", labels_mod.ANCHOR_4_PLUS_NC,
509
+ ))
510
+ self.assertEqual(contract.output_layout, labels_mod.ANCHOR_4_PLUS_NC)
511
+ # A flag that states ONE field must not read as disagreeing with a card
512
+ # that states three — the agreement is checked per field, not per tuple.
513
+ self.assertIn("--variant-from-card", source)
514
+
515
+ def test_a_card_and_a_flag_that_DISAGREE_are_refused(self):
516
+ reason = self.assertRefuses(PROBE._resolve_variant, labels_mod, self.args(
517
+ "--variant-from-card", self._card(self.MEASURED),
518
+ "--output-layout", labels_mod.END2END_N_6,
519
+ ))["reason"]
520
+ self.assertIn("DISAGREES", reason)
521
+ self.assertIn(labels_mod.ANCHOR_4_PLUS_NC, reason)
522
+ self.assertIn(labels_mod.END2END_N_6, reason)
523
+
524
+ def test_a_disagreeing_scale_is_refused_too(self):
525
+ normalized = dict(self.MEASURED, box_units="normalized", box_scale_to_pixels=640)
526
+ reason = self.assertRefuses(PROBE._resolve_variant, labels_mod, self.args(
527
+ "--variant-from-card", self._card(normalized),
528
+ "--output-layout", labels_mod.ANCHOR_4_PLUS_NC,
529
+ "--box-units", "normalized", "--box-scale-to-pixels", "320",
530
+ ))["reason"]
531
+ self.assertIn("box_scale_to_pixels", reason)
532
+
533
+
534
+ @unittest.skipIf(PROBE is None, NO_PROBE)
535
+ class TestClassAxisWidthWhenTheGraphDeclaresNone(ProbeCase):
536
+ """`end2end_n_6` has no class axis, so the width comes from the class LIST."""
537
+
538
+ def _classes_file(self, blob):
539
+ handle = tempfile.NamedTemporaryFile("w", suffix=".json", delete=False)
540
+ json.dump(blob, handle)
541
+ handle.close()
542
+ self.addCleanup(lambda: pathlib.Path(handle.name).unlink(missing_ok=True))
543
+ return handle.name
544
+
545
+ def test_a_dense_list_sets_the_width_to_one_past_its_last_slot(self):
546
+ args = self.args("--classes-file", self._classes_file(["forklift", "pallet", "person"]))
547
+ slot_names, source, width = PROBE._resolve_slot_names(
548
+ labels_mod, args, None, "(no graph metadata)", None,
549
+ )
550
+ self.assertEqual(slot_names, {0: "forklift", 1: "pallet", 2: "person"})
551
+ self.assertIn("--classes-file", source)
552
+ self.assertEqual(width, 3)
553
+
554
+ def test_a_sparse_map_sets_the_width_from_its_highest_slot(self):
555
+ args = self.args("--classes-file", self._classes_file({"0": "forklift", "9": "person"}))
556
+ _, _, width = PROBE._resolve_slot_names(labels_mod, args, None, "(no graph metadata)", None)
557
+ self.assertEqual(width, 10)
558
+
559
+ def test_a_graph_declaring_fewer_names_than_its_axis_is_still_ambiguous(self):
560
+ # The check that DOES depend on a width, unchanged: it fires whenever the
561
+ # graph declares one, and is skipped only when the layout carries none.
562
+ args = self.args()
563
+ reason = self.assertRefuses(
564
+ PROBE._resolve_slot_names, labels_mod, args, ["person", "car"], "graph metadata key 'names'", 91,
565
+ )["reason"]
566
+ self.assertIn("--slot-offset", reason)
567
+
568
+ def test_the_same_list_is_unambiguous_when_the_graph_declares_no_axis(self):
569
+ args = self.args()
570
+ slot_names, _, width = PROBE._resolve_slot_names(
571
+ labels_mod, args, ["person", "car"], "graph metadata key 'names'", None,
572
+ )
573
+ self.assertEqual(slot_names, {0: "person", 1: "car"})
574
+ self.assertEqual(width, 2)
575
+
576
+
577
+ @unittest.skipIf(PROBE is None, NO_PROBE)
578
+ class TestTheCliSurface(ProbeCase):
579
+ """The flags this script offers, asserted so a rename cannot be silent.
580
+
581
+ `publish-sensing-model.js` builds `--variant-from-card` by hand into an
582
+ `argv` array and reads the probe's stdout back. Nothing type-checks that
583
+ join, so a flag renamed on one side surfaces as "the probe failed" with an
584
+ argparse usage string attached — at publish time, on a real artifact.
585
+ """
586
+
587
+ EXPECTED = {
588
+ "-h", "--help", "--mode", "--measure-payload", "--artifact", "--model-id",
589
+ "--classes-file", "--slot-offset",
590
+ "--variant-from-card", "--output-layout", "--box-units", "--box-scale-to-pixels",
591
+ }
592
+
593
+ def test_the_option_strings_are_exactly_these(self):
594
+ offered = set()
595
+ for action in PROBE.build_parser()._actions:
596
+ offered.update(action.option_strings)
597
+ self.assertEqual(offered, self.EXPECTED)
598
+
599
+ def test_the_declaration_flags_all_default_to_none(self):
600
+ # NOT to the RF-DETR values. `_resolve_variant` has to be able to tell
601
+ # "nobody declared a layout" from "somebody declared this one", because
602
+ # only the first may fall back to a default.
603
+ args = self.args()
604
+ self.assertIsNone(args.variant_from_card)
605
+ self.assertIsNone(args.output_layout)
606
+ self.assertIsNone(args.box_units)
607
+ self.assertIsNone(args.box_scale_to_pixels)
608
+
609
+ def test_slot_offset_still_records_whether_it_was_typed(self):
610
+ self.assertFalse(self.args().slot_offset_given)
611
+ self.assertEqual(self.args().slot_offset, 0)
612
+ typed = self.args("--slot-offset", "0")
613
+ self.assertTrue(typed.slot_offset_given)
614
+ self.assertEqual(typed.slot_offset, 0)
615
+
616
+
617
+ if __name__ == "__main__": # pragma: no cover
618
+ unittest.main()