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,227 @@
1
+ """Digital-flow v1 normalizer: v0 parse output to a ResultBundle.
2
+
3
+ Maps the ``run_digital_flow`` parse shape (per-stage gate table
4
+ with id/label/status/evidence plus top-level gds/netlist/spef
5
+ paths, drc_total, and lvs_match) onto a typed stages table, a
6
+ deliverables table, bound views, measurements, and findings.
7
+ Pure function of the parsed output: no I/O, no tool calls,
8
+ deterministic ids derived from the run id. File paths ride the
9
+ deliverables table as evidence; no bundle artifact is fabricated
10
+ without a recorded digest.
11
+ """
12
+
13
+ TASK = "run_digital_flow"
14
+
15
+ _DIAG_CAP = 240
16
+
17
+ _SEVERITY = {"error": "error", "warning": "warning"}
18
+
19
+
20
+ def _cap(text):
21
+ """Cap one diagnostic string at the bundle prose limit.
22
+
23
+ Args:
24
+ text: Raw diagnostic text, possibly unbounded tool prose.
25
+
26
+ Returns:
27
+ The text unchanged when short, else truncated with an
28
+ ellipsis marker.
29
+ """
30
+ text = text if isinstance(text, str) else str(text)
31
+ if len(text) <= _DIAG_CAP:
32
+ return text
33
+ return text[: _DIAG_CAP - 3] + "..."
34
+
35
+
36
+ def _quantity(value, unit):
37
+ """Build one v1 Quantity, mapping unitless to dimensionless.
38
+
39
+ Args:
40
+ value: Numeric value.
41
+ unit: Unit string, possibly empty for unitless measures.
42
+
43
+ Returns:
44
+ Quantity mapping with an explicit unit.
45
+ """
46
+ return {"value": float(value), "unit": unit if unit else "1"}
47
+
48
+
49
+ def _provenance(project_id, run_id, tools, provenance):
50
+ """Build ResultProvenance, defaulting to a placeholder closure.
51
+
52
+ Args:
53
+ project_id: Project the run belongs to.
54
+ run_id: Run the bundle describes.
55
+ tools: Tool ingredient mappings (id, version, digest?).
56
+ provenance: Full provenance mapping when the caller has
57
+ one; a deterministic placeholder otherwise.
58
+
59
+ Returns:
60
+ Provenance mapping valid against the v1 schema.
61
+ """
62
+ if provenance is not None:
63
+ return provenance
64
+ ref = {"scope": "project", "id": project_id, "revision": "r1"}
65
+ return {
66
+ "snapshot": dict(ref),
67
+ "input_lock": dict(ref),
68
+ "closure_hash": "0" * 64,
69
+ "tools": [
70
+ {
71
+ "id": item["id"],
72
+ "version": item.get("version", ""),
73
+ "digest": item.get("digest", "0" * 64),
74
+ }
75
+ for item in tools
76
+ ],
77
+ "parents": [],
78
+ }
79
+
80
+
81
+ def normalize(parsed, *, run_id, project_id="project", target=None,
82
+ tools=(), provenance=None):
83
+ """Normalize one digital-flow parse output to a v1 ResultBundle.
84
+
85
+ Stage gates become a table dataset (id, label, status) plus
86
+ a deliverables table (gds/netlist/spef paths); drc_total and
87
+ lvs_match ride measurements with PASS/FAIL status; failed
88
+ gates become findings with capped messages.
89
+
90
+ Args:
91
+ parsed: v0 parse mapping with ``stages[]``,
92
+ ``violations[]``, deliverable paths, ``drc_total``,
93
+ and ``lvs_match``.
94
+ run_id: Run the bundle describes (anchors all ids).
95
+ project_id: Project the run belongs to.
96
+ target: v1 target mapping; project kind by default.
97
+ tools: Tool ingredient mappings for provenance.
98
+ provenance: Full provenance mapping when the caller has
99
+ one; a deterministic placeholder otherwise.
100
+
101
+ Returns:
102
+ Schema-valid ResultBundle mapping.
103
+ """
104
+ stages = parsed.get("stages") or []
105
+ violations = parsed.get("violations") or []
106
+ stages_id = f"{run_id}:dataset:stages"
107
+ files_id = f"{run_id}:dataset:deliverables"
108
+ deliverables = [
109
+ (name, parsed.get(name))
110
+ for name in ("gds", "netlist", "spef")
111
+ if parsed.get(name)
112
+ ]
113
+ datasets = [
114
+ {
115
+ "id": stages_id,
116
+ "kind": "table",
117
+ "row_count": len(stages),
118
+ "columns": [
119
+ {
120
+ "id": f"{stages_id}:id",
121
+ "name": "id",
122
+ "dtype": "utf8",
123
+ "unit": "1",
124
+ "data": [s.get("id", "") for s in stages],
125
+ },
126
+ {
127
+ "id": f"{stages_id}:label",
128
+ "name": "label",
129
+ "dtype": "utf8",
130
+ "unit": "1",
131
+ "data": [s.get("label", "") for s in stages],
132
+ },
133
+ {
134
+ "id": f"{stages_id}:status",
135
+ "name": "status",
136
+ "dtype": "utf8",
137
+ "unit": "1",
138
+ "data": [s.get("status", "") for s in stages],
139
+ },
140
+ ],
141
+ "conditions": [],
142
+ },
143
+ {
144
+ "id": files_id,
145
+ "kind": "table",
146
+ "row_count": len(deliverables),
147
+ "columns": [
148
+ {
149
+ "id": f"{files_id}:artifact",
150
+ "name": "artifact",
151
+ "dtype": "utf8",
152
+ "unit": "1",
153
+ "data": [name for name, _ in deliverables],
154
+ },
155
+ {
156
+ "id": f"{files_id}:path",
157
+ "name": "path",
158
+ "dtype": "utf8",
159
+ "unit": "1",
160
+ "data": [str(path) for _, path in deliverables],
161
+ },
162
+ ],
163
+ "conditions": [],
164
+ },
165
+ ]
166
+ views = [
167
+ {
168
+ "id": f"{run_id}:view:stages",
169
+ "kind": "table",
170
+ "dataset_id": stages_id,
171
+ "name": "stage gates",
172
+ },
173
+ {
174
+ "id": f"{run_id}:view:deliverables",
175
+ "kind": "table",
176
+ "dataset_id": files_id,
177
+ "name": "deliverables",
178
+ },
179
+ ]
180
+ measurements = []
181
+ drc_total = parsed.get("drc_total")
182
+ if drc_total is not None:
183
+ measurements.append(
184
+ {
185
+ "name": "drc_total",
186
+ "value": _quantity(drc_total, "1"),
187
+ "status": "PASS" if drc_total == 0 else "FAIL",
188
+ }
189
+ )
190
+ if parsed.get("lvs_match") is not None:
191
+ match = bool(parsed.get("lvs_match"))
192
+ measurements.append(
193
+ {
194
+ "name": "lvs_match",
195
+ "value": _quantity(1.0 if match else 0.0, "1"),
196
+ "status": "PASS" if match else "FAIL",
197
+ }
198
+ )
199
+ findings = [
200
+ {
201
+ "id": f"{run_id}:finding:{index}",
202
+ "severity": _SEVERITY.get(v.get("severity"), "info"),
203
+ "code": v.get("rule", "digital_flow_gate"),
204
+ "message": _cap(v.get("message", "")),
205
+ }
206
+ for index, v in enumerate(violations)
207
+ ]
208
+ return {
209
+ "schema": "rocketeda/results/v1",
210
+ "ref": {"scope": "project", "id": run_id, "revision": "r1"},
211
+ "run_id": run_id,
212
+ "task": TASK,
213
+ "target": dict(target)
214
+ if target is not None
215
+ else {"project_id": project_id, "kind": "project"},
216
+ "state": "complete",
217
+ "freshness": "current",
218
+ "convergence": "not_applicable",
219
+ "provenance": _provenance(project_id, run_id, tools, provenance),
220
+ "datasets": datasets,
221
+ "views": views,
222
+ "measurements": measurements,
223
+ "findings": findings,
224
+ "artifacts": [],
225
+ "logs": [],
226
+ "gaps": [],
227
+ }
@@ -0,0 +1,462 @@
1
+ """Run-dir machine-file readout for the digital_flow gates.
2
+
3
+ LibreLane writes one state_out.json plus metrics.json/metrics.csv
4
+ per step dir (docs/tools/librelane.md section 2). The D22/D25/D26/D27
5
+ verdicts read those machine files only: report text is evidence, never
6
+ the verdict. Steps without machine files fail loudly (key drift means
7
+ an upstream step change, never a silent pass).
8
+ """
9
+
10
+ import csv
11
+ import json
12
+ from pathlib import Path
13
+
14
+ STATE_FILES = ("state_out.json", "metrics.json", "metrics.csv")
15
+
16
+ # Signoff JSON reports the live 3.0.17 run writes under step dirs
17
+ # (docs/tools/librelane.md section 6): exact filenames, never globs.
18
+ SIGNOFF_DRC_NAME = "drc.klayout.json"
19
+ SIGNOFF_LVS_NAME = "lvs.netgen.json"
20
+
21
+ STA_WS_KEYS = (
22
+ "timing__setup__ws",
23
+ "timing__hold__ws",
24
+ "wns_setup",
25
+ "wns_hold",
26
+ )
27
+ STA_TNS_KEYS = (
28
+ "timing__setup__tns",
29
+ "timing__hold__tns",
30
+ "tns_setup",
31
+ "tns_hold",
32
+ )
33
+ DRC_KEYS = ("drc__violations", "drc_total", "violations")
34
+ # LibreLane 3.x signoff counts (exact names, no corner suffix).
35
+ DRC_ERROR_KEYS = ("magic__drc_error__count", "klayout__drc_error__count")
36
+ LVS_KEYS = ("lvs__match", "lvs_match", "match")
37
+ # LibreLane 3.x LVS mismatch counts (0 means match).
38
+ LVS_MISMATCH_KEYS = (
39
+ "design__lvs_unmatched_device__count",
40
+ "design__lvs_unmatched_net__count",
41
+ "design__lvs_unmatched_pin__count",
42
+ )
43
+ EQY_KEYS = ("equiv__equivalent", "equivalent", "equiv_pass", "equiv__proven")
44
+
45
+
46
+ def read_metrics(step_dir):
47
+ """Merge one step dir's machine metrics into a flat mapping.
48
+
49
+ Args:
50
+ step_dir: Step directory path holding state_out.json,
51
+ metrics.json, or metrics.csv.
52
+
53
+ Returns:
54
+ (metrics, files): flat metric mapping (later files fill
55
+ missing keys only) plus the state file paths read.
56
+ """
57
+ metrics = {}
58
+ files = []
59
+ state = Path(step_dir) / "state_out.json"
60
+ raw = _read_json(state)
61
+ if isinstance(raw, dict):
62
+ for key, value in raw.get("metrics", raw).items():
63
+ metrics.setdefault(str(key), value)
64
+ files.append(str(state))
65
+ snap = Path(step_dir) / "metrics.json"
66
+ raw = _read_json(snap)
67
+ if isinstance(raw, dict):
68
+ for key, value in raw.items():
69
+ metrics.setdefault(str(key), value)
70
+ files.append(str(snap))
71
+ table = Path(step_dir) / "metrics.csv"
72
+ for key, value in _read_csv(table).items():
73
+ metrics.setdefault(key, value)
74
+ if metrics:
75
+ files.append(str(table)) if table.is_file() else None
76
+ return metrics, [path for path in files if Path(path).is_file()]
77
+
78
+
79
+ def read_run_states(run_dir):
80
+ """Read machine metrics plus state views for every step dir.
81
+
82
+ Args:
83
+ run_dir: runs/<tag>/ dir path, or None when the flow never
84
+ created it.
85
+
86
+ Returns:
87
+ Step-dir name to {"metrics": mapping, "files": paths,
88
+ "views": paths}.
89
+ """
90
+ states = {}
91
+ if run_dir is None or not Path(run_dir).is_dir():
92
+ return states
93
+ for child in sorted(Path(run_dir).iterdir()):
94
+ if child.is_dir():
95
+ metrics, files = read_metrics(child)
96
+ states[child.name] = {
97
+ "metrics": metrics,
98
+ "files": files,
99
+ "views": read_views(child),
100
+ }
101
+ return states
102
+
103
+
104
+ def read_views(step_dir):
105
+ """Read state view paths from one step dir's state_out.json.
106
+
107
+ LibreLane State dumps its DesignFormat views as path-valued
108
+ entries alongside the ``metrics`` mapping (docs/tools/librelane.md
109
+ section 2): a top-level string is one view path, a list holds
110
+ several, a mapping holds named ones. Only paths resolving to real
111
+ files count — dangling entries are dropped, never failed on.
112
+
113
+ Args:
114
+ step_dir: Step directory path holding state_out.json.
115
+
116
+ Returns:
117
+ Sorted existing view file paths (strings).
118
+ """
119
+ raw = _read_json(Path(step_dir) / "state_out.json")
120
+ if not isinstance(raw, dict):
121
+ return []
122
+ candidates = []
123
+ for key, value in raw.items():
124
+ if key == "metrics":
125
+ continue
126
+ if isinstance(value, str):
127
+ candidates.append(value)
128
+ elif isinstance(value, list):
129
+ candidates.extend(item for item in value if isinstance(item, str))
130
+ elif isinstance(value, dict):
131
+ candidates.extend(
132
+ item for item in value.values() if isinstance(item, str)
133
+ )
134
+ found = []
135
+ for candidate in candidates:
136
+ path = Path(candidate)
137
+ if not path.is_absolute():
138
+ path = Path(step_dir) / candidate
139
+ if path.is_file():
140
+ found.append(str(path))
141
+ return sorted(set(found))
142
+
143
+
144
+ def _signoff_file(run_dir, name):
145
+ """First exact-name signoff report under a run root, or None.
146
+
147
+ Args:
148
+ run_dir: runs/<tag>/ dir path, or None when the flow never
149
+ created it.
150
+ name: Exact report filename (no globs, no substrings).
151
+
152
+ Returns:
153
+ Sorted-first matching path, or None.
154
+ """
155
+ if run_dir is None or not Path(run_dir).is_dir():
156
+ return None
157
+ matches = sorted(
158
+ path
159
+ for path in Path(run_dir).rglob(name)
160
+ if path.is_file() and path.name == name
161
+ )
162
+ return str(matches[0]) if matches else None
163
+
164
+
165
+ def signoff_drc_total(run_dir):
166
+ """Read the signoff DRC total from drc.klayout.json.
167
+
168
+ The live 3.0.17 run writes ``{rule: count, ..., "total": N}``
169
+ (docs/tools/librelane.md section 6); only that exact ``total``
170
+ key judges — per-rule counts are evidence, never the verdict.
171
+
172
+ Args:
173
+ run_dir: runs/<tag>/ dir path, or None when the flow never
174
+ created it.
175
+
176
+ Returns:
177
+ (total, path): integer total plus the report path, or
178
+ (None, None) when the report or its total is absent.
179
+ """
180
+ path = _signoff_file(run_dir, SIGNOFF_DRC_NAME)
181
+ raw = _read_json(path) if path is not None else None
182
+ if not isinstance(raw, dict):
183
+ return None, None
184
+ total = number(raw.get("total"))
185
+ if total is None:
186
+ return None, None
187
+ return int(total), path
188
+
189
+
190
+ def signoff_lvs_report(run_dir):
191
+ """Locate the signoff lvs.netgen.json report under a run root.
192
+
193
+ The per-element pin-comparison list has no pinned pass shape, so
194
+ the report rides as evidence only — it never judges the gate.
195
+
196
+ Args:
197
+ run_dir: runs/<tag>/ dir path, or None when the flow never
198
+ created it.
199
+
200
+ Returns:
201
+ Report path, or None when absent.
202
+ """
203
+ return _signoff_file(run_dir, SIGNOFF_LVS_NAME)
204
+
205
+
206
+ def lookup(metrics, *names):
207
+ """Case-insensitive exact lookup over metric keys.
208
+
209
+ Args:
210
+ metrics: Flat metric mapping from read_metrics.
211
+ names: Candidate key names in priority order.
212
+
213
+ Returns:
214
+ (key, value) for the first candidate present, else
215
+ (None, None).
216
+ """
217
+ lowered = {str(key).lower(): key for key in metrics}
218
+ for name in names:
219
+ if name.lower() in lowered:
220
+ key = lowered[name.lower()]
221
+ return key, metrics[key]
222
+ return None, None
223
+
224
+
225
+ def number(value):
226
+ """Coerce one metric value to float, or None when not numeric.
227
+
228
+ Args:
229
+ value: Raw metric value (number, numeric string, or bool).
230
+
231
+ Returns:
232
+ Float value, or None when the value carries no number.
233
+ """
234
+ if isinstance(value, bool):
235
+ return float(value)
236
+ if isinstance(value, (int, float)):
237
+ return float(value)
238
+ if isinstance(value, str):
239
+ try:
240
+ return float(value.strip())
241
+ except ValueError:
242
+ return None
243
+ return None
244
+
245
+
246
+ def boolean(value):
247
+ """Coerce one metric value to bool, or None when unclear.
248
+
249
+ Accepts 1/0, true/false, yes/no, pass/fail, and match/mismatch
250
+ spellings (case-insensitive).
251
+
252
+ Args:
253
+ value: Raw metric value.
254
+
255
+ Returns:
256
+ True, False, or None when the value is not a verdict shape.
257
+ """
258
+ if isinstance(value, bool):
259
+ return value
260
+ if isinstance(value, (int, float)):
261
+ return bool(value)
262
+ if isinstance(value, str):
263
+ word = value.strip().lower()
264
+ if word in ("1", "true", "yes", "pass", "passed", "match",
265
+ "equivalent", "proven"):
266
+ return True
267
+ if word in ("0", "false", "no", "fail", "failed", "mismatch",
268
+ "inequivalent", "unproven"):
269
+ return False
270
+ return None
271
+
272
+
273
+ def _prefixed_numerals(metrics, *bases):
274
+ """Collect numeric values for exact or corner-suffixed keys.
275
+
276
+ LibreLane 3.x writes corner-suffixed keys
277
+ (`timing__setup__ws__corner:nom_tt_025C_1v80`); older steps
278
+ write the bare base name. Both are machine evidence.
279
+
280
+ Args:
281
+ metrics: Flat metric mapping.
282
+ bases: Base key names to accept exactly or with a
283
+ `__corner:*` (or any `__`-suffixed) extension.
284
+
285
+ Returns:
286
+ (key, numeral) pairs for every matching numeric entry.
287
+ """
288
+ found = []
289
+ for key, value in metrics.items():
290
+ for base in bases:
291
+ if key == base or key.startswith(base + "__"):
292
+ numeral = number(value)
293
+ if numeral is not None:
294
+ found.append((str(key), numeral))
295
+ break
296
+ return found
297
+
298
+
299
+ def sta_verdict(metrics):
300
+ """Judge post-PNR timing from STA machine metrics.
301
+
302
+ Worst slack wins across corners: any negative corner slack
303
+ fails the gate.
304
+
305
+ Args:
306
+ metrics: Merged STA step metrics (ws/tns slack keys,
307
+ bare or corner-suffixed).
308
+
309
+ Returns:
310
+ (ok, detail): ok True/False, or None when no STA metric key
311
+ is present; detail names the deciding key or the keys
312
+ seen.
313
+ """
314
+ slacks = _prefixed_numerals(metrics, *(STA_WS_KEYS + STA_TNS_KEYS))
315
+ if not slacks:
316
+ return None, f"keys present: {sorted(metrics) or 'none'}"
317
+ worst = min(slacks, key=lambda item: item[1])
318
+ if worst[1] < 0:
319
+ return False, f"timing violated ({worst[0]}={worst[1]})"
320
+ return True, f"worst slack {worst[1]} ns ({worst[0]})"
321
+
322
+
323
+ def drc_verdict(metrics):
324
+ """Judge foundry-deck DRC from DRC machine metrics.
325
+
326
+ Args:
327
+ metrics: Merged DRC step metrics (violation-count keys).
328
+
329
+ Returns:
330
+ (ok, detail, total): total None when no DRC count key is
331
+ present; detail names the deciding key or keys seen.
332
+ """
333
+ seen = []
334
+ for name in DRC_KEYS + DRC_ERROR_KEYS:
335
+ key, value = lookup(metrics, name)
336
+ numeral = number(value) if key is not None else None
337
+ if numeral is not None:
338
+ seen.append((key, int(numeral)))
339
+ if seen:
340
+ total = sum(count for _, count in seen)
341
+ names = "+".join(key for key, _ in seen)
342
+ if total > 0:
343
+ return False, f"{total} DRC violations ({names})", total
344
+ return True, f"DRC total 0 ({names})", total
345
+ return None, f"keys present: {sorted(metrics) or 'none'}", None
346
+
347
+
348
+ def lvs_verdict(metrics):
349
+ """Judge LVS from LVS machine metrics.
350
+
351
+ Args:
352
+ metrics: Merged LVS step metrics (match keys).
353
+
354
+ Returns:
355
+ (ok, detail, match): match None when no LVS match key is
356
+ present; detail names the deciding key or keys seen.
357
+ """
358
+ seen = []
359
+ for name in LVS_MISMATCH_KEYS:
360
+ key, value = lookup(metrics, name)
361
+ numeral = number(value) if key is not None else None
362
+ if numeral is not None:
363
+ mismatched = int(numeral)
364
+ seen.append((key, mismatched))
365
+ if mismatched > 0:
366
+ return False, f"{mismatched} unmatched ({key})", False
367
+ if seen:
368
+ return True, f"netlists match ({seen[0][0]}=0)", True
369
+ for name in LVS_KEYS:
370
+ key, value = lookup(metrics, name)
371
+ verdict = boolean(value) if key is not None else None
372
+ if verdict is not None:
373
+ if verdict:
374
+ return True, f"netlists match ({key})", True
375
+ return False, f"netlists do not match ({key})", False
376
+ return None, f"keys present: {sorted(metrics) or 'none'}", None
377
+
378
+
379
+ def equiv_verdict(metrics):
380
+ """Judge equivalence from EQY machine metrics.
381
+
382
+ Args:
383
+ metrics: Merged equivalence step metrics (proven keys).
384
+
385
+ Returns:
386
+ (ok, detail, proven): proven None when no equivalence key
387
+ is present; detail names the deciding key or keys seen.
388
+ """
389
+ for name in EQY_KEYS:
390
+ key, value = lookup(metrics, name)
391
+ verdict = boolean(value) if key is not None else None
392
+ if verdict is not None:
393
+ if verdict:
394
+ return True, f"equivalence proven ({key})", True
395
+ return False, f"equivalence failure ({key})", False
396
+ return None, f"keys present: {sorted(metrics) or 'none'}", None
397
+
398
+
399
+ def missing_reason(stage_id, label, metrics):
400
+ """Loud missing-machine-file reason for one gated stage.
401
+
402
+ Args:
403
+ stage_id: Gate id (D22/D25/D26/D27).
404
+ label: Short metric kind (STA/DRC/LVS/equivalence).
405
+ metrics: Merged step metrics (possibly empty).
406
+
407
+ Returns:
408
+ Reason naming the machine files plus the keys seen.
409
+ """
410
+ seen = sorted(metrics) or "none"
411
+ return (
412
+ f"no {label} machine-file metrics "
413
+ f"(state_out.json/metrics.csv) for {stage_id}; "
414
+ f"keys present: {seen}"
415
+ )
416
+
417
+
418
+ def _read_json(path):
419
+ """Decode one JSON file, or None when absent/unparseable.
420
+
421
+ Args:
422
+ path: JSON file path.
423
+
424
+ Returns:
425
+ Decoded value, or None.
426
+ """
427
+ try:
428
+ return json.loads(Path(path).read_text(encoding="utf-8"))
429
+ except (OSError, ValueError):
430
+ return None
431
+
432
+
433
+ def _read_csv(path):
434
+ """Decode one Metric,Value CSV file into a flat mapping.
435
+
436
+ Args:
437
+ path: CSV file path with a Metric,Value header row.
438
+
439
+ Returns:
440
+ Key to coerced (number-or-string) value mapping.
441
+ """
442
+ try:
443
+ rows = Path(path).read_text(encoding="utf-8").splitlines()
444
+ except OSError:
445
+ return {}
446
+ parsed = {}
447
+ reader = csv.DictReader(rows)
448
+ if reader.fieldnames != ["Metric", "Value"]:
449
+ return {}
450
+ for row in reader:
451
+ try:
452
+ key = (row["Metric"] or "").strip()
453
+ raw = (row["Value"] or "").strip()
454
+ except (AttributeError, KeyError):
455
+ continue
456
+ if not key:
457
+ continue
458
+ try:
459
+ parsed[key] = float(raw)
460
+ except ValueError:
461
+ parsed[key] = raw
462
+ return parsed
@@ -0,0 +1,7 @@
1
+ # KLayout DRC adapter
2
+
3
+ KlayoutDrcAdapter serves run_drc when deck.tool is klayout. It supports synthetic width/space rules and foundry .lydrc decks. For a foundry deck it runs KLayout in batch mode and parses the report database; for synthetic rules it renders a pya check script and normalizes the findings.
4
+
5
+ The adapter is registered in Engine. The implementation supports synthetic GDS rules and the .lydrc report-database path. Qualification remains process-specific: readiness depends on the registered PDK deck and installed KLayout; use get_capabilities before relying on a deck.
6
+
7
+ KLayout can exit zero when violations are present. The verdict comes from parsed output, never the exit code alone. The older Ruby-DSL probe describes a separate batch limitation; XML-macro .lydrc decks use the current batch recipe. See the [KLayout DRC probe](../../../docs/probe-klayout-drc.md).
@@ -0,0 +1,7 @@
1
+ # Magic DRC adapter
2
+
3
+ MagicDrcAdapter serves run_drc with deck.tool set to magic, the current default. It loads a cell, runs headless DRC, and parses rule messages and bounding boxes into normalized violations.
4
+
5
+ The adapter is registered in Engine. Its documented qualification uses Magic 8.3.685 with the scmos technology. Process-specific support depends on a loadable technology and deck; query get_capabilities and heed preflight results before promising PDK coverage.
6
+
7
+ Magic may exit successfully despite DRC errors or a bad cell. The verdict comes from parsed output, not the process exit code. The alternate KLayout adapter handles synthetic rules and foundry .lydrc decks.