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,287 @@
1
+ """Magic DRC solver adapter (I14): batch DRC behind the ToolAdapter port.
2
+
3
+ Protocol, verified against magic 8.3.685 + scmos tech 8.2.8: Tcl over
4
+ stdin, `drc check` + `drc catchup` under an explicit box (`check`
5
+ rechecks the box area; `count` needs the catchup), `drc count total`
6
+ for the verdict, then `drc find N` + box readout per error area until
7
+ "There aren't that many errors". Magic exits 0 even for unknown techs
8
+ and broken cells, so the verdict always comes from parsed output,
9
+ never from 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_drc"
23
+ _TIMEOUT_S = 120
24
+ _BOX_MARGIN = 5
25
+ _DEFAULT_BOX = (-5, -5, 15, 10)
26
+
27
+ _TOTAL_RE = re.compile(r"Total DRC errors found: (\d+)")
28
+ _AREA_RE = re.compile(r"Error area #(\d+):\n([^\n]+)\n(-?\d+) (-?\d+) (-?\d+) (-?\d+)")
29
+
30
+
31
+ def _budget_or_raise(job):
32
+ """Job solver budget: validated, or the adapter default.
33
+
34
+ Args:
35
+ job: Solver job, maybe with a `timeout_s`.
36
+
37
+ Returns:
38
+ Budget seconds for every `_run` call.
39
+
40
+ Raises:
41
+ AdapterError: invalid_command for a boolean,
42
+ non-number, or out-of-bounds budget.
43
+ """
44
+ timeout = job.get("timeout_s", _TIMEOUT_S)
45
+ if (
46
+ isinstance(timeout, bool)
47
+ or not isinstance(timeout, (int, float))
48
+ or not MIN_TIMEOUT_S <= timeout <= MAX_TIMEOUT_S
49
+ ):
50
+ raise AdapterError(
51
+ "invalid_command", f"Bad timeout_s: {timeout!r}."
52
+ )
53
+ return timeout
54
+
55
+
56
+ def _box_for(cell_file):
57
+ x0 = y0 = x1 = y1 = None
58
+ for line in Path(cell_file).read_text().splitlines():
59
+ parts = line.split()
60
+ if len(parts) == 5 and parts[0] == "rect":
61
+ try:
62
+ coords = tuple(int(v) for v in parts[1:])
63
+ except ValueError:
64
+ continue
65
+ if x0 is None:
66
+ x0, y0, x1, y1 = coords
67
+ else:
68
+ x0 = min(x0, coords[0])
69
+ y0 = min(y0, coords[1])
70
+ x1 = max(x1, coords[2])
71
+ y1 = max(y1, coords[3])
72
+ if x0 is None:
73
+ return _DEFAULT_BOX
74
+ m = _BOX_MARGIN
75
+ return (x0 - m, y0 - m, x1 + m, y1 + m)
76
+
77
+
78
+ class MagicDrcAdapter(ToolAdapter):
79
+ """run_drc via Magic: batch DRC over .mag cells, lambda-unit boxes.
80
+
81
+ Serves the run_drc operation with deck.tool `magic` (the
82
+ operation default). Tcl over stdin against a loaded cell; the
83
+ result is {total, violations[]} with rule/message plus
84
+ lambda-unit bbox areas.
85
+
86
+ Job keys:
87
+ cell (required): Cell name loaded inside magic.
88
+ cell_file (required): Path to the .mag cell file.
89
+ tech: Magic tech name (default scmos).
90
+ deck: Optional mapping; deck.tool selects the solver
91
+ (magic here, klayout for the KLayout path).
92
+ workdir: Run directory (default ".").
93
+ timeout_s: Sim budget in seconds (default 120).
94
+
95
+ Args:
96
+ runner: Optional isolated runner; direct subprocess when None.
97
+
98
+ Caution: Magic exits 0 even for broken cells and unknown techs —
99
+ the verdict always comes from parsed output, never the exit
100
+ code. Timeout rides the job budget.
101
+ """
102
+
103
+ def __init__(self, runner=None):
104
+ self._runner = runner
105
+
106
+ def capabilities(self, target):
107
+ """Report the served operation, tech, and tool name.
108
+
109
+ Args:
110
+ target: Target the capabilities are reported for
111
+ (accepted, unused — one tech per job).
112
+
113
+ Returns:
114
+ Capability mapping with operations, tech, and tool.
115
+ """
116
+ return {"operations": [OPERATION], "tech": "scmos", "tool": "magic"}
117
+
118
+ def preflight(self, job):
119
+ """Check operation, magic binary, tech probe, and cell file.
120
+
121
+ Args:
122
+ job: Solver job with operation run_drc plus cell,
123
+ cell_file, and tech inputs.
124
+
125
+ Returns:
126
+ Checked mapping with tech, cell, and cell_file.
127
+
128
+ Raises:
129
+ AdapterError: unsupported_operation for another
130
+ operation, missing_dependency for a missing magic
131
+ binary or unloadable tech, invalid_reference for a
132
+ missing cell file.
133
+ """
134
+ if job.get("operation") != OPERATION:
135
+ raise AdapterError(
136
+ "unsupported_operation",
137
+ f"Magic DRC handles {OPERATION}, got {job.get('operation')}.",
138
+ )
139
+ timeout = _budget_or_raise(job)
140
+ if shutil.which("magic") is None:
141
+ raise AdapterError("missing_dependency", "magic binary not found.")
142
+ tech = job.get("tech", "scmos")
143
+ probe = self._run(
144
+ tech, "quit\n", job.get("workdir", "."), timeout=timeout
145
+ )
146
+ if f'Using technology "{tech}"' not in probe.stdout:
147
+ raise AdapterError(
148
+ "missing_dependency", f"Cannot load magic tech: {tech}."
149
+ )
150
+ cell_file = job.get("cell_file")
151
+ if not cell_file or not Path(cell_file).is_file():
152
+ raise AdapterError(
153
+ "invalid_reference", f"Cell file not found: {cell_file}."
154
+ )
155
+ return {
156
+ "tech": tech,
157
+ "cell": job.get("cell"),
158
+ "cell_file": cell_file,
159
+ "timeout_s": timeout,
160
+ }
161
+
162
+ def execute(self, job):
163
+ """Run magic DRC: a count pass, then per-area detail.
164
+
165
+ Args:
166
+ job: Solver job with operation run_drc plus cell,
167
+ cell_file, and tech inputs.
168
+
169
+ Returns:
170
+ Native output with exit_code, combined stdout/stderr,
171
+ tech, and cell.
172
+
173
+ Raises:
174
+ AdapterError: missing_dependency when the tech fails
175
+ to load mid-run, runner_unavailable when magic
176
+ output carries no DRC total line, plus the
177
+ preflight codes (execute re-checks first).
178
+ """
179
+ checked = self.preflight(job)
180
+ workdir = job.get("workdir", ".")
181
+ box = _box_for(checked["cell_file"])
182
+ count_script = (
183
+ "load {cell}\nbox values {x0} {y0} {x1} {y1}\n"
184
+ "drc check\ndrc catchup\ndrc count total\nquit\n"
185
+ ).format(cell=checked["cell"], x0=box[0], y0=box[1], x1=box[2], y1=box[3])
186
+ first = self._run(
187
+ checked["tech"], count_script, workdir,
188
+ timeout=checked["timeout_s"],
189
+ )
190
+ if "Failed to load technology" in first.stdout:
191
+ raise AdapterError(
192
+ "missing_dependency", f"Cannot load magic tech: {checked['tech']}."
193
+ )
194
+ match = _TOTAL_RE.search(first.stdout)
195
+ if match is None:
196
+ raise AdapterError(
197
+ "runner_unavailable", "magic output has no DRC total line."
198
+ )
199
+ total = int(match.group(1))
200
+ # Error areas never outnumber error tiles, so total+1 finds always
201
+ # reach the "aren't that many" terminator.
202
+ finds = "".join(
203
+ f"drc find {n}\nputs [box values]\ndrc why\n"
204
+ for n in range(1, total + 2)
205
+ )
206
+ detail_script = (
207
+ "load {cell}\nbox values {x0} {y0} {x1} {y1}\n"
208
+ "drc check\ndrc catchup\n{finds}quit\n"
209
+ ).format(
210
+ cell=checked["cell"],
211
+ x0=box[0],
212
+ y0=box[1],
213
+ x1=box[2],
214
+ y1=box[3],
215
+ finds=finds,
216
+ )
217
+ if total:
218
+ second = self._run(
219
+ checked["tech"], detail_script, workdir,
220
+ timeout=checked["timeout_s"],
221
+ )
222
+ else:
223
+ second = None
224
+ return {
225
+ "exit_code": first.returncode,
226
+ "stdout": first.stdout + (second.stdout if second else ""),
227
+ "stderr": first.stderr + (second.stderr if second else ""),
228
+ "tech": checked["tech"],
229
+ "cell": checked["cell"],
230
+ }
231
+
232
+ def parse(self, native_output):
233
+ """Parse the DRC total plus per-area rule boxes.
234
+
235
+ Args:
236
+ native_output: Native output with magic stdout.
237
+
238
+ Returns:
239
+ {total, violations[]} with rule plus lambda-unit
240
+ bbox areas.
241
+
242
+ Raises:
243
+ AdapterError: runner_unavailable when the output has no
244
+ DRC total line.
245
+ """
246
+ stdout = native_output.get("stdout", "")
247
+ match = _TOTAL_RE.search(stdout)
248
+ if match is None:
249
+ raise AdapterError(
250
+ "runner_unavailable", "magic output has no DRC total line."
251
+ )
252
+ violations = [
253
+ {
254
+ "area": int(area),
255
+ "rule": rule,
256
+ "bbox": [int(x0), int(y0), int(x1), int(y1)],
257
+ }
258
+ for area, rule, x0, y0, x1, y1 in _AREA_RE.findall(stdout)
259
+ ]
260
+ return {"total": int(match.group(1)), "violations": violations}
261
+
262
+ def _run(self, tech, script, workdir, timeout=_TIMEOUT_S):
263
+ argv = ["magic", "-dnull", "-noconsole", "-T", tech]
264
+ if self._runner is not None:
265
+ try:
266
+ return self._runner.run(
267
+ argv, workdir, timeout, (), input_text=script
268
+ )
269
+ except ExecutionError as exc:
270
+ raise AdapterError(exc.code, str(exc)) from exc
271
+ if not Path(workdir).is_dir():
272
+ raise AdapterError(
273
+ "invalid_reference", f"Workdir not found: {workdir}."
274
+ )
275
+ try:
276
+ return subprocess.run(
277
+ argv,
278
+ input=script,
279
+ capture_output=True,
280
+ text=True,
281
+ timeout=timeout,
282
+ cwd=workdir,
283
+ )
284
+ except subprocess.TimeoutExpired as exc:
285
+ raise AdapterError(
286
+ "runner_unavailable", f"magic timed out after {timeout}s."
287
+ ) from exc
@@ -0,0 +1,184 @@
1
+ """Magic DRC v1 normalizer: v0 parse output to a ResultBundle.
2
+
3
+ Maps the ``run_drc`` magic parse shape (``violations[]`` with
4
+ rule, lambda-unit area, and bbox) onto a typed rule table, a
5
+ bound view, and findings. Pure function of the parsed output: no
6
+ I/O, no tool calls, deterministic ids derived from the run id.
7
+ Magic reports no severity or message per box (see ESCAPE-HARDEN),
8
+ so every box fails as an error finding naming its rule.
9
+ """
10
+
11
+ TASK = "run_drc"
12
+
13
+ _DIAG_CAP = 240
14
+
15
+
16
+ def _cap(text):
17
+ """Cap one diagnostic string at the bundle prose limit.
18
+
19
+ Args:
20
+ text: Raw diagnostic text, possibly unbounded tool prose.
21
+
22
+ Returns:
23
+ The text unchanged when short, else truncated with an
24
+ ellipsis marker.
25
+ """
26
+ text = text if isinstance(text, str) else str(text)
27
+ if len(text) <= _DIAG_CAP:
28
+ return text
29
+ return text[: _DIAG_CAP - 3] + "..."
30
+
31
+
32
+ def _bbox_text(bbox):
33
+ """Render one lambda-unit bbox as comma-joined coordinates.
34
+
35
+ Args:
36
+ bbox: Coordinate list from the v0 violation, if any.
37
+
38
+ Returns:
39
+ Comma-joined coordinate text, or an empty string when the
40
+ violation carries no bbox.
41
+ """
42
+ if not bbox:
43
+ return ""
44
+ return ",".join(str(c) for c in bbox)
45
+
46
+
47
+ def _message(violation):
48
+ """Render one violation message from its rule, area, and bbox.
49
+
50
+ Args:
51
+ violation: v0 violation with rule, area, and bbox.
52
+
53
+ Returns:
54
+ Capped message naming the rule and its geometry.
55
+ """
56
+ rule = violation.get("rule", "drc")
57
+ area = violation.get("area")
58
+ bbox = _bbox_text(violation.get("bbox"))
59
+ text = f"{rule} at [{bbox}] area {area}"
60
+ return _cap(text)
61
+
62
+
63
+ def _provenance(project_id, run_id, tools, provenance):
64
+ """Build ResultProvenance, defaulting to a placeholder closure.
65
+
66
+ Args:
67
+ project_id: Project the run belongs to.
68
+ run_id: Run the bundle describes.
69
+ tools: Tool ingredient mappings (id, version, digest?).
70
+ provenance: Full provenance mapping when the caller has
71
+ one; a deterministic placeholder otherwise.
72
+
73
+ Returns:
74
+ Provenance mapping valid against the v1 schema.
75
+ """
76
+ if provenance is not None:
77
+ return provenance
78
+ ref = {"scope": "project", "id": project_id, "revision": "r1"}
79
+ return {
80
+ "snapshot": dict(ref),
81
+ "input_lock": dict(ref),
82
+ "closure_hash": "0" * 64,
83
+ "tools": [
84
+ {
85
+ "id": item["id"],
86
+ "version": item.get("version", ""),
87
+ "digest": item.get("digest", "0" * 64),
88
+ }
89
+ for item in tools
90
+ ],
91
+ "parents": [],
92
+ }
93
+
94
+
95
+ def normalize(parsed, *, run_id, project_id="project", target=None,
96
+ tools=(), provenance=None):
97
+ """Normalize one magic DRC parse output to a v1 ResultBundle.
98
+
99
+ Rule boxes become a table dataset (rule, area, bbox) plus a
100
+ bound table view; every box also becomes an error finding
101
+ naming its rule.
102
+
103
+ Args:
104
+ parsed: v0 parse mapping with ``violations[]``.
105
+ run_id: Run the bundle describes (anchors all ids).
106
+ project_id: Project the run belongs to.
107
+ target: v1 target mapping; project kind by default.
108
+ tools: Tool ingredient mappings for provenance.
109
+ provenance: Full provenance mapping when the caller has
110
+ one; a deterministic placeholder otherwise.
111
+
112
+ Returns:
113
+ Schema-valid ResultBundle mapping.
114
+ """
115
+ violations = parsed.get("violations") or []
116
+ table_id = f"{run_id}:dataset:violations"
117
+ datasets = [
118
+ {
119
+ "id": table_id,
120
+ "kind": "table",
121
+ "row_count": len(violations),
122
+ "columns": [
123
+ {
124
+ "id": f"{table_id}:rule",
125
+ "name": "rule",
126
+ "dtype": "utf8",
127
+ "unit": "1",
128
+ "data": [v.get("rule", "") for v in violations],
129
+ },
130
+ {
131
+ "id": f"{table_id}:area",
132
+ "name": "area",
133
+ "dtype": "float64",
134
+ "unit": "1",
135
+ "data": [float(v.get("area", 0)) for v in violations],
136
+ },
137
+ {
138
+ "id": f"{table_id}:bbox",
139
+ "name": "bbox",
140
+ "dtype": "utf8",
141
+ "unit": "1",
142
+ "data": [_bbox_text(v.get("bbox")) for v in violations],
143
+ },
144
+ ],
145
+ "conditions": [],
146
+ }
147
+ ]
148
+ views = [
149
+ {
150
+ "id": f"{run_id}:view:violations",
151
+ "kind": "table",
152
+ "dataset_id": table_id,
153
+ "name": "drc violations",
154
+ }
155
+ ]
156
+ findings = [
157
+ {
158
+ "id": f"{run_id}:finding:{index}",
159
+ "severity": "error",
160
+ "code": v.get("rule", "drc") or "drc",
161
+ "message": _message(v),
162
+ }
163
+ for index, v in enumerate(violations)
164
+ ]
165
+ return {
166
+ "schema": "rocketeda/results/v1",
167
+ "ref": {"scope": "project", "id": run_id, "revision": "r1"},
168
+ "run_id": run_id,
169
+ "task": TASK,
170
+ "target": dict(target)
171
+ if target is not None
172
+ else {"project_id": project_id, "kind": "project"},
173
+ "state": "complete",
174
+ "freshness": "current",
175
+ "convergence": "not_applicable",
176
+ "provenance": _provenance(project_id, run_id, tools, provenance),
177
+ "datasets": datasets,
178
+ "views": views,
179
+ "measurements": [],
180
+ "findings": findings,
181
+ "artifacts": [],
182
+ "logs": [],
183
+ "gaps": [],
184
+ }
@@ -0,0 +1,188 @@
1
+ """KLayout DRC v1 normalizer: v0 parse output to a ResultBundle.
2
+
3
+ Maps the ``run_drc`` klayout parse shape (``violations[]`` with
4
+ rule, message, and optional metric ``bbox_um``) onto a typed rule
5
+ table, a bound view, and findings. Pure function of the parsed
6
+ output: no I/O, no tool calls, deterministic ids derived from
7
+ the run id. Deck mode carries messages; synthetic mode carries
8
+ only rule plus bbox, so a missing message falls back to the rule
9
+ and its geometry (see ESCAPE-HARDEN).
10
+ """
11
+
12
+ TASK = "run_drc"
13
+
14
+ _DIAG_CAP = 240
15
+
16
+
17
+ def _cap(text):
18
+ """Cap one diagnostic string at the bundle prose limit.
19
+
20
+ Args:
21
+ text: Raw diagnostic text, possibly unbounded tool prose.
22
+
23
+ Returns:
24
+ The text unchanged when short, else truncated with an
25
+ ellipsis marker.
26
+ """
27
+ text = text if isinstance(text, str) else str(text)
28
+ if len(text) <= _DIAG_CAP:
29
+ return text
30
+ return text[: _DIAG_CAP - 3] + "..."
31
+
32
+
33
+ def _bbox_text(bbox_um):
34
+ """Render one metric bbox as comma-joined coordinates.
35
+
36
+ Args:
37
+ bbox_um: Metric coordinate list, if any.
38
+
39
+ Returns:
40
+ Comma-joined coordinate text, or an empty string when the
41
+ violation carries no geometry.
42
+ """
43
+ if not bbox_um:
44
+ return ""
45
+ return ",".join(str(c) for c in bbox_um)
46
+
47
+
48
+ def _message(violation):
49
+ """Render one violation message, falling back to rule geometry.
50
+
51
+ Args:
52
+ violation: v0 violation with rule, message, and bbox_um.
53
+
54
+ Returns:
55
+ Capped message text naming the rule.
56
+ """
57
+ message = violation.get("message")
58
+ if message:
59
+ return _cap(message)
60
+ rule = violation.get("rule", "drc")
61
+ bbox = _bbox_text(violation.get("bbox_um"))
62
+ return _cap(f"{rule} at [{bbox}] um")
63
+
64
+
65
+ def _provenance(project_id, run_id, tools, provenance):
66
+ """Build ResultProvenance, defaulting to a placeholder closure.
67
+
68
+ Args:
69
+ project_id: Project the run belongs to.
70
+ run_id: Run the bundle describes.
71
+ tools: Tool ingredient mappings (id, version, digest?).
72
+ provenance: Full provenance mapping when the caller has
73
+ one; a deterministic placeholder otherwise.
74
+
75
+ Returns:
76
+ Provenance mapping valid against the v1 schema.
77
+ """
78
+ if provenance is not None:
79
+ return provenance
80
+ ref = {"scope": "project", "id": project_id, "revision": "r1"}
81
+ return {
82
+ "snapshot": dict(ref),
83
+ "input_lock": dict(ref),
84
+ "closure_hash": "0" * 64,
85
+ "tools": [
86
+ {
87
+ "id": item["id"],
88
+ "version": item.get("version", ""),
89
+ "digest": item.get("digest", "0" * 64),
90
+ }
91
+ for item in tools
92
+ ],
93
+ "parents": [],
94
+ }
95
+
96
+
97
+ def normalize(parsed, *, run_id, project_id="project", target=None,
98
+ tools=(), provenance=None):
99
+ """Normalize one klayout DRC parse output to a v1 ResultBundle.
100
+
101
+ Violations become a table dataset (rule, message, bbox) plus
102
+ a bound table view; every violation also becomes an error
103
+ finding with a capped message.
104
+
105
+ Args:
106
+ parsed: v0 parse mapping with ``violations[]``.
107
+ run_id: Run the bundle describes (anchors all ids).
108
+ project_id: Project the run belongs to.
109
+ target: v1 target mapping; project kind by default.
110
+ tools: Tool ingredient mappings for provenance.
111
+ provenance: Full provenance mapping when the caller has
112
+ one; a deterministic placeholder otherwise.
113
+
114
+ Returns:
115
+ Schema-valid ResultBundle mapping.
116
+ """
117
+ violations = parsed.get("violations") or []
118
+ table_id = f"{run_id}:dataset:violations"
119
+ datasets = [
120
+ {
121
+ "id": table_id,
122
+ "kind": "table",
123
+ "row_count": len(violations),
124
+ "columns": [
125
+ {
126
+ "id": f"{table_id}:rule",
127
+ "name": "rule",
128
+ "dtype": "utf8",
129
+ "unit": "1",
130
+ "data": [v.get("rule", "") for v in violations],
131
+ },
132
+ {
133
+ "id": f"{table_id}:message",
134
+ "name": "message",
135
+ "dtype": "utf8",
136
+ "unit": "1",
137
+ "data": [_message(v) for v in violations],
138
+ },
139
+ {
140
+ "id": f"{table_id}:bbox",
141
+ "name": "bbox",
142
+ "dtype": "utf8",
143
+ "unit": "1",
144
+ "data": [
145
+ _bbox_text(v.get("bbox_um")) for v in violations
146
+ ],
147
+ },
148
+ ],
149
+ "conditions": [],
150
+ }
151
+ ]
152
+ views = [
153
+ {
154
+ "id": f"{run_id}:view:violations",
155
+ "kind": "table",
156
+ "dataset_id": table_id,
157
+ "name": "drc violations",
158
+ }
159
+ ]
160
+ findings = [
161
+ {
162
+ "id": f"{run_id}:finding:{index}",
163
+ "severity": "error",
164
+ "code": v.get("rule", "drc") or "drc",
165
+ "message": _message(v),
166
+ }
167
+ for index, v in enumerate(violations)
168
+ ]
169
+ return {
170
+ "schema": "rocketeda/results/v1",
171
+ "ref": {"scope": "project", "id": run_id, "revision": "r1"},
172
+ "run_id": run_id,
173
+ "task": TASK,
174
+ "target": dict(target)
175
+ if target is not None
176
+ else {"project_id": project_id, "kind": "project"},
177
+ "state": "complete",
178
+ "freshness": "current",
179
+ "convergence": "not_applicable",
180
+ "provenance": _provenance(project_id, run_id, tools, provenance),
181
+ "datasets": datasets,
182
+ "views": views,
183
+ "measurements": [],
184
+ "findings": findings,
185
+ "artifacts": [],
186
+ "logs": [],
187
+ "gaps": [],
188
+ }