rocket-engine 0.0.2b1__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 (241) hide show
  1. rocket_engine/__init__.py +8 -0
  2. rocket_engine/__main__.py +12 -0
  3. rocket_engine/adapters/README.md +54 -0
  4. rocket_engine/adapters/__init__.py +7 -0
  5. rocket_engine/adapters/api.py +39 -0
  6. rocket_engine/adapters/base.py +46 -0
  7. rocket_engine/adapters/cace/__init__.py +163 -0
  8. rocket_engine/adapters/circulax/README.md +10 -0
  9. rocket_engine/adapters/circulax/__init__.py +112 -0
  10. rocket_engine/adapters/circulax/_driver.py +190 -0
  11. rocket_engine/adapters/circulax/adapter.py +538 -0
  12. rocket_engine/adapters/circulax/results.py +291 -0
  13. rocket_engine/adapters/cli/README.md +34 -0
  14. rocket_engine/adapters/cli/__init__.py +305 -0
  15. rocket_engine/adapters/connectivity/__init__.py +196 -0
  16. rocket_engine/adapters/connectivity/results.py +214 -0
  17. rocket_engine/adapters/cosim/README.md +9 -0
  18. rocket_engine/adapters/cosim/__init__.py +1390 -0
  19. rocket_engine/adapters/cosim/results.py +214 -0
  20. rocket_engine/adapters/devsim/__init__.py +171 -0
  21. rocket_engine/adapters/digital_flow/README.md +9 -0
  22. rocket_engine/adapters/digital_flow/__init__.py +1121 -0
  23. rocket_engine/adapters/digital_flow/results.py +227 -0
  24. rocket_engine/adapters/digital_flow/state.py +462 -0
  25. rocket_engine/adapters/drc/README.klayout.md +7 -0
  26. rocket_engine/adapters/drc/README.md +7 -0
  27. rocket_engine/adapters/drc/__init__.py +1 -0
  28. rocket_engine/adapters/drc/klayout.py +952 -0
  29. rocket_engine/adapters/drc/magic.py +287 -0
  30. rocket_engine/adapters/drc/results.py +184 -0
  31. rocket_engine/adapters/drc/results_kl.py +188 -0
  32. rocket_engine/adapters/elmer/__init__.py +163 -0
  33. rocket_engine/adapters/envelope.py +120 -0
  34. rocket_engine/adapters/equivalence/__init__.py +156 -0
  35. rocket_engine/adapters/errors.py +181 -0
  36. rocket_engine/adapters/extract_magic/README.md +15 -0
  37. rocket_engine/adapters/extract_magic/__init__.py +425 -0
  38. rocket_engine/adapters/extract_magic/results.py +209 -0
  39. rocket_engine/adapters/formal_sby/README.md +7 -0
  40. rocket_engine/adapters/formal_sby/__init__.py +773 -0
  41. rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
  42. rocket_engine/adapters/formal_sby/results.py +185 -0
  43. rocket_engine/adapters/gdsfactory/__init__.py +194 -0
  44. rocket_engine/adapters/gdsfactory/_driver.py +459 -0
  45. rocket_engine/adapters/gdsfactory/adapter.py +609 -0
  46. rocket_engine/adapters/gdsfactory/results.py +181 -0
  47. rocket_engine/adapters/hdl_sim/README.md +7 -0
  48. rocket_engine/adapters/hdl_sim/__init__.py +369 -0
  49. rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
  50. rocket_engine/adapters/hdl_sim/results.py +178 -0
  51. rocket_engine/adapters/hdl_sim/verilator.py +167 -0
  52. rocket_engine/adapters/http/README.md +15 -0
  53. rocket_engine/adapters/http/__init__.py +458 -0
  54. rocket_engine/adapters/http/events.py +63 -0
  55. rocket_engine/adapters/ipc/README.md +9 -0
  56. rocket_engine/adapters/ipc/__init__.py +111 -0
  57. rocket_engine/adapters/lvs_netgen/README.md +20 -0
  58. rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
  59. rocket_engine/adapters/lvs_netgen/results.py +168 -0
  60. rocket_engine/adapters/mcp/README.md +19 -0
  61. rocket_engine/adapters/mcp/__init__.py +85 -0
  62. rocket_engine/adapters/mcp/api-schema.json +6107 -0
  63. rocket_engine/adapters/mcp/pull.py +246 -0
  64. rocket_engine/adapters/mcp/server.py +794 -0
  65. rocket_engine/adapters/meep/__init__.py +167 -0
  66. rocket_engine/adapters/meow/README.md +7 -0
  67. rocket_engine/adapters/meow/__init__.py +190 -0
  68. rocket_engine/adapters/meow/_driver.py +227 -0
  69. rocket_engine/adapters/meow/_envelope.py +108 -0
  70. rocket_engine/adapters/meow/adapter.py +491 -0
  71. rocket_engine/adapters/meow/results.py +218 -0
  72. rocket_engine/adapters/mutation/__init__.py +192 -0
  73. rocket_engine/adapters/ngspice/README.md +28 -0
  74. rocket_engine/adapters/ngspice/__init__.py +627 -0
  75. rocket_engine/adapters/ngspice/results.py +215 -0
  76. rocket_engine/adapters/postlayout/README.md +28 -0
  77. rocket_engine/adapters/postlayout/__init__.py +17 -0
  78. rocket_engine/adapters/postlayout/adapter.py +1062 -0
  79. rocket_engine/adapters/postlayout/results.py +330 -0
  80. rocket_engine/adapters/postlayout/translate.py +71 -0
  81. rocket_engine/adapters/profile.py +220 -0
  82. rocket_engine/adapters/sax/README.md +23 -0
  83. rocket_engine/adapters/sax/__init__.py +516 -0
  84. rocket_engine/adapters/sax/_driver.py +148 -0
  85. rocket_engine/adapters/sax/results.py +291 -0
  86. rocket_engine/adapters/stdio_console.py +1151 -0
  87. rocket_engine/adapters/syntax/README.md +5 -0
  88. rocket_engine/adapters/syntax/__init__.py +366 -0
  89. rocket_engine/adapters/syntax/results.py +181 -0
  90. rocket_engine/adapters/synth_yosys/README.md +7 -0
  91. rocket_engine/adapters/synth_yosys/__init__.py +655 -0
  92. rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
  93. rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
  94. rocket_engine/adapters/synth_yosys/results.py +173 -0
  95. rocket_engine/adapters/timing_sta/README.md +9 -0
  96. rocket_engine/adapters/timing_sta/__init__.py +726 -0
  97. rocket_engine/adapters/view_sync/__init__.py +185 -0
  98. rocket_engine/adapters/view_sync/results.py +189 -0
  99. rocket_engine/adapters/vlsir/__init__.py +142 -0
  100. rocket_engine/adapters/xschem/__init__.py +440 -0
  101. rocket_engine/adapters/xschem/deck.py +266 -0
  102. rocket_engine/clients/__init__.py +5 -0
  103. rocket_engine/clients/runtime.py +255 -0
  104. rocket_engine/composition.py +2279 -0
  105. rocket_engine/execution/README.md +30 -0
  106. rocket_engine/execution/__init__.py +150 -0
  107. rocket_engine/execution/apig/__init__.py +1 -0
  108. rocket_engine/execution/apig/pool.py +463 -0
  109. rocket_engine/execution/apig/submit.py +124 -0
  110. rocket_engine/execution/block_runs_scope.py +479 -0
  111. rocket_engine/execution/coalesce.py +414 -0
  112. rocket_engine/execution/console/__init__.py +1 -0
  113. rocket_engine/execution/console/apply_session.py +318 -0
  114. rocket_engine/execution/console/lifecycle.py +265 -0
  115. rocket_engine/execution/dispatch.py +205 -0
  116. rocket_engine/execution/errors.py +31 -0
  117. rocket_engine/execution/ports.py +797 -0
  118. rocket_engine/execution/pyexec/__init__.py +1 -0
  119. rocket_engine/execution/pyexec/apply_context.py +547 -0
  120. rocket_engine/execution/pyexec/approvals.py +173 -0
  121. rocket_engine/execution/pyexec/runner.py +404 -0
  122. rocket_engine/execution/pyexec/support.py +464 -0
  123. rocket_engine/execution/python_exec.py +897 -0
  124. rocket_engine/execution/queue.py +85 -0
  125. rocket_engine/execution/replay_safe.py +601 -0
  126. rocket_engine/execution/routing/__init__.py +1 -0
  127. rocket_engine/execution/routing/calls.py +364 -0
  128. rocket_engine/execution/routing/namespace.py +785 -0
  129. rocket_engine/execution/routing/ports.py +102 -0
  130. rocket_engine/execution/routing/tools.py +346 -0
  131. rocket_engine/execution/runner.py +311 -0
  132. rocket_engine/execution/runtime.py +1149 -0
  133. rocket_engine/execution/service.py +279 -0
  134. rocket_engine/execution/worker.py +464 -0
  135. rocket_engine/interfaces/README.md +26 -0
  136. rocket_engine/interfaces/__init__.py +58 -0
  137. rocket_engine/interfaces/apig_assets.py +407 -0
  138. rocket_engine/interfaces/celllink.py +461 -0
  139. rocket_engine/interfaces/documents.py +92 -0
  140. rocket_engine/interfaces/envelopes.py +131 -0
  141. rocket_engine/interfaces/errors.py +16 -0
  142. rocket_engine/interfaces/layout.py +410 -0
  143. rocket_engine/interfaces/layout_schema.yaml +278 -0
  144. rocket_engine/interfaces/paths.py +223 -0
  145. rocket_engine/interfaces/pdks.py +550 -0
  146. rocket_engine/interfaces/ports.py +72 -0
  147. rocket_engine/interfaces/references.py +115 -0
  148. rocket_engine/interfaces/refs.py +85 -0
  149. rocket_engine/interfaces/runparams.py +304 -0
  150. rocket_engine/interfaces/setup.py +552 -0
  151. rocket_engine/interfaces/setup_catalogue.yaml +243 -0
  152. rocket_engine/interfaces/setup_probes.py +66 -0
  153. rocket_engine/interfaces/vocabulary.py +58 -0
  154. rocket_engine/interfaces/yaml_dump.py +31 -0
  155. rocket_engine/library/README.md +22 -0
  156. rocket_engine/library/__init__.py +13 -0
  157. rocket_engine/library/apig_catalog.py +371 -0
  158. rocket_engine/library/catalog.py +185 -0
  159. rocket_engine/library/errors.py +16 -0
  160. rocket_engine/mutate/__init__.py +776 -0
  161. rocket_engine/mutate/operators.py +700 -0
  162. rocket_engine/ops/__init__.py +1 -0
  163. rocket_engine/ops/deferred.py +279 -0
  164. rocket_engine/ops/editors.py +404 -0
  165. rocket_engine/ops/evidence.py +194 -0
  166. rocket_engine/ops/gates.py +262 -0
  167. rocket_engine/ops/ports.py +76 -0
  168. rocket_engine/ops/queries.py +417 -0
  169. rocket_engine/ops/runs.py +553 -0
  170. rocket_engine/ops/submit.py +614 -0
  171. rocket_engine/project/README.md +27 -0
  172. rocket_engine/project/__init__.py +6 -0
  173. rocket_engine/project/canvas/__init__.py +55 -0
  174. rocket_engine/project/canvas/compile.py +109 -0
  175. rocket_engine/project/canvas/integrate.py +48 -0
  176. rocket_engine/project/canvas/schema.py +241 -0
  177. rocket_engine/project/canvas/templates.py +46 -0
  178. rocket_engine/project/canvas/validate.py +603 -0
  179. rocket_engine/project/edit/__init__.py +5 -0
  180. rocket_engine/project/edit/cas.py +87 -0
  181. rocket_engine/project/edit/transactions.py +282 -0
  182. rocket_engine/project/errors.py +16 -0
  183. rocket_engine/project/frontend13.py +685 -0
  184. rocket_engine/project/geometry/__init__.py +23 -0
  185. rocket_engine/project/geometry/ops.py +451 -0
  186. rocket_engine/project/geometry/snap.py +103 -0
  187. rocket_engine/project/ip/__init__.py +1 -0
  188. rocket_engine/project/ip/blocks.py +195 -0
  189. rocket_engine/project/ip/entry.schema.json +78 -0
  190. rocket_engine/project/ip/lifecycle.py +402 -0
  191. rocket_engine/project/ip/verbs.py +123 -0
  192. rocket_engine/project/layers/__init__.py +6 -0
  193. rocket_engine/project/layers/bindings.py +115 -0
  194. rocket_engine/project/layers/catalog.py +228 -0
  195. rocket_engine/project/layers/presets.py +43 -0
  196. rocket_engine/project/ledger/__init__.py +1 -0
  197. rocket_engine/project/ledger/docs.py +167 -0
  198. rocket_engine/project/ledger/entities.py +176 -0
  199. rocket_engine/project/ledger/events.py +29 -0
  200. rocket_engine/project/ledger/runs.py +257 -0
  201. rocket_engine/project/ledger/shared.py +174 -0
  202. rocket_engine/project/ledger/snapshots.py +385 -0
  203. rocket_engine/project/ledger/wires.py +552 -0
  204. rocket_engine/project/process/__init__.py +14 -0
  205. rocket_engine/project/process/model.py +193 -0
  206. rocket_engine/project/process/recipes.py +121 -0
  207. rocket_engine/project/schematic/__init__.py +1 -0
  208. rocket_engine/project/schematic/hierarchy.py +312 -0
  209. rocket_engine/project/schematic/instances.py +393 -0
  210. rocket_engine/project/schematic/sheets.py +198 -0
  211. rocket_engine/project/service.py +2941 -0
  212. rocket_engine/project/symbols/__init__.py +1 -0
  213. rocket_engine/project/symbols/cells.py +324 -0
  214. rocket_engine/project/symbols/linkage.py +356 -0
  215. rocket_engine/project/symbols/symbols.py +352 -0
  216. rocket_engine/project/templates/__init__.py +303 -0
  217. rocket_engine/project/templates/errors.py +25 -0
  218. rocket_engine/project/testbench/__init__.py +49 -0
  219. rocket_engine/project/testbench/blocks.py +619 -0
  220. rocket_engine/project/testbench/templates.py +134 -0
  221. rocket_engine/stores/README.md +12 -0
  222. rocket_engine/stores/__init__.py +6 -0
  223. rocket_engine/stores/ecs.py +1508 -0
  224. rocket_engine/stores/layers.py +1023 -0
  225. rocket_engine/stores/settings.py +1188 -0
  226. rocket_engine/stores/wireplace.py +1324 -0
  227. rocket_engine/workflows/README.md +18 -0
  228. rocket_engine/workflows/__init__.py +53 -0
  229. rocket_engine/workflows/capability_plan.py +102 -0
  230. rocket_engine/workflows/completion.py +228 -0
  231. rocket_engine/workflows/connectgraph.py +734 -0
  232. rocket_engine/workflows/connectgraph_emit.py +336 -0
  233. rocket_engine/workflows/gates.py +144 -0
  234. rocket_engine/workflows/photonic_wire.py +76 -0
  235. rocket_engine/workflows/stages.py +324 -0
  236. rocket_engine/workflows/viewsync.py +399 -0
  237. rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
  238. rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
  239. rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
  240. rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
  241. rocket_engine-0.0.2b1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,8 @@
