chipwright 0.2.0__py3-none-any.whl

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.
Files changed (49) hide show
  1. chipwright/__init__.py +11 -0
  2. chipwright/adapt.py +57 -0
  3. chipwright/build.py +329 -0
  4. chipwright/cache.py +65 -0
  5. chipwright/cli.py +242 -0
  6. chipwright/config.py +106 -0
  7. chipwright/data/behavior_prompts.yaml +108 -0
  8. chipwright/data/index.yaml +327 -0
  9. chipwright/device.py +143 -0
  10. chipwright/device_adb.py +121 -0
  11. chipwright/env.py +229 -0
  12. chipwright/errors.py +41 -0
  13. chipwright/llm_reference.py +114 -0
  14. chipwright/modalities/__init__.py +3 -0
  15. chipwright/modalities/asr_encoder.py +90 -0
  16. chipwright/modalities/cv.py +34 -0
  17. chipwright/modalities/llm.py +300 -0
  18. chipwright/recipe.py +205 -0
  19. chipwright/registry.py +65 -0
  20. chipwright/resolver.py +197 -0
  21. chipwright/run.py +173 -0
  22. chipwright/sdk.py +140 -0
  23. chipwright/tags.py +56 -0
  24. chipwright/task_eval.py +190 -0
  25. chipwright/tests/conftest.py +12 -0
  26. chipwright/tests/test_adapt.py +61 -0
  27. chipwright/tests/test_build.py +279 -0
  28. chipwright/tests/test_cache.py +147 -0
  29. chipwright/tests/test_device_adb.py +140 -0
  30. chipwright/tests/test_env.py +73 -0
  31. chipwright/tests/test_env_adb.py +120 -0
  32. chipwright/tests/test_llm_modality.py +259 -0
  33. chipwright/tests/test_packaging.py +147 -0
  34. chipwright/tests/test_precision_support.py +128 -0
  35. chipwright/tests/test_registry.py +107 -0
  36. chipwright/tests/test_resolver.py +93 -0
  37. chipwright/tests/test_run_llm.py +47 -0
  38. chipwright/tests/test_tags.py +95 -0
  39. chipwright/tests/test_task_eval.py +164 -0
  40. chipwright/tests/test_verify.py +92 -0
  41. chipwright/tests/test_verify_llm.py +244 -0
  42. chipwright/verify.py +24 -0
  43. chipwright/verify_llm.py +203 -0
  44. chipwright-0.2.0.dist-info/METADATA +268 -0
  45. chipwright-0.2.0.dist-info/RECORD +49 -0
  46. chipwright-0.2.0.dist-info/WHEEL +5 -0
  47. chipwright-0.2.0.dist-info/entry_points.txt +2 -0
  48. chipwright-0.2.0.dist-info/licenses/LICENSE +21 -0
  49. chipwright-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,93 @@
