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,889 @@
|
|
|
1
|
+
"""Netgen LVS solver adapter (EXTRACT-SKY130): layout vs schematic.
|
|
2
|
+
|
|
3
|
+
Protocol, verified against netgen 1.5.316 + sky130A_setup.tcl: the
|
|
4
|
+
one-shot `netgen -batch lvs <layout> <schematic> <setup> <report>`
|
|
5
|
+
compares TOP-level circuits, so both sides are first flattened to
|
|
6
|
+
top level (leaf-cell normalization: the single `.subckt` wrapper is
|
|
7
|
+
dropped; a library file needs `cell` to select its block). Netgen
|
|
8
|
+
exits 0 on match and mismatch alike, so the verdict always comes
|
|
9
|
+
from parsed output, never the exit code. Batch runs with DISPLAY
|
|
10
|
+
scrubbed: netgen's Tk init fails against an unreachable display.
|
|
11
|
+
Because netgen matches by topology, a rename of a singly-connected
|
|
12
|
+
net escapes it (LVS-ESCAPE): the flattened net-name inventories are
|
|
13
|
+
compared too, and name-only-on-one-side nets fail as
|
|
14
|
+
`lvs_net_mismatch`.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import shutil
|
|
20
|
+
import subprocess
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from ...execution.errors import ExecutionError
|
|
24
|
+
from ...interfaces import ToolAdapter
|
|
25
|
+
from ...interfaces.runparams import MAX_TIMEOUT_S, MIN_TIMEOUT_S
|
|
26
|
+
from ..errors import AdapterError
|
|
27
|
+
|
|
28
|
+
OPERATION = "run_lvs"
|
|
29
|
+
_TIMEOUT_S = 120
|
|
30
|
+
_IDENTIFY_TIMEOUT_S = 10
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _budget_or_raise(job):
|
|
34
|
+
"""Job solver budget: validated, or the adapter default.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
job: Solver job, maybe with a `timeout_s`.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Budget seconds for every `_run` call.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
AdapterError: invalid_command for a boolean,
|
|
44
|
+
non-number, or out-of-bounds budget.
|
|
45
|
+
"""
|
|
46
|
+
timeout = job.get("timeout_s", _TIMEOUT_S)
|
|
47
|
+
if (
|
|
48
|
+
isinstance(timeout, bool)
|
|
49
|
+
or not isinstance(timeout, (int, float))
|
|
50
|
+
or not MIN_TIMEOUT_S <= timeout <= MAX_TIMEOUT_S
|
|
51
|
+
):
|
|
52
|
+
raise AdapterError(
|
|
53
|
+
"invalid_command", f"Bad timeout_s: {timeout!r}."
|
|
54
|
+
)
|
|
55
|
+
return timeout
|
|
56
|
+
|
|
57
|
+
# Observed impostor markers (Oct 2026): the `netgen` apt package is
|
|
58
|
+
# the Schoeberl mesh generator — same name, different tool. Denylist
|
|
59
|
+
# only: NGSPICE-LIVE promotes this to a strict allowlist once the
|
|
60
|
+
# real LVS banner is verifiable against an installed binary.
|
|
61
|
+
_IMPOSTOR_RE = re.compile(
|
|
62
|
+
r"Schoeberl|Vienna University|RWTH Aachen|Johannes Kepler"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
_SUBCKT_RE = re.compile(r"^\s*\.subckt\s+(\S+)", re.IGNORECASE)
|
|
66
|
+
_ENDS_RE = re.compile(r"^\s*\.ends\b", re.IGNORECASE)
|
|
67
|
+
_DEVICE_RE = re.compile(
|
|
68
|
+
r"^X(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)(.*)$",
|
|
69
|
+
re.IGNORECASE,
|
|
70
|
+
)
|
|
71
|
+
_MATCH_RE = re.compile(r"(Circuits match uniquely|Netlists match uniquely)\.")
|
|
72
|
+
_MISMATCH_RES = (
|
|
73
|
+
re.compile(r"Netlists do not match\."),
|
|
74
|
+
re.compile(r"\*\*\* MISMATCH \*\*\*"),
|
|
75
|
+
)
|
|
76
|
+
_NET_RE = re.compile(r"Net:\s+(\S+)")
|
|
77
|
+
_INSTANCE_RE = re.compile(r"Instance:\s+(\S+)")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def slice_subckt(text, cell):
|
|
81
|
+
"""Return the wrapped `.subckt cell ... .ends` block from a library.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
text: Netlist library text to slice.
|
|
85
|
+
cell: Subcircuit name to select (case-insensitive).
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The wrapped .subckt block with its closing .ends.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
AdapterError: invalid_command when the cell is absent or
|
|
92
|
+
has no closing .ends.
|
|
93
|
+
"""
|
|
94
|
+
lines = text.splitlines()
|
|
95
|
+
start = next(
|
|
96
|
+
(
|
|
97
|
+
i
|
|
98
|
+
for i, line in enumerate(lines)
|
|
99
|
+
if (m := _SUBCKT_RE.match(line)) and m.group(1).lower() == cell.lower()
|
|
100
|
+
),
|
|
101
|
+
None,
|
|
102
|
+
)
|
|
103
|
+
if start is None:
|
|
104
|
+
raise AdapterError(
|
|
105
|
+
"invalid_command", f"No .subckt {cell} in the netlist."
|
|
106
|
+
)
|
|
107
|
+
end = next(
|
|
108
|
+
(i for i in range(start + 1, len(lines)) if _ENDS_RE.match(lines[i])),
|
|
109
|
+
None,
|
|
110
|
+
)
|
|
111
|
+
if end is None:
|
|
112
|
+
raise AdapterError(
|
|
113
|
+
"invalid_command", f".subckt {cell} has no closing .ends."
|
|
114
|
+
)
|
|
115
|
+
return "\n".join(lines[start : end + 1]) + "\n"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def flatten_spice(text, cell=None):
|
|
119
|
+
"""Normalize a netlist to top level for netgen's one-shot lvs.
|
|
120
|
+
|
|
121
|
+
A bare netlist passes through; a single `.subckt` wrapper is
|
|
122
|
+
dropped; a library needs `cell` to select its block, else the
|
|
123
|
+
call refuses instead of guessing.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
text: Netlist text to normalize.
|
|
127
|
+
cell: Subcircuit to select from a library, or None for a
|
|
128
|
+
bare or single-wrapper netlist.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Top-level netlist text with .subckt/.ends lines dropped.
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
AdapterError: invalid_command for an ambiguous multi-block
|
|
135
|
+
library without cell, or a missing/unclosed block.
|
|
136
|
+
"""
|
|
137
|
+
if cell is not None:
|
|
138
|
+
text = slice_subckt(text, cell)
|
|
139
|
+
elif sum(1 for line in text.splitlines() if _SUBCKT_RE.match(line)) > 1:
|
|
140
|
+
raise AdapterError(
|
|
141
|
+
"invalid_command", "Ambiguous netlist: pass cell to select a block."
|
|
142
|
+
)
|
|
143
|
+
return "\n".join(
|
|
144
|
+
line
|
|
145
|
+
for line in text.splitlines()
|
|
146
|
+
if not _SUBCKT_RE.match(line) and not _ENDS_RE.match(line)
|
|
147
|
+
) + "\n"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _device_nets(tokens):
|
|
151
|
+
"""Net names of one X/M device line's token body.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
tokens: Bare tokens after the device name (nets, class,
|
|
155
|
+
then `k=v` params).
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Net names: every bare token except the trailing class.
|
|
159
|
+
"""
|
|
160
|
+
params_at = next(
|
|
161
|
+
(i for i, token in enumerate(tokens) if "=" in token), len(tokens)
|
|
162
|
+
)
|
|
163
|
+
bare = tokens[:params_at]
|
|
164
|
+
if params_at < len(tokens):
|
|
165
|
+
bare = bare[:-1] if bare else []
|
|
166
|
+
return bare
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def inventory_nets(text):
|
|
170
|
+
"""Net names of the flattened netlist's X and M device lines.
|
|
171
|
+
|
|
172
|
+
Netgen's one-shot `lvs` matches by topology, so a rename of a
|
|
173
|
+
singly-connected net escapes it (LVS-ESCAPE). This inventory
|
|
174
|
+
keeps the dropped name evidence: comment, subckt/ends, empty,
|
|
175
|
+
and non-device lines never contribute; params (`k=v`) never
|
|
176
|
+
count as nets; the class token is the last bare token before
|
|
177
|
+
the params start. M-lines cover the klt/reference format both
|
|
178
|
+
bandgap sides use; X-lines cover ext2spice output.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
text: Flattened netlist text to inventory.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Set of net names on X and M device lines.
|
|
185
|
+
"""
|
|
186
|
+
nets = set()
|
|
187
|
+
for line in text.splitlines():
|
|
188
|
+
stripped = line.strip()
|
|
189
|
+
if not stripped or stripped.startswith("*") or stripped.startswith("."):
|
|
190
|
+
continue
|
|
191
|
+
tokens = stripped.split()
|
|
192
|
+
if not tokens or not tokens[0][:1].upper() in ("X", "M"):
|
|
193
|
+
continue
|
|
194
|
+
nets.update(_device_nets(tokens[1:]))
|
|
195
|
+
return nets
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _split_subckts(text):
|
|
199
|
+
"""Subckt blocks of a netlist in file order.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
text: Netlist text, possibly hierarchical.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
List of (name, ports, body lines) per `.subckt` block;
|
|
206
|
+
lines outside any block are dropped.
|
|
207
|
+
"""
|
|
208
|
+
blocks = []
|
|
209
|
+
current = None
|
|
210
|
+
for line in text.splitlines():
|
|
211
|
+
match = _SUBCKT_RE.match(line)
|
|
212
|
+
if match is not None:
|
|
213
|
+
parts = line.strip().split()
|
|
214
|
+
current = [parts[1], parts[2:], []]
|
|
215
|
+
blocks.append(current)
|
|
216
|
+
continue
|
|
217
|
+
if _ENDS_RE.match(line):
|
|
218
|
+
current = None
|
|
219
|
+
continue
|
|
220
|
+
if current is not None:
|
|
221
|
+
current[2].append(line)
|
|
222
|
+
return [(name, ports, body) for name, ports, body in blocks]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _instance_class(line):
|
|
226
|
+
"""Class token of an X instance line, or None.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
line: One netlist line (already stripped).
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Last bare token before params start, or None when the
|
|
233
|
+
line is not an X instance line.
|
|
234
|
+
"""
|
|
235
|
+
tokens = line.split()
|
|
236
|
+
if not tokens or not tokens[0].upper().startswith("X"):
|
|
237
|
+
return None
|
|
238
|
+
body = tokens[1:]
|
|
239
|
+
params_at = next(
|
|
240
|
+
(i for i, token in enumerate(body) if "=" in token), len(body)
|
|
241
|
+
)
|
|
242
|
+
bare = body[:params_at]
|
|
243
|
+
if params_at < len(body):
|
|
244
|
+
bare = bare[:-1] if bare else []
|
|
245
|
+
if not bare:
|
|
246
|
+
return None
|
|
247
|
+
return bare[-1]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def flatten_hierarchy(text, top=None):
|
|
251
|
+
"""Inline hierarchical subckt instances to flat top level.
|
|
252
|
+
|
|
253
|
+
Magic ext2spice writes one `.subckt` per layout cell, but
|
|
254
|
+
netgen's one-shot LVS compares TOP-level circuits, so the
|
|
255
|
+
engine-extracted bandgap netlist needs inlining past the
|
|
256
|
+
single-wrapper strip of `flatten_spice`. Leaf instances whose
|
|
257
|
+
class names a known block expand with port nets mapped onto
|
|
258
|
+
the instance nets and private nets qualified as
|
|
259
|
+
`INST_net`; X/M device cards map the same way. Cards that are
|
|
260
|
+
neither X nor M device lines pass through unmapped
|
|
261
|
+
(documented boundary: engine-extracted leaves are FET-only).
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
text: Hierarchical netlist text to flatten.
|
|
265
|
+
top: Subckt to flatten from, or None to take the single
|
|
266
|
+
never-instantiated (root) block.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Flat netlist text with no `.subckt`/`.ends` lines.
|
|
270
|
+
Texts without any block pass through unchanged.
|
|
271
|
+
|
|
272
|
+
Raises:
|
|
273
|
+
AdapterError: invalid_command for a missing top, or
|
|
274
|
+
for zero or several roots without top.
|
|
275
|
+
"""
|
|
276
|
+
blocks = {
|
|
277
|
+
name.lower(): (name, ports, body)
|
|
278
|
+
for name, ports, body in _split_subckts(text)
|
|
279
|
+
}
|
|
280
|
+
if not blocks:
|
|
281
|
+
return text if text.endswith("\n") else text + "\n"
|
|
282
|
+
referenced = set()
|
|
283
|
+
for _, _, body in blocks.values():
|
|
284
|
+
for line in body:
|
|
285
|
+
cls = _instance_class(line.strip())
|
|
286
|
+
if cls is not None and cls.lower() in blocks:
|
|
287
|
+
referenced.add(cls.lower())
|
|
288
|
+
if top is not None:
|
|
289
|
+
root_key = top.lower()
|
|
290
|
+
if root_key not in blocks:
|
|
291
|
+
raise AdapterError(
|
|
292
|
+
"invalid_command", f"No .subckt {top} in the netlist."
|
|
293
|
+
)
|
|
294
|
+
else:
|
|
295
|
+
roots = [key for key in blocks if key not in referenced]
|
|
296
|
+
if len(roots) != 1:
|
|
297
|
+
raise AdapterError(
|
|
298
|
+
"invalid_command",
|
|
299
|
+
"Ambiguous hierarchy: pass top to select the root block.",
|
|
300
|
+
)
|
|
301
|
+
root_key = roots[0]
|
|
302
|
+
|
|
303
|
+
def emit(block_key, scope, prefix, out):
|
|
304
|
+
"""Inline one block: ports map via scope, privates qualify."""
|
|
305
|
+
_, ports, body = blocks[block_key]
|
|
306
|
+
for line in body:
|
|
307
|
+
stripped = line.strip()
|
|
308
|
+
cls = _instance_class(stripped)
|
|
309
|
+
if cls is not None and cls.lower() in blocks:
|
|
310
|
+
tokens = stripped.split()
|
|
311
|
+
inst = tokens[0]
|
|
312
|
+
nets = _device_nets(tokens[1:])
|
|
313
|
+
_, sub_ports, _ = blocks[cls.lower()]
|
|
314
|
+
child_scope = {}
|
|
315
|
+
for port, net in zip(sub_ports, nets):
|
|
316
|
+
child_scope[port] = scope.get(net, net)
|
|
317
|
+
child_prefix = f"{prefix}_{inst}" if prefix else inst
|
|
318
|
+
emit(cls.lower(), child_scope, child_prefix, out)
|
|
319
|
+
continue
|
|
320
|
+
mapped = _map_device_line(stripped, scope, prefix)
|
|
321
|
+
if mapped is None:
|
|
322
|
+
out.append(line)
|
|
323
|
+
continue
|
|
324
|
+
if prefix:
|
|
325
|
+
head, _, rest = mapped.partition(" ")
|
|
326
|
+
mapped = f"{prefix}_{head} {rest}"
|
|
327
|
+
out.append(mapped)
|
|
328
|
+
|
|
329
|
+
_, root_ports, _ = blocks[root_key]
|
|
330
|
+
out = []
|
|
331
|
+
emit(root_key, {port: port for port in root_ports}, "", out)
|
|
332
|
+
return "\n".join(out) + "\n"
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _map_device_line(stripped, scope, prefix):
|
|
336
|
+
"""One device line with block-local nets mapped to flat names.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
stripped: One stripped netlist line.
|
|
340
|
+
scope: Local net to flat name mapping (ports bound).
|
|
341
|
+
prefix: Instance qualifier for private nets ("" at root).
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Remapped X/M device line, or None for other cards.
|
|
345
|
+
"""
|
|
346
|
+
tokens = stripped.split()
|
|
347
|
+
if not tokens or tokens[0][:1].upper() not in ("X", "M"):
|
|
348
|
+
return None
|
|
349
|
+
body = tokens[1:]
|
|
350
|
+
params_at = next(
|
|
351
|
+
(i for i, token in enumerate(body) if "=" in token), len(body)
|
|
352
|
+
)
|
|
353
|
+
bare = body[:params_at]
|
|
354
|
+
if not bare:
|
|
355
|
+
return None
|
|
356
|
+
cls = bare[-1] if params_at < len(body) else None
|
|
357
|
+
nets = bare[:-1] if cls is not None else bare
|
|
358
|
+
flat_nets = [scope.get(net, f"{prefix}_{net}" if prefix else net) for net in nets]
|
|
359
|
+
tail = body[params_at:]
|
|
360
|
+
parts = [tokens[0]] + flat_nets
|
|
361
|
+
if cls is not None:
|
|
362
|
+
parts.append(cls)
|
|
363
|
+
parts.extend(tail)
|
|
364
|
+
return " ".join(parts)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def retype_by_bulk(text, n_bulk_nets, p_bulk_nets):
|
|
368
|
+
"""Re-type sky130 X-FET lines by their bulk terminal.
|
|
369
|
+
|
|
370
|
+
Wave-1 bulk-grouping evidence (docs/probe-extract-gds.md):
|
|
371
|
+
magic's uniform-pfet typing is wrong where the bulk says
|
|
372
|
+
otherwise — 52 devices share one common (p) bulk mirroring the
|
|
373
|
+
klt pfet split, the rest bulk to VSS. The bulk net
|
|
374
|
+
adjudicates: a pfet-family line bulked to an n-family net
|
|
375
|
+
re-types to the nfet flavor with its suffix kept
|
|
376
|
+
(`pfet_01v8_hvt` -> `nfet_01v8_hvt`), and vice versa. Bulk
|
|
377
|
+
nets outside both families never re-type (conservative).
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
text: Flat netlist text to adjudicate.
|
|
381
|
+
n_bulk_nets: Net names treated as n-family bulk (VSS...).
|
|
382
|
+
p_bulk_nets: Net names treated as p-family bulk (VDD...).
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
(New text, number of re-typed lines).
|
|
386
|
+
"""
|
|
387
|
+
n_bulk = {net for net in n_bulk_nets}
|
|
388
|
+
p_bulk = {net for net in p_bulk_nets}
|
|
389
|
+
count = 0
|
|
390
|
+
out = []
|
|
391
|
+
for line in text.splitlines():
|
|
392
|
+
stripped = line.strip()
|
|
393
|
+
match = _DEVICE_RE.match(stripped)
|
|
394
|
+
if match is None:
|
|
395
|
+
out.append(line)
|
|
396
|
+
continue
|
|
397
|
+
name, drain, gate, source, bulk, cls, params = match.groups()
|
|
398
|
+
lowered = cls.lower()
|
|
399
|
+
if "pfet" in lowered and bulk in n_bulk:
|
|
400
|
+
cls = cls.replace("pfet", "nfet").replace("PFET", "NFET")
|
|
401
|
+
count += 1
|
|
402
|
+
elif "nfet" in lowered and bulk in p_bulk:
|
|
403
|
+
cls = cls.replace("nfet", "pfet").replace("NFET", "PFET")
|
|
404
|
+
count += 1
|
|
405
|
+
out.append(f"X{name} {drain} {gate} {source} {bulk} {cls}{params}")
|
|
406
|
+
return "\n".join(out) + "\n", count
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _inventory_violations(layout_nets, schematic_nets):
|
|
410
|
+
"""Name-only-on-one-side nets as `lvs_net_mismatch` violations.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
layout_nets: Net names on the layout side.
|
|
414
|
+
schematic_nets: Net names on the schematic side.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Violations naming each asymmetric net with its side.
|
|
418
|
+
"""
|
|
419
|
+
violations = []
|
|
420
|
+
for net in sorted(set(layout_nets) - set(schematic_nets)):
|
|
421
|
+
violations.append(
|
|
422
|
+
{
|
|
423
|
+
"rule": "lvs_net_mismatch",
|
|
424
|
+
"message": f"Net: {net} (only in layout)",
|
|
425
|
+
"net": net,
|
|
426
|
+
"severity": "error",
|
|
427
|
+
}
|
|
428
|
+
)
|
|
429
|
+
for net in sorted(set(schematic_nets) - set(layout_nets)):
|
|
430
|
+
violations.append(
|
|
431
|
+
{
|
|
432
|
+
"rule": "lvs_net_mismatch",
|
|
433
|
+
"message": f"Net: {net} (only in schematic)",
|
|
434
|
+
"net": net,
|
|
435
|
+
"severity": "error",
|
|
436
|
+
}
|
|
437
|
+
)
|
|
438
|
+
return violations
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def netgen_env():
|
|
442
|
+
"""Process env for netgen: DISPLAY scrubbed, the rest intact.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
Process environment mapping without DISPLAY (netgen's Tk
|
|
446
|
+
init fails against an unreachable display).
|
|
447
|
+
"""
|
|
448
|
+
env = dict(os.environ)
|
|
449
|
+
env.pop("DISPLAY", None)
|
|
450
|
+
return env
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _identify_netgen(path):
|
|
454
|
+
"""Banner check: None if usable, else the rejection cause."""
|
|
455
|
+
try:
|
|
456
|
+
proc = subprocess.run(
|
|
457
|
+
[path, "-help"],
|
|
458
|
+
stdin=subprocess.DEVNULL,
|
|
459
|
+
capture_output=True,
|
|
460
|
+
text=True,
|
|
461
|
+
timeout=_IDENTIFY_TIMEOUT_S,
|
|
462
|
+
)
|
|
463
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
464
|
+
return (
|
|
465
|
+
f"netgen at {path} is unprovable: the identity probe "
|
|
466
|
+
"failed (see docs/tickets/NGSPICE-LIVE.md)."
|
|
467
|
+
)
|
|
468
|
+
banner = (proc.stdout or "") + (proc.stderr or "")
|
|
469
|
+
if _IMPOSTOR_RE.search(banner):
|
|
470
|
+
first = banner.strip().splitlines()[0] if banner.strip() else "empty banner"
|
|
471
|
+
return (
|
|
472
|
+
f"netgen at {path} is the Schoeberl mesh-generator "
|
|
473
|
+
f"impostor, not the LVS netgen ({first}); install "
|
|
474
|
+
"opencircuitdesign netgen (see docs/tickets/NGSPICE-LIVE.md)."
|
|
475
|
+
)
|
|
476
|
+
return None
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def resolve_netgen():
|
|
480
|
+
"""Path to a usable LVS netgen, or None.
|
|
481
|
+
|
|
482
|
+
PATH presence is NOT enough: the `netgen` apt package is the
|
|
483
|
+
Schoeberl mesh generator, a same-named different tool (caught
|
|
484
|
+
live Oct 2026). Denylist: only observed impostor markers
|
|
485
|
+
reject; an unknown banner admits — NGSPICE-LIVE promotes this
|
|
486
|
+
to a strict allowlist once the real binary is verifiable.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Path to a usable LVS netgen, or None when missing or
|
|
490
|
+
rejected by the banner probe.
|
|
491
|
+
"""
|
|
492
|
+
found = shutil.which("netgen")
|
|
493
|
+
if found is None:
|
|
494
|
+
return None
|
|
495
|
+
if _identify_netgen(found) is not None:
|
|
496
|
+
return None
|
|
497
|
+
return found
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _count_subckts(text):
|
|
501
|
+
"""Number of `.subckt` blocks in netlist text.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
text: Netlist text to scan.
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Count of `.subckt` header lines (case-insensitive).
|
|
508
|
+
"""
|
|
509
|
+
return sum(1 for line in text.splitlines() if _SUBCKT_RE.match(line))
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _prepare_flat(text, cell):
|
|
513
|
+
"""Flatten one netlist side to top level for the one-shot LVS.
|
|
514
|
+
|
|
515
|
+
A named cell slices a library; a single wrapper (or none)
|
|
516
|
+
strips as before; several blocks without a cell inline the
|
|
517
|
+
hierarchy from its root instead of refusing.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
text: Netlist text to flatten.
|
|
521
|
+
cell: Subcircuit to select, or None.
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
Flat top-level netlist text.
|
|
525
|
+
"""
|
|
526
|
+
if cell is not None:
|
|
527
|
+
if _count_subckts(text) > 1:
|
|
528
|
+
return flatten_hierarchy(text, top=cell)
|
|
529
|
+
return flatten_spice(text, cell)
|
|
530
|
+
if _count_subckts(text) > 1:
|
|
531
|
+
return flatten_hierarchy(text)
|
|
532
|
+
return flatten_spice(text)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _bulk_or_raise(job):
|
|
536
|
+
"""Bulk-net families for p/n adjudication, or None.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
job: Solver job, maybe with a `bulk_nets` mapping of
|
|
540
|
+
`n`/`p` net-name lists.
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
(n_bulk, p_bulk) frozensets, or None when the job names
|
|
544
|
+
no families (retype skipped, old behavior kept).
|
|
545
|
+
|
|
546
|
+
Raises:
|
|
547
|
+
AdapterError: invalid_command for a malformed mapping.
|
|
548
|
+
"""
|
|
549
|
+
bulk = job.get("bulk_nets")
|
|
550
|
+
if bulk is None:
|
|
551
|
+
return None
|
|
552
|
+
if not isinstance(bulk, dict):
|
|
553
|
+
raise AdapterError(
|
|
554
|
+
"invalid_command", f"Bad bulk_nets (not a mapping): {bulk!r}."
|
|
555
|
+
)
|
|
556
|
+
clean = {}
|
|
557
|
+
for family in ("n", "p"):
|
|
558
|
+
nets = bulk.get(family, [])
|
|
559
|
+
if not isinstance(nets, (list, tuple)) or not all(
|
|
560
|
+
isinstance(net, str) and net for net in nets
|
|
561
|
+
):
|
|
562
|
+
raise AdapterError(
|
|
563
|
+
"invalid_command", f"Bad bulk_nets[{family!r}]: {nets!r}."
|
|
564
|
+
)
|
|
565
|
+
clean[family] = frozenset(nets)
|
|
566
|
+
return clean
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _setup_file_or_raise(job):
|
|
570
|
+
deck = job.get("deck") if isinstance(job.get("deck"), dict) else {}
|
|
571
|
+
for candidate in (
|
|
572
|
+
job.get("setup_file"),
|
|
573
|
+
deck.get("path"),
|
|
574
|
+
deck.get("setup_file"),
|
|
575
|
+
deck.get("setup"),
|
|
576
|
+
deck.get("file"),
|
|
577
|
+
):
|
|
578
|
+
if candidate is not None:
|
|
579
|
+
if not candidate or not Path(candidate).is_file():
|
|
580
|
+
raise AdapterError(
|
|
581
|
+
"invalid_reference", f"Netgen setup not found: {candidate}."
|
|
582
|
+
)
|
|
583
|
+
return str(candidate)
|
|
584
|
+
raise AdapterError("invalid_command", "LVS needs a netgen setup file.")
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _parse_mismatches(report):
|
|
588
|
+
violations = []
|
|
589
|
+
net_section, _, device_section = report.partition("DEVICE mismatches:")
|
|
590
|
+
seen = set()
|
|
591
|
+
for name in _NET_RE.findall(net_section):
|
|
592
|
+
if name not in seen:
|
|
593
|
+
seen.add(name)
|
|
594
|
+
violations.append(
|
|
595
|
+
{
|
|
596
|
+
"rule": "lvs_net_mismatch",
|
|
597
|
+
"message": f"Net: {name}",
|
|
598
|
+
"net": name,
|
|
599
|
+
"severity": "error",
|
|
600
|
+
}
|
|
601
|
+
)
|
|
602
|
+
for _ in re.finditer(r"\(no matching net\)", net_section):
|
|
603
|
+
violations.append(
|
|
604
|
+
{
|
|
605
|
+
"rule": "lvs_net_mismatch",
|
|
606
|
+
"message": "(no matching net)",
|
|
607
|
+
"severity": "error",
|
|
608
|
+
}
|
|
609
|
+
)
|
|
610
|
+
seen = set()
|
|
611
|
+
for name in _INSTANCE_RE.findall(device_section):
|
|
612
|
+
if name not in seen:
|
|
613
|
+
seen.add(name)
|
|
614
|
+
violations.append(
|
|
615
|
+
{
|
|
616
|
+
"rule": "lvs_device_mismatch",
|
|
617
|
+
"message": f"Instance: {name}",
|
|
618
|
+
"instance": name,
|
|
619
|
+
"severity": "error",
|
|
620
|
+
}
|
|
621
|
+
)
|
|
622
|
+
for _ in re.finditer(r"\(no matching instance\)", device_section):
|
|
623
|
+
violations.append(
|
|
624
|
+
{
|
|
625
|
+
"rule": "lvs_device_mismatch",
|
|
626
|
+
"message": "(no matching instance)",
|
|
627
|
+
"severity": "error",
|
|
628
|
+
}
|
|
629
|
+
)
|
|
630
|
+
return violations
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
class NetgenLvsAdapter(ToolAdapter):
|
|
634
|
+
"""run_lvs via Netgen: layout vs schematic, TOP-level compare.
|
|
635
|
+
|
|
636
|
+
Serves the run_lvs operation with deck.tool `netgen` (the only
|
|
637
|
+
tool). Both netlists flatten to top level first (a library file
|
|
638
|
+
needs `cell` to select its block), then the one-shot batch LVS
|
|
639
|
+
runs; the result is {match, total, violations[]} with net and
|
|
640
|
+
device mismatches.
|
|
641
|
+
|
|
642
|
+
Job keys:
|
|
643
|
+
layout_spice (required): Layout netlist path.
|
|
644
|
+
schematic_spice (required): Schematic netlist path.
|
|
645
|
+
setup_file (required): Netgen setup path — one setup form
|
|
646
|
+
is required (setup_file or deck.path/setup_file/setup/
|
|
647
|
+
file).
|
|
648
|
+
deck: Optional mapping naming the setup file (see above).
|
|
649
|
+
cell: .subckt block to select from library netlists.
|
|
650
|
+
bulk_nets: Optional {n: [...], p: [...]} bulk-net
|
|
651
|
+
families; X-FET lines bulked to the opposite family
|
|
652
|
+
re-type before compare (bandgap p/n adjudication).
|
|
653
|
+
workdir: Run directory (default ".").
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
runner: Optional isolated runner; direct subprocess when None.
|
|
657
|
+
|
|
658
|
+
Caution: Netgen exits 0 on match and mismatch alike — the
|
|
659
|
+
verdict always comes from parsed output, never the exit
|
|
660
|
+
code. PATH presence is not enough: same-named impostor
|
|
661
|
+
binaries are rejected by banner probe.
|
|
662
|
+
"""
|
|
663
|
+
|
|
664
|
+
def __init__(self, runner=None):
|
|
665
|
+
self._runner = runner
|
|
666
|
+
|
|
667
|
+
def capabilities(self, target):
|
|
668
|
+
"""Report the served operation and tool name.
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
target: Target the capabilities are reported for
|
|
672
|
+
(accepted, unused — one netlist pair per job).
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
Capability mapping with operations and tool.
|
|
676
|
+
"""
|
|
677
|
+
return {"operations": [OPERATION], "tool": "netgen"}
|
|
678
|
+
|
|
679
|
+
def preflight(self, job):
|
|
680
|
+
"""Check operation, banner-proved binary, setup, and netlists.
|
|
681
|
+
|
|
682
|
+
Args:
|
|
683
|
+
job: Solver job with operation run_lvs plus netlist,
|
|
684
|
+
setup, and cell inputs.
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
Checked mapping with setup_file, both netlist paths,
|
|
688
|
+
and cell when given.
|
|
689
|
+
|
|
690
|
+
Raises:
|
|
691
|
+
AdapterError: unsupported_operation for another
|
|
692
|
+
operation, missing_dependency for a missing netgen
|
|
693
|
+
or a banner-rejected impostor, invalid_reference
|
|
694
|
+
for missing netlist files, invalid_command when no
|
|
695
|
+
setup form is given.
|
|
696
|
+
"""
|
|
697
|
+
if job.get("operation") != OPERATION:
|
|
698
|
+
raise AdapterError(
|
|
699
|
+
"unsupported_operation",
|
|
700
|
+
f"Netgen LVS handles {OPERATION}, got {job.get('operation')}.",
|
|
701
|
+
)
|
|
702
|
+
timeout = _budget_or_raise(job)
|
|
703
|
+
found = shutil.which("netgen")
|
|
704
|
+
if found is None:
|
|
705
|
+
raise AdapterError("missing_dependency", "netgen binary not found.")
|
|
706
|
+
cause = _identify_netgen(found)
|
|
707
|
+
if cause is not None:
|
|
708
|
+
raise AdapterError("missing_dependency", cause)
|
|
709
|
+
checked = {"setup_file": _setup_file_or_raise(job)}
|
|
710
|
+
for key in ("layout_spice", "schematic_spice"):
|
|
711
|
+
path = job.get(key)
|
|
712
|
+
if not path or not Path(path).is_file():
|
|
713
|
+
raise AdapterError(
|
|
714
|
+
"invalid_reference", f"Netlist file not found: {path}."
|
|
715
|
+
)
|
|
716
|
+
checked[key] = path
|
|
717
|
+
if job.get("cell") is not None:
|
|
718
|
+
checked["cell"] = str(job["cell"])
|
|
719
|
+
bulk = _bulk_or_raise(job)
|
|
720
|
+
if bulk is not None:
|
|
721
|
+
checked["bulk_nets"] = {
|
|
722
|
+
"n": sorted(bulk["n"]),
|
|
723
|
+
"p": sorted(bulk["p"]),
|
|
724
|
+
}
|
|
725
|
+
checked["timeout_s"] = timeout
|
|
726
|
+
return checked
|
|
727
|
+
|
|
728
|
+
def execute(self, job):
|
|
729
|
+
"""Flatten both sides, then run the one-shot batch LVS.
|
|
730
|
+
|
|
731
|
+
Batch runs with DISPLAY scrubbed: netgen's Tk init fails
|
|
732
|
+
against an unreachable display. Hierarchical sides inline
|
|
733
|
+
from their root; a `bulk_nets` job key re-types uniform
|
|
734
|
+
ext2spice FETs by bulk before compare (bandgap leg).
|
|
735
|
+
|
|
736
|
+
Args:
|
|
737
|
+
job: Solver job with operation run_lvs plus netlist,
|
|
738
|
+
setup, cell, and bulk_nets inputs.
|
|
739
|
+
|
|
740
|
+
Returns:
|
|
741
|
+
Native output with exit_code, stdout/stderr, report
|
|
742
|
+
file/text, both netlist paths, and the per-side
|
|
743
|
+
retype counts.
|
|
744
|
+
|
|
745
|
+
Raises:
|
|
746
|
+
AdapterError: invalid_reference for a missing workdir,
|
|
747
|
+
invalid_command for an ambiguous library without
|
|
748
|
+
cell, runner_unavailable when netgen produces no
|
|
749
|
+
report, plus the preflight codes (execute
|
|
750
|
+
re-checks first).
|
|
751
|
+
"""
|
|
752
|
+
checked = self.preflight(job)
|
|
753
|
+
workdir = job.get("workdir", ".")
|
|
754
|
+
if not Path(workdir).is_dir():
|
|
755
|
+
raise AdapterError(
|
|
756
|
+
"invalid_reference", f"Workdir not found: {workdir}."
|
|
757
|
+
)
|
|
758
|
+
cell = checked.get("cell")
|
|
759
|
+
bulk = checked.get("bulk_nets")
|
|
760
|
+
staged = {}
|
|
761
|
+
flats = {}
|
|
762
|
+
retyped = {}
|
|
763
|
+
for key, name in (
|
|
764
|
+
("layout_spice", "layout_flat.spice"),
|
|
765
|
+
("schematic_spice", "schematic_flat.spice"),
|
|
766
|
+
):
|
|
767
|
+
flat = _prepare_flat(
|
|
768
|
+
Path(checked[key]).read_text(encoding="utf-8"), cell
|
|
769
|
+
)
|
|
770
|
+
count = 0
|
|
771
|
+
if bulk is not None:
|
|
772
|
+
flat, count = retype_by_bulk(flat, bulk["n"], bulk["p"])
|
|
773
|
+
staged[key] = Path(workdir) / name
|
|
774
|
+
staged[key].write_text(flat, encoding="utf-8")
|
|
775
|
+
flats[key] = flat
|
|
776
|
+
retyped[key] = count
|
|
777
|
+
report_file = Path(workdir) / "lvs_report.out"
|
|
778
|
+
proc = self._run(
|
|
779
|
+
[
|
|
780
|
+
"netgen",
|
|
781
|
+
"-batch",
|
|
782
|
+
"lvs",
|
|
783
|
+
str(staged["layout_spice"]),
|
|
784
|
+
str(staged["schematic_spice"]),
|
|
785
|
+
checked["setup_file"],
|
|
786
|
+
str(report_file),
|
|
787
|
+
],
|
|
788
|
+
workdir,
|
|
789
|
+
timeout=checked["timeout_s"],
|
|
790
|
+
)
|
|
791
|
+
if not report_file.is_file():
|
|
792
|
+
raise AdapterError(
|
|
793
|
+
"runner_unavailable",
|
|
794
|
+
f"netgen produced no report: {proc.stderr[:500]}.",
|
|
795
|
+
)
|
|
796
|
+
return {
|
|
797
|
+
"exit_code": proc.returncode,
|
|
798
|
+
"stdout": proc.stdout,
|
|
799
|
+
"stderr": proc.stderr,
|
|
800
|
+
"report_file": str(report_file),
|
|
801
|
+
"report_text": report_file.read_text(encoding="utf-8"),
|
|
802
|
+
"layout_spice": checked["layout_spice"],
|
|
803
|
+
"schematic_spice": checked["schematic_spice"],
|
|
804
|
+
"layout_nets": sorted(inventory_nets(flats["layout_spice"])),
|
|
805
|
+
"schematic_nets": sorted(inventory_nets(flats["schematic_spice"])),
|
|
806
|
+
"layout_retyped": retyped["layout_spice"],
|
|
807
|
+
"schematic_retyped": retyped["schematic_spice"],
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
def parse(self, native_output):
|
|
811
|
+
"""Parse the match line or the net/device mismatches.
|
|
812
|
+
|
|
813
|
+
A match returns zero violations — unless the flattened net
|
|
814
|
+
inventories disagree (a rename of a singly-connected net
|
|
815
|
+
is topologically invisible to netgen, LVS-ESCAPE), in which
|
|
816
|
+
case the name-only-on-one-side nets fail as
|
|
817
|
+
lvs_net_mismatch. Mismatches parse per net and instance
|
|
818
|
+
plus any inventory extras; anything without a verdict
|
|
819
|
+
raises instead of guessing.
|
|
820
|
+
|
|
821
|
+
Args:
|
|
822
|
+
native_output: Native output with the report text/file
|
|
823
|
+
or stdout fallback, plus optional layout_nets /
|
|
824
|
+
schematic_nets inventories from execute (absent in
|
|
825
|
+
legacy outputs, which parse exactly as before).
|
|
826
|
+
|
|
827
|
+
Returns:
|
|
828
|
+
{match, total, violations[]} with lvs_net_mismatch and
|
|
829
|
+
lvs_device_mismatch rules.
|
|
830
|
+
|
|
831
|
+
Raises:
|
|
832
|
+
AdapterError: runner_unavailable when the output has no
|
|
833
|
+
LVS verdict.
|
|
834
|
+
"""
|
|
835
|
+
report = native_output.get("report_text")
|
|
836
|
+
if report is None:
|
|
837
|
+
report_file = native_output.get("report_file")
|
|
838
|
+
if report_file and Path(report_file).is_file():
|
|
839
|
+
report = Path(report_file).read_text(encoding="utf-8")
|
|
840
|
+
else:
|
|
841
|
+
report = native_output.get("stdout", "")
|
|
842
|
+
layout_nets = native_output.get("layout_nets")
|
|
843
|
+
schematic_nets = native_output.get("schematic_nets")
|
|
844
|
+
extra = []
|
|
845
|
+
if layout_nets is not None and schematic_nets is not None:
|
|
846
|
+
extra = _inventory_violations(layout_nets, schematic_nets)
|
|
847
|
+
if _MATCH_RE.search(report):
|
|
848
|
+
if extra:
|
|
849
|
+
return {"match": False, "total": len(extra), "violations": extra}
|
|
850
|
+
return {"match": True, "total": 0, "violations": []}
|
|
851
|
+
if any(rx.search(report) for rx in _MISMATCH_RES):
|
|
852
|
+
violations = _parse_mismatches(report)
|
|
853
|
+
seen = {violation["message"] for violation in violations}
|
|
854
|
+
for violation in extra:
|
|
855
|
+
if violation["message"] not in seen:
|
|
856
|
+
violations.append(violation)
|
|
857
|
+
seen.add(violation["message"])
|
|
858
|
+
return {
|
|
859
|
+
"match": False,
|
|
860
|
+
"total": len(violations),
|
|
861
|
+
"violations": violations,
|
|
862
|
+
}
|
|
863
|
+
raise AdapterError(
|
|
864
|
+
"runner_unavailable", "netgen output has no LVS verdict."
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
def _run(self, argv, workdir, timeout=_TIMEOUT_S):
|
|
868
|
+
if self._runner is not None:
|
|
869
|
+
try:
|
|
870
|
+
return self._runner.run(argv, workdir, timeout)
|
|
871
|
+
except ExecutionError as exc:
|
|
872
|
+
raise AdapterError(exc.code, str(exc)) from exc
|
|
873
|
+
if not Path(workdir).is_dir():
|
|
874
|
+
raise AdapterError(
|
|
875
|
+
"invalid_reference", f"Workdir not found: {workdir}."
|
|
876
|
+
)
|
|
877
|
+
try:
|
|
878
|
+
return subprocess.run(
|
|
879
|
+
argv,
|
|
880
|
+
capture_output=True,
|
|
881
|
+
text=True,
|
|
882
|
+
timeout=timeout,
|
|
883
|
+
cwd=workdir,
|
|
884
|
+
env=netgen_env(),
|
|
885
|
+
)
|
|
886
|
+
except subprocess.TimeoutExpired as exc:
|
|
887
|
+
raise AdapterError(
|
|
888
|
+
"runner_unavailable", f"netgen timed out after {timeout}s."
|
|
889
|
+
) from exc
|