1
+ """RocketEDA shared tooling engine.
2
+
3
+ No logic lives here yet. This package only declares the module boundaries
4
+ so the locked environment and the bundled build can be proven before any
5
+ engine behavior lands. See README.md for the module map and coding rules.
6
+ """
7
+
8
+ __version__ = "0.0.1"
@@ -0,0 +1,12 @@
1
+ """`python -m rocket_engine`: the `rocket-engine` command surface.
2
+
3
+ Thin entry over the CLI inbound adapter (`adapters.cli`); a
4
+ `[project.scripts]` binary alias can point here once packaging
5
+ wires it. Exits: 0 execution-ok (even FAIL verdicts), 1 engine
6
+ error, 2 usage error.
7
+ """
8
+
9
+ from .adapters.cli import main
10
+
11
+ if __name__ == "__main__":
12
+ raise SystemExit(main())
@@ -0,0 +1,54 @@
1
+ # adapters — transport edges and solver integrations
2
+
3
+ This package contains the client transports and wrappers around existing EDA tools. composition.Engine selects a registered solver for each operation. Adapters return normalized results; project state and verdict rules stay in engine services.
4
+
5
+ ## Client transports
6
+
7
+ | Adapter | Current surface |
8
+ |---|---|
9
+ | cli/ | Interactive command loop, one-operation mode, sequential --batch, and JSON output; entry point is python -m rocket_engine. |
10
+ | mcp/ | Project-scoped MCP stdio server with one tool per catalog operation/query; descriptions come from handler docstrings. Most input schemas remain generic. |
11
+ | http/ | HttpTransport.handle routes one JSON command/query envelope; serve_http adds a FastAPI/uvicorn POST listener on loopback only (no auth, ADR-006). SSE is not implemented here. |
12
+ | ipc/ | IpcTransport.handle routes one JSON command/query envelope. Length-prefix framing and Electron child-process lifecycle are not implemented here. |
13
+
14
+ Events are read through the engine event-query surface; HTTP SSE and pushed IPC events are not currently provided by these transport classes.
15
+
16
+ ## Registered solver adapters
17
+
18
+ | Operation | Registered tool(s) |
19
+ |---|---|
20
+ | run_drc | Magic (default), KLayout |
21
+ | run_hdl_test, run_syntax, run_formal, run_synth, run_timing | Icarus, Slang, SBY, Yosys, OpenSTA |
22
+ | run_spice, run_post_layout, run_cosim | ngspice; run_cosim also uses Icarus |
23
+ | run_optical, run_em | SAX/Circulax, MEOW |
24
+ | run_extraction, run_lvs | Magic, Netgen |
25
+ | run_connectivity, run_erc, run_view_sync | connectgraph |
26
+ | generate_views | GDSFactory |
27
+ | run_digital_flow | LibreLane |
28
+
29
+ The exact registration order and default tool are in [composition.py](../composition.py). A registered adapter can still be blocked by a missing executable, incompatible PDK/deck, or an unqualified flow. Use get_capabilities for machine-specific readiness; see the [DIGITAL-FLOW report](../../docs/work/DIGITAL-FLOW/report.md) for the current live-flow limitation.
30
+
31
+ ## Gateway rule: api contract + port adapter
32
+
33
+ `api.py` is the one inbound port: it builds the single submit
34
+ envelope (project_id, operation, arguments, minted-or-caller
35
+ request_id) and calls the engine. Every gateway is that contract
36
+ plus one framing adapter, composed — never inherited:
37
+
38
+ - http/mcp/ipc `handle`: parse the wire frame, enforce scope, call
39
+ `api.submit`, serialize the result or error envelope.
40
+ - cli/console `OpDispatcher.execute`: parse op-call text, stamp the
41
+ head revision on operations, call `api.submit`.
42
+
43
+ Gateways own only I/O framing and wire-appropriate errors
44
+ (UsageError on text wires, AdapterError on framed wires); the
45
+ envelope shape, error payloads (`errors.error_payload`), and status
46
+ mapping (`errors.status_for_code`) exist exactly once and are
47
+ shared. A new transport adds one framing module over `api.submit`
48
+ — never a second envelope builder, never a transport subclass.
49
+
50
+ ## Boundaries
51
+
52
+ Transport adapters validate the envelope and project scope, then call the api contract (`api.submit` → Engine.submit). Solver adapters implement the ToolAdapter port and own tool invocation and output parsing. The intended module boundary forbids solver adapters from importing one another. Current exception: CosimAdapter calls HdlSimAdapter directly for its digital half; treat this as existing boundary debt, not a pattern to extend.
53
+
54
+ For endpoint documentation rules, see [AGENTS.md](../../AGENTS.md). For operation names, see [interfaces](../interfaces/README.md) and the [generated API reference](../../docs/engine-api.md).
@@ -0,0 +1,7 @@
1
+ """One duplex adapter per channel: transports and solver wrappers.
2
+ Maps at the edge; holds no rules, state, or verdicts."""
3
+
4
+ from ..interfaces import ToolAdapter
5
+ from .errors import AdapterError
6
+
7
+ __all__ = ["AdapterError", "ToolAdapter"]
@@ -0,0 +1,39 @@
1
+ """API contract: the one inbound port every gateway calls.
2
+
3
+ Gateways (http, mcp, ipc, cli/dispatcher, console) own only I/O
4
+ framing — parsing their wire format and serializing results. They
5
+ never build submit envelopes themselves and never subclass each
6
+ other: every gateway composes over `submit` here (composition over
7
+ inheritance), so the envelope shape, request-id minting, and error
8
+ propagation exist exactly once.
9
+
10
+ Validation stays at the boundaries: gateways reject unknown names
11
+ with their wire-appropriate error (UsageError on text wires,
12
+ AdapterError on framed wires), and Engine.submit backstops with
13
+ unsupported_operation. Engine errors propagate untouched — mapping
14
+ them to wire envelopes is framing, owned by each gateway with the
15
+ shared shapes from `adapters.errors`.
16
+ """
17
+
18
+ import uuid
19
+
20
+
21
+ def submit(engine, project_id, operation, arguments, request_id=None):
22
+ """Submit one call through the engine port.
23
+
24
+ Args:
25
+ engine: Engine (or stand-in) exposing `submit`.
26
+ project_id: Project the call runs against.
27
+ operation: Cataloged operation or query name.
28
+ arguments: Operation arguments mapping.
29
+ request_id: Envelope id; minted when omitted.
30
+
31
+ Returns:
32
+ Engine result for the call (ack, completion, or data).
33
+ """
34
+ return engine.submit({
35
+ "project_id": project_id,
36
+ "operation": operation,
37
+ "arguments": dict(arguments),
38
+ "request_id": request_id or uuid.uuid4().hex,
39
+ })
@@ -0,0 +1,46 @@
1
+ """Shared transport helpers: JSON parsing and project-scope enforcement."""
2
+
3
+ import json
4
+
5
+ from .errors import AdapterError
6
+
7
+
8
+ def parse_json(text):
9
+ """Parse one JSON object request; reject anything else loudly.
10
+
11
+ Args:
12
+ text: Raw request text, expected to be one JSON object.
13
+
14
+ Returns:
15
+ Parsed request mapping.
16
+
17
+ Raises:
18
+ AdapterError: invalid_command when the text is not JSON or
19
+ not an object.
20
+ """
21
+ try:
22
+ data = json.loads(text)
23
+ except Exception as exc:
24
+ raise AdapterError("invalid_command", f"Cannot parse request: {exc}") from exc
25
+ if not isinstance(data, dict):
26
+ raise AdapterError("invalid_command", "Request must be a JSON object.")
27
+ return data
28
+
29
+
30
+ def enforce_scope(envelope_project, bound_project):
31
+ """Deny envelopes outside the bound project; allow unbound use.
32
+
33
+ Args:
34
+ envelope_project: Project id carried by the envelope.
35
+ bound_project: Project this transport is bound to, or None
36
+ for unbound (any envelope passes).
37
+
38
+ Raises:
39
+ AdapterError: cross_project_denied for an envelope outside
40
+ the bound scope — denied, never routed.
41
+ """
42
+ if bound_project is not None and envelope_project != bound_project:
43
+ raise AdapterError(
44
+ "cross_project_denied",
45
+ f"Bound to {bound_project}, got {envelope_project}.",
46
+ )
@@ -0,0 +1,163 @@
1
+ """CACE characterization adapter: run_characterize port."""
2
+
3
+ import re
4
+ import shutil
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ from ...execution.errors import ExecutionError
9
+ from ...interfaces import ToolAdapter
10
+ from ..errors import AdapterError
11
+
12
+ OPERATION = "run_characterize"
13
+ _TOOL = "cace"
14
+ _TIMEOUT_S = 900
15
+ _STATUS_RE = re.compile(r"\|\s*(Pass|Fail|Error|Unknown|Skip|Cancel)\s",
16
+ re.IGNORECASE)
17
+
18
+
19
+ class CaceAdapter(ToolAdapter):
20
+ """run_characterize via cace: CLI run plus summary.md verdicts.
21
+
22
+ Serves the run_characterize operation with tool `cace`. Runs
23
+ `cace <datasheet>` as a subprocess and gates on the exit code
24
+ (0 pass, 2 spec miss, 1/3/4 error/unknown/canceled); values
25
+ come from summary.md rows plus simulation_summary.csv, never
26
+ flow.log scraping. In-process reads reuse pure helpers only.
27
+
28
+ Job keys:
29
+ datasheet (required): Input datasheet YAML path.
30
+ run_path: Run dir parent (RUNDIR lands beneath it).
31
+ workdir: Run directory (default ".").
32
+ timeout_s: Budget in seconds (default 900).
33
+
34
+ Args:
35
+ runner: Optional isolated runner; direct subprocess when None.
36
+ """
37
+
38
+ def __init__(self, runner=None):
39
+ self._runner = runner
40
+
41
+ def capabilities(self, target):
42
+ """Report the served operation and tool.
43
+
44
+ Args:
45
+ target: Target the capabilities are reported for.
46
+
47
+ Returns:
48
+ Capability mapping with operations and tool.
49
+ """
50
+ return {"operations": [OPERATION], "tool": _TOOL}
51
+
52
+ def preflight(self, job):
53
+ """Check operation, datasheet, and cace presence.
54
+
55
+ Args:
56
+ job: Solver job with operation run_characterize.
57
+
58
+ Returns:
59
+ Checked mapping with datasheet, binary, timeout.
60
+
61
+ Raises:
62
+ AdapterError: unsupported_operation, invalid_reference
63
+ for a missing datasheet, missing_dependency with
64
+ BLOCKED-needs-host when cace is absent (2.13.0 pin).
65
+ """
66
+ if job.get("operation") != OPERATION:
67
+ raise AdapterError(
68
+ "unsupported_operation",
69
+ f"Cace handles {OPERATION}, got {job.get('operation')}.")
70
+ datasheet = job.get("datasheet")
71
+ if not datasheet or not Path(datasheet).is_file():
72
+ raise AdapterError(
73
+ "invalid_reference", f"Datasheet not found: {datasheet}.")
74
+ binary = shutil.which(_TOOL)
75
+ if binary is None:
76
+ raise AdapterError(
77
+ "missing_dependency",
78
+ "BLOCKED-needs-host: cace absent (need cace==2.13.0).")
79
+ try:
80
+ __import__("cace")
81
+ except ImportError as exc:
82
+ raise AdapterError(
83
+ "missing_dependency",
84
+ "BLOCKED-needs-host: cace python package absent.",
85
+ ) from exc
86
+ timeout = job.get("timeout_s", _TIMEOUT_S)
87
+ if (not isinstance(timeout, (int, float))
88
+ or isinstance(timeout, bool) or not timeout > 0):
89
+ raise AdapterError("invalid_command", f"Bad timeout: {timeout!r}.")
90
+ checked = {"datasheet": str(datasheet), "binary": binary,
91
+ "timeout_s": timeout,
92
+ "run_path": job.get("run_path")}
93
+ if self._runner is not None:
94
+ try:
95
+ self._runner.preflight()
96
+ except ExecutionError as exc:
97
+ raise AdapterError(exc.code, str(exc)) from exc
98
+ return checked
99
+
100
+ def execute(self, job):
101
+ """Run cace on the datasheet under a timeout.
102
+
103
+ Args:
104
+ job: Solver job with operation run_characterize.
105
+
106
+ Returns:
107
+ Native output with exit_code, stdout/stderr, summary
108
+ markdown, and run dir.
109
+ """
110
+ checked = self.preflight(job)
111
+ workdir = job.get("workdir", ".")
112
+ if not Path(workdir).is_dir():
113
+ raise AdapterError(
114
+ "invalid_reference", f"Workdir not found: {workdir}.")
115
+ argv = [checked["binary"], checked["datasheet"],
116
+ "--no-progress-bar"]
117
+ if checked["run_path"]:
118
+ argv.extend(["--run-path", str(checked["run_path"])])
119
+ try:
120
+ proc = subprocess.run(argv, capture_output=True, text=True,
121
+ timeout=checked["timeout_s"], cwd=workdir)
122
+ except subprocess.TimeoutExpired as exc:
123
+ raise AdapterError(
124
+ "runner_unavailable", "Cace run timed out.") from exc
125
+ summary = ""
126
+ run_dir = checked["run_path"] or workdir
127
+ for cand in sorted(Path(run_dir).glob("RUN_*/summary.md")):
128
+ summary = cand.read_text(errors="replace")
129
+ run_dir = str(cand.parent)
130
+ break
131
+ return {"exit_code": proc.returncode, "stdout": proc.stdout,
132
+ "stderr": proc.stderr, "summary_md": summary,
133
+ "run_dir": run_dir}
134
+
135
+ def parse(self, native_output):
136
+ """Parse summary.md status cells to pass/fail verdict.
137
+
138
+ Args:
139
+ native_output: Native output with summary markdown.
140
+
141
+ Returns:
142
+ {total, violations[], source}.
143
+
144
+ Raises:
145
+ AdapterError: runner_unavailable with no summary.
146
+ """
147
+ summary = native_output.get("summary_md")
148
+ if not summary or not _STATUS_RE.search(summary):
149
+ raise AdapterError(
150
+ "runner_unavailable", "Cace run left no summary.md.")
151
+ bad = [m for m in ("Fail", "Error") if m.lower() in summary.lower()]
152
+ if native_output.get("exit_code", 0) not in (0,) or bad:
153
+ fails = [line.strip() for line in summary.splitlines()
154
+ if "Fail" in line or "Error" in line]
155
+ violations = [{"rule": "characterize_fail", "severity": "error",
156
+ "message": line[:300] or "Cace spec miss."}
157
+ for line in fails] or [{
158
+ "rule": "characterize_fail",
159
+ "severity": "error",
160
+ "message": "Cace run failed."}]
161
+ return {"total": len(violations), "violations": violations,
162
+ "source": "summary_md"}
163
+ return {"total": 0, "violations": [], "source": "summary_md"}
@@ -0,0 +1,10 @@
1
+ # Circulax time-domain adapter
2
+
3
+ The Circulax package has two roles:
4
+
5
+ - Its RC step smoke qualifies the installed library with a small, known-good and known-bad circuit. It returns solver version, time samples, capacitor voltage, and final voltage.
6
+ - CirculaxAdapter is registered for the run_optical operation as the circulax deck tool. Engine jobs provide the simulation inputs and receive normalized results.
7
+
8
+ The smoke uses a voltage step, resistor, and capacitor compiled through kfnetlist. With Circulax 0.2.5, the 1 kΩ / 1 nF case reaches 0.9933 V at 5 μs; the 100 kΩ / 1 nF case reaches 0.0488 V and misses the threshold. A missing Circulax dependency reports missing_dependency rather than a fabricated verdict.
9
+
10
+ See [adapter.py](adapter.py) and the [solver map](../README.md).
@@ -0,0 +1,112 @@
1
+ """Circulax time-domain smoke: smallest honest RC transient.
2
+
3
+ A V step driving R + C to GND, compiled via kfnetlist and run with
4
+ `Circuit.transient` from a discharged (`y0=zeros`) start. Verified
5
+ against circulax 0.2.5: good RC (1k/1nF, tau=1us) charges
6
+ Vc(1us)=0.6321, Vc(5us)=0.9933; slow RC (100k/1nF) sits at 0.0488.
7
+ The same 0.9 V @5us threshold passes the good run and fails the slow
8
+ run — the good-vs-bad signal. Solver imports stay lazy so bare envs
9
+ fail loud (`missing_dependency`), never fake-green. A full
10
+ `deck.tool: circulax` Engine adapter is follow-up, out of scope.
11
+ """
12
+
13
+ from ..errors import AdapterError
14
+
15
+ CIRCULAX_PIN = "0.2.5"
16
+
17
+
18
+ def circulax_version():
19
+ """Proven solver version, observable per run (uv.lock pin).
20
+
21
+ Returns:
22
+ Installed circulax version, or "unknown" when the package
23
+ carries no version attribute.
24
+
25
+ Raises:
26
+ AdapterError: missing_dependency when circulax is not
27
+ importable.
28
+ """
29
+ try:
30
+ import circulax
31
+ except ImportError as exc:
32
+ raise AdapterError(
33
+ "missing_dependency",
34
+ f"circulax is not importable: {exc} "
35
+ "(uv sync).",
36
+ ) from exc
37
+ return getattr(circulax, "__version__", "unknown")
38
+
39
+
40
+ def run_rc_step(*, R=1e3, C=1e-9, V=1.0, t1=5e-6, points=11, dt0=1e-8):
41
+ """Run the RC step transient live; return the cap voltage trace.
42
+
43
+ Args:
44
+ R: Resistance in ohms (positive).
45
+ C: Capacitance in farads (positive).
46
+ V: Step voltage in volts.
47
+ t1: End time in seconds (positive).
48
+ points: Samples on the grid (at least 2).
49
+ dt0: Initial solver step in seconds (positive).
50
+
51
+ Returns:
52
+ `{circulax_version, R, C, V, t_s[], vc_V[], final_V}` —
53
+ plain floats, JSON-friendly.
54
+
55
+ Raises:
56
+ AdapterError: invalid_command on bad parameters,
57
+ missing_dependency where circulax is uninstalled.
58
+ """
59
+ if not R > 0 or not C > 0 or not t1 > 0 or not dt0 > 0:
60
+ raise AdapterError(
61
+ "invalid_command",
62
+ f"RC step needs positive R/C/t1/dt0, got R={R!r} C={C!r} "
63
+ f"t1={t1!r} dt0={dt0!r}.",
64
+ )
65
+ if not isinstance(points, int) or points < 2:
66
+ raise AdapterError(
67
+ "invalid_command", f"RC step needs points >= 2, got {points!r}."
68
+ )
69
+ try:
70
+ import jax.numpy as jnp
71
+ import kfnetlist as kfnl
72
+ from circulax import compile_circuit
73
+ from circulax.components.electronic import (
74
+ Capacitor,
75
+ Resistor,
76
+ VoltageSource,
77
+ )
78
+ except ImportError as exc:
79
+ raise AdapterError(
80
+ "missing_dependency",
81
+ f"circulax is not importable: {exc} "
82
+ "(uv sync).",
83
+ ) from exc
84
+ netlist = kfnl.Netlist()
85
+ netlist.create_inst(name="V1", kcl="", component="vsource", settings={"V": V})
86
+ netlist.create_inst(name="R1", kcl="", component="resistor", settings={"R": R})
87
+ netlist.create_inst(name="C1", kcl="", component="capacitor", settings={"C": C})
88
+ netlist.create_inst(name="GND", kcl="", component="ground")
89
+ netlist.create_net(kfnl.PortRef("V1", "p1"), kfnl.PortRef("R1", "p1"))
90
+ netlist.create_net(kfnl.PortRef("V1", "p2"), kfnl.PortRef("GND", "p1"))
91
+ netlist.create_net(kfnl.PortRef("R1", "p2"), kfnl.PortRef("C1", "p1"))
92
+ netlist.create_net(kfnl.PortRef("C1", "p2"), kfnl.PortRef("GND", "p1"))
93
+ circuit = compile_circuit(
94
+ netlist,
95
+ {"vsource": VoltageSource, "resistor": Resistor, "capacitor": Capacitor},
96
+ )
97
+ grid = jnp.linspace(0, t1, points)
98
+ solution = circuit.transient(
99
+ t0=0.0, t1=t1, dt0=dt0, saveat=grid, y0=jnp.zeros(circuit.sys_size)
100
+ )
101
+ cap = circuit.port_map["C1,p1"]
102
+ times = [float(t) for t in solution.ts]
103
+ voltages = [float(row[cap]) for row in solution.ys]
104
+ return {
105
+ "circulax_version": circulax_version(),
106
+ "R": R,
107
+ "C": C,
108
+ "V": V,
109
+ "t_s": times,
110
+ "vc_V": voltages,
111
+ "final_V": voltages[-1],
112
+ }
@@ -0,0 +1,190 @@
1
+ """Staged circulax sim driver: job JSON in, result envelope JSON on stdout.
2
+
3
+ The adapter copies this file into the run workdir next to
4
+ `circulax_job.json` and executes `python circulax_driver.py
5
+ circulax_job.json`. One marker-delimited JSON envelope is printed to stdout
6
+ (ROCKETEDA_ENVELOPE_BEGIN/END); chatter outside the markers is
7
+ tolerated and stays in logs.
8
+
9
+ The netlist is built from the project spec (`doc` instances +
10
+ connections + transient): nothing is hardcoded — R/C/V, the
11
+ topology, the time grid, and the observed port all ride the job.
12
+ Only the proven transient path runs (`Circuit.transient` from a
13
+ discharged start); DC/AC/harmonic-balance stay refused in the
14
+ adapter until a proof lands.
15
+
16
+ Envelope: `{"ok": true, "circulax_version, "result": {t_s[],
17
+ v_V[], final_V, observe, t1_s, points}}` or `{"ok": false, "error":
18
+ {code, message, token, line}}`. Exit 0 on sim success, 2 on a
19
+ malformed circuit, 3 when circulax is not importable. Anything else
20
+ explodes with a traceback and exit 1, which parse() reports as
21
+ runner_unavailable rather than mistaking a tool crash for physics.
22
+ """
23
+
24
+ import json
25
+ import re
26
+ import sys
27
+
28
+ _ENVELOPE_BEGIN = "ROCKETEDA_ENVELOPE_BEGIN"
29
+ _ENVELOPE_END = "ROCKETEDA_ENVELOPE_END"
30
+
31
+ _QUOTED_RE = re.compile(r"""['\"]([^'\"]+)['\"]""")
32
+
33
+ _COMPONENTS = ("vsource", "resistor", "capacitor", "ground")
34
+
35
+
36
+ def _token(doc, message):
37
+ instances = doc.get("instances") or {}
38
+ names = list(instances)
39
+ names.extend(list(_COMPONENTS))
40
+ for name in names:
41
+ if name and re.search(r"\b" + re.escape(name) + r"\b", message):
42
+ return name
43
+ for match in _QUOTED_RE.finditer(message):
44
+ return match.group(1)
45
+ return None
46
+
47
+
48
+ def _find_line(text, token):
49
+ if not token:
50
+ return None
51
+ for number, line in enumerate(text.splitlines(), 1):
52
+ if token in line:
53
+ return number
54
+ return None
55
+
56
+
57
+ def _fail(code, message, token, line):
58
+ print(_ENVELOPE_BEGIN)
59
+ print(
60
+ json.dumps(
61
+ {
62
+ "ok": False,
63
+ "error": {
64
+ "code": code,
65
+ "message": message,
66
+ "token": token,
67
+ "line": line,
68
+ },
69
+ }
70
+ )
71
+ )
72
+ print(_ENVELOPE_END)
73
+
74
+
75
+ def main(argv):
76
+ # TODO: why is the output stdout??!!!! reserve the json, pass the python object as is back to the adapter level, and it will fanout from there to any active adapter who is responsible for formatting said outputs!!!!
77
+ """Run one staged circulax transient: job JSON in, envelope on stdout.
78
+
79
+ Args:
80
+ argv: Argument vector; argv[1] is the staged job JSON path.
81
+
82
+ Returns:
83
+ Process exit code: 0 on sim success, 2 on a malformed
84
+ circuit, 3 when circulax is not importable.
85
+
86
+ Raises:
87
+ ValueError: Unknown component or bad observe port (raised
88
+ and caught inside main into the invalid_circuit
89
+ envelope — never escapes).
90
+ """
91
+ with open(argv[1], encoding="utf-8") as handle:
92
+ job = json.load(handle)
93
+ text = job.get("text", "")
94
+ doc = job["doc"]
95
+ try:
96
+ import jax.numpy as jnp
97
+ import kfnetlist as kfnl
98
+ from circulax import compile_circuit
99
+ from circulax.components.electronic import (
100
+ Capacitor,
101
+ Resistor,
102
+ VoltageSource,
103
+ )
104
+ except ImportError as exc:
105
+ _fail(
106
+ "missing_dependency",
107
+ f"circulax is not importable: {exc} "
108
+ "(uv sync).",
109
+ None,
110
+ None,
111
+ )
112
+ return 3
113
+ try:
114
+ import circulax
115
+
116
+ classes = {
117
+ "vsource": VoltageSource,
118
+ "resistor": Resistor,
119
+ "capacitor": Capacitor,
120
+ }
121
+ netlist = kfnl.Netlist()
122
+ for name, inst in doc["instances"].items():
123
+ component = inst["component"]
124
+ if component not in _COMPONENTS:
125
+ raise ValueError(f"Unknown component: {component!r}.")
126
+ netlist.create_inst(
127
+ name=name,
128
+ kcl="",
129
+ component=component,
130
+ settings=dict(inst.get("settings") or {}),
131
+ )
132
+ for left, right in (doc.get("connections") or {}).items():
133
+ left_inst, _, left_port = left.partition(",")
134
+ right_inst, _, right_port = right.partition(",")
135
+ netlist.create_net(
136
+ kfnl.PortRef(left_inst, left_port),
137
+ kfnl.PortRef(right_inst, right_port),
138
+ )
139
+ circuit = compile_circuit(
140
+ netlist,
141
+ {key: classes[key] for key in classes},
142
+ )
143
+ transient = doc["transient"]
144
+ t1 = float(transient["t1_s"])
145
+ points = int(transient["points"])
146
+ dt0 = float(transient.get("dt0_s", 1e-8))
147
+ observe = transient["observe"]
148
+ if observe not in circuit.port_map:
149
+ raise ValueError(f"Unknown observe port: {observe!r}.")
150
+ grid = jnp.linspace(0, t1, points)
151
+ solution = circuit.transient(
152
+ t0=0.0,
153
+ t1=t1,
154
+ dt0=dt0,
155
+ saveat=grid,
156
+ y0=jnp.zeros(circuit.sys_size),
157
+ )
158
+ index = circuit.port_map[observe]
159
+ times = [float(t) for t in solution.ts]
160
+ volts = [float(row[index]) for row in solution.ys]
161
+ except (ValueError, KeyError, TypeError) as exc:
162
+ message = f"{type(exc).__name__}: {exc}"
163
+ token = _token(doc, message)
164
+ _fail("invalid_circuit", message, token, _find_line(text, token))
165
+ return 2
166
+ print(_ENVELOPE_BEGIN)
167
+ print(
168
+ json.dumps(
169
+ {
170
+ "ok": True,
171
+ "circulax_version": getattr(
172
+ circulax, "__version__", "unknown"
173
+ ),
174
+ "result": {
175
+ "t_s": times,
176
+ "v_V": volts,
177
+ "final_V": volts[-1],
178
+ "observe": observe,
179
+ "t1_s": t1,
180
+ "points": points,
181
+ },
182
+ }
183
+ )
184
+ )
185
+ print(_ENVELOPE_END)
186
+ return 0
187
+
188
+
189
+ if __name__ == "__main__":
190
+ sys.exit(main(sys.argv))