tan-cli 0.7.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 (330) hide show
  1. tan/__init__.py +0 -0
  2. tan/__main__.py +120 -0
  3. tan/cli.py +939 -0
  4. tan/command_table.py +265 -0
  5. tan/commands/__init__.py +8 -0
  6. tan/commands/bootstrap_cmd.py +4856 -0
  7. tan/commands/bootstrap_patches.py +164 -0
  8. tan/commands/bootstrap_toolchain_fetch.py +155 -0
  9. tan/commands/bootstrap_toolchain_pin.py +36 -0
  10. tan/commands/build/__init__.py +1 -0
  11. tan/commands/build/configure_inputs.py +277 -0
  12. tan/commands/build/execute.py +1852 -0
  13. tan/commands/build/host_python.py +59 -0
  14. tan/commands/build/link_stale.py +78 -0
  15. tan/commands/build/manifest.py +428 -0
  16. tan/commands/build/materialise.py +81 -0
  17. tan/commands/build/token_substitution.py +381 -0
  18. tan/commands/build/toolchain.py +482 -0
  19. tan/commands/build/user_defines_wipe.py +115 -0
  20. tan/commands/build/workspace_patches.py +65 -0
  21. tan/commands/build_cmd.py +2236 -0
  22. tan/commands/build_output.py +331 -0
  23. tan/commands/clean_cmd.py +1183 -0
  24. tan/commands/completion_cmd.py +786 -0
  25. tan/commands/debug_config_cmd.py +2425 -0
  26. tan/commands/diff_cmd.py +940 -0
  27. tan/commands/doctor_cmd.py +4767 -0
  28. tan/commands/examples_cmd.py +644 -0
  29. tan/commands/explain_cmd.py +1319 -0
  30. tan/commands/faultdecode_cmd.py +931 -0
  31. tan/commands/flash_atoc_guard.py +240 -0
  32. tan/commands/flash_cmd.py +5731 -0
  33. tan/commands/flash_mram_guard.py +150 -0
  34. tan/commands/flash_ram.py +578 -0
  35. tan/commands/flash_raw.py +583 -0
  36. tan/commands/generate_cmd.py +1456 -0
  37. tan/commands/image_cmd.py +753 -0
  38. tan/commands/init_cmd.py +1740 -0
  39. tan/commands/inspect_cmd.py +377 -0
  40. tan/commands/kconfig_cmd.py +734 -0
  41. tan/commands/model_cmd.py +1514 -0
  42. tan/commands/model_device_cmd.py +351 -0
  43. tan/commands/model_device_live.py +234 -0
  44. tan/commands/model_host_cmd.py +430 -0
  45. tan/commands/model_zoo_cmd.py +363 -0
  46. tan/commands/monitor_actions.py +125 -0
  47. tan/commands/monitor_cmd.py +626 -0
  48. tan/commands/monitor_session.py +505 -0
  49. tan/commands/new_som_cmd.py +1522 -0
  50. tan/commands/pinmux_cmd.py +568 -0
  51. tan/commands/presets_cmd.py +1123 -0
  52. tan/commands/probe_cmd.py +421 -0
  53. tan/commands/reset_cmd.py +354 -0
  54. tan/commands/reset_confirm.py +168 -0
  55. tan/commands/run_cmd.py +637 -0
  56. tan/commands/scaffold_cmd.py +490 -0
  57. tan/commands/sdk_cmd.py +1723 -0
  58. tan/commands/size_cmd.py +1020 -0
  59. tan/commands/support_bundle_cmd.py +1073 -0
  60. tan/commands/trace_cmd.py +341 -0
  61. tan/commands/validate_cmd.py +2009 -0
  62. tan/commands/west_forward_cmd.py +736 -0
  63. tan/commands/workspace_patch_check.py +296 -0
  64. tan/core/__init__.py +1 -0
  65. tan/core/alp_conf_pregen.py +100 -0
  66. tan/core/artifact_provenance.py +240 -0
  67. tan/core/atoc_guard.py +276 -0
  68. tan/core/atoc_guard_messages.py +232 -0
  69. tan/core/atoc_replacement.py +118 -0
  70. tan/core/atomic_write.py +178 -0
  71. tan/core/board_context.py +351 -0
  72. tan/core/board_diagnostic.py +111 -0
  73. tan/core/board_files.py +221 -0
  74. tan/core/board_validator.py +542 -0
  75. tan/core/board_validator_compat.py +196 -0
  76. tan/core/board_validator_run.py +129 -0
  77. tan/core/board_validator_schema.py +186 -0
  78. tan/core/board_validator_skew.py +46 -0
  79. tan/core/board_yaml_edit.py +295 -0
  80. tan/core/board_yaml_pos.py +118 -0
  81. tan/core/bootstrap.py +2572 -0
  82. tan/core/build_plan.py +625 -0
  83. tan/core/consent.py +104 -0
  84. tan/core/console_filter.py +245 -0
  85. tan/core/debug_launch.py +1803 -0
  86. tan/core/devicetree_lint.py +168 -0
  87. tan/core/dir_removal.py +229 -0
  88. tan/core/doctor_git.py +172 -0
  89. tan/core/doctor_libraries.py +253 -0
  90. tan/core/doctor_render.py +185 -0
  91. tan/core/doctor_scope.py +66 -0
  92. tan/core/doctor_setools.py +237 -0
  93. tan/core/doctor_stale.py +180 -0
  94. tan/core/document_guards.py +723 -0
  95. tan/core/dp_id.py +97 -0
  96. tan/core/elf_load.py +63 -0
  97. tan/core/error_catalog.py +171 -0
  98. tan/core/example_catalog.py +499 -0
  99. tan/core/example_facets.py +151 -0
  100. tan/core/faultdecode.py +521 -0
  101. tan/core/flash_plan.py +2501 -0
  102. tan/core/flow_d_report.py +381 -0
  103. tan/core/flow_d_uptime.py +164 -0
  104. tan/core/fs_confine.py +130 -0
  105. tan/core/global_flags.py +230 -0
  106. tan/core/host_python.py +210 -0
  107. tan/core/image_bundle.py +247 -0
  108. tan/core/inert.py +147 -0
  109. tan/core/jlink_binary.py +128 -0
  110. tan/core/jlink_probe.py +481 -0
  111. tan/core/jsonc_splice.py +404 -0
  112. tan/core/launch_provenance.py +257 -0
  113. tan/core/link_refusal.py +76 -0
  114. tan/core/metadata_schema.py +376 -0
  115. tan/core/model_check.py +227 -0
  116. tan/core/model_device.py +449 -0
  117. tan/core/model_doctor.py +198 -0
  118. tan/core/model_host.py +48 -0
  119. tan/core/model_list.py +112 -0
  120. tan/core/model_zoo.py +353 -0
  121. tan/core/module_template.py +340 -0
  122. tan/core/mram_link.py +87 -0
  123. tan/core/new_som_ospi.py +94 -0
  124. tan/core/os_class.py +159 -0
  125. tan/core/patch_paths.py +87 -0
  126. tan/core/pending.py +40 -0
  127. tan/core/plain_zephyr_plan.py +138 -0
  128. tan/core/plan_exec.py +373 -0
  129. tan/core/plan_tokens.py +600 -0
  130. tan/core/probe.py +155 -0
  131. tan/core/probe_plan.py +237 -0
  132. tan/core/proxy.py +168 -0
  133. tan/core/publish.py +31 -0
  134. tan/core/python_floor.py +178 -0
  135. tan/core/ram_run.py +694 -0
  136. tan/core/ram_watch.py +231 -0
  137. tan/core/raw_write.py +160 -0
  138. tan/core/reset_plan.py +56 -0
  139. tan/core/run.py +161 -0
  140. tan/core/scaffold.py +2203 -0
  141. tan/core/scaffold_selftest_identity.py +225 -0
  142. tan/core/sdk_default_registry.py +565 -0
  143. tan/core/sdk_discovery.py +996 -0
  144. tan/core/sdk_removal.py +465 -0
  145. tan/core/serial_actions.py +175 -0
  146. tan/core/serial_capture.py +185 -0
  147. tan/core/serial_url.py +45 -0
  148. tan/core/setools.py +608 -0
  149. tan/core/setools_scratch.py +243 -0
  150. tan/core/shapes.py +194 -0
  151. tan/core/size.py +610 -0
  152. tan/core/som_buildability.py +204 -0
  153. tan/core/som_schema_version.py +62 -0
  154. tan/core/subprocess_env.py +201 -0
  155. tan/core/sysbuild_image.py +94 -0
  156. tan/core/system_manifest.py +827 -0
  157. tan/core/text_layout.py +73 -0
  158. tan/core/timestamp.py +182 -0
  159. tan/core/tool_lookup.py +383 -0
  160. tan/core/toolchain_pin.py +117 -0
  161. tan/core/toolchain_provision.py +1224 -0
  162. tan/core/trusted_path.py +134 -0
  163. tan/core/uboot_breakin.py +154 -0
  164. tan/core/uri_reference.py +415 -0
  165. tan/core/user_defines.py +152 -0
  166. tan/core/venv.py +332 -0
  167. tan/core/west_patches.py +143 -0
  168. tan/core/west_workspace_refusal.py +159 -0
  169. tan/core/zephyr_env.py +124 -0
  170. tan/env.py +185 -0
  171. tan/envelope.py +647 -0
  172. tan/exit_codes.py +12 -0
  173. tan/lazy_typer.py +185 -0
  174. tan/model/__init__.py +2 -0
  175. tan/model/_gen_fixture.py +132 -0
  176. tan/model/adapters/__init__.py +116 -0
  177. tan/model/adapters/cpu.py +26 -0
  178. tan/model/adapters/deepx.py +94 -0
  179. tan/model/adapters/drpai.py +242 -0
  180. tan/model/adapters/ethos_u.py +951 -0
  181. tan/model/adapters/executorch.py +55 -0
  182. tan/model/analyze.py +694 -0
  183. tan/model/build.py +304 -0
  184. tan/model/check.py +682 -0
  185. tan/model/manifest.py +625 -0
  186. tan/model/measure.py +216 -0
  187. tan/model/npu_ops_validate.py +130 -0
  188. tan/model/package.py +125 -0
  189. tan/model/perf.py +635 -0
  190. tan/model/perf_apply.py +721 -0
  191. tan/model/prep.py +193 -0
  192. tan/model/sram_fit.py +502 -0
  193. tan/model/targets.py +374 -0
  194. tan/model/tensorio.py +287 -0
  195. tan/net.py +73 -0
  196. tan/output_format.py +213 -0
  197. tan/planner/__init__.py +219 -0
  198. tan/planner/aperture.py +202 -0
  199. tan/planner/buildplan.py +1092 -0
  200. tan/planner/camera_owner.py +192 -0
  201. tan/planner/cameras.py +132 -0
  202. tan/planner/carveout.py +755 -0
  203. tan/planner/cli.py +274 -0
  204. tan/planner/headers.py +315 -0
  205. tan/planner/kconfig.py +2435 -0
  206. tan/planner/kconfig_symbols.py +473 -0
  207. tan/planner/libraries.py +677 -0
  208. tan/planner/link_target.py +237 -0
  209. tan/planner/linux_ownership.py +216 -0
  210. tan/planner/loader.py +1525 -0
  211. tan/planner/manifest.py +116 -0
  212. tan/planner/memory.py +161 -0
  213. tan/planner/memregion.py +56 -0
  214. tan/planner/models.py +533 -0
  215. tan/planner/orchestrator.py +874 -0
  216. tan/planner/ownership.py +203 -0
  217. tan/planner/partition.py +1160 -0
  218. tan/planner/paths.py +32 -0
  219. tan/planner/plain_slice.py +139 -0
  220. tan/planner/project_emit/__init__.py +60 -0
  221. tan/planner/project_emit/bom_netlist.py +375 -0
  222. tan/planner/project_emit/dts.py +580 -0
  223. tan/planner/project_emit/hw_info.py +221 -0
  224. tan/planner/project_emit/native_sim.py +136 -0
  225. tan/planner/project_emit/west_libs.py +213 -0
  226. tan/planner/project_loader.py +264 -0
  227. tan/planner/sdk_capability.py +203 -0
  228. tan/planner/sdk_compat.py +395 -0
  229. tan/planner/secure.py +350 -0
  230. tan/planner/slugs.py +394 -0
  231. tan/planner/som_metadata.py +325 -0
  232. tan/planner/strict_loaders.py +131 -0
  233. tan/planner/template.py +1037 -0
  234. tan/planner/template_pins.py +535 -0
  235. tan/planner/template_rewrite.py +714 -0
  236. tan/planner/topology.py +208 -0
  237. tan/planner/validate.py +365 -0
  238. tan/planner/whole_device_alias.py +67 -0
  239. tan/planner/zephyr_board.py +3510 -0
  240. tan/planner_cli.py +97 -0
  241. tan/planner_emit.py +617 -0
  242. tan/planner_root.py +119 -0
  243. tan/soc_ref.py +58 -0
  244. tan/templates/__init__.py +34 -0
  245. tan/templates/vendored/MANIFEST.md +1862 -0
  246. tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
  247. tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
  248. tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
  249. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
  250. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
  251. tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
  252. tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
  253. tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
  254. tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
  255. tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
  256. tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
  257. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
  258. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
  259. tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
  260. tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
  261. tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
  262. tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
  263. tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
  264. tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
  265. tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
  266. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
  267. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
  268. tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
  269. tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
  270. tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
  271. tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
  272. tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
  273. tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
  274. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
  275. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
  276. tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
  277. tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
  278. tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
  279. tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
  280. tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
  281. tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
  282. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
  283. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
  284. tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
  285. tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
  286. tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
  287. tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
  288. tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
  289. tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
  290. tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
  291. tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
  292. tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
  293. tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
  294. tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
  295. tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
  296. tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
  297. tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
  298. tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
  299. tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
  300. tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
  301. tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
  302. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
  303. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
  304. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
  305. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
  306. tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
  307. tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
  308. tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
  309. tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
  310. tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
  311. tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
  312. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
  313. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
  314. tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
  315. tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
  316. tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
  317. tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
  318. tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
  319. tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
  320. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
  321. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
  322. tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
  323. tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
  324. tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
  325. tan/version.py +102 -0
  326. tan_cli-0.7.0.dist-info/METADATA +125 -0
  327. tan_cli-0.7.0.dist-info/RECORD +330 -0
  328. tan_cli-0.7.0.dist-info/WHEEL +5 -0
  329. tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
  330. tan_cli-0.7.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,430 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Host-tier `tan model prep` (tan-cli#1287): runners and text renderers, kept
3
+ out of `model_cmd.py` (module-size budget); that file owns the typer surface
4
+ and `finish()`.
5
+
6
+ **`prep MODEL.onnx --calibration DIR [--out DIR] [--per-channel]`** validates
7
+ the calibration set (`.npy` samples, shape-matched to the model input), INT8
8
+ quantizes with onnxruntime QDQ static to `<out>/<stem>.int8.onnx`, and reports
9
+ the fp32-vs-int8 accuracy delta -- a silent INT8 accuracy cliff is the failure
10
+ this exists to catch. No vendor toolchain and no hardware; the vendor compile
11
+ (`tan model build`) is a separate step. ONNX input only: `.tflite` conversion
12
+ (tf2onnx/tensorflow) is deferred.
13
+
14
+ **`run MODEL.onnx [--runs N] [--input X.npy]`** is a host reference run
15
+ (onnxruntime CPU, `backend: cpu-host`): median latency over N runs and the
16
+ output argmax. **`ab MODEL.onnx --against OTHER.onnx`** runs both with the same
17
+ input and compares latency and file size. Both are FUNCTIONAL + host-latency
18
+ references, never SoM performance -- `peakSramKib`/`powerMj` stay `null` until
19
+ the bench-gated on-device tier fills the same schema. The input defaults to a
20
+ deterministic seeded sample shaped like the model's first input.
21
+
22
+ Every refusal is a plain `Issue(...)` return (never an exception) so the codes
23
+ stay visible to the issue-code registry gate.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import os
29
+ import shutil
30
+ import tempfile
31
+ from pathlib import Path
32
+ from typing import Any
33
+
34
+ from tan.commands.build_output import ProjectContext
35
+ from tan.core.model_host import broken_extra_message, extra_missing_message, missing_extra_modules
36
+ from tan.core.publish import publish_exclusive
37
+ from tan.envelope import Issue, Project, SdkInfo
38
+ from tan.exit_codes import ExitCode
39
+
40
+ PREP_DATA_SCHEMA_VERSION = "1"
41
+ RUN_DATA_SCHEMA_VERSION = "1"
42
+ AB_DATA_SCHEMA_VERSION = "1"
43
+
44
+ Result = tuple[Project, SdkInfo | None, dict, list[Issue], ExitCode]
45
+
46
+
47
+ def prep_empty_data() -> dict[str, Any]:
48
+ return {
49
+ "schemaVersion": PREP_DATA_SCHEMA_VERSION,
50
+ "source": None,
51
+ "output": None,
52
+ "calibration": None,
53
+ "accuracy": None,
54
+ }
55
+
56
+
57
+ def run_empty_data() -> dict[str, Any]:
58
+ return {"schemaVersion": RUN_DATA_SCHEMA_VERSION, "model": None, "result": None}
59
+
60
+
61
+ def ab_empty_data() -> dict[str, Any]:
62
+ return {"schemaVersion": AB_DATA_SCHEMA_VERSION, "a": None, "b": None, "comparison": None}
63
+
64
+
65
+ def resolve_model_path(context: ProjectContext, raw: str | None, role: str) -> Path | Issue:
66
+ """`raw` as an existing file (relative to the project root), or the
67
+ `model.model-source-missing` refusal."""
68
+ if not raw:
69
+ return Issue(
70
+ "model.model-source-missing",
71
+ "error",
72
+ f"`tan model {role}` needs a model file (.onnx) argument.",
73
+ )
74
+ path = Path(raw)
75
+ if not path.is_absolute():
76
+ path = Path(context.workspace_root) / path
77
+ if not path.is_file():
78
+ return Issue("model.model-source-missing", "error", f"Model file not found: {path}")
79
+ return path
80
+
81
+
82
+ def format_refusal(path: Path, verb: str) -> Issue | None:
83
+ """The `model.model-format-unsupported` refusal for a non-ONNX input."""
84
+ if path.suffix.lower() == ".onnx":
85
+ return None
86
+ hint = (
87
+ " TFLite -> ONNX conversion (tf2onnx/tensorflow) is not available yet."
88
+ if path.suffix.lower() == ".tflite"
89
+ else ""
90
+ )
91
+ return Issue(
92
+ "model.model-format-unsupported",
93
+ "error",
94
+ f"`tan model {verb}` takes an .onnx model; got {path.name}.{hint}",
95
+ )
96
+
97
+
98
+ def _calibration_row(info: Any) -> dict[str, Any]:
99
+ return {"samples": info.samples, "inputName": info.input_name, "inputShape": list(info.input_shape)}
100
+
101
+
102
+ def _accuracy_row(rep: Any) -> dict[str, Any]:
103
+ return {
104
+ "samples": rep.samples,
105
+ "top1AgreementPct": rep.top1_agreement_pct,
106
+ "meanCosine": rep.mean_cosine,
107
+ "maxAbsErr": rep.max_abs_err,
108
+ "verdict": rep.verdict,
109
+ "guidance": rep.guidance,
110
+ }
111
+
112
+
113
+ def run_prep(
114
+ *,
115
+ context: ProjectContext,
116
+ source: str | None,
117
+ calibration: str | None,
118
+ out: str,
119
+ per_channel: bool,
120
+ min_samples: int,
121
+ ) -> Result:
122
+ project, sdk = context.project(), context.sdk
123
+ data = prep_empty_data()
124
+ bad_count = _bad_count("--min-samples", min_samples)
125
+ if bad_count is not None:
126
+ return project, sdk, data, [bad_count], ExitCode.VALIDATION_FAILURE
127
+
128
+ def refuse(issue: Issue, exit_code: ExitCode) -> Result:
129
+ return project, sdk, data, [issue], exit_code
130
+
131
+ path = resolve_model_path(context, source, "prep")
132
+ if isinstance(path, Issue):
133
+ return refuse(path, ExitCode.VALIDATION_FAILURE)
134
+ bad_format = format_refusal(path, "prep")
135
+ if bad_format is not None:
136
+ return refuse(bad_format, ExitCode.VALIDATION_FAILURE)
137
+ data["source"] = path.as_posix()
138
+ missing = missing_extra_modules("prep")
139
+ if missing:
140
+ extra = Issue("model.model-extra-missing", "error", extra_missing_message("prep", missing))
141
+ return refuse(extra, ExitCode.RUNTIME_FAILURE)
142
+ if not calibration:
143
+ need = Issue(
144
+ "model.prep-calibration-invalid",
145
+ "error",
146
+ "`tan model prep` needs --calibration DIR (a directory of .npy samples).",
147
+ )
148
+ return refuse(need, ExitCode.VALIDATION_FAILURE)
149
+
150
+ cal_dir = _under(context, calibration)
151
+ out_dir = _under(context, out)
152
+ done = _quantize(path, cal_dir, out_dir, per_channel, min_samples)
153
+ if isinstance(done, Issue):
154
+ code = ExitCode.VALIDATION_FAILURE
155
+ if done.code in ("model.prep-failed", "model.model-extra-missing"):
156
+ code = ExitCode.RUNTIME_FAILURE
157
+ return refuse(done, code)
158
+ info, quantized, report = done
159
+ data["calibration"] = _calibration_row(info)
160
+ data["output"] = quantized.as_posix()
161
+ data["accuracy"] = _accuracy_row(report)
162
+ issues: list[Issue] = []
163
+ if report.verdict != "good":
164
+ issues.append(Issue("model.prep-accuracy-degraded", "warning", report.guidance or "INT8 accuracy dropped."))
165
+ return project, sdk, data, issues, ExitCode.SUCCESS
166
+
167
+
168
+ def _bad_count(flag: str, value: int) -> Issue | None:
169
+ if value >= 1:
170
+ return None
171
+ return Issue("model.unexpected-argument", "error", f"{flag} must be at least 1 (got {value}).")
172
+
173
+
174
+ def _under(context: ProjectContext, raw: str) -> Path:
175
+ path = Path(raw)
176
+ return path if path.is_absolute() else Path(context.workspace_root) / path
177
+
178
+
179
+ def _missing_ancestors(path: Path) -> list[Path]:
180
+ """`path` and its parents that do not exist yet, outermost first -- the
181
+ directories a `mkdir(parents=True)` would create."""
182
+ missing = []
183
+ for p in (path, *path.parents):
184
+ if p.exists():
185
+ break
186
+ missing.append(p)
187
+ return missing[::-1]
188
+
189
+
190
+ def _quantize(
191
+ path: Path, cal_dir: Path, out_dir: Path, per_channel: bool, min_samples: int
192
+ ) -> tuple[Any, Path, Any] | Issue:
193
+ """Validate calibration, quantize, measure -- or the refusing `Issue`.
194
+ Everything is built in a private staging directory inside `out_dir` and
195
+ published (never overwriting) only after the accuracy run succeeds; a
196
+ failure removes the staging directory and every directory this run created."""
197
+ final = out_dir / f"{path.stem}.int8.onnx"
198
+ if os.path.lexists(final):
199
+ return Issue("model.prep-output-exists", "error", f"{final} already exists; remove it or use --out.")
200
+ created = _missing_ancestors(out_dir)
201
+ staging: Path | None = None
202
+ done = False
203
+ try:
204
+ from tan.model.prep import PrepError, accuracy_delta, model_input, quantize, validate_calibration # noqa: PLC0415
205
+
206
+ try:
207
+ model_input(path)
208
+ except PrepError as err:
209
+ return Issue("model.prep-failed", "error", str(err))
210
+ try:
211
+ info = validate_calibration(cal_dir, path, min_samples=min_samples)
212
+ except PrepError as err:
213
+ return Issue("model.prep-calibration-invalid", "error", str(err))
214
+ try:
215
+ out_dir.mkdir(parents=True, exist_ok=True)
216
+ staging = Path(tempfile.mkdtemp(dir=out_dir, prefix=".tan-prep-"))
217
+ staged = quantize(path, staging / "q.onnx", cal_dir, per_channel=per_channel)
218
+ report = accuracy_delta(path, staged, cal_dir)
219
+ except (PrepError, OSError) as err:
220
+ return Issue("model.prep-failed", "error", str(err))
221
+ try:
222
+ publish_exclusive(staged, final)
223
+ except FileExistsError:
224
+ return Issue("model.prep-output-exists", "error", f"{final} already exists; nothing was overwritten.")
225
+ except OSError as err:
226
+ return Issue("model.prep-failed", "error", f"could not write {final}: {err}")
227
+ done = True
228
+ return info, final, report
229
+ except ImportError as err:
230
+ return Issue("model.model-extra-missing", "error", broken_extra_message("prep", str(err)))
231
+ finally:
232
+ if staging is not None:
233
+ shutil.rmtree(staging, ignore_errors=True)
234
+ if not done:
235
+ for directory in reversed(created):
236
+ try:
237
+ directory.rmdir()
238
+ except OSError:
239
+ break
240
+
241
+
242
+ def render_prep_text(data: dict) -> list[str]:
243
+ if not data.get("output"):
244
+ return []
245
+ cal, acc = data["calibration"], data["accuracy"]
246
+ return [
247
+ f"calibration: {cal['samples']} samples, input {cal['inputName']} {cal['inputShape']}",
248
+ f"quantized {data['source']} -> {data['output']}",
249
+ f"accuracy (fp32 vs int8): {acc['verdict']}; top-1 agreement {acc['top1AgreementPct']}%, "
250
+ f"mean cosine {acc['meanCosine']}, max abs err {acc['maxAbsErr']}",
251
+ ]
252
+
253
+
254
+ def _result_row(result: Any, path: Path) -> dict[str, Any]:
255
+ return {
256
+ "model": path.as_posix(),
257
+ "backend": result.backend,
258
+ "tier": "host",
259
+ "latencyMs": result.latency_ms,
260
+ "outputArgmax": result.output_argmax,
261
+ "peakSramKib": result.peak_sram_kib,
262
+ "powerMj": result.power_mj,
263
+ "runs": result.runs,
264
+ "sizeBytes": path.stat().st_size,
265
+ }
266
+
267
+
268
+ def _measure(path: Path, input_path: Path | None, runs: int, sample: Any) -> tuple[Any, Any] | Issue:
269
+ """`(RunResult, input sample)` for `path`, or the `model.run-failed` refusal.
270
+ `sample` is reused across `ab`'s two models so both see the same input."""
271
+ from tan.model.measure import MeasureError, default_input, run_host # noqa: PLC0415
272
+
273
+ try:
274
+ if sample is None:
275
+ if input_path is not None:
276
+ import numpy as np # noqa: PLC0415
277
+
278
+ sample = np.load(input_path, allow_pickle=False)
279
+ else:
280
+ sample = default_input(path)
281
+ return run_host(path, sample, runs=runs), sample
282
+ except ImportError as err:
283
+ return Issue("model.model-extra-missing", "error", broken_extra_message("run", str(err)))
284
+ except (MeasureError, OSError, ValueError, AttributeError, TypeError) as err:
285
+ return Issue("model.run-failed", "error", str(err))
286
+
287
+
288
+ def _cast_warnings(path: Path, result: Any) -> list[Issue]:
289
+ """`model.input-dtype-cast` (tan-cli#1497) when the sample (the user's
290
+ `--input` .npy, or tan's own generated sample reused for `ab`'s second
291
+ model) was cast to the model input dtype -- float to int truncates
292
+ silently otherwise."""
293
+ note = getattr(result, "input_cast", None)
294
+ if not note:
295
+ return []
296
+ return [Issue("model.input-dtype-cast", "warning", f"{path.name}: {note}")]
297
+
298
+
299
+ def _host_preflight(
300
+ context: ProjectContext, verb: str, raws: list[str | None], data: dict
301
+ ) -> tuple[list[Path], None] | tuple[None, Issue]:
302
+ """Resolve every model path, check format and the extra; the first refusal."""
303
+ paths: list[Path] = []
304
+ for raw in raws:
305
+ path = resolve_model_path(context, raw, verb)
306
+ if isinstance(path, Issue):
307
+ return None, path
308
+ bad = format_refusal(path, verb)
309
+ if bad is not None:
310
+ return None, bad
311
+ paths.append(path)
312
+ missing = missing_extra_modules(verb)
313
+ if missing:
314
+ return None, Issue("model.model-extra-missing", "error", extra_missing_message(verb, missing))
315
+ return paths, None
316
+
317
+
318
+ def _refusal_exit(issue: Issue) -> ExitCode:
319
+ return ExitCode.RUNTIME_FAILURE if issue.code in ("model.model-extra-missing", "model.run-failed") else ExitCode.VALIDATION_FAILURE
320
+
321
+
322
+ def run_run(
323
+ *, context: ProjectContext, source: str | None, runs: int, input_file: str | None
324
+ ) -> Result:
325
+ project, sdk = context.project(), context.sdk
326
+ data = run_empty_data()
327
+ bad_count = _bad_count("--runs", runs)
328
+ if bad_count is not None:
329
+ return project, sdk, data, [bad_count], ExitCode.VALIDATION_FAILURE
330
+ paths, issue = _host_preflight(context, "run", [source], data)
331
+ if issue is not None:
332
+ return project, sdk, data, [issue], _refusal_exit(issue)
333
+ assert paths is not None
334
+ measured = _measure(paths[0], _under(context, input_file) if input_file else None, runs, None)
335
+ if isinstance(measured, Issue):
336
+ return project, sdk, data, [measured], _refusal_exit(measured)
337
+ data["model"] = paths[0].as_posix()
338
+ data["result"] = _result_row(measured[0], paths[0])
339
+ return project, sdk, data, _cast_warnings(paths[0], measured[0]), ExitCode.SUCCESS
340
+
341
+
342
+ def run_ab(
343
+ *,
344
+ context: ProjectContext,
345
+ source: str | None,
346
+ against: str | None,
347
+ runs: int,
348
+ input_file: str | None,
349
+ ) -> Result:
350
+ from tan.model.measure import compare # noqa: PLC0415
351
+
352
+ project, sdk = context.project(), context.sdk
353
+ data = ab_empty_data()
354
+ bad_count = _bad_count("--runs", runs)
355
+ if bad_count is not None:
356
+ return project, sdk, data, [bad_count], ExitCode.VALIDATION_FAILURE
357
+ paths, issue = _host_preflight(context, "ab", [source, against], data)
358
+ if issue is not None:
359
+ return project, sdk, data, [issue], _refusal_exit(issue)
360
+ assert paths is not None
361
+ a_path, b_path = paths
362
+ input_path = _under(context, input_file) if input_file else None
363
+ first = _measure(a_path, input_path, runs, None)
364
+ if isinstance(first, Issue):
365
+ return project, sdk, data, [first], _refusal_exit(first)
366
+ second = _measure(b_path, input_path, runs, first[1])
367
+ if isinstance(second, Issue):
368
+ return project, sdk, data, [second], _refusal_exit(second)
369
+ a_res, b_res = first[0], second[0]
370
+ cmp = compare(a_res, b_res, size_a=a_path.stat().st_size, size_b=b_path.stat().st_size)
371
+ data["a"] = _result_row(a_res, a_path)
372
+ data["b"] = _result_row(b_res, b_path)
373
+ data["comparison"] = {
374
+ "faster": cmp.faster,
375
+ "latencyRatio": cmp.latency_ratio,
376
+ "aLatencyMs": cmp.a_latency_ms,
377
+ "bLatencyMs": cmp.b_latency_ms,
378
+ "sizeDeltaBytes": cmp.size_delta_bytes,
379
+ }
380
+ return (project, sdk, data,
381
+ [*_cast_warnings(a_path, a_res), *_cast_warnings(b_path, b_res)],
382
+ ExitCode.SUCCESS)
383
+
384
+
385
+ def _device_summary(r: dict) -> str:
386
+ """tan-cli#1518: one measured on-device row, labelled with the scope of its
387
+ latency figure and the energy/SRAM it carries -- never as a host reference."""
388
+ parts = [f"{r.get('latencyMs')} ms/inference ({r.get('latencyScope')})"]
389
+ if r.get("peakSramKib") is not None:
390
+ parts.append(f"peak SRAM {r['peakSramKib']} KiB")
391
+ energy = r.get("energy")
392
+ if energy:
393
+ parts.append(f"energy {energy['valueMjPerInference']} mJ/inference ({energy['scope']})")
394
+ return f"{r.get('backend')}: " + ", ".join(parts)
395
+
396
+
397
+ def render_run_text(data: dict) -> list[str]:
398
+ r = data.get("result")
399
+ if not r:
400
+ return []
401
+ if r.get("tier") == "device":
402
+ return [f"{r.get('model') or '(no model file)'}: {_device_summary(r)}",
403
+ "measured on the device (tier: device)"]
404
+ return [
405
+ f"{r['model']}: {r['backend']} median {r['latencyMs']} ms over {r['runs']} runs, "
406
+ f"output argmax {r['outputArgmax']}",
407
+ "host reference only -- not SoM performance",
408
+ ]
409
+
410
+
411
+ def render_ab_text(data: dict) -> list[str]:
412
+ c = data.get("comparison")
413
+ if not c:
414
+ return []
415
+ a, b = data["a"], data["b"]
416
+ if a.get("tier") == "device" and b.get("tier") == "device":
417
+ size = c["sizeDeltaBytes"]
418
+ return [
419
+ f"A {a.get('model') or '(no model file)'}: {_device_summary(a)}",
420
+ f"B {b.get('model') or '(no model file)'}: {_device_summary(b)}",
421
+ f"faster: {c['faster']}; B/A latency ratio {c['latencyRatio']}"
422
+ + ("" if size is None else f"; size delta {size} bytes"),
423
+ "measured on the device (tier: device)",
424
+ ]
425
+ return [
426
+ f"A {a['model']}: {c['aLatencyMs']} ms, {a['sizeBytes']} bytes",
427
+ f"B {b['model']}: {c['bLatencyMs']} ms, {b['sizeBytes']} bytes",
428
+ f"faster: {c['faster']}; B/A latency ratio {c['latencyRatio']}; size delta {c['sizeDeltaBytes']} bytes",
429
+ "host reference only -- not SoM performance",
430
+ ]