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/sram_fit.py ADDED
@@ -0,0 +1,502 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """SRAM0 residency + arena fit for a `Sram_Only` ethos_u compile (tan-cli#1288).
3
+
4
+ **Premise correction, recorded here since the issue that opened this file got
5
+ it wrong.** tan-cli#1288 claimed tan already compares the vela ARENA
6
+ (`req_sram_kib`) against the board's own budget "per tan-cli#1011". It does
7
+ not: #1011 was only the scope decision that `req_sram_kib` stays arena-only
8
+ (alp-sdk#2312 confirms weights are never summed into it) -- no host-side
9
+ comparison against `board.yaml` existed anywhere before this file.
10
+ `tan.model.targets.TargetSpec` carried no arena limit, and `_vela_profile`
11
+ never resolved a SoC-JSON `variants[]` entry, so `sram_banks_kb.SRAM0`
12
+ (nested under `variants[]`, `tan.core.size.sram_banks`) was unreachable from
13
+ model code. This module adds BOTH halves: the arena check the issue thought
14
+ already existed, and the SRAM0 residency check it asked for.
15
+
16
+ alp-sdk's `src/backends/inference/ethos_u_aen.cpp` pins EVERY NPU access to
17
+ the SRAM AXI port under `Sram_Only` -- not just the tensor arena
18
+ (`tan.model.adapters.ethos_u._footprint`'s own docstring has the full vela
19
+ bookkeeping story for why the arena figure is arena-only), but the compiled
20
+ BLOB too: the AEN examples copy the model's weights into
21
+ `section("SRAM0")` right beside the arena. Applies ONLY under that one vela
22
+ memory mode (`SRAM_ONLY_MODE` below) -- every other mode may place the blob
23
+ off SRAM0, so both checks report `"skipped"`, with a reason, everywhere else.
24
+
25
+ **Never "fits", only "fits-unverified".** Every SoC ships
26
+ `inference_arena_sram_kib: 0` today, so there is no VERIFIED inference-arena
27
+ SRAM budget in metadata to compare against -- a pass here means "the
28
+ declared/default budget is not exceeded", never "measured to run". A
29
+ `no-fit` is certain whenever it is proven against a real, sourced number OR a
30
+ valid LOWER BOUND on one (see `resolve_arena_budget`'s own docstring for
31
+ exactly which arena resolutions are sourced enough to certify one, and
32
+ `MIN_ARENA_KIB` below for the lower-bound case an unresolved arena still
33
+ allows).
34
+
35
+ Pure logic (`evaluate_sram_fit`, `resolve_arena_budget`, `FitVerdict`,
36
+ `SramFit`, `ArenaBudget`) is IO-free; the two resolvers that do real
37
+ filesystem reads (`resolve_sram0_kib`, and `read_sdk_som_and_soc`/
38
+ `resolve_variant`/`sram_banks` it reuses) are the only IO in this module --
39
+ the same split every other `tan.model` file makes (`tan.model.check`'s own
40
+ module doc: "IO lives in tan.model, never in tan.core")."""
41
+ from __future__ import annotations
42
+
43
+ import math
44
+ from dataclasses import dataclass
45
+ from pathlib import Path
46
+
47
+ from tan.commands.build_output import read_sdk_som_and_soc
48
+ from tan.core.size import resolve_variant, sram_banks
49
+
50
+ #: `metadata/schemas/board.schema.json` (alp-sdk, verified against the pinned
51
+ #: checkout at commit 79c834e6): `cores.<id>.inference.default_arena_kib` is
52
+ #: `{"type": "integer", "minimum": 16, "default": 128}`. `DEFAULT_ARENA_KIB`
53
+ #: is applied only to a core that RUNS INFERENCE (`_core_uses_inference`) and
54
+ #: that `board.yaml` actually declares, when that core's own `inference:`
55
+ #: block omits an override -- never invented for a core the board never
56
+ #: declares, or never runs inference on, at all (`resolve_arena_budget`
57
+ #: below). `MIN_ARENA_KIB` is the schema's own floor: even when NO core can
58
+ #: be resolved at all, the real arena -- whichever core ends up owning it --
59
+ #: can never be smaller than this, so `blob_kib + MIN_ARENA_KIB` is a valid
60
+ #: certain LOWER BOUND for a SRAM0 `NO_FIT` proof (tan-cli#1288 review round
61
+ #: 2, finding 2).
62
+ DEFAULT_ARENA_KIB = 128
63
+ MIN_ARENA_KIB = 16
64
+
65
+ #: The one vela memory mode that pins BOTH the tensor arena and the compiled
66
+ #: blob (weights) to the same SRAM AXI port
67
+ #: (`src/backends/inference/ethos_u_aen.cpp`) -- every other mode
68
+ #: (`Shared_Sram` / `Dedicated_Sram*`) may place the blob off SRAM0, so this
69
+ #: whole module is a no-op there.
70
+ SRAM_ONLY_MODE = "Sram_Only"
71
+
72
+ #: The three verdict words this module ever emits -- never a bare "fits" (see
73
+ #: the module docstring's "Never fits" paragraph).
74
+ FIT_UNVERIFIED = "fits-unverified"
75
+ NO_FIT = "no-fit"
76
+ SKIPPED = "skipped"
77
+
78
+ #: The caveat every `FIT_UNVERIFIED` arena verdict carries -- the board's OWN
79
+ #: declared/default budget is a real, sourced number, but nothing in
80
+ #: metadata publishes a firmware-verified one to compare it against (the
81
+ #: identical reason the word is "unverified" rather than "fits" at all).
82
+ _ARENA_UNVERIFIED_REASON = "board arena budget not verified against firmware"
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class FitVerdict:
87
+ """One axis's verdict (arena, or SRAM0 residency): a KiB comparison plus
88
+ an optional human note. `reason` is populated for `SKIPPED` (why),
89
+ `FIT_UNVERIFIED` (the "not verified" caveat), and for a `NO_FIT` proven
90
+ only as a LOWER BOUND rather than off one exact sourced number --
91
+ `resolve_arena_budget`'s `"range"` kind's largest-budget bound, or an
92
+ `"unresolved"` budget's `MIN_ARENA_KIB` bound (both name which bound
93
+ proved it). A `NO_FIT` proven off one single exact figure carries no
94
+ `reason`: the number already says everything there is to say."""
95
+
96
+ verdict: str
97
+ needed_kib: int | None = None
98
+ limit_kib: int | None = None
99
+ reason: str | None = None
100
+
101
+ def as_dict(self) -> dict:
102
+ return {"verdict": self.verdict, "neededKib": self.needed_kib,
103
+ "limitKib": self.limit_kib, "reason": self.reason}
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class SramFit:
108
+ """Both axes of a `Sram_Only` ethos_u compile's SRAM fit, plus the
109
+ compiled blob's own size in KiB -- the one shape `tan model build` (the
110
+ refusal decision only) and `tan model check --exact` (the `sramFit`
111
+ envelope block, `tan.core.model_check.backend_report_as_dict`) both
112
+ read."""
113
+
114
+ arena: FitVerdict
115
+ sram0: FitVerdict
116
+ blob_kib: int | None = None
117
+
118
+ @property
119
+ def no_fit(self) -> bool:
120
+ """True the moment EITHER axis is a certain `NO_FIT` -- the
121
+ per-target skip `tan model build` records (the model is refused only
122
+ when no ethos_u target fits). A `SKIPPED` axis
123
+ never contributes: an unresolvable SRAM0 total, or an arena budget
124
+ this project's board.yaml never actually declared, is an unanswered
125
+ question, never treated as a failure (`resolve_arena_budget`)."""
126
+ return self.arena.verdict == NO_FIT or self.sram0.verdict == NO_FIT
127
+
128
+ def as_dict(self) -> dict:
129
+ return {"arena": self.arena.as_dict(), "sram0": self.sram0.as_dict(),
130
+ "blobKib": self.blob_kib}
131
+
132
+
133
+ def _skipped_not_sram_only(memory_mode: str | None) -> SramFit:
134
+ reason = (f"the SRAM0 residency and arena fit checks apply only under "
135
+ f"the Sram_Only vela memory mode; this target resolved "
136
+ f"{memory_mode!r}.")
137
+ skip = FitVerdict(verdict=SKIPPED, reason=reason)
138
+ return SramFit(arena=skip, sram0=skip, blob_kib=None)
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class ArenaBudget:
143
+ """How `resolve_arena_budget` resolved a target's arena budget -- exactly
144
+ one of three shapes, keyed by `kind`:
145
+
146
+ * `"single"` -- ONE board.yaml core's own arena figure is the answer,
147
+ CERTAIN enough for `NO_FIT` (`single_kib`, `cores` names it).
148
+ * `"range"` -- the target's own NPU pairs to no core
149
+ (`TargetSpec.paired_core is None`, a real answer for a shared NPU, e.g.
150
+ the E8/E6/E4's own Ethos-U85 -- not a gap), and board.yaml declares
151
+ MORE THAN ONE core that runs inference (`_core_uses_inference`), so
152
+ nothing sourced picks between them (`min_kib`/`max_kib`/`cores`, the
153
+ declared budgets and the core names that produced them).
154
+ * `"unresolved"` -- no board.yaml in hand, the target's paired core is
155
+ not declared in board.yaml at all, the target's paired core IS
156
+ declared but parked (`os: "off"`, `_core_participates`) or runs no
157
+ inference workload at all, or (paired_core is `None` too) board.yaml
158
+ has no participating core running an inference workload (`reason`
159
+ says which). Never a `NO_FIT` off one exact figure -- but see
160
+ `MIN_ARENA_KIB` for the lower-bound proof it still allows."""
161
+
162
+ kind: str
163
+ single_kib: int | None = None
164
+ min_kib: int | None = None
165
+ max_kib: int | None = None
166
+ cores: tuple[str, ...] = ()
167
+ reason: str | None = None
168
+
169
+
170
+ def _core_arena_kib_or_default(core_slice: dict) -> int:
171
+ """`inference.default_arena_kib` off one already-declared board.yaml core
172
+ slice, or `DEFAULT_ARENA_KIB` when that core's own `inference:` block
173
+ omits it (or declares none at all) -- the core EXISTING in board.yaml is
174
+ what makes this a sourced fact about THIS project, not the presence of
175
+ the tuning key itself."""
176
+ inference = core_slice.get("inference")
177
+ if isinstance(inference, dict):
178
+ value = inference.get("default_arena_kib")
179
+ # bool is an int subclass in Python; a malformed `true`/`false` value
180
+ # is not a KiB count.
181
+ if isinstance(value, int) and not isinstance(value, bool):
182
+ return value
183
+ return DEFAULT_ARENA_KIB
184
+
185
+
186
+ def _core_participates(core_slice: dict) -> bool:
187
+ """Mirrors `tan.planner.orchestrator.iter_buildable_slices`'s own filter
188
+ (tan-cli#1288 review round 3, finding 1) -- that function's own
189
+ docstring calls it "the SINGLE source of WHICH cores build ... so
190
+ `emit_build_plan()` ... always enumerates slices the same way": `os:
191
+ "off"` is the one exclusion, no other. MIRRORED, not imported/edited --
192
+ `tan/planner/` is a hash-audited upstream mirror, and that function reads
193
+ a parsed `Slice.os`, not a raw board.yaml dict.
194
+
195
+ A core the planner never builds a slice for cannot run this model's
196
+ inference regardless of what `libraries:`/`inference:` says about it --
197
+ a project-wide `libraries: [tflite-micro]` entry (no `cores:` scoping)
198
+ must not mark a parked `os: "off"` core as "running inference" just
199
+ because nothing narrows it away."""
200
+ return core_slice.get("os") != "off"
201
+
202
+
203
+ def _core_uses_inference(board_doc: dict, core_id: str, core_slice: dict) -> bool:
204
+ """Does this PARTICIPATING (`_core_participates`) board.yaml core
205
+ genuinely run an inference workload? Mirrors `tan.planner.kconfig.
206
+ _slice_wants_inference`'s two independent signals (tan-cli#1288 review
207
+ round 2, finding 1) -- MIRRORED rather than imported: that function takes
208
+ a fully parsed `BoardProject`/`Slice` (`tan.core.system_manifest`), a
209
+ heavier object this module has no other reason to construct, and editing
210
+ `tan/planner/` itself is forbidden (it is a hash-audited upstream
211
+ mirror).
212
+
213
+ 1. `cores.<core_id>.inference:` declared at all -- app-level tuning
214
+ (`default_arena_kib:`), the signal every inference example declares.
215
+ 2. `libraries:` names `tflite-micro` -- the board.schema.json shape: a
216
+ TOP-LEVEL list, project-wide unless an entry's own `cores:` scopes it
217
+ to include this core (`core_entry`'s schema is `additionalProperties:
218
+ false` with no per-core `libraries:` property of its own -- tan-cli
219
+ #1288 review round 3, finding 2 -- so this is the ONLY shape to read).
220
+ The library signal exists because `inference:` is TUNING, not a
221
+ declaration of intent: an app that never overrides the arena default
222
+ has no reason to write the block at all (`_slice_wants_inference`'s
223
+ own docstring, alp-sdk #874). Matched on the LITERAL name only, never
224
+ the planner's own library-alias table (`library-aliases-v1.json`) --
225
+ and provably safe to: the schema's own library-name pattern
226
+ (`^[a-z][a-z0-9-]*$`, board.schema.json's `libraries[].name`) forbids
227
+ underscores, so the alias table's ONLY `tflite-micro` alias
228
+ (`"tflite_micro": "tflite-micro"`) can never appear as a `libraries:`
229
+ entry in any schema-valid board.yaml in the first place -- the
230
+ canonical spelling is the only one reachable here, not merely the
231
+ only one observed.
232
+
233
+ A core failing BOTH is exactly the AEN801-template shape a `paired_core`
234
+ could still name without this guard: `board.yaml` declaring the core key
235
+ (e.g. for an unrelated peripheral) is not the same fact as the core
236
+ running THIS NPU's model."""
237
+ if isinstance(core_slice.get("inference"), dict):
238
+ return True
239
+ raw_libraries = board_doc.get("libraries")
240
+ libraries = raw_libraries if isinstance(raw_libraries, list) else []
241
+ for entry in libraries:
242
+ if isinstance(entry, str):
243
+ name, scoped_cores = entry, None
244
+ elif isinstance(entry, dict):
245
+ name, scoped_cores = entry.get("name"), entry.get("cores")
246
+ else:
247
+ continue
248
+ if name != "tflite-micro":
249
+ continue
250
+ if scoped_cores is None:
251
+ return True # project-wide: every PARTICIPATING core (caller filters `os: "off"`)
252
+ if isinstance(scoped_cores, list) and core_id in scoped_cores:
253
+ return True
254
+ return False
255
+
256
+
257
+ def resolve_arena_budget(board_doc: dict | None, core_id: str | None) -> ArenaBudget:
258
+ """The arena budget for one ethos_u target, scoped to exactly what
259
+ board.yaml actually says (tan-cli#1288 review, rounds 1 and 2):
260
+
261
+ 1. @core_id resolved (`TargetSpec.paired_core`) AND that core is declared
262
+ under board.yaml's `cores:`, PARTICIPATES (`_core_participates`: not
263
+ parked `os: "off"`), AND runs an inference workload
264
+ (`_core_uses_inference`) -- `"single"`, CERTAIN: THIS core is what
265
+ runs this NPU, full stop.
266
+ 2. @core_id resolved but NOT declared under `cores:`, declared but parked
267
+ (`os: "off"`), or declared-and-participating but running no inference
268
+ at all -- `"unresolved"`: nothing in this project runs THIS model on
269
+ that core, so a board arena figure has no core to attach to. Never a
270
+ `DEFAULT_ARENA_KIB`-based guess.
271
+ 3. @core_id is `None` (a shared NPU with no pairing of its own, e.g. the
272
+ E8/E6/E4's own Ethos-U85) -- collect every board.yaml core that
273
+ PARTICIPATES and runs an inference workload (the only honest way to
274
+ guess "which core(s) might run this", absent a real pairing): exactly
275
+ one -- `"single"`, CERTAIN, use it; two or more -- `"range"` (the
276
+ caller decides what a `NO_FIT`/`FIT_UNVERIFIED` bound can honestly
277
+ claim across them); none at all -- `"unresolved"` (a bare
278
+ `DEFAULT_ARENA_KIB` here would be a number about nothing declared,
279
+ never certain enough to refuse a build on off one exact figure). A
280
+ parked `os: "off"` core is excluded here EVEN when a project-wide
281
+ `libraries:` entry (no `cores:` scoping) would otherwise mark it --
282
+ the planner never builds it a slice at all, so it cannot be the core
283
+ that runs this model regardless of what the library list says
284
+ (tan-cli#1288 review round 3, finding 1: the AEN801 template's own
285
+ `a32_cluster: {os: "off"}` used to count).
286
+ 4. @board_doc itself absent -- `"unresolved"`, same reasoning as 3's
287
+ empty case: nothing to inspect at all."""
288
+ if not isinstance(board_doc, dict):
289
+ return ArenaBudget(kind="unresolved",
290
+ reason="no board.yaml in hand; arena/SRAM0 residency not checked")
291
+ raw_cores = board_doc.get("cores")
292
+ cores = raw_cores if isinstance(raw_cores, dict) else {}
293
+
294
+ if core_id:
295
+ slice_ = cores.get(core_id)
296
+ if not isinstance(slice_, dict):
297
+ return ArenaBudget(kind="unresolved", reason=(
298
+ f"core {core_id!r} not declared in board.yaml; nothing in "
299
+ f"this project runs on it"))
300
+ if not _core_participates(slice_):
301
+ return ArenaBudget(kind="unresolved", reason=(
302
+ f"core {core_id!r} is parked (os: \"off\"); nothing in this "
303
+ f"project runs on it"))
304
+ if not _core_uses_inference(board_doc, core_id, slice_):
305
+ return ArenaBudget(kind="unresolved", reason=(
306
+ f"core {core_id!r} is declared in board.yaml but runs no "
307
+ f"inference workload (no inference: block, no tflite-micro "
308
+ f"library); nothing in this project runs THIS model on it"))
309
+ return ArenaBudget(kind="single", single_kib=_core_arena_kib_or_default(slice_),
310
+ cores=(core_id,))
311
+
312
+ inference_cores = sorted(
313
+ name for name, slice_ in cores.items()
314
+ if isinstance(slice_, dict) and _core_participates(slice_)
315
+ and _core_uses_inference(board_doc, name, slice_)
316
+ )
317
+ if not inference_cores:
318
+ return ArenaBudget(kind="unresolved", reason=(
319
+ "board.yaml declares no core running an inference workload; "
320
+ "arena/SRAM0 residency not checked"))
321
+ budgets = {name: _core_arena_kib_or_default(cores[name]) for name in inference_cores}
322
+ if len(inference_cores) == 1:
323
+ only = inference_cores[0]
324
+ return ArenaBudget(kind="single", single_kib=budgets[only], cores=(only,))
325
+ return ArenaBudget(kind="range", min_kib=min(budgets.values()),
326
+ max_kib=max(budgets.values()), cores=tuple(inference_cores))
327
+
328
+
329
+ def resolve_sram0_kib(sku: str, metadata_root: Path) -> tuple[float | None, str | None]:
330
+ """`(sram0_kib, None)` off the SoC-JSON variant @sku resolves to, or
331
+ `(None, reason)` when any leg of that resolution fails.
332
+
333
+ Reused, not re-walked: `tan.commands.build_output.read_sdk_som_and_soc`
334
+ is the SAME metadata-layout walk `tan size`/`tan debug-config` already
335
+ use (its own docstring: "must call ONE reader, not two that could
336
+ disagree"), and `tan.core.size.resolve_variant`/`sram_banks` are the SAME
337
+ pure variant/bank resolution `tan size`'s own budget uses -- this module
338
+ adds no second, potentially-drifting reader."""
339
+ walked = read_sdk_som_and_soc(str(metadata_root), sku)
340
+ if walked is None:
341
+ return None, f"unreadable SoM preset for {sku}; SRAM0 residency not checked"
342
+ _silicon, silicon_variant, variants, _soc_flash_mb, _soc_cores = walked
343
+ variant = resolve_variant(silicon_variant, sku, variants)
344
+ if variant is None:
345
+ return None, f"no SoC variant resolved for {sku}; SRAM0 residency not checked"
346
+ for name, kib in sram_banks(variant):
347
+ if name == "SRAM0":
348
+ return kib, None
349
+ return None, (f"the SoC variant resolved for {sku} declares no SRAM0 bank; "
350
+ f"SRAM0 residency not checked")
351
+
352
+
353
+ def _evaluate_sram0_single(blob_kib: int, arena_kib: int, sram0_total: float | None,
354
+ sram0_reason: str | None) -> FitVerdict:
355
+ """`blob_kib + arena_kib` against @sram0_total, for a `"single"` (CERTAIN)
356
+ arena resolution -- both sides of the sum are exact, sourced numbers."""
357
+ if sram0_total is None:
358
+ return FitVerdict(verdict=SKIPPED, reason=sram0_reason)
359
+ needed = blob_kib + arena_kib
360
+ limit_kib = int(sram0_total)
361
+ if needed > sram0_total:
362
+ return FitVerdict(verdict=NO_FIT, needed_kib=needed, limit_kib=limit_kib)
363
+ return FitVerdict(
364
+ verdict=FIT_UNVERIFIED, needed_kib=needed, limit_kib=limit_kib,
365
+ reason=(f"blob {blob_kib} + arena {arena_kib} = {needed} KiB of SRAM0; "
366
+ f"firmware's own SRAM0 use not checked"),
367
+ )
368
+
369
+
370
+ def _evaluate_sram0_range(blob_kib: int, budget: ArenaBudget, sram0_total: float | None,
371
+ sram0_reason: str | None) -> FitVerdict:
372
+ """The `"range"` shape's own SRAM0 three-way split (tan-cli#1288 review
373
+ round 2, finding 3) -- NOT the same two-way split `_evaluate_sram0_single`
374
+ uses, because neither bound alone may assert BOTH directions here:
375
+
376
+ * `NO_FIT` iff `blob_kib + min > total` -- fails even at the SMALLEST
377
+ declared core's budget, so every other candidate (which only needs
378
+ MORE SRAM0) fails too: a certain lower bound.
379
+ * `FIT_UNVERIFIED` iff `blob_kib + max <= total` -- fits even at the
380
+ LARGEST declared core's budget, so every candidate fits: as certain a
381
+ pass as any `FIT_UNVERIFIED` gets.
382
+ * Otherwise `SKIPPED`: some candidates would fit, others would not, and
383
+ nothing sourced picks between them -- asserting either word here would
384
+ overclaim (the exact defect round 2 found: the old code asserted
385
+ `FIT_UNVERIFIED` off the MIN alone, silently passing a blob that could
386
+ genuinely fail under a different, equally-plausible core)."""
387
+ if sram0_total is None:
388
+ return FitVerdict(verdict=SKIPPED, reason=sram0_reason)
389
+ limit_kib = int(sram0_total)
390
+ names = ", ".join(budget.cores)
391
+ needed_lo = blob_kib + budget.min_kib
392
+ needed_hi = blob_kib + budget.max_kib
393
+ if needed_lo > sram0_total:
394
+ return FitVerdict(
395
+ verdict=NO_FIT, needed_kib=needed_lo, limit_kib=limit_kib,
396
+ reason=(f"needs at least {needed_lo} KiB of SRAM0 even at the smallest "
397
+ f"declared core budget ({budget.min_kib} KiB across cores {names})"),
398
+ )
399
+ if needed_hi <= sram0_total:
400
+ return FitVerdict(
401
+ verdict=FIT_UNVERIFIED, needed_kib=needed_hi, limit_kib=limit_kib,
402
+ reason=(f"blob {blob_kib} + arena {budget.max_kib} KiB (the LARGEST declared "
403
+ f"core budget across cores {names}) = {needed_hi} KiB of SRAM0; "
404
+ f"firmware's own SRAM0 use not checked"),
405
+ )
406
+ return FitVerdict(verdict=SKIPPED, reason=(
407
+ f"ambiguous across cores {names} (declared budgets {budget.min_kib}-"
408
+ f"{budget.max_kib} KiB, needing {needed_lo}-{needed_hi} of {limit_kib} KiB "
409
+ f"SRAM0) with no paired_core to narrow which one actually runs this NPU"
410
+ ))
411
+
412
+
413
+ def _evaluate_sram0_lower_bound(blob_kib: int, reason: str,
414
+ sram0_total: float | None,
415
+ sram0_reason: str | None) -> FitVerdict:
416
+ """The `"unresolved"` shape's own SRAM0 check (tan-cli#1288 review round
417
+ 2, finding 2): with NO board.yaml core resolved at all, the real arena is
418
+ still bounded below by `MIN_ARENA_KIB` (`board.schema.json`'s own
419
+ `minimum`) -- so `blob_kib + MIN_ARENA_KIB` is a valid certain LOWER
420
+ BOUND a `NO_FIT` can be proven against, even though nothing here can
421
+ assert a PASS: the real arena could be arbitrarily larger. Only ever
422
+ `NO_FIT` or `SKIPPED`, NEVER `FIT_UNVERIFIED`."""
423
+ if sram0_total is None:
424
+ return FitVerdict(verdict=SKIPPED, reason=sram0_reason)
425
+ needed = blob_kib + MIN_ARENA_KIB
426
+ limit_kib = int(sram0_total)
427
+ if needed > sram0_total:
428
+ return FitVerdict(
429
+ verdict=NO_FIT, needed_kib=needed, limit_kib=limit_kib,
430
+ reason=(f"needs at least {needed} KiB of SRAM0 even at the smallest "
431
+ f"possible arena ({MIN_ARENA_KIB} KiB, board.schema.json's own "
432
+ f"default_arena_kib minimum) -- {reason}"),
433
+ )
434
+ return FitVerdict(verdict=SKIPPED, reason=reason)
435
+
436
+
437
+ def _evaluate_arena_single(req_sram_kib: int, arena_kib: int) -> FitVerdict:
438
+ if req_sram_kib > arena_kib:
439
+ return FitVerdict(verdict=NO_FIT, needed_kib=req_sram_kib, limit_kib=arena_kib)
440
+ return FitVerdict(verdict=FIT_UNVERIFIED, needed_kib=req_sram_kib, limit_kib=arena_kib,
441
+ reason=_ARENA_UNVERIFIED_REASON)
442
+
443
+
444
+ def _evaluate_arena_range(req_sram_kib: int, budget: ArenaBudget) -> FitVerdict:
445
+ lo, hi, names = budget.min_kib, budget.max_kib, ", ".join(budget.cores)
446
+ if req_sram_kib > hi:
447
+ return FitVerdict(
448
+ verdict=NO_FIT, needed_kib=req_sram_kib, limit_kib=hi,
449
+ reason=(f"exceeds even the largest declared core budget ({hi} KiB, "
450
+ f"across cores {names} with no paired_core to narrow which "
451
+ f"one actually runs this NPU)"),
452
+ )
453
+ if req_sram_kib <= lo:
454
+ return FitVerdict(
455
+ verdict=FIT_UNVERIFIED, needed_kib=req_sram_kib, limit_kib=lo,
456
+ reason=(f"{_ARENA_UNVERIFIED_REASON} (fits every declared core's budget, "
457
+ f"{lo}-{hi} KiB across cores {names})"),
458
+ )
459
+ return FitVerdict(verdict=SKIPPED, reason=(
460
+ f"ambiguous across cores {names} (declared budgets {lo}-{hi} KiB) with no "
461
+ f"paired_core to narrow which one actually runs this NPU; needs {req_sram_kib} KiB"
462
+ ))
463
+
464
+
465
+ def evaluate_sram_fit(*, memory_mode: str | None, req_sram_kib: int, blob_len_bytes: int,
466
+ board_doc: dict | None, paired_core: str | None,
467
+ sku: str, metadata_root: Path) -> SramFit:
468
+ """The whole check for one compiled ethos_u target.
469
+
470
+ `req_sram_kib` is the arena, already ceil'd
471
+ (`tan.model.adapters.ethos_u._footprint`, tan-cli#1011's own arena-only
472
+ scope -- never re-derived here). `blob_len_bytes` is `len(Blob.payload)`,
473
+ the compiled weights -- the SECOND half of `Sram_Only` residency #1288
474
+ exists to check, which nothing read before this module.
475
+
476
+ `SramFit.no_fit` is what `tan model build` refuses on; `SramFit.as_dict()`
477
+ is the `sramFit` envelope block `tan model check --exact` attaches.
478
+ `resolve_arena_budget`'s own docstring is the source of truth for exactly
479
+ which of its three shapes may ever produce a `NO_FIT` here."""
480
+ if memory_mode != SRAM_ONLY_MODE:
481
+ return _skipped_not_sram_only(memory_mode)
482
+
483
+ budget = resolve_arena_budget(board_doc, paired_core)
484
+ blob_kib = math.ceil(blob_len_bytes / 1024) if blob_len_bytes > 0 else 0
485
+ sram0_total, sram0_reason = resolve_sram0_kib(sku, metadata_root)
486
+
487
+ if budget.kind == "unresolved":
488
+ arena = FitVerdict(verdict=SKIPPED, reason=budget.reason)
489
+ sram0 = _evaluate_sram0_lower_bound(blob_kib, budget.reason, sram0_total, sram0_reason)
490
+ return SramFit(arena=arena, sram0=sram0, blob_kib=blob_kib)
491
+
492
+ if budget.kind == "single":
493
+ arena = _evaluate_arena_single(req_sram_kib, budget.single_kib)
494
+ sram0 = _evaluate_sram0_single(blob_kib, budget.single_kib, sram0_total, sram0_reason)
495
+ return SramFit(arena=arena, sram0=sram0, blob_kib=blob_kib)
496
+
497
+ # budget.kind == "range": paired_core is None and MORE THAN ONE board core
498
+ # runs inference -- see `_evaluate_sram0_range`'s own docstring for the
499
+ # three-way split it uses instead of `_evaluate_sram0_single`'s two-way one.
500
+ arena = _evaluate_arena_range(req_sram_kib, budget)
501
+ sram0 = _evaluate_sram0_range(blob_kib, budget, sram0_total, sram0_reason)
502
+ return SramFit(arena=arena, sram0=sram0, blob_kib=blob_kib)