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