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,425 @@
1
+ """Magic extraction solver adapter (EXTRACT-SKY130, EXTRACT-GDS): layout to .spice.
2
+
3
+ Protocol, verified against magic 8.3.685 + sky130A tech 1.0.588:
4
+ Tcl over stdin (`load`, `select top cell`, `extract all`,
5
+ `ext2spice lvs`, `ext2spice`), then the `<cell>.ext` + `<cell>.spice`
6
+ artifacts are read back from the workdir. The tech may be a store
7
+ deck path (`tech_file`, hash-checked via resolve_deck by the caller)
8
+ or a plain magic tech name. Magic exits 0 even for broken cells, so
9
+ the verdict always comes from parsed output, never the exit code.
10
+ """
11
+
12
+ import re
13
+ import shutil
14
+ import subprocess
15
+ from pathlib import Path
16
+
17
+ from ...execution.errors import ExecutionError
18
+ from ...interfaces import ToolAdapter
19
+ from ...interfaces.runparams import MAX_TIMEOUT_S, MIN_TIMEOUT_S
20
+ from ..errors import AdapterError
21
+
22
+ OPERATION = "run_extraction"
23
+ _TIMEOUT_S = 120
24
+
25
+
26
+ def _budget_or_raise(job):
27
+ """Job solver budget: validated, or the adapter default.
28
+
29
+ Args:
30
+ job: Solver job, maybe with a `timeout_s`.
31
+
32
+ Returns:
33
+ Budget seconds for every `_run` call.
34
+
35
+ Raises:
36
+ AdapterError: invalid_command for a boolean,
37
+ non-number, or out-of-bounds budget.
38
+ """
39
+ timeout = job.get("timeout_s", _TIMEOUT_S)
40
+ if (
41
+ isinstance(timeout, bool)
42
+ or not isinstance(timeout, (int, float))
43
+ or not MIN_TIMEOUT_S <= timeout <= MAX_TIMEOUT_S
44
+ ):
45
+ raise AdapterError(
46
+ "invalid_command", f"Bad timeout_s: {timeout!r}."
47
+ )
48
+ return timeout
49
+ _MARKER = "exttospice finished"
50
+ _SUBCKT_RE = re.compile(r"^\s*\.subckt\b", re.IGNORECASE | re.MULTILINE)
51
+ _DEVICE_RE = re.compile(
52
+ r"^X(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)(.*)$",
53
+ re.IGNORECASE,
54
+ )
55
+ _PARAM_RE = re.compile(r"\b([wl])=([0-9.eE+-]+)", re.IGNORECASE)
56
+ # The cell name is embedded in a Tcl script under braces: braces stop
57
+ # substitution, but `}` breaks out and backslash-newline still folds,
58
+ # so every Tcl-active character is rejected up front.
59
+ _CELL_BAD_RE = re.compile(r"[{}\[\]$\\;\"\n\r]")
60
+
61
+ _SCRIPT = (
62
+ "load {cell}\nselect top cell\nextract all\n"
63
+ "ext2spice lvs\next2spice\nquit\n"
64
+ )
65
+
66
+ _GDS_SCRIPT = (
67
+ "gds read {{{gds}}}\nload {cell}\nselect top cell\nextract all\n"
68
+ "ext2spice lvs\next2spice\nquit\n"
69
+ )
70
+
71
+
72
+ def _source_or_raise(job):
73
+ """Layout source: exactly one of cell_file (.mag) or gds_file (GDSII).
74
+
75
+ Args:
76
+ job: Solver job with operation run_extraction plus cell and a
77
+ layout source.
78
+
79
+ Returns:
80
+ (source, path) where source is "mag" or "gds".
81
+
82
+ Raises:
83
+ AdapterError: invalid_command when both sources are given
84
+ (ambiguous) or the GDS path has Tcl-active characters
85
+ (it is embedded in the `gds read` script under braces);
86
+ invalid_reference when the chosen source file is missing.
87
+ """
88
+ cell_file = job.get("cell_file")
89
+ gds_file = job.get("gds_file")
90
+ if gds_file is not None and cell_file is not None:
91
+ raise AdapterError(
92
+ "invalid_command", "Extraction needs one source: cell_file or gds_file."
93
+ )
94
+ if gds_file is not None:
95
+ if not gds_file or _CELL_BAD_RE.search(str(gds_file)):
96
+ raise AdapterError(
97
+ "invalid_command", f"Bad GDS file (Tcl-unsafe characters): {gds_file!r}."
98
+ )
99
+ if not Path(gds_file).is_file():
100
+ raise AdapterError(
101
+ "invalid_reference", f"GDS file not found: {gds_file}."
102
+ )
103
+ return "gds", str(gds_file)
104
+ if not cell_file or not Path(cell_file).is_file():
105
+ raise AdapterError(
106
+ "invalid_reference", f"Cell file not found: {cell_file}."
107
+ )
108
+ return "mag", str(cell_file)
109
+
110
+
111
+ def _tech_arg_or_raise(job):
112
+ deck = job.get("deck") if isinstance(job.get("deck"), dict) else {}
113
+ for candidate in (
114
+ job.get("tech_file"),
115
+ deck.get("path"),
116
+ deck.get("tech_file"),
117
+ deck.get("file"),
118
+ ):
119
+ if candidate is not None:
120
+ if not candidate or not Path(candidate).is_file():
121
+ raise AdapterError(
122
+ "invalid_reference", f"Magic tech file not found: {candidate}."
123
+ )
124
+ return str(candidate)
125
+ tech = job.get("tech")
126
+ if tech is None:
127
+ raise AdapterError(
128
+ "invalid_command", "Extraction needs a tech_file or tech name."
129
+ )
130
+ if not isinstance(tech, str) or not tech:
131
+ raise AdapterError("invalid_command", f"Bad magic tech name: {tech!r}.")
132
+ return tech
133
+
134
+
135
+ class MagicExtractAdapter(ToolAdapter):
136
+ """run_extraction via Magic: .mag cells to .ext/.spice netlists.
137
+
138
+ Serves the run_extraction operation with deck.tool `magic` (the
139
+ only tool). Loads the cell, extracts all, and converts with
140
+ ext2spice; the result carries the netlist artifact paths plus
141
+ device counts.
142
+
143
+ Job keys:
144
+ cell (required): Cell name loaded and extracted.
145
+ cell_file: Path to the .mag cell file — one source is
146
+ required (cell_file or gds_file, never both).
147
+ gds_file: Path to a GDSII layout file; the adapter runs
148
+ `gds read` before `load`, then extracts as usual.
149
+ tech (required): Magic tech name — one tech form is
150
+ required (tech, tech_file, or deck.path/tech_file/file).
151
+ tech_file: Magic tech file path (a hash-checked store deck
152
+ by the caller).
153
+ deck: Optional mapping naming the tech file
154
+ (deck.path/tech_file/file).
155
+ workdir: Run directory (default ".").
156
+
157
+ Args:
158
+ runner: Optional isolated runner; direct subprocess when None.
159
+
160
+ Caution: Magic exits 0 even for broken cells — the verdict
161
+ always comes from parsed output, never the exit code.
162
+ """
163
+
164
+ def __init__(self, runner=None):
165
+ self._runner = runner
166
+
167
+ def capabilities(self, target):
168
+ """Report the served operation and tool name.
169
+
170
+ Args:
171
+ target: Target the capabilities are reported for
172
+ (accepted, unused — one cell per job).
173
+
174
+ Returns:
175
+ Capability mapping with operations and tool.
176
+ """
177
+ return {"operations": [OPERATION], "tool": "magic"}
178
+
179
+ def preflight(self, job):
180
+ """Check operation, cell, tech, binary, and cell file.
181
+
182
+ Cheap deterministic checks first, machine-dependent ones
183
+ last.
184
+
185
+ Args:
186
+ job: Solver job with operation run_extraction plus
187
+ cell, cell_file, and tech inputs.
188
+
189
+ Returns:
190
+ Checked mapping with tech_arg, cell, source, and
191
+ source_file (plus cell_file or gds_file).
192
+
193
+ Raises:
194
+ AdapterError: unsupported_operation for another
195
+ operation, invalid_command for a missing cell, a
196
+ Tcl-unsafe cell name, no tech form, both sources,
197
+ or a Tcl-unsafe GDS path, missing_dependency for a
198
+ missing magic binary or unloadable tech,
199
+ invalid_reference for a missing cell, GDS, or tech
200
+ file.
201
+ """
202
+ # Cheap deterministic checks first (operation, cell, tech),
203
+ # machine-dependent ones last (binary, tech probe).
204
+ if job.get("operation") != OPERATION:
205
+ raise AdapterError(
206
+ "unsupported_operation",
207
+ f"Magic extraction handles {OPERATION}, got {job.get('operation')}.",
208
+ )
209
+ cell = job.get("cell")
210
+ if not cell or not isinstance(cell, str):
211
+ raise AdapterError("invalid_command", "Extraction needs a cell name.")
212
+ timeout = _budget_or_raise(job)
213
+ if _CELL_BAD_RE.search(cell):
214
+ raise AdapterError(
215
+ "invalid_command",
216
+ f"Bad cell name (Tcl-unsafe characters): {cell!r}.",
217
+ )
218
+ tech_arg = _tech_arg_or_raise(job)
219
+ if shutil.which("magic") is None:
220
+ raise AdapterError("missing_dependency", "magic binary not found.")
221
+ probe = self._run(
222
+ tech_arg, "quit\n", job.get("workdir", "."), timeout=timeout
223
+ )
224
+ if Path(tech_arg).is_file():
225
+ if 'Using technology "' not in probe.stdout:
226
+ raise AdapterError(
227
+ "missing_dependency", f"Cannot load magic tech: {tech_arg}."
228
+ )
229
+ elif f'Using technology "{tech_arg}"' not in probe.stdout:
230
+ raise AdapterError(
231
+ "missing_dependency", f"Cannot load magic tech: {tech_arg}."
232
+ )
233
+ source, source_file = _source_or_raise(job)
234
+ checked = {
235
+ "tech_arg": tech_arg,
236
+ "cell": cell,
237
+ "source": source,
238
+ "source_file": source_file,
239
+ "timeout_s": timeout,
240
+ }
241
+ if source == "gds":
242
+ checked["gds_file"] = source_file
243
+ else:
244
+ checked["cell_file"] = source_file
245
+ return checked
246
+
247
+ def execute(self, job):
248
+ """Stage the source, extract all, and convert with ext2spice.
249
+
250
+ A GDS source is read with `gds read` before `load`; a .mag
251
+ source loads directly. Both converge on the same extract +
252
+ ext2spice tail.
253
+
254
+ Args:
255
+ job: Solver job with operation run_extraction plus
256
+ cell, one source (cell_file or gds_file), and tech
257
+ inputs.
258
+
259
+ Returns:
260
+ Native output with exit_code, stdout/stderr, tech,
261
+ cell, source, and the .ext/.spice artifact paths
262
+ and text.
263
+
264
+ Raises:
265
+ AdapterError: invalid_reference for a missing workdir,
266
+ missing_dependency when the tech fails to load
267
+ mid-run, runner_unavailable when the output has no
268
+ ext2spice marker or no .ext/.spice pair, plus the
269
+ preflight codes (execute re-checks first).
270
+ """
271
+ checked = self.preflight(job)
272
+ workdir = job.get("workdir", ".")
273
+ if not Path(workdir).is_dir():
274
+ raise AdapterError(
275
+ "invalid_reference", f"Workdir not found: {workdir}."
276
+ )
277
+ staged = Path(workdir) / Path(checked["source_file"]).name
278
+ if Path(checked["source_file"]).resolve() != staged.resolve():
279
+ shutil.copy(checked["source_file"], staged)
280
+ if checked["source"] == "gds":
281
+ proc = self._run_gds(
282
+ checked["tech_arg"], str(staged), checked["cell"], workdir,
283
+ timeout=checked["timeout_s"],
284
+ )
285
+ else:
286
+ proc = self._run(
287
+ checked["tech_arg"], _SCRIPT.format(cell=checked["cell"]), workdir,
288
+ timeout=checked["timeout_s"],
289
+ )
290
+ if "Failed to load technology" in proc.stdout:
291
+ raise AdapterError(
292
+ "missing_dependency",
293
+ f"Cannot load magic tech: {checked['tech_arg']}.",
294
+ )
295
+ if _MARKER not in proc.stdout:
296
+ raise AdapterError(
297
+ "runner_unavailable", "magic output has no ext2spice marker."
298
+ )
299
+ ext_file = Path(workdir) / f"{checked['cell']}.ext"
300
+ spice_file = Path(workdir) / f"{checked['cell']}.spice"
301
+ if not ext_file.is_file() or not spice_file.is_file():
302
+ raise AdapterError(
303
+ "runner_unavailable", "magic produced no .ext/.spice pair."
304
+ )
305
+ return {
306
+ "exit_code": proc.returncode,
307
+ "stdout": proc.stdout,
308
+ "stderr": proc.stderr,
309
+ "tech": checked["tech_arg"],
310
+ "cell": checked["cell"],
311
+ "source": checked["source"],
312
+ "ext_file": str(ext_file),
313
+ "spice_file": str(spice_file),
314
+ "spice_text": spice_file.read_text(encoding="utf-8"),
315
+ }
316
+
317
+ def parse(self, native_output):
318
+ """Parse the ext2spice marker plus the device table.
319
+
320
+ Args:
321
+ native_output: Native output with magic stdout and the
322
+ spice text or file.
323
+
324
+ Returns:
325
+ Result with cell, artifact paths, total and
326
+ device_count (both the device inventory size),
327
+ devices, and no violations.
328
+
329
+ Raises:
330
+ AdapterError: runner_unavailable when the output has no
331
+ ext2spice marker, no spice text, or no subckt block.
332
+ """
333
+ stdout = native_output.get("stdout", "")
334
+ if _MARKER not in stdout:
335
+ raise AdapterError(
336
+ "runner_unavailable", "magic output has no ext2spice marker."
337
+ )
338
+ spice_text = native_output.get("spice_text")
339
+ if spice_text is None:
340
+ spice_file = native_output.get("spice_file")
341
+ if not spice_file or not Path(spice_file).is_file():
342
+ raise AdapterError(
343
+ "runner_unavailable", "magic extraction has no spice text."
344
+ )
345
+ spice_text = Path(spice_file).read_text(encoding="utf-8")
346
+ if _SUBCKT_RE.search(spice_text) is None:
347
+ raise AdapterError(
348
+ "runner_unavailable", "extracted spice has no subckt block."
349
+ )
350
+ devices = []
351
+ for line in spice_text.splitlines():
352
+ match = _DEVICE_RE.match(line.strip())
353
+ if match is None:
354
+ continue
355
+ name, *nets, cls, params = match.groups()
356
+ numbers = {
357
+ key.lower(): float(value)
358
+ for key, value in _PARAM_RE.findall(params)
359
+ }
360
+ devices.append(
361
+ {
362
+ "name": f"X{name}",
363
+ "nets": nets,
364
+ "class": cls,
365
+ "w": numbers.get("w"),
366
+ "l": numbers.get("l"),
367
+ }
368
+ )
369
+ return {
370
+ "cell": native_output.get("cell"),
371
+ "ext_file": native_output.get("ext_file"),
372
+ "spice_file": native_output.get("spice_file"),
373
+ "total": len(devices),
374
+ "device_count": len(devices),
375
+ "devices": devices,
376
+ "violations": [],
377
+ }
378
+
379
+ def _run_gds(self, tech, gds_path, cell, workdir, timeout=_TIMEOUT_S):
380
+ """Run magic on a GDSII source: `gds read`, then the usual script.
381
+
382
+ Args:
383
+ tech: Magic tech name or tech file path.
384
+ gds_path: GDSII file staged for the run (already
385
+ Tcl-safety checked by preflight).
386
+ cell: Cell name loaded after the GDS read.
387
+ workdir: Run directory.
388
+ timeout: Budget seconds for the run.
389
+
390
+ Returns:
391
+ Completed process with magic stdout/stderr.
392
+ """
393
+ return self._run(
394
+ tech,
395
+ _GDS_SCRIPT.format(gds=gds_path, cell=cell),
396
+ workdir,
397
+ timeout=timeout,
398
+ )
399
+
400
+ def _run(self, tech, script, workdir, timeout=_TIMEOUT_S):
401
+ argv = ["magic", "-dnull", "-noconsole", "-T", tech]
402
+ if self._runner is not None:
403
+ try:
404
+ return self._runner.run(
405
+ argv, workdir, timeout, (), input_text=script
406
+ )
407
+ except ExecutionError as exc:
408
+ raise AdapterError(exc.code, str(exc)) from exc
409
+ if not Path(workdir).is_dir():
410
+ raise AdapterError(
411
+ "invalid_reference", f"Workdir not found: {workdir}."
412
+ )
413
+ try:
414
+ return subprocess.run(
415
+ argv,
416
+ input=script,
417
+ capture_output=True,
418
+ text=True,
419
+ timeout=timeout,
420
+ cwd=workdir,
421
+ )
422
+ except subprocess.TimeoutExpired as exc:
423
+ raise AdapterError(
424
+ "runner_unavailable", f"magic timed out after {timeout}s."
425
+ ) from exc
@@ -0,0 +1,209 @@
1
+ """Magic-extraction v1 normalizer: v0 parse output to a ResultBundle.
2
+
3
+ Maps the ``run_extraction`` parse shape (device inventory with
4
+ name, nets, class, and w/l plus the device count) onto a typed
5
+ devices table, a bound view, a device-count measurement, and no
6
+ findings on clean extraction. Pure function of the parsed
7
+ output: no I/O, no tool calls, deterministic ids derived from
8
+ the run id.
9
+ """
10
+
11
+ TASK = "run_extraction"
12
+
13
+ _DIAG_CAP = 240
14
+
15
+ _SEVERITY = {"error": "error", "warning": "warning"}
16
+
17
+
18
+ def _cap(text):
19
+ """Cap one diagnostic string at the bundle prose limit.
20
+
21
+ Args:
22
+ text: Raw diagnostic text, possibly unbounded tool prose.
23
+
24
+ Returns:
25
+ The text unchanged when short, else truncated with an
26
+ ellipsis marker.
27
+ """
28
+ text = text if isinstance(text, str) else str(text)
29
+ if len(text) <= _DIAG_CAP:
30
+ return text
31
+ return text[: _DIAG_CAP - 3] + "..."
32
+
33
+
34
+ def _quantity(value, unit):
35
+ """Build one v1 Quantity, mapping unitless to dimensionless.
36
+
37
+ Args:
38
+ value: Numeric value.
39
+ unit: Unit string, possibly empty for unitless measures.
40
+
41
+ Returns:
42
+ Quantity mapping with an explicit unit.
43
+ """
44
+ return {"value": float(value), "unit": unit if unit else "1"}
45
+
46
+
47
+ def _number(value):
48
+ """Render one optional device dimension as table text.
49
+
50
+ Args:
51
+ value: Numeric dimension, or None when the device class
52
+ carries none.
53
+
54
+ Returns:
55
+ Numeric text, or an empty string for a missing dimension.
56
+ """
57
+ if value is None:
58
+ return ""
59
+ return str(float(value))
60
+
61
+
62
+ def _provenance(project_id, run_id, tools, provenance):
63
+ """Build ResultProvenance, defaulting to a placeholder closure.
64
+
65
+ Args:
66
+ project_id: Project the run belongs to.
67
+ run_id: Run the bundle describes.
68
+ tools: Tool ingredient mappings (id, version, digest?).
69
+ provenance: Full provenance mapping when the caller has
70
+ one; a deterministic placeholder otherwise.
71
+
72
+ Returns:
73
+ Provenance mapping valid against the v1 schema.
74
+ """
75
+ if provenance is not None:
76
+ return provenance
77
+ ref = {"scope": "project", "id": project_id, "revision": "r1"}
78
+ return {
79
+ "snapshot": dict(ref),
80
+ "input_lock": dict(ref),
81
+ "closure_hash": "0" * 64,
82
+ "tools": [
83
+ {
84
+ "id": item["id"],
85
+ "version": item.get("version", ""),
86
+ "digest": item.get("digest", "0" * 64),
87
+ }
88
+ for item in tools
89
+ ],
90
+ "parents": [],
91
+ }
92
+
93
+
94
+ def normalize(parsed, *, run_id, project_id="project", target=None,
95
+ tools=(), provenance=None):
96
+ """Normalize one magic-extraction parse output to a ResultBundle.
97
+
98
+ Device rows become a table dataset (name, class, nets, w, l)
99
+ plus a bound table view; the device count rides a PASS
100
+ measurement; violations (never present on clean extraction)
101
+ become findings with capped messages.
102
+
103
+ Args:
104
+ parsed: v0 parse mapping with ``devices[]``,
105
+ ``device_count``, and ``violations``.
106
+ run_id: Run the bundle describes (anchors all ids).
107
+ project_id: Project the run belongs to.
108
+ target: v1 target mapping; project kind by default.
109
+ tools: Tool ingredient mappings for provenance.
110
+ provenance: Full provenance mapping when the caller has
111
+ one; a deterministic placeholder otherwise.
112
+
113
+ Returns:
114
+ Schema-valid ResultBundle mapping.
115
+ """
116
+ devices = parsed.get("devices") or []
117
+ violations = parsed.get("violations") or []
118
+ table_id = f"{run_id}:dataset:devices"
119
+ datasets = [
120
+ {
121
+ "id": table_id,
122
+ "kind": "table",
123
+ "row_count": len(devices),
124
+ "columns": [
125
+ {
126
+ "id": f"{table_id}:name",
127
+ "name": "name",
128
+ "dtype": "utf8",
129
+ "unit": "1",
130
+ "data": [d.get("name", "") for d in devices],
131
+ },
132
+ {
133
+ "id": f"{table_id}:class",
134
+ "name": "class",
135
+ "dtype": "utf8",
136
+ "unit": "1",
137
+ "data": [d.get("class", "") for d in devices],
138
+ },
139
+ {
140
+ "id": f"{table_id}:nets",
141
+ "name": "nets",
142
+ "dtype": "utf8",
143
+ "unit": "1",
144
+ "data": [
145
+ ",".join(d.get("nets") or []) for d in devices
146
+ ],
147
+ },
148
+ {
149
+ "id": f"{table_id}:w",
150
+ "name": "w",
151
+ "dtype": "utf8",
152
+ "unit": "1",
153
+ "data": [_number(d.get("w")) for d in devices],
154
+ },
155
+ {
156
+ "id": f"{table_id}:l",
157
+ "name": "l",
158
+ "dtype": "utf8",
159
+ "unit": "1",
160
+ "data": [_number(d.get("l")) for d in devices],
161
+ },
162
+ ],
163
+ "conditions": [],
164
+ }
165
+ ]
166
+ views = [
167
+ {
168
+ "id": f"{run_id}:view:devices",
169
+ "kind": "table",
170
+ "dataset_id": table_id,
171
+ "name": "extracted devices",
172
+ }
173
+ ]
174
+ measurements = [
175
+ {
176
+ "name": "device_count",
177
+ "value": _quantity(parsed.get("device_count", 0), "1"),
178
+ "status": "PASS",
179
+ }
180
+ ]
181
+ findings = [
182
+ {
183
+ "id": f"{run_id}:finding:{index}",
184
+ "severity": _SEVERITY.get(v.get("severity"), "info"),
185
+ "code": v.get("rule", "extraction"),
186
+ "message": _cap(v.get("message", "")),
187
+ }
188
+ for index, v in enumerate(violations)
189
+ ]
190
+ return {
191
+ "schema": "rocketeda/results/v1",
192
+ "ref": {"scope": "project", "id": run_id, "revision": "r1"},
193
+ "run_id": run_id,
194
+ "task": TASK,
195
+ "target": dict(target)
196
+ if target is not None
197
+ else {"project_id": project_id, "kind": "project"},
198
+ "state": "complete",
199
+ "freshness": "current",
200
+ "convergence": "not_applicable",
201
+ "provenance": _provenance(project_id, run_id, tools, provenance),
202
+ "datasets": datasets,
203
+ "views": views,
204
+ "measurements": measurements,
205
+ "findings": findings,
206
+ "artifacts": [],
207
+ "logs": [],
208
+ "gaps": [],
209
+ }
@@ -0,0 +1,7 @@
1
+ # SymbiYosys formal adapter
2
+
3
+ FormalSbyAdapter serves run_formal with SBY. It creates a task from the job's project sources, mode, depth, and engines, runs it under a timeout, and parses the status file and DONE summary into scoped evidence.
4
+
5
+ Results distinguish proven, bounded, bounded-vacuous, unknown, and timeout. A PASS always carries its proof scope and checked properties. A proof with no active property is reported as bounded-vacuous failure, not a clean PASS. Counterexamples and assumptions are included where available.
6
+
7
+ The adapter is registered in Engine as the sby solver. It requires a version-asserted SBY binary, resolved from the job tool_dir or PATH. See the [FORMAL-SBY report](../../../docs/work/FORMAL-SBY/report.md), [FORMAL-EDGE report](../../../docs/work/FORMAL-EDGE/report.md), and [provision plan](../../../docs/provision-plan.md).