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