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,163 @@
1
+ """Elmer FEM adapter: run_thermal behind the ToolAdapter port."""
2
+
3
+ import json
4
+ import re
5
+ import shutil
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ from ...execution.errors import ExecutionError
10
+ from ...interfaces import ToolAdapter
11
+ from ..errors import AdapterError
12
+
13
+ OPERATION = "run_thermal"
14
+ _TOOL = "ElmerSolver"
15
+ _DUMP = "savescalars.json"
16
+ _TIMEOUT_S = 600
17
+ _NS_RE = re.compile(r"^(NS|SS) \(ITER=", re.M)
18
+
19
+
20
+ class ElmerAdapter(ToolAdapter):
21
+ """run_thermal via Elmer: SaveScalars dump parsed to verdict.
22
+
23
+ Serves the run_thermal operation with tool `elmer`. Channel
24
+ priority is SaveScalars JSON first, CSV next, VTU fields as
25
+ artifacts, deterministic NS/SS stdout last. No solver Python
26
+ API exists, so execution stays subprocess.
27
+
28
+ Job keys:
29
+ sif (required): Elmer case.sif path.
30
+ workdir: Run directory (default ".").
31
+ timeout_s: Budget in seconds (default 600).
32
+
33
+ Args:
34
+ runner: Optional isolated runner; direct subprocess when None.
35
+ """
36
+
37
+ def __init__(self, runner=None):
38
+ self._runner = runner
39
+
40
+ def capabilities(self, target):
41
+ """Report the served operation and tool.
42
+
43
+ Args:
44
+ target: Target the capabilities are reported for.
45
+
46
+ Returns:
47
+ Capability mapping with operations and tool.
48
+ """
49
+ return {"operations": [OPERATION], "tool": "elmer",
50
+ "dump": _DUMP}
51
+
52
+ def preflight(self, job):
53
+ """Check operation, sif, and ElmerSolver presence.
54
+
55
+ Args:
56
+ job: Solver job with operation run_thermal plus sif.
57
+
58
+ Returns:
59
+ Checked mapping with sif, binary, and timeout.
60
+
61
+ Raises:
62
+ AdapterError: unsupported_operation, invalid_reference
63
+ for a missing sif, missing_dependency when
64
+ ElmerSolver is absent (BLOCKED-needs-host).
65
+ """
66
+ if job.get("operation") != OPERATION:
67
+ raise AdapterError(
68
+ "unsupported_operation",
69
+ f"Elmer handles {OPERATION}, got {job.get('operation')}.")
70
+ sif = job.get("sif")
71
+ if not sif or not Path(sif).is_file():
72
+ raise AdapterError(
73
+ "invalid_reference", f"Case sif not found: {sif}.")
74
+ binary = shutil.which(_TOOL)
75
+ if binary is None:
76
+ raise AdapterError(
77
+ "missing_dependency",
78
+ "BLOCKED-needs-host: ElmerSolver not on PATH "
79
+ "(CSC PPA route).")
80
+ timeout = job.get("timeout_s", _TIMEOUT_S)
81
+ if (not isinstance(timeout, (int, float))
82
+ or isinstance(timeout, bool) or not timeout > 0):
83
+ raise AdapterError("invalid_command", f"Bad timeout: {timeout!r}.")
84
+ checked = {"sif": str(sif), "binary": binary,
85
+ "timeout_s": timeout}
86
+ if self._runner is not None:
87
+ try:
88
+ self._runner.preflight()
89
+ except ExecutionError as exc:
90
+ raise AdapterError(exc.code, str(exc)) from exc
91
+ return checked
92
+
93
+ def execute(self, job):
94
+ """Run ElmerSolver on the sif under a timeout.
95
+
96
+ Args:
97
+ job: Solver job with operation run_thermal plus sif.
98
+
99
+ Returns:
100
+ Native output with exit_code, stdout/stderr, dump, file.
101
+ """
102
+ checked = self.preflight(job)
103
+ workdir = job.get("workdir", ".")
104
+ if not Path(workdir).is_dir():
105
+ raise AdapterError(
106
+ "invalid_reference", f"Workdir not found: {workdir}.")
107
+ try:
108
+ proc = subprocess.run(
109
+ [checked["binary"], checked["sif"]], capture_output=True,
110
+ text=True, timeout=checked["timeout_s"], cwd=workdir)
111
+ except subprocess.TimeoutExpired as exc:
112
+ raise AdapterError(
113
+ "runner_unavailable", "Elmer run timed out.") from exc
114
+ dump_path = Path(workdir) / _DUMP
115
+ dump = None
116
+ if dump_path.is_file():
117
+ try:
118
+ dump = json.loads(dump_path.read_text())
119
+ except ValueError:
120
+ dump = None
121
+ return {"exit_code": proc.returncode, "stdout": proc.stdout,
122
+ "stderr": proc.stderr, "dump": dump,
123
+ "dump_file": _DUMP}
124
+
125
+ def parse(self, native_output):
126
+ """Parse the SaveScalars dump, else NS/SS lines, else fail.
127
+
128
+ Args:
129
+ native_output: Native output with the dump mapping.
130
+
131
+ Returns:
132
+ {total, violations[], measurements[], source}.
133
+
134
+ Raises:
135
+ AdapterError: runner_unavailable with no dump and no
136
+ iteration lines.
137
+ """
138
+ dump = native_output.get("dump")
139
+ if isinstance(dump, dict):
140
+ if dump.get("converged") is False or not dump.get("scalars"):
141
+ return {"total": 1, "violations": [{
142
+ "rule": "fem_converge", "severity": "error",
143
+ "message": "Elmer solve did not converge."}],
144
+ "measurements": [], "source": "dump",
145
+ "dump_file": native_output.get("dump_file", _DUMP)}
146
+ measurements = []
147
+ for name, value in (dump.get("scalars") or {}).items():
148
+ try:
149
+ measurements.append({"name": name, "value": float(value),
150
+ "unit": "SI"})
151
+ except (TypeError, ValueError):
152
+ continue
153
+ return {"total": 0, "violations": [], "measurements": measurements,
154
+ "source": "dump",
155
+ "dump_file": native_output.get("dump_file", _DUMP)}
156
+ text = (native_output.get("stdout") or "")
157
+ if _NS_RE.search(text):
158
+ return {"total": 0, "violations": [],
159
+ "measurements": [{"name": "iterations", "value": 1.0,
160
+ "unit": "count"}],
161
+ "source": "stdio_fallback"}
162
+ raise AdapterError(
163
+ "runner_unavailable", "Elmer output has no SaveScalars verdict.")
@@ -0,0 +1,120 @@
1
+ """Shared marker-delimited envelope extraction for solver drivers.
2
+
3
+ Solver stdout is untrusted: warnings, banners, and progress lines may
4
+ surround the one JSON envelope the driver prints. Drivers frame the
5
+ envelope with ``ROCKETEDA_ENVELOPE_BEGIN``/``ROCKETEDA_ENVELOPE_END``
6
+ markers (one marker per line, JSON between); this helper extracts the
7
+ framed document and tolerates any chatter outside it. Legacy clean
8
+ stdout (exactly one JSON document, no markers) still parses, and
9
+ unmarked chatter falls back to a best-effort envelope scan so old
10
+ drivers degrade gracefully. Chatter is never fatal: the native stdout
11
+ (with warnings) stays intact for the Engine run log, which stores it
12
+ verbatim (see ``Engine._run_operation``); only the envelope is parsed.
13
+ """
14
+
15
+ import json
16
+
17
+ from .errors import AdapterError
18
+
19
+ ENVELOPE_BEGIN = "ROCKETEDA_ENVELOPE_BEGIN"
20
+ ENVELOPE_END = "ROCKETEDA_ENVELOPE_END"
21
+
22
+
23
+ def emit_envelope(payload):
24
+ """Emit one marker-delimited envelope document.
25
+
26
+ Args:
27
+ payload: Envelope mapping with an ``ok`` boolean.
28
+
29
+ Returns:
30
+ Marker-framed envelope text (begin line, JSON line, end line).
31
+ """
32
+ return "\n".join(
33
+ [ENVELOPE_BEGIN, json.dumps(payload), ENVELOPE_END]
34
+ ) + "\n"
35
+
36
+
37
+ def _scan(text):
38
+ """Last envelope-shaped JSON object in text, or None when absent."""
39
+ decoder = json.JSONDecoder()
40
+ found = None
41
+ start = 0
42
+ while True:
43
+ index = text.find("{", start)
44
+ if index == -1:
45
+ return found
46
+ try:
47
+ data, _ = decoder.raw_decode(text[index:])
48
+ except ValueError:
49
+ start = index + 1
50
+ continue
51
+ if isinstance(data, dict) and isinstance(data.get("ok"), bool):
52
+ found = data
53
+ start = index + 1
54
+
55
+
56
+ def extract_envelope(stdout, *, tool):
57
+ """Extract one driver envelope from possibly chattered stdout.
58
+
59
+ Markers first, then the legacy whole-stdout document, then a
60
+ best-effort envelope scan for unmarked chatter. The native stdout
61
+ is never mutated: warnings stay in it for the Engine run log.
62
+
63
+ Args:
64
+ stdout: Driver stdout text (bytes are decoded as UTF-8).
65
+ tool: Driver label for the no-envelope message (sax,
66
+ circulax, views, meow).
67
+
68
+ Returns:
69
+ Envelope mapping with an ``ok`` boolean.
70
+
71
+ Raises:
72
+ AdapterError: runner_unavailable when no JSON envelope is
73
+ found or the framed document is not an envelope.
74
+ """
75
+ if isinstance(stdout, (bytes, bytearray)):
76
+ text = bytes(stdout).decode("utf-8", errors="replace")
77
+ elif isinstance(stdout, str):
78
+ text = stdout
79
+ elif stdout is None:
80
+ text = ""
81
+ else:
82
+ raise AdapterError(
83
+ "runner_unavailable",
84
+ f"{tool} driver printed no JSON envelope.",
85
+ )
86
+ message = f"{tool} driver printed no JSON envelope."
87
+ begin = text.rfind(ENVELOPE_BEGIN)
88
+ if begin != -1:
89
+ end = text.find(ENVELOPE_END, begin + len(ENVELOPE_BEGIN))
90
+ if end == -1:
91
+ raise AdapterError("runner_unavailable", message)
92
+ candidate = text[begin + len(ENVELOPE_BEGIN):end].strip()
93
+ try:
94
+ data = json.loads(candidate)
95
+ except (TypeError, ValueError):
96
+ data = _scan(candidate)
97
+ if data is None:
98
+ raise AdapterError(
99
+ "runner_unavailable", message
100
+ ) from None
101
+ return data
102
+ if not isinstance(data, dict) or not isinstance(
103
+ data.get("ok"), bool
104
+ ):
105
+ raise AdapterError("runner_unavailable", message)
106
+ return data
107
+ try:
108
+ data = json.loads(text)
109
+ except (TypeError, ValueError):
110
+ pass
111
+ else:
112
+ if not isinstance(data, dict) or not isinstance(
113
+ data.get("ok"), bool
114
+ ):
115
+ raise AdapterError("runner_unavailable", message)
116
+ return data
117
+ found = _scan(text)
118
+ if found is None:
119
+ raise AdapterError("runner_unavailable", message)
120
+ return found
@@ -0,0 +1,156 @@
1
+ """Equivalence adapter: run_formal with the eqy provider."""
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_formal"
13
+ _TOOL = "eqy"
14
+ _DONE_RE = re.compile(r"DONE \((PASS|FAIL|UNKNOWN|ERROR), rc=(\d+)\)")
15
+
16
+
17
+ class EqyAdapter(ToolAdapter):
18
+ """run_formal via eqy: verdict file plus DONE plus partitions.
19
+
20
+ Serves the run_formal operation with tool `eqy`. Gating is the
21
+ V1 verdict filename (PASS/FAIL/UNKNOWN/ERROR), confirmed by the
22
+ DONE line; per-partition S1 status rows plus partition JSON
23
+ carry the table. Never imports eqy.py (exit-unsafe).
24
+
25
+ Job keys:
26
+ config (required): Eqy .eqy config path.
27
+ workdir: Run directory (default ".").
28
+ timeout_s: Budget in seconds (default 600).
29
+
30
+ Args:
31
+ runner: Optional isolated runner; direct subprocess when None.
32
+ """
33
+
34
+ def __init__(self, runner=None):
35
+ self._runner = runner
36
+
37
+ def capabilities(self, target):
38
+ """Report the served operation and tool.
39
+
40
+ Args:
41
+ target: Target the capabilities are reported for.
42
+
43
+ Returns:
44
+ Capability mapping with operations and tool.
45
+ """
46
+ return {"operations": [OPERATION], "tool": _TOOL}
47
+
48
+ def preflight(self, job):
49
+ """Check operation, config, and eqy presence.
50
+
51
+ Args:
52
+ job: Solver job with operation run_formal plus config.
53
+
54
+ Returns:
55
+ Checked mapping with config, binary, and timeout.
56
+
57
+ Raises:
58
+ AdapterError: unsupported_operation, invalid_reference
59
+ for a missing config, missing_dependency when eqy
60
+ is absent (BLOCKED-needs-host).
61
+ """
62
+ if job.get("operation") != OPERATION:
63
+ raise AdapterError(
64
+ "unsupported_operation",
65
+ f"Eqy handles {OPERATION}, got {job.get('operation')}.")
66
+ config = job.get("config")
67
+ if not config or not Path(config).is_file():
68
+ raise AdapterError(
69
+ "invalid_reference", f"Eqy config not found: {config}.")
70
+ binary = shutil.which(_TOOL)
71
+ if binary is None:
72
+ raise AdapterError(
73
+ "missing_dependency",
74
+ "BLOCKED-needs-host: eqy not on PATH (oss-cad-suite).")
75
+ timeout = job.get("timeout_s", 600)
76
+ if (not isinstance(timeout, (int, float))
77
+ or isinstance(timeout, bool) or not timeout > 0):
78
+ raise AdapterError("invalid_command", f"Bad timeout: {timeout!r}.")
79
+ checked = {"config": str(config), "binary": binary,
80
+ "timeout_s": timeout}
81
+ if self._runner is not None:
82
+ try:
83
+ self._runner.preflight()
84
+ except ExecutionError as exc:
85
+ raise AdapterError(exc.code, str(exc)) from exc
86
+ return checked
87
+
88
+ def execute(self, job):
89
+ """Run eqy on the config under a timeout.
90
+
91
+ Args:
92
+ job: Solver job with operation run_formal plus config.
93
+
94
+ Returns:
95
+ Native output with exit_code, stdout/stderr, verdict
96
+ file, and partitions.
97
+ """
98
+ checked = self.preflight(job)
99
+ workdir = job.get("workdir", ".")
100
+ if not Path(workdir).is_dir():
101
+ raise AdapterError(
102
+ "invalid_reference", f"Workdir not found: {workdir}.")
103
+ try:
104
+ proc = subprocess.run(
105
+ [checked["binary"], checked["config"]], capture_output=True,
106
+ text=True, timeout=checked["timeout_s"], cwd=workdir)
107
+ except subprocess.TimeoutExpired as exc:
108
+ raise AdapterError(
109
+ "runner_unavailable", "Eqy run timed out.") from exc
110
+ verdict = None
111
+ for name in ("PASS", "FAIL", "UNKNOWN", "ERROR"):
112
+ if (Path(workdir) / name).is_file():
113
+ verdict = name
114
+ break
115
+ return {"exit_code": proc.returncode, "stdout": proc.stdout,
116
+ "stderr": proc.stderr, "verdict_file": verdict,
117
+ "partitions": []}
118
+
119
+ def parse(self, native_output):
120
+ """Parse the V1 verdict file plus DONE line.
121
+
122
+ Args:
123
+ native_output: Native output with verdict file/stdout.
124
+
125
+ Returns:
126
+ {total, violations[], verdict, source}.
127
+
128
+ Raises:
129
+ AdapterError: runner_unavailable with no verdict file
130
+ and no DONE line.
131
+ """
132
+ verdict = native_output.get("verdict_file")
133
+ match = _DONE_RE.search(native_output.get("stdout") or "")
134
+ if match is not None:
135
+ verdict = verdict or match.group(1)
136
+ if verdict not in ("PASS", "FAIL", "UNKNOWN", "ERROR"):
137
+ raise AdapterError(
138
+ "runner_unavailable", "Eqy output has no verdict.")
139
+ partitions = native_output.get("partitions") or []
140
+ failed = [p for p in partitions
141
+ if str(p.get("status", "")).split()[0:1] == ["FAIL"]
142
+ or p.get("status") == "FAIL"]
143
+ if verdict == "PASS" and not failed:
144
+ return {"total": 0, "violations": [], "verdict": verdict,
145
+ "partitions": partitions, "source": "verdict_file"}
146
+ violations = [{
147
+ "rule": "equivalence_fail", "severity": "error",
148
+ "message": f"Eqy verdict {verdict}."}]
149
+ for part in failed:
150
+ violations.append({"rule": "equivalence_partition",
151
+ "severity": "error",
152
+ "message": f"Partition {part.get('name')}"
153
+ " failed."})
154
+ return {"total": len(violations), "violations": violations,
155
+ "verdict": verdict, "partitions": partitions,
156
+ "source": "verdict_file"}
@@ -0,0 +1,181 @@
1
+ """Error type for adapter violations. Carries a stable machine code.
2
+
3
+ The v0->v1 code table lives here too: every v0 engine code maps to
4
+ its v1 catalog code (or None where v1 has no counterpart) plus the
5
+ HTTP status the transports serve. Statuses for mapped codes follow
6
+ contracts/v1/catalog/errors.json exactly (RULING-02: v1 wins); the
7
+ parity test pins that. Emitted payloads keep the v0 code — fidelity
8
+ to what the engine raised — while the status comes from this table.
9
+ """
10
+
11
+
12
+ class AdapterError(Exception):
13
+ """Adapter violation with a stable machine code.
14
+
15
+ Args:
16
+ code: Stable machine-readable error code.
17
+ message: Human-readable detail.
18
+ fields: Itemized fields, when the error names any.
19
+ """
20
+
21
+ def __init__(self, code, message, fields=None):
22
+ super().__init__(message)
23
+ self.code = code
24
+ self.fields = fields or []
25
+
26
+
27
+ #: v0 code -> (v1 catalog code or None, HTTP status). None marks a
28
+ #: v0-only row: no v1 counterpart exists, so the v0 code is served
29
+ #: with the tabled status. Unknown codes default to 400 with the
30
+ #: v0 code kept — stable engine codes are client-caused; uncoded
31
+ #: crashes are 500s handled separately, never through this table.
32
+ _CODE_TABLE = {
33
+ # Approvals: run-state conflicts.
34
+ "approval_denied": ("invalid_run_state", 409),
35
+ "approval_expired": ("invalid_run_state", 409),
36
+ "approval_forbidden": ("invalid_run_state", 409),
37
+ "approval_invalid": ("invalid_run_state", 409),
38
+ "approval_pending": ("invalid_run_state", 409),
39
+ # Conflicts.
40
+ "request_conflict": ("request_conflict", 409),
41
+ "unmet_dependency": (None, 409),
42
+ "resync_required": ("read_set_incomplete", 409),
43
+ "revision_conflict": ("revision_conflict", 409),
44
+ "console_busy": ("request_conflict", 409),
45
+ "coalesced_run_lost": ("request_conflict", 409),
46
+ # Duplicates.
47
+ "component_exists": ("duplicate_configuration", 422),
48
+ "project_exists": ("duplicate_configuration", 422),
49
+ # Capabilities and runtime.
50
+ "missing_dependency": ("capability_unavailable", 422),
51
+ "isolation_unavailable": ("runtime_unavailable", 503),
52
+ "runner_unavailable": ("runtime_unavailable", 503),
53
+ # Malformed input.
54
+ "invalid_address": ("invalid_request", 400),
55
+ "invalid_arguments": ("invalid_request", 400),
56
+ "invalid_cell": ("invalid_request", 400),
57
+ "invalid_command": ("invalid_request", 400),
58
+ "invalid_component": ("invalid_request", 400),
59
+ "invalid_data": ("invalid_request", 400),
60
+ "invalid_document": ("invalid_request", 400),
61
+ "invalid_entity": ("invalid_request", 400),
62
+ "invalid_kind": ("invalid_request", 400),
63
+ "invalid_ledger": ("invalid_request", 400),
64
+ "invalid_path": ("invalid_request", 400),
65
+ "invalid_policy": ("invalid_request", 400),
66
+ "invalid_predicate": ("invalid_request", 400),
67
+ "invalid_project": ("invalid_request", 400),
68
+ "invalid_reference": ("invalid_request", 400),
69
+ "invalid_run_param": ("invalid_request", 400),
70
+ "invalid_run_request": ("invalid_request", 400),
71
+ "invalid_schema": ("invalid_request", 400),
72
+ "invalid_yaml": ("invalid_request", 400),
73
+ "unknown_run_param": ("invalid_request", 400),
74
+ "type_mismatch": ("invalid_request", 400),
75
+ "empty_cell": ("invalid_request", 400),
76
+ "empty_entry": ("invalid_request", 400),
77
+ "empty_policy": ("invalid_request", 400),
78
+ "future_format": ("invalid_request", 400),
79
+ "gitignore_mismatch": ("invalid_request", 400),
80
+ "batch_unreadable": ("invalid_request", 400),
81
+ "cell_unreadable": ("invalid_request", 400),
82
+ "path_escape": ("invalid_request", 400),
83
+ # Unknown ids.
84
+ "not_found": ("not_found", 404),
85
+ "missing_entry": ("not_found", 404),
86
+ "missing_policy": ("not_found", 404),
87
+ "unknown_cell": ("not_found", 404),
88
+ "unknown_component": ("not_found", 404),
89
+ "unknown_document": ("not_found", 404),
90
+ "unknown_entity": ("not_found", 404),
91
+ "unknown_request": ("not_found", 404),
92
+ "unknown_revision": ("not_found", 404),
93
+ "unknown_run": ("not_found", 404),
94
+ "unknown_topic": ("not_found", 404),
95
+ # Unsupported (semantic, not malformed).
96
+ "unsupported_operation": ("unknown_task", 422),
97
+ "unsupported_version": ("unknown_task", 422),
98
+ # v0-only rows: no v1 counterpart; status is the contract.
99
+ "cross_project_denied": (None, 403),
100
+ "cross_origin_denied": (None, 403),
101
+ "backfill_readonly": (None, 403),
102
+ "pool_full": (None, 429),
103
+ "server_busy": (None, 503),
104
+ "body_too_large": (None, 413),
105
+ "unsupported_media_type": (None, 415),
106
+ "method_not_allowed": (None, 405),
107
+ "credential_revoked": (None, 500),
108
+ "credential_unknown": (None, 500),
109
+ "non_loopback_bind_denied": (None, 500),
110
+ "internal_error": ("internal_error", 500),
111
+ }
112
+
113
+ #: v0 code -> v1 catalog code (None where v1 has no counterpart).
114
+ V0_TO_V1 = {code: v1 for code, (v1, _) in _CODE_TABLE.items()}
115
+
116
+ #: Codes that ask the client to back off (served with Retry-After).
117
+ _RETRYABLE = frozenset({"pool_full", "server_busy"})
118
+
119
+
120
+ def status_for_code(code):
121
+ """HTTP status for one v0 error code.
122
+
123
+ Args:
124
+ code: Stable v0 error code.
125
+
126
+ Returns:
127
+ Tabled status, or 400 for an unlisted code (the code is
128
+ still emitted — the table has a gap, not the engine).
129
+ """
130
+ return _CODE_TABLE.get(code, (None, 400))[1]
131
+
132
+
133
+ def v1_for_code(code):
134
+ """v1 catalog code for one v0 error code, if one exists.
135
+
136
+ Args:
137
+ code: Stable v0 error code.
138
+
139
+ Returns:
140
+ v1 code, or None where v1 has no counterpart.
141
+ """
142
+ return _CODE_TABLE.get(code, (None, 400))[0]
143
+
144
+
145
+ def retry_after_for(code, seconds):
146
+ """Retry-After value for one v0 code, when the client must wait.
147
+
148
+ Args:
149
+ code: Stable v0 error code.
150
+ seconds: Backoff the transport advertises.
151
+
152
+ Returns:
153
+ Seconds as a string for pool_full/server_busy, else None.
154
+ """
155
+ if code in _RETRYABLE:
156
+ return str(seconds)
157
+ return None
158
+
159
+
160
+ def error_payload(exc):
161
+ """Wire payload for one failure: code + message + fields.
162
+
163
+ Coded engine errors serialize untouched (fields only when the
164
+ error names any); uncoded crashes become a generic internal
165
+ error — raw exception text never crosses the wire.
166
+
167
+ Args:
168
+ exc: Raised failure.
169
+
170
+ Returns:
171
+ Mapping with code and message, plus fields when present.
172
+ """
173
+ code = getattr(exc, "code", None)
174
+ if code is None:
175
+ return {"code": "internal_error",
176
+ "message": "Internal engine error."}
177
+ payload = {"code": code, "message": str(exc) or code}
178
+ fields = getattr(exc, "fields", None)
179
+ if fields:
180
+ payload["fields"] = list(fields)
181
+ return payload
@@ -0,0 +1,15 @@
1
+ # adapters/extract_magic — Magic extraction solver
2
+
3
+ Batch extraction behind the `ToolAdapter` port: preflight refuses
4
+ missing binaries, unloadable techs, and missing cells; execute runs
5
+ magic headless (`-dnull -noconsole`) over `extract all` + `ext2spice
6
+ lvs`; parse reads back the `<cell>.ext`/`<cell>.spice` pair into
7
+ `{device_count, devices[]}` with class + w/l per FET.
8
+
9
+ Verified against magic 8.3.685 + sky130A tech 1.0.588 on
10
+ `sky130_fd_sc_hd__inv_2` from the registered PDK (4 FETs, exact
11
+ w/l). Magic exits 0 even on failure, so the verdict comes from
12
+ parsed output, never the exit code.
13
+
14
+ Status: EXTRACT-SKY130 GREEN. See `docs/probe-extract-lvs.md`.
15
+ Status: EXTRACT-GDS GREEN. See `docs/probe-extract-gds.md`.