1
+ """The resolver is a pure function — the four outcomes are testable without a board."""
2
+ import os
3
+ import sys
4
+
5
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
6
+
7
+ from chipwright.env import TargetKey
8
+ from chipwright.registry import Registry
9
+ from chipwright.resolver import Outcome, Variant, resolve
10
+ from chipwright.tags import ArtifactTag
11
+
12
+
13
+ def test_tag_roundtrip():
14
+ s = "whisper-small-en-1.2.0+qnn2.37-htpv68-w8a16-win30s.qnnctx"
15
+ t = ArtifactTag.parse(s)
16
+ assert (t.name, t.version, t.sdk, t.arch, t.quant, t.shape, t.ext) == \
17
+ ("whisper-small-en", "1.2.0", "2.37", "v68", "w8a16", "win30s", "qnnctx")
18
+ assert str(t) == s
19
+ assert t.is_context_binary
20
+
21
+
22
+ def test_tag_rejects_garbage():
23
+ for bad in ["whisper.qnnctx", "x-1.0+htpv68.qnnctx", "x-1.0.0+qnn2.37-htpv68-w8a16.qnnctx"]:
24
+ try:
25
+ ArtifactTag.parse(bad); assert False, bad
26
+ except ValueError:
27
+ pass
28
+
29
+
30
+ def _v(**kw):
31
+ base = dict(model="m", version="1.0.0", arch="v68", quant="w8a16", shape="win30s",
32
+ sdk_tested="2.37.1", sdk_compatible=">=2.34,<2.40")
33
+ base.update(kw)
34
+ return Variant(**base)
35
+
36
+
37
+ def test_use_exact():
38
+ t = TargetKey(htp_arch="v68", qairt="2.37.1")
39
+ d = resolve(t, [_v(url="u", sha256="s")])
40
+ assert d.outcome is Outcome.USE and d.ok
41
+
42
+
43
+ def test_use_with_warn_sdk_in_range():
44
+ t = TargetKey(htp_arch="v68", qairt="2.38.0") # in range, not the tested point
45
+ d = resolve(t, [_v(url="u", sha256="s")])
46
+ assert d.outcome is Outcome.USE_WITH_WARN and d.ok and d.warning
47
+
48
+
49
+ def test_offline_unknown_sdk_warns():
50
+ t = TargetKey(htp_arch="v68", qairt=None)
51
+ d = resolve(t, [_v(url="u", sha256="s")])
52
+ assert d.outcome is Outcome.USE_WITH_WARN
53
+
54
+
55
+ def test_build_recipe_only():
56
+ t = TargetKey(htp_arch="v68", qairt="2.37.1")
57
+ d = resolve(t, [_v(recipe="r.yaml")]) # no url
58
+ assert d.outcome is Outcome.BUILD and not d.ok
59
+
60
+
61
+ def test_fail_arch_names_axis():
62
+ t = TargetKey(htp_arch="v68", qairt="2.37.1")
63
+ d = resolve(t, [_v(arch="v73", url="u", sha256="s")])
64
+ assert d.outcome is Outcome.FAIL and d.axis == "htp_arch"
65
+
66
+
67
+ def test_fail_sdk_out_of_range_no_recipe():
68
+ t = TargetKey(htp_arch="v68", qairt="2.50.0") # outside the range, artifact but no recipe
69
+ d = resolve(t, [_v(url="u", sha256="s")])
70
+ assert d.outcome is Outcome.FAIL and d.axis == "qairt"
71
+
72
+
73
+ def test_sdk_out_of_range_but_recipe_builds():
74
+ t = TargetKey(htp_arch="v68", qairt="2.50.0")
75
+ d = resolve(t, [_v(url="u", sha256="s"), _v(recipe="r.yaml")])
76
+ assert d.outcome is Outcome.BUILD
77
+
78
+
79
+ def test_seed_registry_loads():
80
+ reg = Registry()
81
+ assert "laya-decision" in reg.models() and "whisper-small-en" in reg.models()
82
+ v68 = TargetKey(htp_arch="v68", qairt="2.37.1")
83
+ assert resolve(v68, reg.variants("laya-decision")).ok # USE (exact or SDK-warn)
84
+ assert resolve(v68, reg.variants("whisper-small-en")).outcome is Outcome.BUILD
85
+ assert resolve(v68, reg.variants("qwen2.5-1.5b-instruct")).outcome is Outcome.BUILD # v68 variant exists
86
+ assert resolve(v68, reg.variants("qwen2.5-7b-instruct")).axis == "htp_arch" # v73/v75 only
87
+
88
+
89
+ if __name__ == "__main__":
90
+ fns = [f for n, f in sorted(globals().items()) if n.startswith("test_")]
91
+ for f in fns:
92
+ f(); print("ok", f.__name__)
93
+ print(f"\n{len(fns)} passed")
@@ -0,0 +1,47 @@
1
+ """The LLM run path refuses by name when it can't gate — integration guards for run_llm_variant.
2
+
3
+ Mirrors the CV/ASR refusal-path tests: a pass is never reported without the gate, and the LLM path
4
+ refuses with a message naming the fix (no Genie bundle / no pinned reference), not a raw traceback.
5
+ No board needed — these never reach adb.
6
+ """
7
+ import os
8
+ import sys
9
+
10
+ import pytest
11
+
12
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
13
+
14
+ from chipwright import run as runmod
15
+ from chipwright.resolver import Variant
16
+
17
+
18
+ def _llm_variant(**kw):
19
+ base = dict(model="qwen-x", version="1", arch="v81", quant="w4a16", shape="ctx4096",
20
+ sdk_tested="2.51", sdk_compatible=">=2.34,<2.60")
21
+ base.update(kw)
22
+ return Variant(**base)
23
+
24
+
25
+ def test_llm_run_refuses_when_no_bundle():
26
+ """An LLM variant with no Genie bundle directory refuses by name (build / CW_REGISTRY)."""
27
+ v = _llm_variant(run={"modality": "llm"}) # no local bundle dir
28
+ with pytest.raises(RuntimeError, match="bundle|chipwright build|CW_REGISTRY"):
29
+ runmod.run_variant(v)
30
+
31
+
32
+ def test_llm_run_refuses_when_no_pinned_reference(tmp_path):
33
+ """A bundle present but no pinned reference → refuse: the generative gate can't run."""
34
+ bundle = tmp_path / "bundle"
35
+ bundle.mkdir()
36
+ (bundle / "genie_config.json").write_text("{}")
37
+ v = _llm_variant(local=str(bundle), run={"modality": "llm"}) # bundle, but no `reference`
38
+ with pytest.raises(RuntimeError, match="pinned reference|gate can't run"):
39
+ runmod.run_variant(v)
40
+
41
+
42
+ def test_looks_refused_classifier():
43
+ """The v1 behavioral classifier tells a refusal from a compliant answer."""
44
+ assert runmod._looks_refused("I can't help with that.") is True
45
+ assert runmod._looks_refused("I'm sorry, but I cannot assist.") is True
46
+ assert runmod._looks_refused("The capital of France is Paris.") is False
47
+ assert runmod._looks_refused("") is False
@@ -0,0 +1,95 @@
1
+ """ArtifactTag — the wheel-tag grammar. Strict parse, round-trip, context-binary classification.
2
+
3
+ Pure string logic, no board. Also runnable as `python3 test_tags.py`.
4
+ """
5
+ import os
6
+ import sys
7
+
8
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
9
+
10
+ from chipwright.tags import ArtifactTag
11
+
12
+
13
+ def test_parse_roundtrips_valid_tag():
14
+ s = "whisper-small-en-1.2.0+qnn2.37-htpv68-w8a16-win30s.qnnctx"
15
+ t = ArtifactTag.parse(s)
16
+ assert t.name == "whisper-small-en"
17
+ assert t.version == "1.2.0"
18
+ assert t.sdk == "2.37"
19
+ assert t.arch == "v68"
20
+ assert t.quant == "w8a16"
21
+ assert t.shape == "win30s"
22
+ assert t.ext == "qnnctx"
23
+ # str() is the exact inverse of parse()
24
+ assert str(t) == s
25
+
26
+
27
+ def test_parse_handles_hyphenated_name_and_fp_quant():
28
+ # name may contain hyphens (greedy up to the last -<semver>+); quant may be fp16/fp32.
29
+ s = "laya-decision-tiny-10.20.30+qnn2.34-htpv73-fp16-ctx4096.dlc"
30
+ t = ArtifactTag.parse(s)
31
+ assert t.name == "laya-decision-tiny"
32
+ assert t.version == "10.20.30"
33
+ assert t.arch == "v73"
34
+ assert t.quant == "fp16"
35
+ assert t.ext == "dlc"
36
+ assert str(t) == s
37
+
38
+
39
+ def test_parse_strips_surrounding_whitespace():
40
+ s = "m-1.0.0+qnn2.37-htpv68-w8a16-win30s.bin"
41
+ t = ArtifactTag.parse(" " + s + "\n")
42
+ assert str(t) == s
43
+
44
+
45
+ def test_rejects_missing_version():
46
+ for bad in [
47
+ "whisper.qnnctx", # no version at all
48
+ "whisper-small+qnn2.37-htpv68-w8a16-win30s.qnnctx", # name but no semver
49
+ "whisper-1.0+qnn2.37-htpv68-w8a16-win30s.qnnctx", # two-part version, not semver
50
+ ]:
51
+ _assert_rejected(bad)
52
+
53
+
54
+ def test_rejects_missing_arch():
55
+ for bad in [
56
+ "whisper-1.0.0+qnn2.37-w8a16-win30s.qnnctx", # no htp arch segment
57
+ "whisper-1.0.0+qnn2.37-htp68-w8a16-win30s.qnnctx", # arch missing the 'v'
58
+ "whisper-1.0.0+htpv68-w8a16-win30s.qnnctx", # no +qnn sdk tag
59
+ ]:
60
+ _assert_rejected(bad)
61
+
62
+
63
+ def test_rejects_bad_extension():
64
+ for bad in [
65
+ "whisper-1.0.0+qnn2.37-htpv68-w8a16-win30s.onnx", # not an allowed ext
66
+ "whisper-1.0.0+qnn2.37-htpv68-w8a16-win30s", # no ext
67
+ "whisper-1.0.0+qnn2.37-htpv68-w8a16-win30s.qnnctx.bak",
68
+ ]:
69
+ _assert_rejected(bad)
70
+
71
+
72
+ def test_is_context_binary_true_for_qnnctx_and_bin():
73
+ for ext in ("qnnctx", "bin"):
74
+ s = f"m-1.0.0+qnn2.37-htpv68-w8a16-win30s.{ext}"
75
+ assert ArtifactTag.parse(s).is_context_binary, ext
76
+
77
+
78
+ def test_is_context_binary_false_for_dlc():
79
+ s = "m-1.0.0+qnn2.37-htpv68-w8a16-win30s.dlc"
80
+ assert ArtifactTag.parse(s).is_context_binary is False
81
+
82
+
83
+ def _assert_rejected(bad: str):
84
+ try:
85
+ ArtifactTag.parse(bad)
86
+ except ValueError:
87
+ return
88
+ raise AssertionError(f"expected ValueError for {bad!r}")
89
+
90
+
91
+ if __name__ == "__main__":
92
+ fns = [f for n, f in sorted(globals().items()) if n.startswith("test_")]
93
+ for f in fns:
94
+ f(); print("ok", f.__name__)
95
+ print(f"\n{len(fns)} passed")
@@ -0,0 +1,164 @@
1
+ """Layer-2 task eval + behavioral-probe data — record-assembly, the lazy-import guard, the prompt sets.
2
+
3
+ Pure / no heavy deps: the record-assembly is exercised with a FAKE scorer (no qai-hub, no model, no
4
+ dataset), and the lazy-import-missing path is forced deterministically regardless of whether
5
+ `qai-hub-models` happens to be installed. Runnable as `python3 test_task_eval.py`.
6
+ """
7
+ import builtins
8
+ import os
9
+ import sys
10
+
11
+ import pytest
12
+ import yaml
13
+
14
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
15
+
16
+ import chipwright
17
+ from chipwright import task_eval
18
+ from chipwright.task_eval import (
19
+ TaskSpec, TASKS, resolve_task, task_record, run_task_eval, _qai_hub_scorer, _require_qai_hub,
20
+ )
21
+
22
+
23
+ # --- pure record-assembly (task_record) -----------------------------------------------------------
24
+
25
+ def test_task_record_shape_and_types():
26
+ rec = task_record("wikitext_perplexity", 10.58, "wikitext", "test", 256)
27
+ assert rec == {
28
+ "metric": "wikitext_perplexity", "value": 10.58, "dataset": "wikitext",
29
+ "split": "test", "samples": 256,
30
+ }
31
+ assert isinstance(rec["value"], float) and isinstance(rec["samples"], int)
32
+
33
+
34
+ def test_task_record_coerces_value_and_samples():
35
+ rec = task_record("mmlu_accuracy", 1, "mmlu", "test", "256") # int value, str samples
36
+ assert rec["value"] == 1.0 and isinstance(rec["value"], float)
37
+ assert rec["samples"] == 256 and isinstance(rec["samples"], int)
38
+
39
+
40
+ # --- resolve_task ---------------------------------------------------------------------------------
41
+
42
+ def test_resolve_task_known_names():
43
+ assert resolve_task("wikitext").metric == "wikitext_perplexity"
44
+ assert resolve_task("mmlu").dataset == "mmlu"
45
+ assert resolve_task("tiny_mmlu").evaluator == "mmlu"
46
+
47
+
48
+ def test_resolve_task_passthrough_spec():
49
+ spec = TaskSpec("x", "m", "d", "perplexity")
50
+ assert resolve_task(spec) is spec
51
+
52
+
53
+ def test_resolve_task_unknown_refuses_by_name():
54
+ with pytest.raises(ValueError, match="unknown task"):
55
+ resolve_task("not_a_dataset")
56
+
57
+
58
+ # --- run_task_eval with an injected FAKE scorer (no heavy dep, no model, no dataset) ---------------
59
+
60
+ def test_run_task_eval_with_fake_scorer_assembles_records():
61
+ calls = []
62
+
63
+ def fake_scorer(spec, split, samples):
64
+ calls.append((resolve_task(spec).name, split, samples))
65
+ return {"wikitext_perplexity": 10.58, "mmlu_accuracy": 0.554}[resolve_task(spec).metric]
66
+
67
+ out = run_task_eval(tasks=("wikitext", "mmlu"), samples=256, split="test", scorer=fake_scorer)
68
+ assert out == [
69
+ {"metric": "wikitext_perplexity", "value": 10.58, "dataset": "wikitext",
70
+ "split": "test", "samples": 256},
71
+ {"metric": "mmlu_accuracy", "value": 0.554, "dataset": "mmlu",
72
+ "split": "test", "samples": 256},
73
+ ]
74
+ # the scorer saw each task with the requested split/samples (provenance threads through)
75
+ assert calls == [("wikitext", "test", 256), ("mmlu", "test", 256)]
76
+
77
+
78
+ def test_run_task_eval_fake_scorer_needs_no_generator_or_dep():
79
+ # the injectable seam is exactly what makes this testable with no qai-hub and no model present
80
+ out = run_task_eval(tasks=("tiny_mmlu",), samples=100, scorer=lambda s, sp, n: 0.61)
81
+ assert out == [{"metric": "mmlu_accuracy", "value": 0.61, "dataset": "tiny_mmlu",
82
+ "split": "test", "samples": 100}]
83
+
84
+
85
+ # --- the lazy-import guard: raises BY NAME when qai-hub-models is absent ---------------------------
86
+
87
+ def test_require_qai_hub_raises_by_name_when_absent(monkeypatch):
88
+ # force the import to fail regardless of whether qai-hub-models is installed in this env
89
+ real_import = builtins.__import__
90
+
91
+ def fake_import(name, *a, **k):
92
+ if name.startswith("qai_hub_models"):
93
+ raise ImportError("No module named 'qai_hub_models'")
94
+ return real_import(name, *a, **k)
95
+
96
+ monkeypatch.setattr(builtins, "__import__", fake_import)
97
+ with pytest.raises(RuntimeError, match=r"qai-hub-models|chipwright\[eval\]"):
98
+ _require_qai_hub()
99
+
100
+
101
+ def test_qai_hub_scorer_refuses_missing_generator_by_name(monkeypatch):
102
+ # with the dep "present" (stubbed), a missing generator is still refused by name before any download
103
+ monkeypatch.setattr(task_eval, "_require_qai_hub", lambda: {})
104
+ with pytest.raises(RuntimeError, match="generator"):
105
+ _qai_hub_scorer(None)
106
+
107
+
108
+ def test_qai_hub_scorer_refuses_missing_tokenizer_by_name(monkeypatch):
109
+ monkeypatch.setattr(task_eval, "_require_qai_hub", lambda: {})
110
+ with pytest.raises(RuntimeError, match="tokenizer"):
111
+ _qai_hub_scorer(object(), tokenizer=None)
112
+
113
+
114
+ def test_importing_task_eval_does_not_import_qai_hub():
115
+ # the heavy dep must NOT be pulled in at module import (preflight discipline)
116
+ assert "qai_hub_models" not in sys.modules
117
+
118
+
119
+ # --- the behavioral-probe data file ---------------------------------------------------------------
120
+
121
+ def _load_behavior_prompts():
122
+ path = os.path.join(os.path.dirname(chipwright.__file__), "data", "behavior_prompts.yaml")
123
+ with open(path) as f:
124
+ return yaml.safe_load(f)
125
+
126
+
127
+ def test_behavior_prompts_loads_with_both_sets_nonempty():
128
+ data = _load_behavior_prompts()
129
+ assert set(data) >= {"harmful", "benign"}
130
+ assert isinstance(data["harmful"], list) and len(data["harmful"]) > 0
131
+ assert isinstance(data["benign"], list) and len(data["benign"]) > 0
132
+
133
+
134
+ def test_behavior_prompts_are_surface_twinned_and_well_formed():
135
+ data = _load_behavior_prompts()
136
+ # aligned by index (every harmful item has a benign control at the same position)
137
+ assert len(data["harmful"]) == len(data["benign"]) == 24
138
+ for p in data["harmful"] + data["benign"]:
139
+ assert isinstance(p, str) and p.strip() # non-empty strings, no stray nulls
140
+ # the two sets are disjoint — a benign twin must not accidentally repeat a harmful prompt
141
+ assert set(data["harmful"]).isdisjoint(data["benign"])
142
+
143
+
144
+ if __name__ == "__main__":
145
+ fns = [f for n, f in sorted(globals().items()) if n.startswith("test_") and callable(f)]
146
+ import types
147
+
148
+ class _MP: # minimal monkeypatch shim for the __main__ runner
149
+ def __init__(self): self._undo = []
150
+ def setattr(self, obj, name, val):
151
+ old = getattr(obj, name); self._undo.append((obj, name, old)); setattr(obj, name, val)
152
+ def undo(self):
153
+ for obj, name, old in reversed(self._undo): setattr(obj, name, old)
154
+
155
+ passed = 0
156
+ for f in fns:
157
+ mp = _MP()
158
+ try:
159
+ n = f.__code__.co_argcount
160
+ f(mp) if n == 1 else f()
161
+ print("ok", f.__name__); passed += 1
162
+ finally:
163
+ mp.undo()
164
+ print(f"\n{passed} passed")
@@ -0,0 +1,92 @@
1
+ """Numerical verification — cosine similarity and the per-output pass/fail report.
2
+
3
+ Pure numpy, no board. Runnable as `python3 test_verify.py`.
4
+ """
5
+ import math
6
+ import os
7
+ import sys
8
+
9
+ import numpy as np
10
+
11
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
12
+
13
+ from chipwright.verify import cosine, report
14
+
15
+
16
+ def test_cosine_of_identical_vectors_is_one():
17
+ a = np.array([1.0, 2.0, 3.0, 4.0])
18
+ assert cosine(a, a) == 1.0
19
+
20
+
21
+ def test_cosine_of_orthogonal_vectors_is_zero():
22
+ a = np.array([1.0, 0.0])
23
+ b = np.array([0.0, 1.0])
24
+ assert abs(cosine(a, b)) < 1e-12
25
+
26
+
27
+ def test_cosine_of_opposite_vectors_is_minus_one():
28
+ a = np.array([1.0, 2.0, 3.0])
29
+ assert math.isclose(cosine(a, -a), -1.0, rel_tol=1e-12)
30
+
31
+
32
+ def test_cosine_is_shape_agnostic_via_ravel():
33
+ # a 2-D NPU output and its flattened reference compare equal
34
+ a = np.arange(6, dtype=np.float64).reshape(2, 3)
35
+ b = np.arange(6, dtype=np.float64)
36
+ assert math.isclose(cosine(a, b), 1.0, rel_tol=1e-12)
37
+
38
+
39
+ def test_cosine_handles_zero_vector_without_crashing():
40
+ a = np.zeros(4)
41
+ b = np.array([1.0, 2.0, 3.0, 4.0])
42
+ assert cosine(a, b) == 0.0
43
+ assert cosine(a, a) == 0.0
44
+
45
+
46
+ def test_report_passes_when_all_outputs_meet_threshold():
47
+ npu = {"logits": np.array([1.0, 2.0, 3.0]), "hidden": np.array([0.5, 0.5])}
48
+ cpu = {"logits": np.array([1.0, 2.0, 3.0]), "hidden": np.array([0.5, 0.5])}
49
+ r = report(npu, cpu, threshold=0.99)
50
+ assert r["pass"] is True
51
+ assert math.isclose(r["cosine_min"], 1.0, rel_tol=1e-12)
52
+ assert r["threshold"] == 0.99
53
+
54
+
55
+ def test_report_fails_when_an_output_is_below_threshold():
56
+ npu = {"good": np.array([1.0, 1.0]), "bad": np.array([1.0, 0.0])}
57
+ cpu = {"good": np.array([1.0, 1.0]), "bad": np.array([0.0, 1.0])} # orthogonal -> cosine 0
58
+ r = report(npu, cpu, threshold=0.99)
59
+ assert r["pass"] is False
60
+ assert math.isclose(r["cosine_min"], 0.0, abs_tol=1e-12)
61
+
62
+
63
+ def test_report_per_output_dict_is_keyed_by_output_name():
64
+ npu = {"a": np.array([1.0, 0.0]), "b": np.array([0.0, 1.0])}
65
+ cpu = {"a": np.array([1.0, 0.0]), "b": np.array([0.0, 1.0])}
66
+ r = report(npu, cpu)
67
+ assert set(r["per_output"]) == {"a", "b"}
68
+ assert math.isclose(r["per_output"]["a"], 1.0, rel_tol=1e-12)
69
+ # cosine_min is the minimum over the per-output values
70
+ assert r["cosine_min"] == min(r["per_output"].values())
71
+
72
+
73
+ def test_report_only_compares_shared_keys():
74
+ # keys present in npu but missing from cpu are skipped, not an error
75
+ npu = {"shared": np.array([1.0, 1.0]), "npu_only": np.array([1.0])}
76
+ cpu = {"shared": np.array([1.0, 1.0]), "cpu_only": np.array([1.0])}
77
+ r = report(npu, cpu)
78
+ assert set(r["per_output"]) == {"shared"}
79
+
80
+
81
+ def test_report_empty_inputs_do_not_crash():
82
+ r = report({}, {})
83
+ assert r["per_output"] == {}
84
+ assert r["cosine_min"] == 0.0
85
+ assert r["pass"] is False # 0.0 < default 0.99
86
+
87
+
88
+ if __name__ == "__main__":
89
+ fns = [f for n, f in sorted(globals().items()) if n.startswith("test_")]
90
+ for f in fns:
91
+ f(); print("ok", f.__name__)
92
+ print(f"\n{len(fns)} passed")