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
tan/model/targets.py ADDED
@@ -0,0 +1,374 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Derive .alpmodel compile targets from a SoM SKU (silicon-determined).
3
+
4
+ Targets come from the host SoC's npus[] *and* from any on-module discrete
5
+ accelerator (e.g. the DEEPX DX-M1 on V2M SoMs). A discrete accelerator is any
6
+ *other* SoC JSON whose variants[].alp_module_skus lists this SKU -- so the SoC
7
+ JSON's alp_module_skus stays the single source of truth (no hardcoded
8
+ backend->silicon map)."""
9
+ from __future__ import annotations
10
+ import json
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import NamedTuple
14
+
15
+ from tan.soc_ref import resolve_soc_path
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class TargetSpec:
20
+ backend: str # cpu | ethos_u | drpai | deepx_dxm1
21
+ silicon_ref: str # SoC ref e.g. "alif:ensemble:e7" | "deepx:dx:m1" | "*"
22
+ accel_config: str # vela accel-config e.g. "ethos-u55-256"; "" when N/A
23
+ # The SILICON's vela invocation profile, from the SoC spec's
24
+ # `npu_toolchain.vela` block (alp-sdk #1470) -- ethos_u targets only, None
25
+ # everywhere else. Optional with None defaults so no existing TargetSpec
26
+ # construction site has to change, and so a SoC spec that predates the
27
+ # block resolves to "pass no profile flag", which is exactly today's
28
+ # behaviour rather than a guess.
29
+ vela_memory_mode: str | None = None # Arm built-in, e.g. "Sram_Only"
30
+ vela_system_config: str | None = None # ONLY when it needs no vendor config
31
+ # The other half of that block: the vendor-gated System_Config and the file
32
+ # that defines it. Carried SEPARATELY from `vela_system_config` -- which
33
+ # stays "safe to put on a command line alone" by construction -- because
34
+ # these two are legal only TOGETHER and only alongside a `--config` the
35
+ # customer supplies through `ALP_VELA_CONFIG`
36
+ # (`tan.model.adapters.ethos_u`). None for every part shipped today: no SoC
37
+ # spec names a `system_config` at all.
38
+ vela_vendor_system_config: str | None = None
39
+ # The vendor `.ini`'s FILENAME as metadata declares it
40
+ # (`vendor_config_filename`, e.g. `ensemble_vela.ini`) -- a DIAGNOSTIC
41
+ # fact, never a path: `tan.model.adapters.ethos_u`'s footprint refusal
42
+ # names it to explain why tan cannot resolve a complete profile for this
43
+ # part. A part whose metadata declares none gets no file named, so no
44
+ # customer is ever sent after another vendor's download.
45
+ vela_vendor_config_filename: str | None = None
46
+ # Does this SoC declare ANY external DRAM interface
47
+ # (`external_memory_interfaces[].kind`)? `False` is the machine-checkable
48
+ # evidence behind the zero-SRAM refusal: vela's default profiles are
49
+ # DRAM-backed, and a working set placed in DRAM on a part that declares no
50
+ # DRAM interface describes memory the module does not have. `None` means
51
+ # the spec says nothing, and an unknown must never be reported as a "no"
52
+ # -- see `_soc_declares_dram`.
53
+ soc_declares_dram: bool | None = None
54
+ # Which CPU core drives this accelerator, from the SoC spec's own
55
+ # `npus[].paired_core` -- `m55_hp` for the E8's high-perf Ethos-U55,
56
+ # `m55_he` for its high-efficiency one. `None` where the spec declares no
57
+ # pairing (the E8's Ethos-U85 today), and that `None` is a real answer, not
58
+ # a gap to fill in: the metadata does not know which core drives it, so
59
+ # nothing downstream may infer one. A bench-measured perf point's identity
60
+ # includes the core (alp-sdk `f724d3e4`), and this is the only sourced
61
+ # answer tan has for it.
62
+ paired_core: str | None = None
63
+
64
+
65
+ class _VelaProfile(NamedTuple):
66
+ """What one SoC spec's `npu_toolchain.vela` block resolves to, split by
67
+ what is SAFE TO PASS: `system_config` is an Arm built-in that vela accepts
68
+ on its own, `vendor_system_config` is a name that exists only inside
69
+ `vendor_config_filename` and is rc=1 without it."""
70
+ memory_mode: str | None
71
+ system_config: str | None
72
+ vendor_system_config: str | None
73
+ vendor_config_filename: str | None
74
+
75
+
76
+ #: Substrings that make an `external_memory_interfaces[].kind` a DRAM
77
+ #: interface, matched case-insensitively against the declared kind. Covers
78
+ #: every DRAM spelling in metadata today -- `LPDDR4/4X` (renesas:rzv2n:n44), `LPDDR4X / LPDDR5` (deepx:dx:m1) -- and nothing else:
79
+ #: `HexSPI`, `OctalSPI`, `FlexSPI`, `xSPI` and `SD/eMMC` (the ONLY kinds the
80
+ #: six Alif Ensemble parts declare) are not DRAM.
81
+ #:
82
+ #: Matched on `kind` alone and NOT on the interface's `supports` list, which
83
+ #: is where the Alif parts carry `HyperBus`. A HyperBus port can host HyperRAM,
84
+ #: but that is a populated-device question this spec does not answer, and vela's
85
+ #: `Dram` memory area is not it -- reading `supports` here would turn an
86
+ #: attachable-bus fact into a "this part has DRAM" claim nothing sourced.
87
+ _DRAM_INTERFACE_TOKENS = ("DDR", "DRAM")
88
+
89
+
90
+ def _soc_declares_dram(soc: dict) -> bool | None:
91
+ """Does this SoC spec declare an external DRAM interface? `None` when it
92
+ declares no `external_memory_interfaces` at all.
93
+
94
+ THE `None` IS THE POINT, and it fails closed. The consumer
95
+ (`tan.model.adapters.ethos_u._refuse_zero_sram_footprint`) states "this SoC
96
+ declares no DRAM interface" as EVIDENCE in a refusal a customer sizes
97
+ hardware from, so it may only ever say that on an explicit `False`. A spec
98
+ that carries no interface list has not declared the absence of DRAM; it has
99
+ said nothing, and saying nothing back is the only honest answer. Same
100
+ reading for an EMPTY list, which is a stub far more often than a positive
101
+ statement that a part has no external memory at all."""
102
+ interfaces = soc.get("external_memory_interfaces")
103
+ if not isinstance(interfaces, list) or not interfaces:
104
+ return None
105
+ for iface in interfaces:
106
+ kind = str(iface.get("kind", "")) if isinstance(iface, dict) else ""
107
+ if any(token in kind.upper() for token in _DRAM_INTERFACE_TOKENS):
108
+ return True
109
+ return False
110
+
111
+
112
+ def _vela_profile(soc: dict) -> _VelaProfile:
113
+ """How to invoke vela for THIS SoC, read from its `npu_toolchain.vela`
114
+ block. All-None for a spec that carries no block -- never a guessed
115
+ profile; a wrong one compiles a command stream for memory the module does
116
+ not have.
117
+
118
+ THE SYSTEM_CONFIG GUARD IS LOAD-BEARING. `--memory-mode` values are Arm
119
+ built-ins (the SoC schema constrains `memory_mode` to the `[Memory_Mode.*]`
120
+ sections `ethos-u-vela` 5.1.0 ships), so passing one always works. A
121
+ `System_Config` name usually is NOT: the Alif Ensemble parts' own
122
+ `Ethos_U85_SRAM_Only` / `RTSS_HE_SRAM_Only` live only in the proprietary
123
+ `ensemble_vela.ini`, and handing vela a section it cannot resolve is a hard
124
+ rc=1, measured verbatim:
125
+
126
+ ethosu.vela.errors.CliOptionError: 'Error: Incorrect argument to CLI
127
+ option --system-config=Ethos_U85_SRAM_Only: Section
128
+ System_Config.Ethos_U85_SRAM_Only not found in Vela config file'
129
+
130
+ So a `system_config` is carried ONLY when the block declares
131
+ `system_config_requires_vendor_config` EXPLICITLY FALSE. It fails CLOSED,
132
+ not open: an ABSENT key withholds the name, exactly as `true` does. The
133
+ defence for reading it as falsy was that alp-sdk's SoC schema makes the key
134
+ `required` -- true today, but enforced in the OTHER repo, against a
135
+ checkout tan binds at an arbitrary version (`ALP_SDK_ROOT`, any tag or
136
+ branch a user has on disk). That is the same cross-repo drift class
137
+ `tests/gates/test_planner_relocation_freshness.py` exists for, and here the
138
+ failure mode is not a stale hash: an unannotated `system_config` would walk
139
+ straight onto vela's command line and turn a build into a hard rc=1. `is
140
+ False` is the only reading under which a spec that never made the promise
141
+ cannot make it by omission.
142
+
143
+ An ABSENT `system_config` is likewise "nothing to pass", never "no vendor
144
+ config needed": no Alif spec names one today (an Alif System_Config
145
+ describes one CORE SUBSYSTEM, not a die), so this branch yields None for
146
+ every part currently shipped -- correct, and the mechanism is here for the
147
+ spec that eventually does carry a built-in one.
148
+
149
+ A vendor-gated name is WITHHELD FROM `system_config` AND CARRIED ON
150
+ `vendor_system_config`, rather than dropped. The two fields are exactly
151
+ "safe alone" versus "legal only with its `.ini`": the adapter may put the
152
+ first on a command line unconditionally, and the second only alongside a
153
+ `--config` pointing at a file the customer supplied through
154
+ `ALP_VELA_CONFIG`. Splitting them here rather than passing one name plus a
155
+ boolean keeps the fail-closed guard above the only place that decides which
156
+ is which: an adapter cannot accidentally pass a vendor name by reading a
157
+ flag the wrong way round, because it is never handed one in the field it
158
+ passes freely."""
159
+ block = soc.get("npu_toolchain")
160
+ vela = block.get("vela") if isinstance(block, dict) else None
161
+ if not isinstance(vela, dict):
162
+ return _VelaProfile(None, None, None, None)
163
+ memory_mode = vela.get("memory_mode") or None
164
+ system_config = vela.get("system_config") or None
165
+ if vela.get("system_config_requires_vendor_config") is not False:
166
+ # The filename rides ONLY on this branch. A spec that declares it needs
167
+ # no vendor config has no vendor file to name, and a contradictory spec
168
+ # (`false` plus a filename) must not have that filename quoted at a
169
+ # customer as something they need -- the refusal clause it feeds calls
170
+ # the file proprietary and undistributed, which would then be advice
171
+ # about a download nobody has to make.
172
+ return _VelaProfile(memory_mode, None, system_config,
173
+ vela.get("vendor_config_filename") or None)
174
+ return _VelaProfile(memory_mode, system_config, None, None)
175
+
176
+
177
+ def _npu_backend(npu_type: str, subtype: str) -> str | None:
178
+ if npu_type.startswith("ethos-u"):
179
+ return "ethos_u"
180
+ if "drp" in npu_type or "drp" in subtype: # renesas DRP-AI ("drp-ai" / "ai-mac+drp")
181
+ return "drpai"
182
+ if npu_type.startswith("dx") or "deepx" in npu_type:
183
+ return "deepx_dxm1"
184
+ return None
185
+
186
+
187
+ def _soc_targets(soc: dict, silicon_ref: str) -> list[TargetSpec]:
188
+ """One TargetSpec per mappable NPU in a SoC's npus[] (deduped by the caller)."""
189
+ out: list[TargetSpec] = []
190
+ profile = _vela_profile(soc)
191
+ declares_dram = _soc_declares_dram(soc)
192
+ npus = soc.get("npus", [])
193
+ if not isinstance(npus, list):
194
+ # `npus` is schema-typed `"type": "array"` but unvalidated on read
195
+ # (tan-cli#964 is the read-path schema check; this is the last line
196
+ # of defence, not a substitute). A schema-invalid SoC JSON can hand
197
+ # this key a non-list scalar, which `for npu in npus` would raise
198
+ # `TypeError: ... is not iterable` on. Same "no mappable NPUs"
199
+ # outcome a genuinely-absent `npus` key already produces.
200
+ npus = []
201
+ for npu in npus:
202
+ if not isinstance(npu, dict):
203
+ # One level below the `npus` guard above: an ELEMENT of a
204
+ # schema-valid `npus` array can itself be a non-dict scalar on a
205
+ # schema-invalid SoC JSON (tan-cli#967 review -- the guard below
206
+ # protected `npu.get("type")`'s return value but not the
207
+ # `npu.get` CALL, one line above where the review found it).
208
+ # Skip the element -- the identical "no mappable backend for
209
+ # this entry" outcome an unresolvable `type` already produces
210
+ # two blocks down.
211
+ continue
212
+ # `.get("type", "")`'s default fires only when the key is ABSENT --
213
+ # never when it is present with the wrong type -- so a schema-invalid
214
+ # SoC JSON (unvalidated on read; the real fix is tan-cli#964) can hand
215
+ # a non-string straight into `_npu_backend`'s `.startswith`/`in`
216
+ # calls (tan-cli#965). Guard to the same "" unresolved sentinel the
217
+ # #957 family (cores[] `type`) settled on: an unrecognised/
218
+ # unresolvable type falls back to the identical "no mappable
219
+ # backend, skip this npu" outcome a genuinely-absent `type` key
220
+ # already produces, rather than a fourth behaviour. `.lower()` is
221
+ # deliberately NOT applied here (unlike #965's own snippet): it would
222
+ # newly match e.g. "Ethos-U55" and flip `accel_config` from
223
+ # "Ethos-U55-256" to "ethos-u55-256", which check.py:540-542's
224
+ # `accel_config.startswith(f"ethos-{variant}-")` reads -- exactly the
225
+ # fourth behaviour this guard exists to rule out, not a bug fix.
226
+ raw_type = npu.get("type")
227
+ npu_type = raw_type if isinstance(raw_type, str) else ""
228
+ raw_subtype = npu.get("subtype")
229
+ subtype = raw_subtype if isinstance(raw_subtype, str) else ""
230
+ backend = _npu_backend(npu_type, subtype)
231
+ if backend is None:
232
+ continue
233
+ ethos_u = backend == "ethos_u"
234
+ mac_per_cycle = npu.get("mac_per_cycle")
235
+ if ethos_u and not isinstance(mac_per_cycle, int):
236
+ # `mac_per_cycle` is OPTIONAL in `$defs/npu` -- only `type` plus
237
+ # `anyOf(gops, tops)` are required, so a SCHEMA-VALID SoC JSON
238
+ # (e.g. `{"type": "ethos-u55", "gops": 100}`, no `mac_per_cycle`
239
+ # at all) reaches here with nothing to build `accel_config` from.
240
+ # #964 (read-path schema validation) cannot fix this one: the
241
+ # document already passes the schema. There is no honest partial
242
+ # value to fall back to -- an incomplete string like
243
+ # "ethos-u55-" would be a NEW, un-degradeable shape reaching
244
+ # `check.py`'s `accel_config.startswith(...)` reader, worse than
245
+ # either resolving cleanly or not resolving at all. So this NPU
246
+ # is unmappable for the same reason an unrecognised `type` is:
247
+ # skip it, the identical "no mappable backend for this entry"
248
+ # outcome `backend is None` above already produces (this SoC's
249
+ # OTHER npus[] entries, and the always-present `cpu` target,
250
+ # still resolve).
251
+ continue
252
+ accel = f"{npu_type}-{mac_per_cycle}" if ethos_u else ""
253
+ # The profile is a vela flag, so it rides only on the targets vela
254
+ # compiles: a DRP-AI or DEEPX target on the same die must not carry
255
+ # (and its adapter must never be handed) an Ethos-U memory mode. The
256
+ # DRAM fact is NOT a vela flag -- it is a property of the silicon this
257
+ # target runs on, like `silicon_ref` -- so it rides on every target the
258
+ # spec produces, whether or not any adapter reads it today.
259
+ out.append(TargetSpec(backend=backend, silicon_ref=silicon_ref, accel_config=accel,
260
+ vela_memory_mode=profile.memory_mode if ethos_u else None,
261
+ vela_system_config=profile.system_config if ethos_u else None,
262
+ vela_vendor_system_config=(
263
+ profile.vendor_system_config if ethos_u else None),
264
+ vela_vendor_config_filename=(
265
+ profile.vendor_config_filename if ethos_u else None),
266
+ soc_declares_dram=declares_dram,
267
+ paired_core=npu.get("paired_core") or None))
268
+ return out
269
+
270
+
271
+ def _discrete_socs(sku: str, host_ref: str, metadata_root: Path) -> list[tuple[str, dict]]:
272
+ """SoCs (other than the host) whose variants[].alp_module_skus list this SKU --
273
+ i.e. on-module discrete accelerators wired to the host (DEEPX DX-M1 on V2M)."""
274
+ found: list[tuple[str, dict]] = []
275
+ for path in sorted((metadata_root / "socs").glob("**/*.json")):
276
+ soc = json.loads(path.read_text(encoding="utf-8"))
277
+ if not isinstance(soc, dict):
278
+ # A SoC JSON that parses but is not an object (e.g. a bare
279
+ # array) -- every read below is `soc.get(...)`/`v.get(...)`.
280
+ # This is an ENUMERATION over every file under `socs/`, the same
281
+ # shape `analyze.py`'s `_load_table` guards for a single lookup
282
+ # ("letting a list or scalar through turns a downstream `.get()`
283
+ # into an uncaught AttributeError"): skip the one bad file, don't
284
+ # abort the sweep over the rest of them.
285
+ continue
286
+ ref = soc.get("ref")
287
+ if not ref or ref == host_ref:
288
+ continue
289
+ variants = soc.get("variants", [])
290
+ if not isinstance(variants, list):
291
+ variants = []
292
+ # `variants[]` elements are schema-typed as objects but unvalidated
293
+ # on read, the same element-type gap `_soc_targets`'s `npus[]` guard
294
+ # covers above (tan-cli#967 review, folded in here rather than filed
295
+ # separately -- same file, same defect) -- a non-dict element must
296
+ # not reach `v.get(...)`.
297
+ skus = {s for v in variants if isinstance(v, dict)
298
+ for s in v.get("alp_module_skus", [])}
299
+ if sku in skus:
300
+ found.append((ref, soc))
301
+ return found
302
+
303
+
304
+ def resolve_targets(sku: str, *, metadata_root: Path) -> list[TargetSpec]:
305
+ preset_path = metadata_root / "e1m_modules" / f"{sku}.yaml"
306
+ if not preset_path.is_file():
307
+ raise FileNotFoundError(f"no SoM preset for SKU {sku} at {preset_path}")
308
+ import yaml # noqa: PLC0415 -- deferred (tan-cli#810); see sdk_cmd's `_releases_opener`
309
+
310
+ preset = yaml.safe_load(preset_path.read_text(encoding="utf-8"))
311
+ if not isinstance(preset, dict):
312
+ # Mirrors the `host_soc` guard below (tan-cli#964/#965): a preset
313
+ # YAML that parses but is not a mapping (e.g. a bare list or a bare
314
+ # scalar) must not reach the bare `preset["silicon"]` subscript --
315
+ # that raises a raw TypeError ("list indices must be integers or
316
+ # slices, not str" / "string indices must be integers, not 'str'")
317
+ # instead of a curated error a caller can distinguish from any other
318
+ # TypeError in the call stack (tan-cli#1010).
319
+ raise ValueError(
320
+ f"malformed SoM preset at {preset_path}: expected a YAML "
321
+ f"mapping, got {type(preset).__name__}"
322
+ )
323
+
324
+ # `.get(...)`, not a bare subscript: `preset` is now known to be a
325
+ # mapping (guard above) but `silicon:` is schema-REQUIRED, not merely
326
+ # unvalidated-optional -- a preset that omits it must still fall into
327
+ # the curated `resolve_soc_path()` ValueError path below (a missing key
328
+ # becomes silicon=None, which is not a valid 3-part ref) rather than
329
+ # raising a raw KeyError from this line (tan-cli#1018 review).
330
+ silicon = preset.get("silicon") # host SoC, e.g. "alif:ensemble:e7"
331
+ # resolve_soc_path() returns None where the old inline 3-tuple unpack
332
+ # raised ValueError, so re-raise it here: callers distinguish a
333
+ # malformed ref (ValueError) from a well-formed ref naming a spec that
334
+ # isn't on disk (FileNotFoundError, below), and collapsing the two
335
+ # would be a behaviour change, not a refactor (#1096).
336
+ soc_path = resolve_soc_path(silicon, metadata_root)
337
+ if soc_path is None:
338
+ raise ValueError(
339
+ f"malformed silicon ref {silicon!r} in {preset_path}: expected "
340
+ f"exactly 3 colon-separated parts (<vendor>:<family>:<part>)"
341
+ )
342
+ if not soc_path.is_file():
343
+ raise FileNotFoundError(f"no SoC spec for {silicon} at {soc_path}")
344
+ host_soc = json.loads(soc_path.read_text(encoding="utf-8"))
345
+ if not isinstance(host_soc, dict):
346
+ # Unlike `_discrete_socs`'s enumeration over every socs/*.json below
347
+ # (where one bad file is skipped and the sweep continues), this IS
348
+ # THE ONE host SoC spec `silicon:` names -- there is no "skip it and
349
+ # try the next one". Malformed content at a path that exists is
350
+ # reported the same way this function already reports a malformed
351
+ # ref (above) or a missing path (just above this): raise, don't let
352
+ # it flow into `_soc_targets`'s `.get()` calls as a different,
353
+ # uncaught exception shape.
354
+ raise ValueError(
355
+ f"malformed SoC spec at {soc_path}: expected a JSON object, "
356
+ f"got {type(host_soc).__name__}"
357
+ )
358
+
359
+ specs: list[TargetSpec] = []
360
+ seen: set[tuple[str, str]] = set()
361
+
362
+ def _add(spec: TargetSpec) -> None:
363
+ key = (spec.backend, spec.accel_config)
364
+ if key not in seen:
365
+ seen.add(key)
366
+ specs.append(spec)
367
+
368
+ for spec in _soc_targets(host_soc, silicon): # host SoC NPUs
369
+ _add(spec)
370
+ for ref, dsoc in _discrete_socs(sku, silicon, metadata_root): # on-module discrete NPUs
371
+ for spec in _soc_targets(dsoc, ref):
372
+ _add(spec)
373
+ _add(TargetSpec(backend="cpu", silicon_ref="*", accel_config="")) # CPU always present
374
+ return specs
tan/model/tensorio.py ADDED
@@ -0,0 +1,287 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Extract model tensor + operator descriptors from a TFLite flatbuffer.
3
+
4
+ `extract_io` feeds the `.alpmodel` manifest (Stage-1a `Tensor` records,
5
+ dtype/rank/shape/scale/zp -> mirrors alp_inference_tensor_t). `extract_ops`
6
+ feeds `tan.model.analyze`'s static NPU-eligibility screen (ADR-0028 amendment):
7
+ per-operator name + shape/const-ness + a best-effort MAC estimate, so the
8
+ screen can weight coverage by compute rather than by op count.
9
+
10
+ Both raise `OSError` when the source cannot be read (missing file,
11
+ permission error, ...) -- that is a real failure, never swallowed. Otherwise
12
+ both are best-effort: `[]` (or `[], []`) for a source that IS readable but
13
+ isn't a .tflite, for a missing `tflite` reader (from the `model-io` extra),
14
+ or for bytes that don't parse. The model still packages (or scores as
15
+ `undetermined`), just without this metadata (compile-what's-available)."""
16
+ from __future__ import annotations
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+ from .manifest import Tensor
20
+
21
+ # Shared TFLite TensorType -> SDK dtype-string mapping. Honest about the gap:
22
+ # an unmapped type (e.g. INT64/BOOL) reads as "tflite:<code>" rather than a
23
+ # silently wrong "f32" -- the SDK Tensor.dtype vocabulary has no such types.
24
+ # Built by reflecting on tflite.TensorType rather than hand-transcribing the
25
+ # numeric codes: the codes are non-contiguous with the SDK dtype vocabulary
26
+ # (six wanted types out of eleven declared), so a hand-maintained literal
27
+ # would need to track schema.fbs by hand for no benefit over reflection.
28
+ _DTYPE_WANTED = {"FLOAT32": "f32", "FLOAT16": "f16", "INT32": "int32",
29
+ "UINT8": "uint8", "INT16": "int16", "INT8": "int8"}
30
+
31
+
32
+ def tflite_reader_available() -> bool:
33
+ """True when the `tflite` reader (the `model-io` extra) is importable on
34
+ this host. `extract_io`/`extract_ops` deliberately swallow a missing
35
+ reader into an empty best-effort result -- the right contract for
36
+ `build_model`'s "compile what's available" stance -- but a caller that
37
+ needs to tell "the reader isn't installed" apart from "this model
38
+ genuinely has no operators" (both look like `[]` from `extract_ops`,
39
+ `tan.model.check`'s MAJOR 2 review) reads this directly instead of
40
+ re-deriving it from an empty list, which the second case returns too."""
41
+ try:
42
+ import tflite # noqa: F401
43
+ except ImportError:
44
+ return False
45
+ return True
46
+
47
+
48
+ def _dtype_names(tflite) -> dict[int, str]:
49
+ tt = tflite.TensorType
50
+ return {getattr(tt, k): v for k, v in _DTYPE_WANTED.items()}
51
+
52
+
53
+ def _dtype_name(dtype_names: dict[int, str], code: int) -> str:
54
+ return dtype_names.get(code, f"tflite:{code}")
55
+
56
+
57
+ def extract_io(source: Path, *, raw: bytes | None = None) -> tuple[list[Tensor], list[Tensor]]:
58
+ # Reuse the caller's already-read bytes when provided (build_model reads the
59
+ # source once for the manifest sha); an OSError on our own read is a real
60
+ # failure, not a parse problem -- read (or take the caller's bytes) BEFORE
61
+ # *any* format-dependent short-circuit below (the suffix check included),
62
+ # so "the source is unreadable" is never masked by "the reader isn't
63
+ # installed" OR by "this isn't a .tflite source". All three would
64
+ # otherwise collapse to the identical `[], []`, which silently hides a
65
+ # real missing/unreadable source -- on a bare-install host (no `model-io`
66
+ # extra) for .tflite, and on EVERY host for .onnx, since ONNX I/O
67
+ # extraction has no reader yet at all.
68
+ if raw is None:
69
+ raw = source.read_bytes()
70
+ if source.suffix.lower() != ".tflite":
71
+ return [], [] # ONNX I/O extraction is a later follow-up
72
+ try:
73
+ import tflite
74
+ except ImportError:
75
+ return [], [] # parser not installed -> skip
76
+ try:
77
+ model = tflite.Model.GetRootAs(raw, 0)
78
+ if model.SubgraphsLength() == 0:
79
+ return [], []
80
+ g = model.Subgraphs(0)
81
+ dtype_names = _dtype_names(tflite)
82
+ ins = [_describe(g, g.Inputs(i), dtype_names) for i in range(g.InputsLength())]
83
+ outs = [_describe(g, g.Outputs(i), dtype_names) for i in range(g.OutputsLength())]
84
+ return ins, outs
85
+ except Exception:
86
+ return [], [] # malformed / unexpected schema -> no metadata
87
+
88
+
89
+ def _describe(g, idx: int, dtype_names: dict[int, str]) -> Tensor:
90
+ ten = g.Tensors(idx)
91
+ shape = [int(ten.Shape(i)) for i in range(ten.ShapeLength())]
92
+ qp = ten.Quantization()
93
+ scale = float(qp.Scale(0)) if qp is not None and qp.ScaleLength() else 0.0
94
+ zp = int(qp.ZeroPoint(0)) if qp is not None and qp.ZeroPointLength() else 0
95
+ return Tensor(dtype=_dtype_name(dtype_names, ten.Type()), rank=len(shape), shape=shape,
96
+ scale=scale, zp=zp)
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class TensorDesc:
101
+ """One operator's input/output tensor, static-shape only (no runtime data)."""
102
+
103
+ shape: list[int]
104
+ dtype: str
105
+ nbytes: int # 0 for a non-const (activation) tensor
106
+ is_const: bool # True when the flatbuffer carries baked-in data (weights/bias)
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class OpDesc:
111
+ """One model operator, as spelled in its own source vocabulary (TFLite
112
+ builtin-op name today; an ONNX op-type string once ONNX extraction lands)."""
113
+
114
+ op: str
115
+ inputs: list[TensorDesc] = field(default_factory=list)
116
+ outputs: list[TensorDesc] = field(default_factory=list)
117
+ macs: int = 0 # best-effort static estimate; 0 when not computable
118
+ # The vocabulary `op` is spelled in -- "tflite" (the only extractor that
119
+ # exists today) or "onnx" once ONNX extraction lands. `tan.model.analyze`
120
+ # compares THIS against a support table's own `op_namespace`, never the
121
+ # caller-supplied `src_format`, so a mislabelled format cannot make two
122
+ # incomparable op vocabularies look like a match. Required kw_only, no
123
+ # default: a silent "tflite" default let a caller build an ONNX-spelled
124
+ # OpDesc that still claimed the TFLite vocabulary, which the namespace
125
+ # guard in analyze.py then trusted -- every construction site (extractor
126
+ # or test) must say which vocabulary it means.
127
+ op_namespace: str = field(kw_only=True)
128
+
129
+
130
+ def extract_ops(source: Path, *, raw: bytes | None = None) -> list[OpDesc]:
131
+ """Walk a TFLite flatbuffer's first subgraph into `OpDesc`s for the static
132
+ NPU-eligibility screen. Raises `OSError` when @source cannot be read --
133
+ a real failure, surfaced regardless of format. Otherwise best-effort:
134
+ `[]` for a readable non-.tflite source (ONNX extraction is a follow-on),
135
+ a missing `tflite` reader, or unparseable bytes."""
136
+ # Read (or take the caller's bytes) BEFORE any format-dependent
137
+ # short-circuit -- suffix check included -- so an unreadable/missing
138
+ # source raises regardless of extension, rather than a `.onnx` source
139
+ # reading identically to "this model genuinely has no operators".
140
+ if raw is None:
141
+ raw = source.read_bytes()
142
+ if source.suffix.lower() != ".tflite":
143
+ return []
144
+ try:
145
+ import tflite
146
+ except ImportError:
147
+ return []
148
+ try:
149
+ model = tflite.Model.GetRootAs(raw, 0)
150
+ if model.SubgraphsLength() == 0:
151
+ return []
152
+ g = model.Subgraphs(0)
153
+ opcodes = [model.OperatorCodes(i) for i in range(model.OperatorCodesLength())]
154
+ names = _builtin_op_names(tflite)
155
+ dtype_names = _dtype_names(tflite)
156
+ custom_code = tflite.BuiltinOperator.CUSTOM
157
+ ops: list[OpDesc] = []
158
+ for i in range(g.OperatorsLength()):
159
+ op = g.Operators(i)
160
+ oc = opcodes[op.OpcodeIndex()]
161
+ code = oc.BuiltinCode() or oc.DeprecatedBuiltinCode()
162
+ if code == custom_code:
163
+ raw_name = oc.CustomCode()
164
+ name = raw_name.decode("utf-8", "replace") if raw_name else "CUSTOM"
165
+ else:
166
+ name = names.get(code, f"tflite:{code}")
167
+ in_tensors = [_tensor_desc(model, g, op.Inputs(j), dtype_names)
168
+ for j in range(op.InputsLength()) if op.Inputs(j) >= 0]
169
+ # -1 marks an absent optional input (e.g. a bias-less CONV_2D).
170
+ out_tensors = [_tensor_desc(model, g, op.Outputs(j), dtype_names)
171
+ for j in range(op.OutputsLength()) if op.Outputs(j) >= 0]
172
+ ops.append(OpDesc(op=name, inputs=in_tensors, outputs=out_tensors,
173
+ macs=_estimate_macs(name, in_tensors, out_tensors),
174
+ op_namespace="tflite"))
175
+ return ops
176
+ except Exception:
177
+ return [] # malformed / unexpected schema -> no metadata
178
+
179
+
180
+ def _builtin_op_names(tflite) -> dict[int, str]:
181
+ bo = tflite.BuiltinOperator
182
+ return {getattr(bo, n): n for n in dir(bo) if n.isupper() and isinstance(getattr(bo, n), int)}
183
+
184
+
185
+ def _tensor_desc(model, g, idx: int, dtype_names: dict[int, str]) -> TensorDesc:
186
+ ten = g.Tensors(idx)
187
+ shape = [int(ten.Shape(i)) for i in range(ten.ShapeLength())]
188
+ buf = model.Buffers(ten.Buffer())
189
+ nbytes = buf.DataLength() if buf is not None else 0
190
+ return TensorDesc(shape=shape, dtype=_dtype_name(dtype_names, ten.Type()), nbytes=nbytes,
191
+ is_const=nbytes > 0)
192
+
193
+
194
+ # Positional index of the filter/weights input tensor per TFLite operator
195
+ # kernel (kFilterTensor / kWeightsTensor in the TFLite C++ kernels), keyed by
196
+ # builtin-op name. NOT "the first const input of the expected rank" -- that
197
+ # heuristic silently grabs whichever constant happens to come first once a
198
+ # graph has been const-folded, which shadows the real filter and can inflate
199
+ # the MAC estimate by many times (see test_conv_2d_filter_is_selected_
200
+ # positionally_not_by_first_matching_const). Every op below carries a
201
+ # MANDATORY (never optional) tensor at index 0 -- the activation for
202
+ # FULLY_CONNECTED/CONV_2D/DEPTHWISE_CONV_2D, kOutputShapeTensor for
203
+ # TRANSPOSE_CONV -- so the filter index below never shifts under an
204
+ # absent-optional-input reindex; only the trailing bias tensor is ever
205
+ # optional (see _ACTIVATION_INPUT_INDEX below for TRANSPOSE_CONV's one
206
+ # further wrinkle: its activation isn't at index 0 the way it is here).
207
+ _FILTER_INPUT_INDEX = {
208
+ "FULLY_CONNECTED": 1, # kInputTensor=0, kWeightsTensor=1, kBiasTensor=2 (optional)
209
+ "CONV_2D": 1, # kInputTensor=0, kFilterTensor=1, kBiasTensor=2 (optional)
210
+ "DEPTHWISE_CONV_2D": 1, # kInputTensor=0, kFilterTensor=1, kBiasTensor=2 (optional)
211
+ "TRANSPOSE_CONV": 1, # kOutputShapeTensor=0, kWeightsTensor=1, kDataInputTensor=2, kBiasTensor=3 (optional)
212
+ }
213
+
214
+ # TRANSPOSE_CONV alone needs the INPUT activation positionally too (its
215
+ # filter is applied per input position, not per output position -- see the
216
+ # formula below), and that activation is not at index 0 the way it is for
217
+ # every other op here.
218
+ _ACTIVATION_INPUT_INDEX = {
219
+ "TRANSPOSE_CONV": 2, # kDataInputTensor
220
+ }
221
+
222
+
223
+ def _filter_weight(op_name: str, inputs: list[TensorDesc], rank: int) -> TensorDesc | None:
224
+ idx = _FILTER_INPUT_INDEX.get(op_name)
225
+ if idx is None or idx >= len(inputs):
226
+ return None
227
+ w = inputs[idx]
228
+ return w if w.is_const and len(w.shape) == rank else None
229
+
230
+
231
+ def _elem_count(shape: list[int]) -> int:
232
+ n = 1
233
+ for d in shape:
234
+ n *= max(int(d), 0)
235
+ return n
236
+
237
+
238
+ def _estimate_macs(op_name: str, inputs: list[TensorDesc], outputs: list[TensorDesc]) -> int:
239
+ """Best-effort MAC count from static shapes for the compute-dominant op
240
+ kinds (conv/dense); 0 for everything else. MAC weighting exists to stop
241
+ op-count coverage from hiding a conv backbone behind a wall of cheap
242
+ elementwise ops, not to model every op's cost exactly -- an upper-bound
243
+ screening heuristic, same stance as the rest of this module."""
244
+ if not outputs:
245
+ return 0
246
+
247
+ if op_name == "TRANSPOSE_CONV":
248
+ # Unlike CONV_2D, the filter slides over the INPUT (the op upsamples:
249
+ # output is stride-larger than input), so costing off the output
250
+ # element count -- as CONV_2D correctly does -- over-counts by
251
+ # stride^2. Cost off the actual input-activation tensor's element
252
+ # count instead -- this branch never reads out_elems, so it must sit
253
+ # ahead of (not behind) the out_elems==0 guard below: an output shape
254
+ # that happens to compute to 0 elements (e.g. a dynamic dim) must not
255
+ # zero out a fully-computable in_elems * kh * kw * out_c estimate.
256
+ w = _filter_weight(op_name, inputs, 4) # [out_c, kh, kw, in_c] (OHWI)
257
+ if w is None:
258
+ return 0
259
+ out_c, kh, kw, _ = w.shape
260
+ act_idx = _ACTIVATION_INPUT_INDEX[op_name]
261
+ if act_idx >= len(inputs):
262
+ return 0
263
+ in_elems = _elem_count(inputs[act_idx].shape)
264
+ if in_elems == 0:
265
+ return 0
266
+ return in_elems * kh * kw * out_c
267
+
268
+ out_elems = _elem_count(outputs[0].shape)
269
+ if out_elems == 0:
270
+ return 0
271
+
272
+ if op_name == "FULLY_CONNECTED":
273
+ w = _filter_weight(op_name, inputs, 2) # [out_features, in_features]
274
+ return out_elems * w.shape[-1] if w else 0
275
+ if op_name == "CONV_2D":
276
+ w = _filter_weight(op_name, inputs, 4) # [out_c, kh, kw, in_c] (OHWI)
277
+ if w is None:
278
+ return 0
279
+ _, kh, kw, in_c = w.shape
280
+ return out_elems * kh * kw * in_c
281
+ if op_name == "DEPTHWISE_CONV_2D":
282
+ w = _filter_weight(op_name, inputs, 4) # [1, kh, kw, out_c]
283
+ if w is None:
284
+ return 0
285
+ _, kh, kw, _ = w.shape
286
+ return out_elems * kh * kw
287
+ return 0