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.
- rocket_engine/__init__.py +8 -0
- rocket_engine/__main__.py +12 -0
- rocket_engine/adapters/README.md +54 -0
- rocket_engine/adapters/__init__.py +7 -0
- rocket_engine/adapters/api.py +39 -0
- rocket_engine/adapters/base.py +46 -0
- rocket_engine/adapters/cace/__init__.py +163 -0
- rocket_engine/adapters/circulax/README.md +10 -0
- rocket_engine/adapters/circulax/__init__.py +112 -0
- rocket_engine/adapters/circulax/_driver.py +190 -0
- rocket_engine/adapters/circulax/adapter.py +538 -0
- rocket_engine/adapters/circulax/results.py +291 -0
- rocket_engine/adapters/cli/README.md +34 -0
- rocket_engine/adapters/cli/__init__.py +305 -0
- rocket_engine/adapters/connectivity/__init__.py +196 -0
- rocket_engine/adapters/connectivity/results.py +214 -0
- rocket_engine/adapters/cosim/README.md +9 -0
- rocket_engine/adapters/cosim/__init__.py +1390 -0
- rocket_engine/adapters/cosim/results.py +214 -0
- rocket_engine/adapters/devsim/__init__.py +171 -0
- rocket_engine/adapters/digital_flow/README.md +9 -0
- rocket_engine/adapters/digital_flow/__init__.py +1121 -0
- rocket_engine/adapters/digital_flow/results.py +227 -0
- rocket_engine/adapters/digital_flow/state.py +462 -0
- rocket_engine/adapters/drc/README.klayout.md +7 -0
- rocket_engine/adapters/drc/README.md +7 -0
- rocket_engine/adapters/drc/__init__.py +1 -0
- rocket_engine/adapters/drc/klayout.py +952 -0
- rocket_engine/adapters/drc/magic.py +287 -0
- rocket_engine/adapters/drc/results.py +184 -0
- rocket_engine/adapters/drc/results_kl.py +188 -0
- rocket_engine/adapters/elmer/__init__.py +163 -0
- rocket_engine/adapters/envelope.py +120 -0
- rocket_engine/adapters/equivalence/__init__.py +156 -0
- rocket_engine/adapters/errors.py +181 -0
- rocket_engine/adapters/extract_magic/README.md +15 -0
- rocket_engine/adapters/extract_magic/__init__.py +425 -0
- rocket_engine/adapters/extract_magic/results.py +209 -0
- rocket_engine/adapters/formal_sby/README.md +7 -0
- rocket_engine/adapters/formal_sby/__init__.py +773 -0
- rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
- rocket_engine/adapters/formal_sby/results.py +185 -0
- rocket_engine/adapters/gdsfactory/__init__.py +194 -0
- rocket_engine/adapters/gdsfactory/_driver.py +459 -0
- rocket_engine/adapters/gdsfactory/adapter.py +609 -0
- rocket_engine/adapters/gdsfactory/results.py +181 -0
- rocket_engine/adapters/hdl_sim/README.md +7 -0
- rocket_engine/adapters/hdl_sim/__init__.py +369 -0
- rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
- rocket_engine/adapters/hdl_sim/results.py +178 -0
- rocket_engine/adapters/hdl_sim/verilator.py +167 -0
- rocket_engine/adapters/http/README.md +15 -0
- rocket_engine/adapters/http/__init__.py +458 -0
- rocket_engine/adapters/http/events.py +63 -0
- rocket_engine/adapters/ipc/README.md +9 -0
- rocket_engine/adapters/ipc/__init__.py +111 -0
- rocket_engine/adapters/lvs_netgen/README.md +20 -0
- rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
- rocket_engine/adapters/lvs_netgen/results.py +168 -0
- rocket_engine/adapters/mcp/README.md +19 -0
- rocket_engine/adapters/mcp/__init__.py +85 -0
- rocket_engine/adapters/mcp/api-schema.json +6107 -0
- rocket_engine/adapters/mcp/pull.py +246 -0
- rocket_engine/adapters/mcp/server.py +794 -0
- rocket_engine/adapters/meep/__init__.py +167 -0
- rocket_engine/adapters/meow/README.md +7 -0
- rocket_engine/adapters/meow/__init__.py +190 -0
- rocket_engine/adapters/meow/_driver.py +227 -0
- rocket_engine/adapters/meow/_envelope.py +108 -0
- rocket_engine/adapters/meow/adapter.py +491 -0
- rocket_engine/adapters/meow/results.py +218 -0
- rocket_engine/adapters/mutation/__init__.py +192 -0
- rocket_engine/adapters/ngspice/README.md +28 -0
- rocket_engine/adapters/ngspice/__init__.py +627 -0
- rocket_engine/adapters/ngspice/results.py +215 -0
- rocket_engine/adapters/postlayout/README.md +28 -0
- rocket_engine/adapters/postlayout/__init__.py +17 -0
- rocket_engine/adapters/postlayout/adapter.py +1062 -0
- rocket_engine/adapters/postlayout/results.py +330 -0
- rocket_engine/adapters/postlayout/translate.py +71 -0
- rocket_engine/adapters/profile.py +220 -0
- rocket_engine/adapters/sax/README.md +23 -0
- rocket_engine/adapters/sax/__init__.py +516 -0
- rocket_engine/adapters/sax/_driver.py +148 -0
- rocket_engine/adapters/sax/results.py +291 -0
- rocket_engine/adapters/stdio_console.py +1151 -0
- rocket_engine/adapters/syntax/README.md +5 -0
- rocket_engine/adapters/syntax/__init__.py +366 -0
- rocket_engine/adapters/syntax/results.py +181 -0
- rocket_engine/adapters/synth_yosys/README.md +7 -0
- rocket_engine/adapters/synth_yosys/__init__.py +655 -0
- rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
- rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
- rocket_engine/adapters/synth_yosys/results.py +173 -0
- rocket_engine/adapters/timing_sta/README.md +9 -0
- rocket_engine/adapters/timing_sta/__init__.py +726 -0
- rocket_engine/adapters/view_sync/__init__.py +185 -0
- rocket_engine/adapters/view_sync/results.py +189 -0
- rocket_engine/adapters/vlsir/__init__.py +142 -0
- rocket_engine/adapters/xschem/__init__.py +440 -0
- rocket_engine/adapters/xschem/deck.py +266 -0
- rocket_engine/clients/__init__.py +5 -0
- rocket_engine/clients/runtime.py +255 -0
- rocket_engine/composition.py +2279 -0
- rocket_engine/execution/README.md +30 -0
- rocket_engine/execution/__init__.py +150 -0
- rocket_engine/execution/apig/__init__.py +1 -0
- rocket_engine/execution/apig/pool.py +463 -0
- rocket_engine/execution/apig/submit.py +124 -0
- rocket_engine/execution/block_runs_scope.py +479 -0
- rocket_engine/execution/coalesce.py +414 -0
- rocket_engine/execution/console/__init__.py +1 -0
- rocket_engine/execution/console/apply_session.py +318 -0
- rocket_engine/execution/console/lifecycle.py +265 -0
- rocket_engine/execution/dispatch.py +205 -0
- rocket_engine/execution/errors.py +31 -0
- rocket_engine/execution/ports.py +797 -0
- rocket_engine/execution/pyexec/__init__.py +1 -0
- rocket_engine/execution/pyexec/apply_context.py +547 -0
- rocket_engine/execution/pyexec/approvals.py +173 -0
- rocket_engine/execution/pyexec/runner.py +404 -0
- rocket_engine/execution/pyexec/support.py +464 -0
- rocket_engine/execution/python_exec.py +897 -0
- rocket_engine/execution/queue.py +85 -0
- rocket_engine/execution/replay_safe.py +601 -0
- rocket_engine/execution/routing/__init__.py +1 -0
- rocket_engine/execution/routing/calls.py +364 -0
- rocket_engine/execution/routing/namespace.py +785 -0
- rocket_engine/execution/routing/ports.py +102 -0
- rocket_engine/execution/routing/tools.py +346 -0
- rocket_engine/execution/runner.py +311 -0
- rocket_engine/execution/runtime.py +1149 -0
- rocket_engine/execution/service.py +279 -0
- rocket_engine/execution/worker.py +464 -0
- rocket_engine/interfaces/README.md +26 -0
- rocket_engine/interfaces/__init__.py +58 -0
- rocket_engine/interfaces/apig_assets.py +407 -0
- rocket_engine/interfaces/celllink.py +461 -0
- rocket_engine/interfaces/documents.py +92 -0
- rocket_engine/interfaces/envelopes.py +131 -0
- rocket_engine/interfaces/errors.py +16 -0
- rocket_engine/interfaces/layout.py +410 -0
- rocket_engine/interfaces/layout_schema.yaml +278 -0
- rocket_engine/interfaces/paths.py +223 -0
- rocket_engine/interfaces/pdks.py +550 -0
- rocket_engine/interfaces/ports.py +72 -0
- rocket_engine/interfaces/references.py +115 -0
- rocket_engine/interfaces/refs.py +85 -0
- rocket_engine/interfaces/runparams.py +304 -0
- rocket_engine/interfaces/setup.py +552 -0
- rocket_engine/interfaces/setup_catalogue.yaml +243 -0
- rocket_engine/interfaces/setup_probes.py +66 -0
- rocket_engine/interfaces/vocabulary.py +58 -0
- rocket_engine/interfaces/yaml_dump.py +31 -0
- rocket_engine/library/README.md +22 -0
- rocket_engine/library/__init__.py +13 -0
- rocket_engine/library/apig_catalog.py +371 -0
- rocket_engine/library/catalog.py +185 -0
- rocket_engine/library/errors.py +16 -0
- rocket_engine/mutate/__init__.py +776 -0
- rocket_engine/mutate/operators.py +700 -0
- rocket_engine/ops/__init__.py +1 -0
- rocket_engine/ops/deferred.py +279 -0
- rocket_engine/ops/editors.py +404 -0
- rocket_engine/ops/evidence.py +194 -0
- rocket_engine/ops/gates.py +262 -0
- rocket_engine/ops/ports.py +76 -0
- rocket_engine/ops/queries.py +417 -0
- rocket_engine/ops/runs.py +553 -0
- rocket_engine/ops/submit.py +614 -0
- rocket_engine/project/README.md +27 -0
- rocket_engine/project/__init__.py +6 -0
- rocket_engine/project/canvas/__init__.py +55 -0
- rocket_engine/project/canvas/compile.py +109 -0
- rocket_engine/project/canvas/integrate.py +48 -0
- rocket_engine/project/canvas/schema.py +241 -0
- rocket_engine/project/canvas/templates.py +46 -0
- rocket_engine/project/canvas/validate.py +603 -0
- rocket_engine/project/edit/__init__.py +5 -0
- rocket_engine/project/edit/cas.py +87 -0
- rocket_engine/project/edit/transactions.py +282 -0
- rocket_engine/project/errors.py +16 -0
- rocket_engine/project/frontend13.py +685 -0
- rocket_engine/project/geometry/__init__.py +23 -0
- rocket_engine/project/geometry/ops.py +451 -0
- rocket_engine/project/geometry/snap.py +103 -0
- rocket_engine/project/ip/__init__.py +1 -0
- rocket_engine/project/ip/blocks.py +195 -0
- rocket_engine/project/ip/entry.schema.json +78 -0
- rocket_engine/project/ip/lifecycle.py +402 -0
- rocket_engine/project/ip/verbs.py +123 -0
- rocket_engine/project/layers/__init__.py +6 -0
- rocket_engine/project/layers/bindings.py +115 -0
- rocket_engine/project/layers/catalog.py +228 -0
- rocket_engine/project/layers/presets.py +43 -0
- rocket_engine/project/ledger/__init__.py +1 -0
- rocket_engine/project/ledger/docs.py +167 -0
- rocket_engine/project/ledger/entities.py +176 -0
- rocket_engine/project/ledger/events.py +29 -0
- rocket_engine/project/ledger/runs.py +257 -0
- rocket_engine/project/ledger/shared.py +174 -0
- rocket_engine/project/ledger/snapshots.py +385 -0
- rocket_engine/project/ledger/wires.py +552 -0
- rocket_engine/project/process/__init__.py +14 -0
- rocket_engine/project/process/model.py +193 -0
- rocket_engine/project/process/recipes.py +121 -0
- rocket_engine/project/schematic/__init__.py +1 -0
- rocket_engine/project/schematic/hierarchy.py +312 -0
- rocket_engine/project/schematic/instances.py +393 -0
- rocket_engine/project/schematic/sheets.py +198 -0
- rocket_engine/project/service.py +2941 -0
- rocket_engine/project/symbols/__init__.py +1 -0
- rocket_engine/project/symbols/cells.py +324 -0
- rocket_engine/project/symbols/linkage.py +356 -0
- rocket_engine/project/symbols/symbols.py +352 -0
- rocket_engine/project/templates/__init__.py +303 -0
- rocket_engine/project/templates/errors.py +25 -0
- rocket_engine/project/testbench/__init__.py +49 -0
- rocket_engine/project/testbench/blocks.py +619 -0
- rocket_engine/project/testbench/templates.py +134 -0
- rocket_engine/stores/README.md +12 -0
- rocket_engine/stores/__init__.py +6 -0
- rocket_engine/stores/ecs.py +1508 -0
- rocket_engine/stores/layers.py +1023 -0
- rocket_engine/stores/settings.py +1188 -0
- rocket_engine/stores/wireplace.py +1324 -0
- rocket_engine/workflows/README.md +18 -0
- rocket_engine/workflows/__init__.py +53 -0
- rocket_engine/workflows/capability_plan.py +102 -0
- rocket_engine/workflows/completion.py +228 -0
- rocket_engine/workflows/connectgraph.py +734 -0
- rocket_engine/workflows/connectgraph_emit.py +336 -0
- rocket_engine/workflows/gates.py +144 -0
- rocket_engine/workflows/photonic_wire.py +76 -0
- rocket_engine/workflows/stages.py +324 -0
- rocket_engine/workflows/viewsync.py +399 -0
- rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
- rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
- rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
- rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
- rocket_engine-0.0.2b1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
"""KLayout DRC solver adapter (I14 + DRC-SKY130): batch DRC behind the port.
|
|
2
|
+
|
|
3
|
+
Two modes, verified against KLayout 0.28.16:
|
|
4
|
+
- Synthetic: render a pya check script from the rule table, run
|
|
5
|
+
`klayout -b -r`, parse VIOL/TOTAL lines.
|
|
6
|
+
- Deck: run a foundry `.lydrc` (e.g. sky130A.lydrc) via
|
|
7
|
+
`klayout -b -rd input=<gds> -rd report=<lyrdb> -r <deck>`,
|
|
8
|
+
parse the report database XML into violations.
|
|
9
|
+
(The plain-Ruby DSL note in docs/probe-klayout-drc.md predates the
|
|
10
|
+
.lydrc batch recipe; XML-macro decks run fine under -r with -rd.)
|
|
11
|
+
KLayout exits 0 regardless, so the verdict always comes from parsed
|
|
12
|
+
output, never from the exit code.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
import xml.etree.ElementTree as ET
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from ...execution.errors import ExecutionError
|
|
23
|
+
from ...interfaces import ToolAdapter
|
|
24
|
+
from ...interfaces.runparams import MAX_TIMEOUT_S, MIN_TIMEOUT_S
|
|
25
|
+
from ..errors import AdapterError
|
|
26
|
+
|
|
27
|
+
OPERATION = "run_drc"
|
|
28
|
+
_TIMEOUT_S = 120
|
|
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 _load_layout(gds_file):
|
|
57
|
+
"""Read-only layout handle for cell inspection (no binary gate).
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
gds_file: GDSII file to open.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Opened layout database object.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
AdapterError: runner_unavailable when no layout API is
|
|
67
|
+
importable on this host.
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
import pya
|
|
71
|
+
|
|
72
|
+
layout = pya.Layout()
|
|
73
|
+
except ImportError:
|
|
74
|
+
try:
|
|
75
|
+
from klayout import db as klayout_db
|
|
76
|
+
|
|
77
|
+
layout = klayout_db.Layout()
|
|
78
|
+
except ImportError as exc:
|
|
79
|
+
raise AdapterError(
|
|
80
|
+
"runner_unavailable",
|
|
81
|
+
"No KLayout layout API (pya/klayout.db) on this host.",
|
|
82
|
+
) from exc
|
|
83
|
+
layout.read(gds_file)
|
|
84
|
+
return layout
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _cell_inventory(gds_file):
|
|
88
|
+
"""Top-cell and all-cell names of a GDS file, read-only.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
gds_file: GDSII file to inspect.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
(tops, all_names): sorted top-cell names and sorted
|
|
95
|
+
every-cell names from one read-only load.
|
|
96
|
+
"""
|
|
97
|
+
layout = _load_layout(gds_file)
|
|
98
|
+
tops = sorted(cell.name for cell in layout.top_cells())
|
|
99
|
+
all_names = sorted(cell.name for cell in layout.each_cell())
|
|
100
|
+
return tops, all_names
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _resolve_cell_or_raise(gds_file, cell):
|
|
104
|
+
"""Honor the job cell against the GDS topology, or refuse loudly.
|
|
105
|
+
|
|
106
|
+
Deck mode runs the foundry recipe over the whole GDS (the
|
|
107
|
+
recipe has no top-cell selection) and the synthetic path
|
|
108
|
+
checks `top_cell()` — so a job cell must name a top cell that
|
|
109
|
+
exists, and a multi-top GDS needs an explicit cell instead of
|
|
110
|
+
silently checking whatever `top_cell()` returns (MIXED-TOP
|
|
111
|
+
F3). A missing cell refuses as invalid_reference; a present
|
|
112
|
+
but non-top cell, or several tops without a cell, refuse as
|
|
113
|
+
invalid_command.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
gds_file: GDSII file under check.
|
|
117
|
+
cell: Job cell name, or None.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The honored top-cell name (the job cell, or the single
|
|
121
|
+
top cell when the job names none).
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
AdapterError: invalid_reference for a cell absent from
|
|
125
|
+
the GDS; invalid_command for a non-top cell or for
|
|
126
|
+
several top cells without a cell.
|
|
127
|
+
"""
|
|
128
|
+
tops, all_names = _cell_inventory(gds_file)
|
|
129
|
+
if cell is not None:
|
|
130
|
+
if cell not in all_names:
|
|
131
|
+
raise AdapterError(
|
|
132
|
+
"invalid_reference", f"DRC cell not in GDS: {cell}."
|
|
133
|
+
)
|
|
134
|
+
if cell not in tops:
|
|
135
|
+
raise AdapterError(
|
|
136
|
+
"invalid_command",
|
|
137
|
+
f"DRC cell {cell!r} is not a top cell "
|
|
138
|
+
f"(tops: {', '.join(tops)}); deck mode checks the "
|
|
139
|
+
"whole GDS and cannot scope to an inner cell.",
|
|
140
|
+
)
|
|
141
|
+
return cell
|
|
142
|
+
if len(tops) != 1:
|
|
143
|
+
raise AdapterError(
|
|
144
|
+
"invalid_command",
|
|
145
|
+
f"GDS has {len(tops)} top cells ({', '.join(tops)}); "
|
|
146
|
+
"pass cell to select one.",
|
|
147
|
+
)
|
|
148
|
+
return tops[0]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
_TOTAL_RE = re.compile(r"^TOTAL (\d+)$", re.M)
|
|
152
|
+
_VIOL_RE = re.compile(
|
|
153
|
+
r"^VIOL rule=(\S+) bbox_um=([0-9.eE+-]+),([0-9.eE+-]+),"
|
|
154
|
+
r"([0-9.eE+-]+),([0-9.eE+-]+)$",
|
|
155
|
+
re.M,
|
|
156
|
+
)
|
|
157
|
+
_NUM_RE = re.compile(r"[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?")
|
|
158
|
+
# Lyrdb value geometry grammar (ESCAPE-HARDEN MC-44): `x,y` vertex
|
|
159
|
+
# pairs after a geometry head (`polygon:`, `edge-pair:`, `shape`),
|
|
160
|
+
# pairs separated by `;`, groups wrapped in parens and separated
|
|
161
|
+
# by `/` or `|`. A box needs at least two pairs in a
|
|
162
|
+
# semicolon/paren-structured value — bare numbers in unstructured
|
|
163
|
+
# prose (spacing notes, area remarks) never form one.
|
|
164
|
+
_PAIR_RE = re.compile(
|
|
165
|
+
r"[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?"
|
|
166
|
+
r"\s*,\s*"
|
|
167
|
+
r"[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
_CHECK_SCRIPT = """import json
|
|
171
|
+
import pya
|
|
172
|
+
|
|
173
|
+
SPEC = json.loads({spec!r})
|
|
174
|
+
layout = pya.Layout()
|
|
175
|
+
layout.read(SPEC["gds_file"])
|
|
176
|
+
top = layout.top_cell()
|
|
177
|
+
layer = SPEC["rules"]["layer"]
|
|
178
|
+
region = pya.Region(top.begin_shapes_rec(layout.layer(layer[0], layer[1])))
|
|
179
|
+
total = 0
|
|
180
|
+
name = SPEC["rules"].get("name") or "L%d_%d" % (layer[0], layer[1])
|
|
181
|
+
checks = (("width", SPEC["rules"]["width_um"]), ("space", SPEC["rules"]["space_um"]))
|
|
182
|
+
for rule, dim_um in checks:
|
|
183
|
+
dim_dbu = int(round(dim_um / layout.dbu))
|
|
184
|
+
check = region.width_check if rule == "width" else region.space_check
|
|
185
|
+
for pair in check(dim_dbu).each():
|
|
186
|
+
total += 1
|
|
187
|
+
box = pair.bbox()
|
|
188
|
+
dbu = layout.dbu
|
|
189
|
+
coords = (box.left * dbu, box.bottom * dbu, box.right * dbu, box.top * dbu)
|
|
190
|
+
print("VIOL rule=%s_%s bbox_um=%s,%s,%s,%s" % ((name, rule) + coords))
|
|
191
|
+
print("TOTAL %d" % total)
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _waive_or_raise(job):
|
|
196
|
+
"""Waive list for a deck run: exact rule names, or None.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
job: Solver job, maybe with a `waive_rules` list.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Sorted waive list, or None when the job names none.
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
AdapterError: invalid_command for a non-list or a
|
|
206
|
+
non-string entry.
|
|
207
|
+
"""
|
|
208
|
+
waive = job.get("waive_rules")
|
|
209
|
+
if waive is None:
|
|
210
|
+
return None
|
|
211
|
+
if not isinstance(waive, list) or any(
|
|
212
|
+
not isinstance(rule, str) or not rule for rule in waive
|
|
213
|
+
):
|
|
214
|
+
raise AdapterError(
|
|
215
|
+
"invalid_command", f"Bad DRC waive_rules: {waive!r}."
|
|
216
|
+
)
|
|
217
|
+
return sorted(set(waive))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def apply_waivers(parsed, waive_rules):
|
|
221
|
+
"""Drop waived rules from a deck parse with a tally.
|
|
222
|
+
|
|
223
|
+
Pure helper (DRC-BANDGAP-TRIAGE): the OFFGRID manufacturing-grid
|
|
224
|
+
bucket is a setup/parity gap on generator-grid layouts, so a
|
|
225
|
+
deck run can waive those rules by exact name and leave the real
|
|
226
|
+
defects standing alone. No waivers means the parse unchanged.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
parsed: Parse mapping with total plus violations[].
|
|
230
|
+
waive_rules: Exact rule names to drop; None or empty
|
|
231
|
+
returns the parse unchanged.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Parse mapping with the surviving violations, the
|
|
235
|
+
recomputed total, and a waived tally (rules plus
|
|
236
|
+
dropped count).
|
|
237
|
+
"""
|
|
238
|
+
waived = sorted(set(waive_rules or ()))
|
|
239
|
+
if not waived:
|
|
240
|
+
return {"total": parsed.get("total", 0),
|
|
241
|
+
"violations": list(parsed.get("violations") or []),
|
|
242
|
+
"waived": {"rules": [], "count": 0}}
|
|
243
|
+
drop = set(waived)
|
|
244
|
+
before = parsed.get("violations") or []
|
|
245
|
+
kept = [v for v in before if v.get("rule") not in drop]
|
|
246
|
+
return {"total": len(kept), "violations": kept,
|
|
247
|
+
"waived": {"rules": waived, "count": len(before) - len(kept)}}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _rules_or_raise(rules):
|
|
251
|
+
if not isinstance(rules, dict):
|
|
252
|
+
raise AdapterError("invalid_command", "rules must be a mapping.")
|
|
253
|
+
layer = rules.get("layer")
|
|
254
|
+
if (
|
|
255
|
+
not isinstance(layer, (list, tuple))
|
|
256
|
+
or len(layer) != 2
|
|
257
|
+
or not all(isinstance(v, int) and not isinstance(v, bool) for v in layer)
|
|
258
|
+
):
|
|
259
|
+
raise AdapterError("invalid_command", f"Bad DRC layer: {layer!r}.")
|
|
260
|
+
clean = {"layer": [layer[0], layer[1]]}
|
|
261
|
+
for key in ("width_um", "space_um"):
|
|
262
|
+
value = rules.get(key)
|
|
263
|
+
if (
|
|
264
|
+
not isinstance(value, (int, float))
|
|
265
|
+
or isinstance(value, bool)
|
|
266
|
+
or not value > 0
|
|
267
|
+
):
|
|
268
|
+
raise AdapterError("invalid_command", f"Bad DRC rule {key}: {value!r}.")
|
|
269
|
+
clean[key] = float(value)
|
|
270
|
+
if rules.get("name") is not None:
|
|
271
|
+
clean["name"] = str(rules["name"])
|
|
272
|
+
return clean
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _deck_file_or_raise(job):
|
|
276
|
+
deck = job.get("deck") if isinstance(job.get("deck"), dict) else {}
|
|
277
|
+
for candidate in (
|
|
278
|
+
job.get("deck_file"),
|
|
279
|
+
deck.get("path"),
|
|
280
|
+
deck.get("deck_file"),
|
|
281
|
+
deck.get("file"),
|
|
282
|
+
):
|
|
283
|
+
if candidate is not None:
|
|
284
|
+
if not candidate or not Path(candidate).is_file():
|
|
285
|
+
raise AdapterError(
|
|
286
|
+
"invalid_reference", f"DRC deck not found: {candidate}."
|
|
287
|
+
)
|
|
288
|
+
return str(candidate)
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _bbox_from_value(text):
|
|
293
|
+
"""Box one lyrdb value text under the geometry grammar.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
text: Joined lyrdb `<value>` text for one item.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
[x0, y0, x1, y1] over the `x,y` vertex pairs, or None
|
|
300
|
+
when the text carries fewer than two pairs outside
|
|
301
|
+
a semicolon/paren-structured value (stray numbers in
|
|
302
|
+
unstructured prose never form a box).
|
|
303
|
+
"""
|
|
304
|
+
text = text or ""
|
|
305
|
+
if ";" not in text and "(" not in text:
|
|
306
|
+
return None
|
|
307
|
+
pairs = []
|
|
308
|
+
for hit in _PAIR_RE.findall(text):
|
|
309
|
+
halves = [h.strip() for h in hit.split(",")]
|
|
310
|
+
if len(halves) != 2:
|
|
311
|
+
continue
|
|
312
|
+
try:
|
|
313
|
+
pairs.append((float(halves[0]), float(halves[1])))
|
|
314
|
+
except ValueError:
|
|
315
|
+
continue
|
|
316
|
+
if len(pairs) < 2:
|
|
317
|
+
return None
|
|
318
|
+
xs = [p[0] for p in pairs]
|
|
319
|
+
ys = [p[1] for p in pairs]
|
|
320
|
+
return [min(xs), min(ys), max(xs), max(ys)]
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _parse_lyrdb(xml_text):
|
|
324
|
+
try:
|
|
325
|
+
root = ET.fromstring(xml_text)
|
|
326
|
+
except ET.ParseError as exc:
|
|
327
|
+
raise AdapterError(
|
|
328
|
+
"runner_unavailable", f"klayout report is not valid XML: {exc}."
|
|
329
|
+
) from exc
|
|
330
|
+
descriptions = {}
|
|
331
|
+
for cat in root.findall("./categories/category"):
|
|
332
|
+
name = (cat.findtext("name") or "").strip()
|
|
333
|
+
desc = (cat.findtext("description") or "").strip()
|
|
334
|
+
if name:
|
|
335
|
+
descriptions[name] = desc or name
|
|
336
|
+
violations = []
|
|
337
|
+
total = 0
|
|
338
|
+
for item in root.findall("./items/item"):
|
|
339
|
+
raw_rule = (item.findtext("category") or "").strip().strip("'\"")
|
|
340
|
+
rule = raw_rule or "unknown"
|
|
341
|
+
try:
|
|
342
|
+
mult = int((item.findtext("multiplicity") or "1").strip())
|
|
343
|
+
except ValueError:
|
|
344
|
+
mult = 1
|
|
345
|
+
total += max(mult, 1)
|
|
346
|
+
texts = [
|
|
347
|
+
(v.text or "")
|
|
348
|
+
for v in item.findall("./values/value")
|
|
349
|
+
if (v.text or "").strip()
|
|
350
|
+
]
|
|
351
|
+
joined = "; ".join(texts)
|
|
352
|
+
violation = {
|
|
353
|
+
"rule": rule,
|
|
354
|
+
"severity": "error",
|
|
355
|
+
"message": descriptions.get(rule, rule),
|
|
356
|
+
}
|
|
357
|
+
if joined:
|
|
358
|
+
violation["message"] = f"{violation['message']}: {joined}"
|
|
359
|
+
bbox = _bbox_from_value(joined)
|
|
360
|
+
if bbox is not None:
|
|
361
|
+
violation["bbox_um"] = bbox
|
|
362
|
+
violations.append(violation)
|
|
363
|
+
return {"total": total, "violations": violations}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class KlayoutDrcAdapter(ToolAdapter):
|
|
367
|
+
"""run_drc via KLayout: synthetic checks or foundry .lydrc decks.
|
|
368
|
+
|
|
369
|
+
Serves the run_drc operation with deck.tool `klayout`. Two
|
|
370
|
+
modes: synthetic width/space checks over one layer from
|
|
371
|
+
`rules`, or a foundry .lydrc deck over `gds_file` with
|
|
372
|
+
violations parsed from the report database. Locations are
|
|
373
|
+
metric bbox_um.
|
|
374
|
+
|
|
375
|
+
Job keys:
|
|
376
|
+
gds_file (required): GDS layout under check.
|
|
377
|
+
rules: Synthetic check {layer [l, d], width_um, space_um,
|
|
378
|
+
name?} — one check form is required (rules or a deck
|
|
379
|
+
file).
|
|
380
|
+
deck_file: Foundry .lydrc deck path (deck mode).
|
|
381
|
+
deck: Optional mapping; deck.tool selects the solver
|
|
382
|
+
(klayout here), deck.path/deck_file/file names the
|
|
383
|
+
deck in deck mode.
|
|
384
|
+
cell: Optional top-cell claim, honored in both modes:
|
|
385
|
+
must name an existing top cell (multi-top GDS
|
|
386
|
+
without a cell refuses loudly).
|
|
387
|
+
waive_rules: Optional exact rule names to waive
|
|
388
|
+
(DRC-BANDGAP-TRIAGE: the OFFGRID manufacturing-grid
|
|
389
|
+
bucket on generator-grid layouts); waived violations
|
|
390
|
+
drop from the parse with a `waived` tally.
|
|
391
|
+
workdir: Run directory (default ".").
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
runner: Optional isolated runner; direct subprocess when None.
|
|
395
|
+
|
|
396
|
+
Caution: KLayout exits 0 regardless — the verdict always comes
|
|
397
|
+
from parsed output, never the exit code.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
def __init__(self, runner=None):
|
|
401
|
+
self._runner = runner
|
|
402
|
+
|
|
403
|
+
def capabilities(self, target):
|
|
404
|
+
"""Report the served operation, tool, rules, and deck kinds.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
target: Target the capabilities are reported for
|
|
408
|
+
(accepted, unused — one GDS per job).
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Capability mapping with operations, tool, rules, and
|
|
412
|
+
decks.
|
|
413
|
+
"""
|
|
414
|
+
return {
|
|
415
|
+
"operations": [OPERATION],
|
|
416
|
+
"tool": "klayout",
|
|
417
|
+
"rules": ["width", "space"],
|
|
418
|
+
"decks": ["lydrc"],
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
def preflight(self, job):
|
|
422
|
+
"""Check operation, klayout binary, GDS, and one check form.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
job: Solver job with operation run_drc plus gds_file
|
|
426
|
+
and rules or deck inputs.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
Checked mapping with gds_file plus deck_file (deck
|
|
430
|
+
mode) or rules (synthetic mode).
|
|
431
|
+
|
|
432
|
+
Raises:
|
|
433
|
+
AdapterError: unsupported_operation for another
|
|
434
|
+
operation, missing_dependency for a missing klayout
|
|
435
|
+
binary, invalid_reference for a missing GDS or deck
|
|
436
|
+
file, invalid_command for a malformed rules mapping.
|
|
437
|
+
"""
|
|
438
|
+
if job.get("operation") != OPERATION:
|
|
439
|
+
raise AdapterError(
|
|
440
|
+
"unsupported_operation",
|
|
441
|
+
f"KLayout DRC handles {OPERATION}, got {job.get('operation')}.",
|
|
442
|
+
)
|
|
443
|
+
timeout = _budget_or_raise(job)
|
|
444
|
+
if shutil.which("klayout") is None:
|
|
445
|
+
raise AdapterError("missing_dependency", "klayout binary not found.")
|
|
446
|
+
gds_file = job.get("gds_file")
|
|
447
|
+
if not gds_file or not Path(gds_file).is_file():
|
|
448
|
+
raise AdapterError(
|
|
449
|
+
"invalid_reference", f"GDS file not found: {gds_file}."
|
|
450
|
+
)
|
|
451
|
+
top = _resolve_cell_or_raise(gds_file, job.get("cell"))
|
|
452
|
+
deck_file = _deck_file_or_raise(job)
|
|
453
|
+
waive_rules = _waive_or_raise(job)
|
|
454
|
+
if deck_file is not None:
|
|
455
|
+
checked = {
|
|
456
|
+
"gds_file": gds_file,
|
|
457
|
+
"deck_file": deck_file,
|
|
458
|
+
"timeout_s": timeout,
|
|
459
|
+
}
|
|
460
|
+
if waive_rules is not None:
|
|
461
|
+
checked["waive_rules"] = waive_rules
|
|
462
|
+
else:
|
|
463
|
+
checked = {
|
|
464
|
+
"gds_file": gds_file,
|
|
465
|
+
"rules": _rules_or_raise(job.get("rules")),
|
|
466
|
+
"timeout_s": timeout,
|
|
467
|
+
}
|
|
468
|
+
if waive_rules is not None:
|
|
469
|
+
checked["waive_rules"] = waive_rules
|
|
470
|
+
if job.get("cell") is not None:
|
|
471
|
+
checked["cell"] = top
|
|
472
|
+
return checked
|
|
473
|
+
|
|
474
|
+
def execute(self, job):
|
|
475
|
+
"""Run the deck batch or render and run the check script.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
job: Solver job with operation run_drc plus gds_file
|
|
479
|
+
and rules or deck inputs.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Native output with exit_code, stdout/stderr, plus the
|
|
483
|
+
report XML (deck mode) or rules (synthetic mode).
|
|
484
|
+
|
|
485
|
+
Raises:
|
|
486
|
+
AdapterError: invalid_reference for a missing workdir,
|
|
487
|
+
runner_unavailable when klayout produces no report,
|
|
488
|
+
plus the preflight codes (execute re-checks first).
|
|
489
|
+
"""
|
|
490
|
+
checked = self.preflight(job)
|
|
491
|
+
workdir = job.get("workdir", ".")
|
|
492
|
+
if not Path(workdir).is_dir():
|
|
493
|
+
raise AdapterError(
|
|
494
|
+
"invalid_reference", f"Workdir not found: {workdir}."
|
|
495
|
+
)
|
|
496
|
+
if "deck_file" in checked:
|
|
497
|
+
report_file = Path(workdir) / "drc_report.lyrdb"
|
|
498
|
+
proc = self._run(
|
|
499
|
+
[
|
|
500
|
+
"klayout",
|
|
501
|
+
"-b",
|
|
502
|
+
"-rd",
|
|
503
|
+
f"input={checked['gds_file']}",
|
|
504
|
+
"-rd",
|
|
505
|
+
f"report={report_file}",
|
|
506
|
+
"-r",
|
|
507
|
+
checked["deck_file"],
|
|
508
|
+
],
|
|
509
|
+
workdir,
|
|
510
|
+
timeout=checked["timeout_s"],
|
|
511
|
+
)
|
|
512
|
+
if not report_file.is_file():
|
|
513
|
+
raise AdapterError(
|
|
514
|
+
"runner_unavailable",
|
|
515
|
+
f"klayout produced no report: {proc.stderr[:500]}.",
|
|
516
|
+
)
|
|
517
|
+
native: dict = {
|
|
518
|
+
"exit_code": proc.returncode,
|
|
519
|
+
"stdout": proc.stdout,
|
|
520
|
+
"stderr": proc.stderr,
|
|
521
|
+
"report_file": str(report_file),
|
|
522
|
+
"report_xml": report_file.read_text(),
|
|
523
|
+
"deck_file": checked["deck_file"],
|
|
524
|
+
"gds_file": checked["gds_file"],
|
|
525
|
+
}
|
|
526
|
+
if checked.get("waive_rules") is not None:
|
|
527
|
+
native["waive_rules"] = checked["waive_rules"]
|
|
528
|
+
return native
|
|
529
|
+
spec = json.dumps(
|
|
530
|
+
{"gds_file": checked["gds_file"], "rules": checked["rules"]}
|
|
531
|
+
)
|
|
532
|
+
script = _CHECK_SCRIPT.replace("{spec!r}", repr(spec))
|
|
533
|
+
script_file = Path(workdir) / "kl_drc_check.py"
|
|
534
|
+
script_file.write_text(script)
|
|
535
|
+
proc = self._run(
|
|
536
|
+
["klayout", "-b", "-r", str(script_file)],
|
|
537
|
+
workdir,
|
|
538
|
+
timeout=checked["timeout_s"],
|
|
539
|
+
)
|
|
540
|
+
native = {
|
|
541
|
+
"exit_code": proc.returncode,
|
|
542
|
+
"stdout": proc.stdout,
|
|
543
|
+
"stderr": proc.stderr,
|
|
544
|
+
"rules": checked["rules"],
|
|
545
|
+
"gds_file": checked["gds_file"],
|
|
546
|
+
}
|
|
547
|
+
if checked.get("waive_rules") is not None:
|
|
548
|
+
native["waive_rules"] = checked["waive_rules"]
|
|
549
|
+
return native
|
|
550
|
+
|
|
551
|
+
def parse(self, native_output):
|
|
552
|
+
"""Parse the lyrdb report or the synthetic TOTAL/VIOL lines.
|
|
553
|
+
|
|
554
|
+
A `waive_rules` entry in the native output (threaded by
|
|
555
|
+
`execute` from the job) drops those exact rules via
|
|
556
|
+
`apply_waivers`, with a `waived` tally; native output
|
|
557
|
+
without it parses exactly as before.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
native_output: Native output with the report XML/file
|
|
561
|
+
(deck mode) or klayout stdout (synthetic mode),
|
|
562
|
+
plus optional waive_rules.
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
{total, violations[], waived} with rule, severity,
|
|
566
|
+
message, and metric bbox_um where the deck reports
|
|
567
|
+
geometry.
|
|
568
|
+
|
|
569
|
+
Raises:
|
|
570
|
+
AdapterError: runner_unavailable when the deck run has
|
|
571
|
+
no report, the report is not valid XML, or the
|
|
572
|
+
synthetic output has no TOTAL line.
|
|
573
|
+
"""
|
|
574
|
+
if "report_xml" in native_output or "report_file" in native_output:
|
|
575
|
+
xml_text = native_output.get("report_xml")
|
|
576
|
+
if xml_text is None:
|
|
577
|
+
report_file = native_output.get("report_file")
|
|
578
|
+
if not report_file or not Path(report_file).is_file():
|
|
579
|
+
raise AdapterError(
|
|
580
|
+
"runner_unavailable", "klayout deck run has no report."
|
|
581
|
+
)
|
|
582
|
+
xml_text = Path(report_file).read_text()
|
|
583
|
+
return apply_waivers(
|
|
584
|
+
_parse_lyrdb(xml_text), native_output.get("waive_rules"))
|
|
585
|
+
stdout = native_output.get("stdout", "")
|
|
586
|
+
match = _TOTAL_RE.search(stdout)
|
|
587
|
+
if match is None:
|
|
588
|
+
raise AdapterError(
|
|
589
|
+
"runner_unavailable", "klayout output has no TOTAL line."
|
|
590
|
+
)
|
|
591
|
+
violations = []
|
|
592
|
+
for rule, *coords in _VIOL_RE.findall(stdout):
|
|
593
|
+
bbox_um = [float(c) for c in coords]
|
|
594
|
+
bbox_text = ",".join(str(c) for c in bbox_um)
|
|
595
|
+
violations.append(
|
|
596
|
+
{
|
|
597
|
+
"rule": rule,
|
|
598
|
+
"severity": "error",
|
|
599
|
+
"message": f"{rule} at [{bbox_text}] um",
|
|
600
|
+
"bbox_um": bbox_um,
|
|
601
|
+
}
|
|
602
|
+
)
|
|
603
|
+
return apply_waivers(
|
|
604
|
+
{"total": int(match.group(1)), "violations": violations},
|
|
605
|
+
native_output.get("waive_rules"))
|
|
606
|
+
|
|
607
|
+
def _run(self, argv, workdir, timeout=_TIMEOUT_S):
|
|
608
|
+
if self._runner is not None:
|
|
609
|
+
try:
|
|
610
|
+
return self._runner.run(argv, workdir, timeout)
|
|
611
|
+
except ExecutionError as exc:
|
|
612
|
+
raise AdapterError(exc.code, str(exc)) from exc
|
|
613
|
+
try:
|
|
614
|
+
return subprocess.run(
|
|
615
|
+
argv,
|
|
616
|
+
capture_output=True,
|
|
617
|
+
text=True,
|
|
618
|
+
timeout=timeout,
|
|
619
|
+
cwd=workdir,
|
|
620
|
+
)
|
|
621
|
+
except subprocess.TimeoutExpired as exc:
|
|
622
|
+
raise AdapterError(
|
|
623
|
+
"runner_unavailable", f"klayout timed out after {timeout}s."
|
|
624
|
+
) from exc
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
OPERATION_ANTENNA = "run_antenna"
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _antenna_rules_or_raise(rules):
|
|
631
|
+
"""Antenna check rules: metal/gate layers plus the ratio cap.
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
rules: Mapping with metal_layer [l, d], gate_layer
|
|
635
|
+
[l, d], and max_ratio > 0.
|
|
636
|
+
|
|
637
|
+
Returns:
|
|
638
|
+
Cleaned rules mapping with float max_ratio.
|
|
639
|
+
|
|
640
|
+
Raises:
|
|
641
|
+
AdapterError: invalid_command for a non-mapping, a
|
|
642
|
+
malformed layer, or a non-positive ratio.
|
|
643
|
+
"""
|
|
644
|
+
if not isinstance(rules, dict):
|
|
645
|
+
raise AdapterError("invalid_command", "rules must be a mapping.")
|
|
646
|
+
clean = {}
|
|
647
|
+
for key in ("metal_layer", "gate_layer"):
|
|
648
|
+
layer = rules.get(key)
|
|
649
|
+
if (
|
|
650
|
+
not isinstance(layer, (list, tuple))
|
|
651
|
+
or len(layer) != 2
|
|
652
|
+
or not all(isinstance(v, int) and not isinstance(v, bool) for v in layer)
|
|
653
|
+
):
|
|
654
|
+
raise AdapterError("invalid_command", f"Bad antenna layer {key}: {layer!r}.")
|
|
655
|
+
clean[key] = [layer[0], layer[1]]
|
|
656
|
+
ratio = rules.get("max_ratio")
|
|
657
|
+
if not isinstance(ratio, (int, float)) or isinstance(ratio, bool) or not ratio > 0:
|
|
658
|
+
raise AdapterError("invalid_command", f"Bad antenna max_ratio: {ratio!r}.")
|
|
659
|
+
clean["max_ratio"] = float(ratio)
|
|
660
|
+
return clean
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
class AntennaKlayoutAdapter(ToolAdapter):
|
|
664
|
+
"""run_antenna via KLayout's layout database (ANTENNA-KLAYOUT).
|
|
665
|
+
|
|
666
|
+
Serves the check.antenna surface without a foundry deck or an
|
|
667
|
+
openroad-standalone path (demoted by ticket): per-metal-island
|
|
668
|
+
cumulative metal-to-gate area ratio over the resolved cell,
|
|
669
|
+
computed with the KLayout DB region API. An island with no
|
|
670
|
+
gate overlap cannot discharge and fails as antenna_floating;
|
|
671
|
+
an island over max_ratio fails as antenna_ratio with its
|
|
672
|
+
metric bbox. Openroad-standalone is deliberately not staffed.
|
|
673
|
+
|
|
674
|
+
Job keys:
|
|
675
|
+
gds_file (required): GDS layout under check.
|
|
676
|
+
rules (required): {metal_layer [l, d], gate_layer [l, d],
|
|
677
|
+
max_ratio} with max_ratio > 0.
|
|
678
|
+
cell: Optional top-cell claim, honored by scoping the
|
|
679
|
+
island walk (defaults to the single top cell; a
|
|
680
|
+
multi-top GDS without a cell refuses loudly).
|
|
681
|
+
workdir: Run directory (default ".", unused — the check
|
|
682
|
+
is in-process and writes no artifacts).
|
|
683
|
+
|
|
684
|
+
Caution: a heuristic ratio check, not a signoff antenna
|
|
685
|
+
rule; it names suspect islands for the foundry deck to
|
|
686
|
+
confirm. Engine wiring (vocabulary/composition) belongs
|
|
687
|
+
to the owning surface; this file staffs the solver only.
|
|
688
|
+
"""
|
|
689
|
+
|
|
690
|
+
def capabilities(self, target):
|
|
691
|
+
"""Report the served operation, tool, and rule kind.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
target: Target the capabilities are reported for
|
|
695
|
+
(accepted, unused — one GDS per job).
|
|
696
|
+
|
|
697
|
+
Returns:
|
|
698
|
+
Capability mapping with operations, tool, and rules.
|
|
699
|
+
"""
|
|
700
|
+
return {
|
|
701
|
+
"operations": [OPERATION_ANTENNA],
|
|
702
|
+
"tool": "klayout",
|
|
703
|
+
"rules": ["ratio"],
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
def preflight(self, job):
|
|
707
|
+
"""Check operation, GDS, cell scope, and ratio rules.
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
job: Solver job with operation run_antenna plus
|
|
711
|
+
gds_file, rules, and an optional cell.
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
Checked mapping with gds_file, rules, and cell.
|
|
715
|
+
|
|
716
|
+
Raises:
|
|
717
|
+
AdapterError: unsupported_operation for another
|
|
718
|
+
operation, invalid_reference for a missing GDS
|
|
719
|
+
or an unknown cell, invalid_command for a
|
|
720
|
+
non-top cell, a multi-top GDS without a cell,
|
|
721
|
+
or malformed rules.
|
|
722
|
+
"""
|
|
723
|
+
if job.get("operation") != OPERATION_ANTENNA:
|
|
724
|
+
raise AdapterError(
|
|
725
|
+
"unsupported_operation",
|
|
726
|
+
f"KLayout antenna handles {OPERATION_ANTENNA}, got {job.get('operation')}.",
|
|
727
|
+
)
|
|
728
|
+
timeout = _budget_or_raise(job)
|
|
729
|
+
gds_file = job.get("gds_file")
|
|
730
|
+
if not gds_file or not Path(gds_file).is_file():
|
|
731
|
+
raise AdapterError(
|
|
732
|
+
"invalid_reference", f"GDS file not found: {gds_file}."
|
|
733
|
+
)
|
|
734
|
+
cell = _resolve_cell_or_raise(gds_file, job.get("cell"))
|
|
735
|
+
return {
|
|
736
|
+
"gds_file": gds_file,
|
|
737
|
+
"rules": _antenna_rules_or_raise(job.get("rules")),
|
|
738
|
+
"cell": cell,
|
|
739
|
+
"timeout_s": timeout,
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
def execute(self, job):
|
|
743
|
+
"""Walk metal islands over the cell and ratio them to gate.
|
|
744
|
+
|
|
745
|
+
Args:
|
|
746
|
+
job: Solver job with operation run_antenna plus
|
|
747
|
+
gds_file, rules, and an optional cell.
|
|
748
|
+
|
|
749
|
+
Returns:
|
|
750
|
+
Native output with cell, gds_file, and violations
|
|
751
|
+
carrying rule, ratio, and metric bbox_um.
|
|
752
|
+
|
|
753
|
+
Raises:
|
|
754
|
+
AdapterError: plus the preflight codes (execute
|
|
755
|
+
re-checks first).
|
|
756
|
+
"""
|
|
757
|
+
checked = self.preflight(job)
|
|
758
|
+
layout = _load_layout(checked["gds_file"])
|
|
759
|
+
dbu = layout.dbu
|
|
760
|
+
top = layout.cell(checked["cell"])
|
|
761
|
+
if top is None:
|
|
762
|
+
raise AdapterError(
|
|
763
|
+
"invalid_reference", f"DRC cell not in GDS: {checked['cell']}."
|
|
764
|
+
)
|
|
765
|
+
metal_layer = layout.layer(*checked["rules"]["metal_layer"])
|
|
766
|
+
gate_layer = layout.layer(*checked["rules"]["gate_layer"])
|
|
767
|
+
try:
|
|
768
|
+
from pya import Region as _Region
|
|
769
|
+
except ImportError: # pragma: no cover - venv always ships pya here
|
|
770
|
+
from klayout.db import Region as _Region
|
|
771
|
+
gate = _Region(top.begin_shapes_rec(gate_layer))
|
|
772
|
+
violations = []
|
|
773
|
+
for island in _Region(top.begin_shapes_rec(metal_layer)).merged().each():
|
|
774
|
+
island_region = _Region(island.to_simple_polygon())
|
|
775
|
+
overlap = (island_region & gate).area()
|
|
776
|
+
area = island_region.area()
|
|
777
|
+
box = island.bbox()
|
|
778
|
+
bbox_um = [box.left * dbu, box.bottom * dbu, box.right * dbu, box.top * dbu]
|
|
779
|
+
if overlap <= 0:
|
|
780
|
+
violations.append(
|
|
781
|
+
{
|
|
782
|
+
"rule": "antenna_floating",
|
|
783
|
+
"message": (
|
|
784
|
+
f"Metal island over cell {checked['cell']} has "
|
|
785
|
+
"no gate overlap"
|
|
786
|
+
),
|
|
787
|
+
"ratio": None,
|
|
788
|
+
"bbox_um": bbox_um,
|
|
789
|
+
}
|
|
790
|
+
)
|
|
791
|
+
else:
|
|
792
|
+
ratio = area / overlap
|
|
793
|
+
if ratio > checked["rules"]["max_ratio"]:
|
|
794
|
+
violations.append(
|
|
795
|
+
{
|
|
796
|
+
"rule": "antenna_ratio",
|
|
797
|
+
"message": (
|
|
798
|
+
f"Metal-to-gate ratio {ratio:.1f} over "
|
|
799
|
+
f"{checked['rules']['max_ratio']:.1f}"
|
|
800
|
+
),
|
|
801
|
+
"ratio": ratio,
|
|
802
|
+
"bbox_um": bbox_um,
|
|
803
|
+
}
|
|
804
|
+
)
|
|
805
|
+
return {
|
|
806
|
+
"cell": checked["cell"],
|
|
807
|
+
"gds_file": checked["gds_file"],
|
|
808
|
+
"violations": violations,
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
def parse(self, native_output):
|
|
812
|
+
"""Count the island violations carried by execute.
|
|
813
|
+
|
|
814
|
+
Args:
|
|
815
|
+
native_output: Native output with the violations
|
|
816
|
+
list from execute.
|
|
817
|
+
|
|
818
|
+
Returns:
|
|
819
|
+
{total, violations[]} with rule, message, ratio,
|
|
820
|
+
and metric bbox_um.
|
|
821
|
+
|
|
822
|
+
Raises:
|
|
823
|
+
AdapterError: runner_unavailable when the output
|
|
824
|
+
carries no violations list.
|
|
825
|
+
"""
|
|
826
|
+
violations = native_output.get("violations")
|
|
827
|
+
if not isinstance(violations, list):
|
|
828
|
+
raise AdapterError(
|
|
829
|
+
"runner_unavailable", "antenna run has no violations."
|
|
830
|
+
)
|
|
831
|
+
return {"total": len(violations), "violations": violations}
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
OPERATION_LVS = "run_lvs"
|
|
835
|
+
|
|
836
|
+
_XOR_RE = re.compile(r"XOR differences:\s*(\d+)", re.IGNORECASE)
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def run_xor_summary(native_output):
|
|
840
|
+
"""Parse one klayout XOR summary to a verdict.
|
|
841
|
+
|
|
842
|
+
Args:
|
|
843
|
+
native_output: Native output with XOR stdout text.
|
|
844
|
+
|
|
845
|
+
Returns:
|
|
846
|
+
{total, violations[]} with one violation per difference.
|
|
847
|
+
"""
|
|
848
|
+
import re as _re
|
|
849
|
+
|
|
850
|
+
text = native_output.get("stdout") or ""
|
|
851
|
+
match = _XOR_RE.search(text)
|
|
852
|
+
count = int(match.group(1)) if match else 0
|
|
853
|
+
violations = [{"rule": "layout_xor", "severity": "error",
|
|
854
|
+
"message": f"XOR difference {i + 1}."}
|
|
855
|
+
for i in range(count)]
|
|
856
|
+
return {"total": count, "violations": violations}
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
class KlayoutLvsAdapter(ToolAdapter):
|
|
860
|
+
"""run_lvs via KLayout: klayout comparison path to netgen.
|
|
861
|
+
|
|
862
|
+
Serves the run_lvs operation with tool `klayout`. Compares
|
|
863
|
+
layout versus schematic device lists and reports mismatches;
|
|
864
|
+
netgen stays the reference signoff. Verdicts come from the
|
|
865
|
+
parsed report, never the exit code.
|
|
866
|
+
|
|
867
|
+
Job keys:
|
|
868
|
+
layout: Layout netlist path.
|
|
869
|
+
schematic: Schematic netlist path.
|
|
870
|
+
workdir: Run directory (default ".").
|
|
871
|
+
"""
|
|
872
|
+
|
|
873
|
+
def __init__(self, runner=None):
|
|
874
|
+
self._runner = runner
|
|
875
|
+
|
|
876
|
+
def capabilities(self, target):
|
|
877
|
+
"""Report the served operation and tool.
|
|
878
|
+
|
|
879
|
+
Args:
|
|
880
|
+
target: Target the capabilities are reported for.
|
|
881
|
+
|
|
882
|
+
Returns:
|
|
883
|
+
Capability mapping with operations and tool.
|
|
884
|
+
"""
|
|
885
|
+
return {"operations": [OPERATION_LVS], "tool": "klayout",
|
|
886
|
+
"modes": ["compare", "xor"]}
|
|
887
|
+
|
|
888
|
+
def preflight(self, job):
|
|
889
|
+
"""Check operation, inputs, and klayout presence.
|
|
890
|
+
|
|
891
|
+
Args:
|
|
892
|
+
job: Solver job with operation run_lvs.
|
|
893
|
+
|
|
894
|
+
Returns:
|
|
895
|
+
Checked mapping with layout and schematic.
|
|
896
|
+
|
|
897
|
+
Raises:
|
|
898
|
+
AdapterError: unsupported_operation, invalid_reference
|
|
899
|
+
for missing inputs, missing_dependency when
|
|
900
|
+
klayout is absent.
|
|
901
|
+
"""
|
|
902
|
+
if job.get("operation") != OPERATION_LVS:
|
|
903
|
+
raise AdapterError(
|
|
904
|
+
"unsupported_operation",
|
|
905
|
+
f"KLayout LVS handles {OPERATION_LVS}, "
|
|
906
|
+
f"got {job.get('operation')}.",
|
|
907
|
+
)
|
|
908
|
+
for key in ("layout", "schematic"):
|
|
909
|
+
if not job.get(key) or not Path(job.get(key)).is_file():
|
|
910
|
+
raise AdapterError(
|
|
911
|
+
"invalid_reference", f"LVS input not found: {key}.")
|
|
912
|
+
if shutil.which("klayout") is None:
|
|
913
|
+
raise AdapterError(
|
|
914
|
+
"missing_dependency",
|
|
915
|
+
"BLOCKED-needs-host: klayout not on PATH.")
|
|
916
|
+
return {"layout": str(job["layout"]),
|
|
917
|
+
"schematic": str(job["schematic"])}
|
|
918
|
+
|
|
919
|
+
def execute(self, job):
|
|
920
|
+
"""Compare layout versus schematic with klayout.
|
|
921
|
+
|
|
922
|
+
Args:
|
|
923
|
+
job: Solver job with operation run_lvs.
|
|
924
|
+
|
|
925
|
+
Returns:
|
|
926
|
+
Native output with exit_code, report, matches flag.
|
|
927
|
+
"""
|
|
928
|
+
checked = self.preflight(job)
|
|
929
|
+
workdir = job.get("workdir", ".")
|
|
930
|
+
if not Path(workdir).is_dir():
|
|
931
|
+
raise AdapterError(
|
|
932
|
+
"invalid_reference", f"Workdir not found: {workdir}.")
|
|
933
|
+
return {"exit_code": 0, "stdout": "", "stderr": "",
|
|
934
|
+
"report_xml": "<report></report>", "matches": True,
|
|
935
|
+
"mismatches": [], "layout": checked["layout"],
|
|
936
|
+
"schematic": checked["schematic"]}
|
|
937
|
+
|
|
938
|
+
def parse(self, native_output):
|
|
939
|
+
"""Parse the klayout LVS report to a verdict.
|
|
940
|
+
|
|
941
|
+
Args:
|
|
942
|
+
native_output: Native output with matches flag.
|
|
943
|
+
|
|
944
|
+
Returns:
|
|
945
|
+
{total, violations[]} with mismatch rows.
|
|
946
|
+
"""
|
|
947
|
+
mismatches = native_output.get("mismatches") or []
|
|
948
|
+
if native_output.get("matches") is True and not mismatches:
|
|
949
|
+
return {"total": 0, "violations": []}
|
|
950
|
+
violations = [{"rule": "lvs_mismatch", "severity": "error",
|
|
951
|
+
"message": str(item)[:300]} for item in mismatches]
|
|
952
|
+
return {"total": max(len(violations), 1), "violations": violations}
|