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,538 @@
1
+ """Circulax time-domain solver adapter: run_optical behind the ToolAdapter port.
2
+
3
+ Protocol, mirroring the SAX adapter: stage the sim driver plus a job
4
+ JSON into the workdir, run `python circulax_driver.py
5
+ circulax_job.json` under a timeout, then parse the JSON envelope on
6
+ stdout. The verdict always comes from parsed output: missed voltage
7
+ bounds are FAIL, a malformed circuit synthesizes an error violation
8
+ with file/line/sim_time, and only a circulax-less interpreter raises
9
+ missing_dependency.
10
+
11
+ Generalization law: the project circuit YAML is the input — R/C/V,
12
+ topology, time grid, observed port, and targets all ride the spec.
13
+ `run_rc_step` (the sibling smoke) qualifies the install; this op
14
+ generalizes it and must never hardcode the proof values. Only the
15
+ proven transient path runs; DC/AC/harmonic-balance specs are refused
16
+ until a proof lands.
17
+
18
+ Tool resolution: an explicit job `tool_dir` containing a python
19
+ wins, else the engine interpreter (circulax arrives via the pip
20
+ `photonic` group). Where the tools live on disk is B10 scope.
21
+ """
22
+
23
+ import json
24
+ import math
25
+ import re
26
+ import subprocess
27
+ import sys
28
+ from pathlib import Path
29
+
30
+ from ruamel.yaml import YAML
31
+
32
+ try:
33
+ from ...execution.errors import ExecutionError
34
+ except ImportError:
35
+ from rocket_engine.execution.errors import ExecutionError
36
+ try:
37
+ from ...execution.runner import tool_roots
38
+ except ImportError:
39
+ from rocket_engine.execution.runner import tool_roots
40
+ try:
41
+ from ...interfaces import ToolAdapter
42
+ except ImportError:
43
+ from rocket_engine.interfaces import ToolAdapter
44
+ try:
45
+ from ..envelope import extract_envelope
46
+ except ImportError:
47
+ from rocket_engine.adapters.envelope import extract_envelope
48
+ try:
49
+ from ..errors import AdapterError
50
+ except ImportError:
51
+ from rocket_engine.adapters.errors import AdapterError
52
+
53
+ OPERATION = "run_optical"
54
+ _TIMEOUT_S = 120
55
+ _DRIVER_NAME = "circulax_driver.py"
56
+ _JOB_NAME = "circulax_job.json"
57
+ _PORT_REF_RE = re.compile(r"^[^,\s]+,[^,\s]+$")
58
+ _COMPONENTS = ("vsource", "resistor", "capacitor", "ground")
59
+
60
+
61
+ def _is_number(value):
62
+ return (
63
+ isinstance(value, (int, float))
64
+ and not isinstance(value, bool)
65
+ and math.isfinite(value)
66
+ )
67
+
68
+
69
+ def _check_instance(name, inst, spec):
70
+ if not isinstance(inst, dict) or inst.get("component") not in _COMPONENTS:
71
+ raise AdapterError(
72
+ "invalid_command",
73
+ f"Instance {name!r} needs a component model "
74
+ f"({', '.join(_COMPONENTS)}): {spec}.",
75
+ )
76
+ settings = inst.get("settings", {})
77
+ if not isinstance(settings, dict):
78
+ raise AdapterError(
79
+ "invalid_command",
80
+ f"Instance {name!r} settings must be a mapping: {spec}.",
81
+ )
82
+ component = inst["component"]
83
+ if component == "vsource" and not _is_number(settings.get("V")):
84
+ raise AdapterError(
85
+ "invalid_command",
86
+ f"Instance {name!r} needs a numeric V setting: {spec}.",
87
+ )
88
+ if component == "resistor" and not (
89
+ _is_number(settings.get("R")) and settings["R"] > 0
90
+ ):
91
+ raise AdapterError(
92
+ "invalid_command",
93
+ f"Instance {name!r} needs a positive R setting: {spec}.",
94
+ )
95
+ if component == "capacitor" and not (
96
+ _is_number(settings.get("C")) and settings["C"] > 0
97
+ ):
98
+ raise AdapterError(
99
+ "invalid_command",
100
+ f"Instance {name!r} needs a positive C setting: {spec}.",
101
+ )
102
+
103
+
104
+ def _check_doc(doc, spec):
105
+ if not isinstance(doc, dict):
106
+ raise AdapterError(
107
+ "invalid_command", f"Circuit spec is not a mapping: {spec}."
108
+ )
109
+ instances = doc.get("instances")
110
+ if not isinstance(instances, dict) or not instances:
111
+ raise AdapterError(
112
+ "invalid_command", f"Circuit spec needs instances: {spec}."
113
+ )
114
+ for name, inst in instances.items():
115
+ _check_instance(name, inst, spec)
116
+ connections = doc.get("connections", {})
117
+ if not isinstance(connections, dict) or not all(
118
+ isinstance(k, str) and isinstance(v, str)
119
+ for k, v in connections.items()
120
+ ):
121
+ raise AdapterError(
122
+ "invalid_command", f"Circuit connections must be a mapping: {spec}."
123
+ )
124
+ for key, val in connections.items():
125
+ if not _PORT_REF_RE.match(key) or not _PORT_REF_RE.match(val):
126
+ raise AdapterError(
127
+ "invalid_command",
128
+ f"Bad connection {key!r}: {val!r} in {spec} "
129
+ "(want `instance,port`).",
130
+ )
131
+ for ref in (key, val):
132
+ if ref.partition(",")[0] not in instances:
133
+ raise AdapterError(
134
+ "invalid_command",
135
+ f"Connection names unknown instance {ref!r}: {spec}.",
136
+ )
137
+ transient = doc.get("transient")
138
+ if not isinstance(transient, dict):
139
+ raise AdapterError(
140
+ "invalid_command", f"Circuit spec needs a transient: {spec}."
141
+ )
142
+ t1, points = transient.get("t1_s"), transient.get("points")
143
+ if not _is_number(t1) or not t1 > 0:
144
+ raise AdapterError(
145
+ "invalid_command", f"Bad transient t1_s: {t1!r} in {spec}."
146
+ )
147
+ if not isinstance(points, int) or isinstance(points, bool) or points < 2:
148
+ raise AdapterError(
149
+ "invalid_command", f"Bad transient points: {points!r} in {spec}."
150
+ )
151
+ dt0 = transient.get("dt0_s", 1e-8)
152
+ if not _is_number(dt0) or not dt0 > 0:
153
+ raise AdapterError(
154
+ "invalid_command", f"Bad transient dt0_s: {dt0!r} in {spec}."
155
+ )
156
+ observe = transient.get("observe")
157
+ if (
158
+ not isinstance(observe, str)
159
+ or not _PORT_REF_RE.match(observe)
160
+ or observe.partition(",")[0] not in instances
161
+ ):
162
+ raise AdapterError(
163
+ "invalid_command",
164
+ f"Bad transient observe port: {observe!r} in {spec} "
165
+ "(want `instance,port` of a known instance).",
166
+ )
167
+ targets = doc.get("targets", [])
168
+ if not isinstance(targets, list):
169
+ raise AdapterError(
170
+ "invalid_command", f"Circuit targets must be a list: {spec}."
171
+ )
172
+ for target in targets:
173
+ if not isinstance(target, dict):
174
+ raise AdapterError(
175
+ "invalid_command", f"Bad target {target!r}: {spec}."
176
+ )
177
+ at_s = target.get("at_s")
178
+ if not _is_number(at_s) or not 0 <= at_s <= t1:
179
+ raise AdapterError(
180
+ "invalid_command",
181
+ f"Target time outside transient: {at_s!r} in {spec}.",
182
+ )
183
+ bounds = {
184
+ key: target[key]
185
+ for key in ("min_V", "max_V")
186
+ if target.get(key) is not None
187
+ }
188
+ if not bounds or not all(_is_number(v) for v in bounds.values()):
189
+ raise AdapterError(
190
+ "invalid_command",
191
+ f"Target needs numeric min_V and/or max_V: {target!r}.",
192
+ )
193
+
194
+
195
+ def _load_spec(spec):
196
+ try:
197
+ text = Path(spec).read_text(encoding="utf-8")
198
+ except OSError as exc:
199
+ raise AdapterError(
200
+ "invalid_reference", f"Circuit spec unreadable: {spec} ({exc})."
201
+ ) from exc
202
+ try:
203
+ doc = YAML(typ="safe").load(text)
204
+ except Exception as exc:
205
+ mark = getattr(exc, "problem_mark", None) or getattr(
206
+ exc, "context_mark", None
207
+ )
208
+ where = (
209
+ f"{spec}:{mark.line + 1}:{mark.column + 1}"
210
+ if mark is not None
211
+ else spec
212
+ )
213
+ raise AdapterError(
214
+ "invalid_reference", f"Circuit spec is not YAML: {where} ({exc})."
215
+ ) from exc
216
+ _check_doc(doc, spec)
217
+ return text, doc
218
+
219
+
220
+ def _target_line(spec_text):
221
+ for number, line in enumerate((spec_text or "").splitlines(), 1):
222
+ if "targets" in line:
223
+ return number
224
+ return None
225
+
226
+
227
+ class CirculaxAdapter(ToolAdapter):
228
+ """run_optical via circulax: time-domain transient circuit sim.
229
+
230
+ Serves the run_optical operation with deck.tool `circulax` (the
231
+ second tool; sax stays the default). Stages the sim driver plus
232
+ a job JSON, runs it under a timeout, and parses the JSON
233
+ envelope: the observed voltage trace plus per-target
234
+ measurements against min_V/max_V bounds.
235
+
236
+ Job keys:
237
+ sources (required): Exactly one circuit spec YAML path.
238
+ timeout_s: Sim budget in seconds (default 120).
239
+ tool_dir: Directory holding the sim python; the engine
240
+ interpreter otherwise.
241
+ deck: Optional mapping; deck.tool must be circulax here.
242
+ workdir: Run directory (default ".").
243
+
244
+ Args:
245
+ runner: Optional isolated runner; direct subprocess when None.
246
+
247
+ Caution: Missed voltage bounds are FAIL and a malformed circuit
248
+ synthesizes an error violation with file/line/sim_time —
249
+ the verdict always comes from parsed output, never the
250
+ exit code.
251
+ """
252
+
253
+ def __init__(self, runner=None):
254
+ self._runner = runner
255
+
256
+ def capabilities(self, target):
257
+ """Report the served operation and tool name.
258
+
259
+ Args:
260
+ target: Target the capabilities are reported for
261
+ (accepted, unused — one circuit per job).
262
+
263
+ Returns:
264
+ Capability mapping with operations and tool.
265
+ """
266
+ return {"operations": [OPERATION], "tool": "circulax"}
267
+
268
+ def preflight(self, job):
269
+ """Check operation, timeout, spec shape, and sim python.
270
+
271
+ Cheap deterministic checks first, tool resolution last, the
272
+ runner probe closing it out.
273
+
274
+ Args:
275
+ job: Solver job with operation run_optical plus the
276
+ circuit spec source and timeout inputs.
277
+
278
+ Returns:
279
+ Checked mapping with the resolved python, spec, spec
280
+ text, parsed doc, and timeout_s.
281
+
282
+ Raises:
283
+ AdapterError: unsupported_operation for another
284
+ operation, invalid_command for a bad timeout or
285
+ circuit spec, invalid_reference unless exactly one
286
+ readable spec source is given, missing_dependency
287
+ for an unresolvable sim python.
288
+ """
289
+ if job.get("operation") != OPERATION:
290
+ raise AdapterError(
291
+ "unsupported_operation",
292
+ f"Circulax handles {OPERATION}, got {job.get('operation')}.",
293
+ )
294
+ timeout = job.get("timeout_s", _TIMEOUT_S)
295
+ if (
296
+ not isinstance(timeout, (int, float))
297
+ or isinstance(timeout, bool)
298
+ or not timeout > 0
299
+ ):
300
+ raise AdapterError("invalid_command", f"Bad timeout_s: {timeout!r}.")
301
+ sources = job.get("sources") or []
302
+ missing = [s for s in sources if not Path(s).is_file()]
303
+ if len(sources) != 1 or missing:
304
+ raise AdapterError(
305
+ "invalid_reference",
306
+ f"run_optical needs one circuit spec, got: {missing or sources}.",
307
+ )
308
+ spec = sources[0]
309
+ text, doc = _load_spec(spec)
310
+ checked = {
311
+ "python": self._resolve(job),
312
+ "spec": spec,
313
+ "text": text,
314
+ "doc": doc,
315
+ "timeout_s": timeout,
316
+ }
317
+ if self._runner is not None:
318
+ try:
319
+ self._runner.preflight()
320
+ except ExecutionError as exc:
321
+ raise AdapterError(exc.code, str(exc)) from exc
322
+ return checked
323
+
324
+ def execute(self, job):
325
+ """Stage the driver plus job JSON, then run the sim.
326
+
327
+ Args:
328
+ job: Solver job with operation run_optical plus the
329
+ circuit spec source and timeout inputs.
330
+
331
+ Returns:
332
+ Native output with exit_code, stdout/stderr, spec,
333
+ spec_text, observe port, and the parsed targets.
334
+
335
+ Raises:
336
+ AdapterError: invalid_reference for a missing workdir,
337
+ runner_unavailable on a sim timeout, plus the
338
+ preflight codes (execute re-checks first).
339
+ """
340
+ checked = self.preflight(job)
341
+ workdir = job.get("workdir", ".")
342
+ if not Path(workdir).is_dir():
343
+ raise AdapterError(
344
+ "invalid_reference", f"Workdir not found: {workdir}."
345
+ )
346
+ workdir_path = Path(workdir)
347
+ driver = Path(__file__).with_name("_driver.py").read_text(
348
+ encoding="utf-8"
349
+ )
350
+ (workdir_path / _DRIVER_NAME).write_text(driver, encoding="utf-8")
351
+ (workdir_path / _JOB_NAME).write_text(
352
+ json.dumps(
353
+ {
354
+ "spec": checked["spec"],
355
+ "text": checked["text"],
356
+ "doc": checked["doc"],
357
+ }
358
+ ),
359
+ encoding="utf-8",
360
+ )
361
+ roots = tool_roots([checked["python"]])
362
+ proc = self._run(
363
+ [checked["python"], _DRIVER_NAME, _JOB_NAME],
364
+ workdir,
365
+ checked["timeout_s"],
366
+ roots,
367
+ )
368
+ return {
369
+ "exit_code": proc.returncode,
370
+ "stdout": proc.stdout,
371
+ "stderr": proc.stderr,
372
+ "spec": checked["spec"],
373
+ "spec_text": checked["text"],
374
+ "observe": checked["doc"]["transient"]["observe"],
375
+ "targets": list(checked["doc"].get("targets") or []),
376
+ }
377
+
378
+ def parse(self, native_output):
379
+ """Parse the driver envelope into a trace and measurements.
380
+
381
+ Malformed circuits synthesize one error violation with
382
+ file/line rather than parsing as clean; voltage bound
383
+ misses carry engine-computed margins.
384
+
385
+ Args:
386
+ native_output: Native output with the driver stdout
387
+ envelope, spec, spec_text, observe port, and
388
+ targets.
389
+
390
+ Returns:
391
+ {total, violations[], measurements[], transient,
392
+ circulax_version} with volt values and margins.
393
+
394
+ Raises:
395
+ AdapterError: missing_dependency when the sim python
396
+ has no circulax, runner_unavailable when the
397
+ driver printed no JSON envelope or no result.
398
+ """
399
+ spec = native_output.get("spec", "")
400
+ envelope = extract_envelope(
401
+ native_output.get("stdout", ""), tool="circulax"
402
+ )
403
+ if not envelope["ok"]:
404
+ error = envelope.get("error") or {}
405
+ code = error.get("code")
406
+ message = error.get("message", "circulax run failed")
407
+ if code == "missing_dependency":
408
+ raise AdapterError("missing_dependency", message)
409
+ if code != "invalid_circuit":
410
+ raise AdapterError(
411
+ "runner_unavailable",
412
+ f"circulax driver failed without a circuit error: "
413
+ f"{message}.",
414
+ )
415
+ violation = {
416
+ "rule": "circuit",
417
+ "severity": "error",
418
+ "message": message,
419
+ "file": spec,
420
+ }
421
+ if error.get("line") is not None:
422
+ violation["line"] = error["line"]
423
+ return {
424
+ "total": 1,
425
+ "violations": [violation],
426
+ "measurements": [],
427
+ "circulax_version": envelope.get("circulax_version"),
428
+ }
429
+ result = envelope.get("result") or {}
430
+ grid = result.get("t_s")
431
+ curve = result.get("v_V")
432
+ if (
433
+ not isinstance(grid, list)
434
+ or len(grid) < 2
435
+ or not all(_is_number(t) for t in grid)
436
+ or not isinstance(curve, list)
437
+ or len(curve) != len(grid)
438
+ or not all(isinstance(v, (int, float)) for v in curve)
439
+ ):
440
+ raise AdapterError(
441
+ "runner_unavailable",
442
+ "circulax driver envelope has no result.",
443
+ )
444
+ observe = native_output.get("observe", "")
445
+ violations = []
446
+ measurements = []
447
+ for target in native_output.get("targets") or []:
448
+ at_s = target["at_s"]
449
+ index = min(range(len(grid)), key=lambda i: abs(grid[i] - at_s))
450
+ actual = float(curve[index])
451
+ name = f"V({observe})@{at_s}s"
452
+ bounds = {
453
+ key: target[key]
454
+ for key in ("min_V", "max_V")
455
+ if target.get(key) is not None
456
+ }
457
+ margins = {}
458
+ if "min_V" in bounds:
459
+ margins["min_V"] = actual - bounds["min_V"]
460
+ if "max_V" in bounds:
461
+ margins["max_V"] = bounds["max_V"] - actual
462
+ failed = [
463
+ f"{kind} {bounds[kind]}"
464
+ for kind, margin in margins.items()
465
+ if margin < 0
466
+ ]
467
+ status = "fail" if failed else "pass"
468
+ margin = min(margins[kind] for kind in bounds)
469
+ measurements.append(
470
+ {
471
+ "name": name,
472
+ "value": actual,
473
+ "unit": "V",
474
+ "target": dict(bounds),
475
+ "margin": margin,
476
+ "status": status,
477
+ }
478
+ )
479
+ if failed:
480
+ violation = {
481
+ "rule": "transient",
482
+ "severity": "error",
483
+ "message": (
484
+ f"{name} = {actual:.6g} V misses "
485
+ f"{', '.join(failed)} V "
486
+ f"(margin {margin:.6g} V)."
487
+ ),
488
+ "file": spec,
489
+ "sim_time": at_s,
490
+ }
491
+ line = _target_line(native_output.get("spec_text"))
492
+ if line is not None:
493
+ violation["line"] = line
494
+ violations.append(violation)
495
+ return {
496
+ "total": len(violations),
497
+ "violations": violations,
498
+ "measurements": measurements,
499
+ "transient": {
500
+ "t_s": list(grid),
501
+ "v_V": [float(v) for v in curve],
502
+ "observe": observe,
503
+ },
504
+ "circulax_version": envelope.get("circulax_version"),
505
+ }
506
+
507
+ @staticmethod
508
+ def _resolve(job):
509
+ tool_dir = job.get("tool_dir")
510
+ if tool_dir:
511
+ for name in ("python3", "python"):
512
+ candidate = Path(tool_dir) / name
513
+ if candidate.is_file():
514
+ return str(candidate)
515
+ raise AdapterError(
516
+ "missing_dependency", f"python not found in {tool_dir}."
517
+ )
518
+ return sys.executable
519
+
520
+ def _run(self, argv, workdir, timeout_s, extra_binds=()):
521
+ if self._runner is not None:
522
+ try:
523
+ return self._runner.run(argv, workdir, timeout_s, extra_binds)
524
+ except ExecutionError as exc:
525
+ raise AdapterError(exc.code, str(exc)) from exc
526
+ try:
527
+ return subprocess.run(
528
+ argv,
529
+ capture_output=True,
530
+ text=True,
531
+ timeout=timeout_s,
532
+ cwd=workdir,
533
+ )
534
+ except subprocess.TimeoutExpired as exc:
535
+ raise AdapterError(
536
+ "runner_unavailable",
537
+ f"circulax sim timed out after {timeout_s}s.",
538
+ ) from exc