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,685 @@
|
|
|
1
|
+
"""FRONTEND-13 engine touchpoints: annotate, ERC, graph, netlist, xprobe, export.
|
|
2
|
+
|
|
3
|
+
Six pure query/op helpers over the ENGINE-ECS seam so IDE, MCP,
|
|
4
|
+
and script clients share one semantic:
|
|
5
|
+
|
|
6
|
+
- annotate: reference-designator assignment (idempotent).
|
|
7
|
+
- erc_check: ERC_TESTER with severities and pin-conflict config.
|
|
8
|
+
- connection_graph: buildConnectionGraph nodes/edges.
|
|
9
|
+
- write_netlist / update_netlist: DoWriteNetlist/UpdateNetlist.
|
|
10
|
+
- xprobe: ROCKET-XPROBE plus the resolvePin-as-query chokepoint.
|
|
11
|
+
- plot_export: plot/export drawing emission (pure SVG/JSON).
|
|
12
|
+
- flow_gate: binary-gated verdicts (sta/librelane absent ->
|
|
13
|
+
loud BLOCKED-needs-host, never faked green).
|
|
14
|
+
|
|
15
|
+
Net data (net_pins) comes from materialize_nets-style extraction
|
|
16
|
+
and is passed in by the caller: when it is missing the helpers
|
|
17
|
+
say so loudly instead of inventing connectivity.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import importlib.util
|
|
21
|
+
import shutil
|
|
22
|
+
|
|
23
|
+
from ..interfaces.errors import ContractError
|
|
24
|
+
|
|
25
|
+
#: Netlist languages write_netlist emits.
|
|
26
|
+
NETLIST_LANGUAGES = ("spice", "verilog")
|
|
27
|
+
|
|
28
|
+
#: Plot/export drawing formats.
|
|
29
|
+
EXPORT_FORMATS = ("svg", "json")
|
|
30
|
+
|
|
31
|
+
#: Tools flow_gate probes: binary on PATH, or importable package.
|
|
32
|
+
GATED_TOOLS = ("sta", "librelane")
|
|
33
|
+
|
|
34
|
+
#: Default ERC severities by rule.
|
|
35
|
+
DEFAULT_ERC_SEVERITIES = {
|
|
36
|
+
"schematic.duplicate_ref": "error",
|
|
37
|
+
"schematic.unknown_symbol": "error",
|
|
38
|
+
"schematic.pin_conflict": "error",
|
|
39
|
+
"schematic.unannotated": "warning",
|
|
40
|
+
"schematic.unmatched_pins": "warning",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#: Default pin-conflict policy by "<type>-<type>" pair.
|
|
44
|
+
DEFAULT_PIN_CONFLICT = {"output-output": "error"}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _all_instances(store):
|
|
48
|
+
"""List every instance mapping, id-sorted."""
|
|
49
|
+
found = []
|
|
50
|
+
for entity in store.entities_with("Instance"):
|
|
51
|
+
try:
|
|
52
|
+
raw = store.get_component(entity, "Instance")
|
|
53
|
+
except LookupError:
|
|
54
|
+
continue
|
|
55
|
+
if not isinstance(raw, dict):
|
|
56
|
+
continue
|
|
57
|
+
item = dict(raw)
|
|
58
|
+
item["id"] = entity
|
|
59
|
+
found.append(item)
|
|
60
|
+
found.sort(key=lambda item: item["id"])
|
|
61
|
+
return found
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _symbol_names(store):
|
|
65
|
+
"""Return the set of project-owned symbol names."""
|
|
66
|
+
names = set()
|
|
67
|
+
for entity in store.entities_with("Symbol"):
|
|
68
|
+
try:
|
|
69
|
+
raw = store.get_component(entity, "Symbol")
|
|
70
|
+
except LookupError:
|
|
71
|
+
continue
|
|
72
|
+
if isinstance(raw, dict) and raw.get("name"):
|
|
73
|
+
names.add(raw["name"])
|
|
74
|
+
return names
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _symbol_pin_types(store, symbol):
|
|
78
|
+
"""Return {pin: pin_type} for one symbol, or None when scoped."""
|
|
79
|
+
if ":" in symbol:
|
|
80
|
+
return None
|
|
81
|
+
for entity in store.entities_with("Symbol"):
|
|
82
|
+
try:
|
|
83
|
+
raw = store.get_component(entity, "Symbol")
|
|
84
|
+
except LookupError:
|
|
85
|
+
continue
|
|
86
|
+
if isinstance(raw, dict) and raw.get("name") == symbol:
|
|
87
|
+
pins = raw.get("pins", [])
|
|
88
|
+
return {
|
|
89
|
+
pin["name"]: pin.get("pin_type", "passive")
|
|
90
|
+
for pin in pins if isinstance(pin, dict)
|
|
91
|
+
}
|
|
92
|
+
return {}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _linkage_for(store, symbol):
|
|
96
|
+
"""Return (cell, pin_map) for one symbol, or (None, {}) unlinked."""
|
|
97
|
+
for entity in store.entities_with("Linkage"):
|
|
98
|
+
try:
|
|
99
|
+
raw = store.get_component(entity, "Linkage")
|
|
100
|
+
except LookupError:
|
|
101
|
+
continue
|
|
102
|
+
if isinstance(raw, dict) and raw.get("symbol") == symbol:
|
|
103
|
+
return raw.get("cell"), {
|
|
104
|
+
entry["symPin"]: entry["cellPin"]
|
|
105
|
+
for entry in raw.get("pin_map", [])
|
|
106
|
+
if isinstance(entry, dict)
|
|
107
|
+
}
|
|
108
|
+
return None, {}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def annotate(store, actor, prefixes=None, sheet=None):
|
|
112
|
+
"""Assign reference designators to unannotated instances.
|
|
113
|
+
|
|
114
|
+
Counters continue past the highest numeric suffix already on
|
|
115
|
+
the sheet set, so reruns never collide; already-annotated
|
|
116
|
+
instances keep their refs (idempotent).
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
store: Entity store serving the ENGINE-ECS seam.
|
|
120
|
+
actor: Attribution recorded on the revision and events.
|
|
121
|
+
prefixes: Symbol name to ref-prefix mapping (symbols
|
|
122
|
+
absent from it are reported, never guessed).
|
|
123
|
+
sheet: Sheet entity id to scope to, or None for all.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Receipt with annotated ([{instance, ref}]) and
|
|
127
|
+
unannotated ([{instance, symbol}]) lists.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
ContractError: invalid_command for a malformed prefix map.
|
|
131
|
+
"""
|
|
132
|
+
prefixes = prefixes or {}
|
|
133
|
+
if not isinstance(prefixes, dict) or not all(
|
|
134
|
+
isinstance(key, str) and isinstance(val, str) and val
|
|
135
|
+
for key, val in prefixes.items()
|
|
136
|
+
):
|
|
137
|
+
raise ContractError(
|
|
138
|
+
"invalid_command",
|
|
139
|
+
"Annotate prefixes must map symbol text to ref prefix.",
|
|
140
|
+
)
|
|
141
|
+
counters = {}
|
|
142
|
+
for item in _all_instances(store):
|
|
143
|
+
ref = item.get("ref", "")
|
|
144
|
+
for prefix in set(prefixes.values()):
|
|
145
|
+
if ref.startswith(prefix):
|
|
146
|
+
tail = ref[len(prefix):]
|
|
147
|
+
if tail.isdigit():
|
|
148
|
+
counters[prefix] = max(
|
|
149
|
+
counters.get(prefix, 0), int(tail)
|
|
150
|
+
)
|
|
151
|
+
annotated = []
|
|
152
|
+
unannotated = []
|
|
153
|
+
for item in _all_instances(store):
|
|
154
|
+
if sheet is not None and item.get("sheet") != sheet:
|
|
155
|
+
continue
|
|
156
|
+
if item.get("ref"):
|
|
157
|
+
continue
|
|
158
|
+
prefix = prefixes.get(item.get("symbol", ""))
|
|
159
|
+
if not prefix:
|
|
160
|
+
unannotated.append(
|
|
161
|
+
{"instance": item["id"],
|
|
162
|
+
"symbol": item.get("symbol", "")}
|
|
163
|
+
)
|
|
164
|
+
continue
|
|
165
|
+
counters[prefix] = counters.get(prefix, 0) + 1
|
|
166
|
+
ref = f"{prefix}{counters[prefix]}"
|
|
167
|
+
store.set_component(
|
|
168
|
+
actor, item["id"], "Instance",
|
|
169
|
+
{**{k: v for k, v in item.items() if k != "id"},
|
|
170
|
+
"ref": ref},
|
|
171
|
+
f"annotate {item['id']} {ref}",
|
|
172
|
+
)
|
|
173
|
+
annotated.append({"instance": item["id"], "ref": ref})
|
|
174
|
+
return {
|
|
175
|
+
"op": "annotate", "actor": actor,
|
|
176
|
+
"annotated": annotated, "unannotated": unannotated,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def erc_check(store, severities=None, pin_conflict=None,
|
|
181
|
+
net_pins=None):
|
|
182
|
+
"""Run the ERC_TESTER rule set over the schematic.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
store: Entity store serving the ENGINE-ECS seam.
|
|
186
|
+
severities: Rule-to-severity overrides over
|
|
187
|
+
DEFAULT_ERC_SEVERITIES.
|
|
188
|
+
pin_conflict: "<type>-<type>" pair to severity overrides
|
|
189
|
+
over DEFAULT_PIN_CONFLICT.
|
|
190
|
+
net_pins: Materialized [{instance, pin, net}] rows for
|
|
191
|
+
the pin-conflict rule; without them that rule is
|
|
192
|
+
skipped loudly (reported, never assumed clean).
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Mapping with findings ([{rule, severity, message}]) and
|
|
196
|
+
verdict ("fail" when any error-severity finding
|
|
197
|
+
exists, else "pass").
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
ContractError: invalid_command for malformed configs.
|
|
201
|
+
"""
|
|
202
|
+
policy = dict(DEFAULT_ERC_SEVERITIES)
|
|
203
|
+
for key, value in (severities or {}).items():
|
|
204
|
+
if key not in DEFAULT_ERC_SEVERITIES:
|
|
205
|
+
raise ContractError(
|
|
206
|
+
"invalid_command", f"Unknown ERC rule: {key!r}."
|
|
207
|
+
)
|
|
208
|
+
if value not in ("error", "warning", "info"):
|
|
209
|
+
raise ContractError(
|
|
210
|
+
"invalid_command",
|
|
211
|
+
f"Bad severity for {key!r}: {value!r}.",
|
|
212
|
+
)
|
|
213
|
+
policy[key] = value
|
|
214
|
+
conflict_policy = dict(DEFAULT_PIN_CONFLICT)
|
|
215
|
+
for key, value in (pin_conflict or {}).items():
|
|
216
|
+
if value not in ("error", "warning", "info"):
|
|
217
|
+
raise ContractError(
|
|
218
|
+
"invalid_command",
|
|
219
|
+
f"Bad pin-conflict severity for {key!r}.",
|
|
220
|
+
)
|
|
221
|
+
conflict_policy[key] = value
|
|
222
|
+
findings = []
|
|
223
|
+
instances = _all_instances(store)
|
|
224
|
+
known = _symbol_names(store)
|
|
225
|
+
seen_refs = {}
|
|
226
|
+
for item in instances:
|
|
227
|
+
ref = item.get("ref", "")
|
|
228
|
+
if ref:
|
|
229
|
+
seen_refs.setdefault(ref, []).append(item["id"])
|
|
230
|
+
symbol = item.get("symbol", "")
|
|
231
|
+
if ":" not in symbol and symbol not in known:
|
|
232
|
+
findings.append({
|
|
233
|
+
"rule": "schematic.unknown_symbol",
|
|
234
|
+
"severity": policy["schematic.unknown_symbol"],
|
|
235
|
+
"message": f"Instance {item['id']!r} references "
|
|
236
|
+
f"unknown symbol {symbol!r}.",
|
|
237
|
+
})
|
|
238
|
+
if not ref:
|
|
239
|
+
findings.append({
|
|
240
|
+
"rule": "schematic.unannotated",
|
|
241
|
+
"severity": policy["schematic.unannotated"],
|
|
242
|
+
"message": f"Instance {item['id']!r} carries no ref.",
|
|
243
|
+
})
|
|
244
|
+
for ref, holders in sorted(seen_refs.items()):
|
|
245
|
+
if len(holders) > 1:
|
|
246
|
+
findings.append({
|
|
247
|
+
"rule": "schematic.duplicate_ref",
|
|
248
|
+
"severity": policy["schematic.duplicate_ref"],
|
|
249
|
+
"message": f"Ref {ref!r} repeats on {holders}.",
|
|
250
|
+
})
|
|
251
|
+
if net_pins is not None:
|
|
252
|
+
by_net = {}
|
|
253
|
+
for row in net_pins:
|
|
254
|
+
by_net.setdefault(row["net"], []).append(row)
|
|
255
|
+
for net in sorted(by_net):
|
|
256
|
+
outputs = [
|
|
257
|
+
row for row in by_net[net]
|
|
258
|
+
if row.get("pin_type") == "output"
|
|
259
|
+
]
|
|
260
|
+
if len(outputs) >= 2:
|
|
261
|
+
severity = conflict_policy.get(
|
|
262
|
+
"output-output", "error"
|
|
263
|
+
)
|
|
264
|
+
findings.append({
|
|
265
|
+
"rule": "schematic.pin_conflict",
|
|
266
|
+
"severity": severity,
|
|
267
|
+
"message": f"Net {net!r} shorts outputs "
|
|
268
|
+
f"{[(r['instance'], r['pin']) for r in outputs]}.",
|
|
269
|
+
})
|
|
270
|
+
for symbol in sorted(known):
|
|
271
|
+
cell, pin_map = _linkage_for(store, symbol)
|
|
272
|
+
if cell is None:
|
|
273
|
+
continue
|
|
274
|
+
pins = set(_symbol_pin_types(store, symbol) or ())
|
|
275
|
+
problems = sorted(pins - set(pin_map))
|
|
276
|
+
if problems:
|
|
277
|
+
findings.append({
|
|
278
|
+
"rule": "schematic.unmatched_pins",
|
|
279
|
+
"severity": policy["schematic.unmatched_pins"],
|
|
280
|
+
"message": f"Symbol {symbol!r} pins {problems} "
|
|
281
|
+
"have no cell-port map.",
|
|
282
|
+
})
|
|
283
|
+
verdict = (
|
|
284
|
+
"fail" if any(f["severity"] == "error" for f in findings)
|
|
285
|
+
else "pass"
|
|
286
|
+
)
|
|
287
|
+
return {"findings": findings, "verdict": verdict}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def connection_graph(store, sheet=None):
|
|
291
|
+
"""Build the connection graph: sheets, instances, bindings.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
store: Entity store serving the ENGINE-ECS seam.
|
|
295
|
+
sheet: Sheet entity id to scope to, or None for all.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Mapping with nodes ([{id, kind, name}]) and edges
|
|
299
|
+
([{from, to, rel}]; "contains" for sheet
|
|
300
|
+
membership, "binds" for hierarchy bindings).
|
|
301
|
+
"""
|
|
302
|
+
nodes = []
|
|
303
|
+
edges = []
|
|
304
|
+
want_sheets = set()
|
|
305
|
+
for entity in store.entities_with("Sheet"):
|
|
306
|
+
try:
|
|
307
|
+
raw = store.get_component(entity, "Sheet")
|
|
308
|
+
except LookupError:
|
|
309
|
+
continue
|
|
310
|
+
if not isinstance(raw, dict):
|
|
311
|
+
continue
|
|
312
|
+
if sheet is not None and entity != sheet:
|
|
313
|
+
continue
|
|
314
|
+
want_sheets.add(entity)
|
|
315
|
+
nodes.append(
|
|
316
|
+
{"id": entity, "kind": "sheet",
|
|
317
|
+
"name": raw.get("name", entity)}
|
|
318
|
+
)
|
|
319
|
+
for item in _all_instances(store):
|
|
320
|
+
if sheet is not None and item.get("sheet") != sheet:
|
|
321
|
+
continue
|
|
322
|
+
nodes.append({
|
|
323
|
+
"id": item["id"], "kind": "instance",
|
|
324
|
+
"name": item.get("ref") or item.get("symbol", ""),
|
|
325
|
+
})
|
|
326
|
+
if item.get("sheet") in want_sheets or sheet is None:
|
|
327
|
+
edges.append({
|
|
328
|
+
"from": item.get("sheet"), "to": item["id"],
|
|
329
|
+
"rel": "contains",
|
|
330
|
+
})
|
|
331
|
+
for entity in store.entities_with("Hierarchy"):
|
|
332
|
+
try:
|
|
333
|
+
raw = store.get_component(entity, "Hierarchy")
|
|
334
|
+
except LookupError:
|
|
335
|
+
continue
|
|
336
|
+
if not isinstance(raw, dict):
|
|
337
|
+
continue
|
|
338
|
+
parent, child = (
|
|
339
|
+
raw.get("parent_sheet"), raw.get("child_sheet")
|
|
340
|
+
)
|
|
341
|
+
if sheet is not None and parent != sheet:
|
|
342
|
+
continue
|
|
343
|
+
edges.append({"from": parent, "to": child, "rel": "binds"})
|
|
344
|
+
nodes.sort(key=lambda node: node["id"])
|
|
345
|
+
edges.sort(key=lambda edge: (str(edge["from"]), str(edge["to"])))
|
|
346
|
+
return {"nodes": nodes, "edges": edges}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def net_occurrences(net_pins, net):
|
|
350
|
+
"""List every (instance, pin) occurrence of one net.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
net_pins: Materialized [{instance, pin, net}] rows from
|
|
354
|
+
net extraction (materialize_nets output shape).
|
|
355
|
+
net: Net name to collect.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Occurrence rows [{instance, pin}] in input order.
|
|
359
|
+
"""
|
|
360
|
+
return [
|
|
361
|
+
{"instance": row["instance"], "pin": row["pin"]}
|
|
362
|
+
for row in net_pins
|
|
363
|
+
if isinstance(row, dict) and row.get("net") == net
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _net_index(net_pins):
|
|
368
|
+
"""Index net_pins rows into {(instance, pin): net}."""
|
|
369
|
+
if net_pins is None:
|
|
370
|
+
raise ContractError(
|
|
371
|
+
"invalid_command",
|
|
372
|
+
"Netlist emission needs net_pins from net extraction; "
|
|
373
|
+
"refusing to invent connectivity.",
|
|
374
|
+
)
|
|
375
|
+
index = {}
|
|
376
|
+
for row in net_pins:
|
|
377
|
+
if (
|
|
378
|
+
isinstance(row, dict) and row.get("instance")
|
|
379
|
+
and row.get("pin") and row.get("net")
|
|
380
|
+
):
|
|
381
|
+
index[(row["instance"], row["pin"])] = row["net"]
|
|
382
|
+
return index
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _instance_nets(store, instance_id, symbol, index):
|
|
386
|
+
"""Return (ordered nets, unconnected pins) for one instance."""
|
|
387
|
+
pins = _symbol_pin_types(store, symbol) or {}
|
|
388
|
+
if pins:
|
|
389
|
+
ordered = list(pins)
|
|
390
|
+
else:
|
|
391
|
+
ordered = sorted({
|
|
392
|
+
pin for (inst, pin) in index if inst == instance_id
|
|
393
|
+
})
|
|
394
|
+
nets = []
|
|
395
|
+
unconnected = []
|
|
396
|
+
for pin in ordered:
|
|
397
|
+
net = index.get((instance_id, pin))
|
|
398
|
+
if net is None:
|
|
399
|
+
nets.append(f"NC_{pin}")
|
|
400
|
+
unconnected.append(pin)
|
|
401
|
+
else:
|
|
402
|
+
nets.append(net)
|
|
403
|
+
return nets, unconnected
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def write_netlist(store, language="spice", sheet=None, title="top",
|
|
407
|
+
net_pins=None):
|
|
408
|
+
"""Write a netlist for the schematic (DoWriteNetlist).
|
|
409
|
+
|
|
410
|
+
Pins with no extracted net emit explicit ``NC_<pin>``
|
|
411
|
+
stubs and are reported under ``unconnected`` — dangling
|
|
412
|
+
connections are named, never dropped.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
store: Entity store serving the ENGINE-ECS seam.
|
|
416
|
+
language: Netlist language ("spice" or "verilog").
|
|
417
|
+
sheet: Sheet entity id to scope to, or None for all.
|
|
418
|
+
title: Netlist/circuit title.
|
|
419
|
+
net_pins: Materialized [{instance, pin, net}] rows
|
|
420
|
+
(required — connectivity is never invented).
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Mapping with language, text, instance count, nets,
|
|
424
|
+
and unconnected pins.
|
|
425
|
+
|
|
426
|
+
Raises:
|
|
427
|
+
ContractError: invalid_language for an unknown
|
|
428
|
+
language, invalid_command without net_pins.
|
|
429
|
+
"""
|
|
430
|
+
if language not in NETLIST_LANGUAGES:
|
|
431
|
+
raise ContractError(
|
|
432
|
+
"invalid_language",
|
|
433
|
+
f"Netlist language must be one of {NETLIST_LANGUAGES}.",
|
|
434
|
+
)
|
|
435
|
+
index = _net_index(net_pins)
|
|
436
|
+
instances = [
|
|
437
|
+
item for item in _all_instances(store)
|
|
438
|
+
if sheet is None or item.get("sheet") == sheet
|
|
439
|
+
]
|
|
440
|
+
nets = sorted(set(index.values()))
|
|
441
|
+
unconnected = []
|
|
442
|
+
if language == "spice":
|
|
443
|
+
lines = [f"* {title}"]
|
|
444
|
+
for item in instances:
|
|
445
|
+
inst_nets, missing = _instance_nets(
|
|
446
|
+
store, item["id"], item.get("symbol", ""), index
|
|
447
|
+
)
|
|
448
|
+
unconnected.extend(
|
|
449
|
+
{"instance": item["id"], "pin": pin}
|
|
450
|
+
for pin in missing
|
|
451
|
+
)
|
|
452
|
+
tag = item.get("ref") or item["id"]
|
|
453
|
+
lines.append(
|
|
454
|
+
f"X{tag} {' '.join(inst_nets)} "
|
|
455
|
+
f"{item.get('symbol', '')}"
|
|
456
|
+
)
|
|
457
|
+
lines.append(".end")
|
|
458
|
+
else:
|
|
459
|
+
lines = [f"module {title} ();"]
|
|
460
|
+
for item in instances:
|
|
461
|
+
inst_nets, missing = _instance_nets(
|
|
462
|
+
store, item["id"], item.get("symbol", ""), index
|
|
463
|
+
)
|
|
464
|
+
unconnected.extend(
|
|
465
|
+
{"instance": item["id"], "pin": pin}
|
|
466
|
+
for pin in missing
|
|
467
|
+
)
|
|
468
|
+
pins = _symbol_pin_types(store, item.get("symbol", "")) or {}
|
|
469
|
+
ordered = list(pins) or sorted({
|
|
470
|
+
pin for (inst, pin) in index
|
|
471
|
+
if inst == item["id"]
|
|
472
|
+
})
|
|
473
|
+
ports = ", ".join(
|
|
474
|
+
f".{pin}({net})"
|
|
475
|
+
for pin, net in zip(ordered, inst_nets)
|
|
476
|
+
)
|
|
477
|
+
tag = item.get("ref") or item["id"]
|
|
478
|
+
lines.append(
|
|
479
|
+
f" {item.get('symbol', '')} {tag} ({ports});"
|
|
480
|
+
)
|
|
481
|
+
lines.append("endmodule")
|
|
482
|
+
return {
|
|
483
|
+
"language": language, "text": "\n".join(lines) + "\n",
|
|
484
|
+
"title": title, "instances": len(instances), "nets": nets,
|
|
485
|
+
"unconnected": unconnected,
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def update_netlist(store, language, prior_text, sheet=None,
|
|
490
|
+
title="top", net_pins=None):
|
|
491
|
+
"""Rewrite a netlist and report whether it changed (UpdateNetlist).
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
store: Entity store serving the ENGINE-ECS seam.
|
|
495
|
+
language: Netlist language ("spice" or "verilog").
|
|
496
|
+
prior_text: Previously emitted netlist text.
|
|
497
|
+
sheet: Sheet entity id to scope to, or None for all.
|
|
498
|
+
title: Netlist/circuit title.
|
|
499
|
+
net_pins: Materialized [{instance, pin, net}] rows.
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Mapping with language, text, and changed flag.
|
|
503
|
+
|
|
504
|
+
Raises:
|
|
505
|
+
ContractError: invalid_language for an unknown
|
|
506
|
+
language, invalid_command without net_pins.
|
|
507
|
+
"""
|
|
508
|
+
fresh = write_netlist(
|
|
509
|
+
store, language=language, sheet=sheet, title=title,
|
|
510
|
+
net_pins=net_pins,
|
|
511
|
+
)
|
|
512
|
+
return {
|
|
513
|
+
"language": language, "text": fresh["text"],
|
|
514
|
+
"changed": fresh["text"] != prior_text,
|
|
515
|
+
"unconnected": fresh["unconnected"],
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def xprobe(store, instance, pin, net_pins=None):
|
|
520
|
+
"""Cross-probe one pin: symbol, cell, cell pin, net (ROCKET-XPROBE).
|
|
521
|
+
|
|
522
|
+
The resolvePin-as-query chokepoint: explicit linkage wins,
|
|
523
|
+
unlisted pins resolve to themselves, and the net comes from
|
|
524
|
+
extracted net_pins — without them the probe reports
|
|
525
|
+
``net_known: False`` instead of guessing.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
store: Entity store serving the ENGINE-ECS seam.
|
|
529
|
+
instance: Instance entity id to probe.
|
|
530
|
+
pin: Symbol pin to probe.
|
|
531
|
+
net_pins: Materialized [{instance, pin, net}] rows, or
|
|
532
|
+
None when no extraction ran.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
Mapping with instance, pin, symbol, cell (None when
|
|
536
|
+
unlinked), cell_pin, net (None when unknown), and
|
|
537
|
+
net_known flag.
|
|
538
|
+
|
|
539
|
+
Raises:
|
|
540
|
+
ContractError: unknown_instance for a missing
|
|
541
|
+
instance, unknown_pin for a pin the symbol lacks.
|
|
542
|
+
"""
|
|
543
|
+
try:
|
|
544
|
+
raw = store.get_component(instance, "Instance")
|
|
545
|
+
except LookupError:
|
|
546
|
+
raise ContractError(
|
|
547
|
+
"unknown_instance",
|
|
548
|
+
f"No such instance entity: {instance!r}.",
|
|
549
|
+
) from None
|
|
550
|
+
if not isinstance(raw, dict):
|
|
551
|
+
raise ContractError(
|
|
552
|
+
"unknown_instance",
|
|
553
|
+
f"No such instance entity: {instance!r}.",
|
|
554
|
+
)
|
|
555
|
+
symbol = raw.get("symbol", "")
|
|
556
|
+
known_pins = _symbol_pin_types(store, symbol)
|
|
557
|
+
if known_pins is not None and pin not in known_pins:
|
|
558
|
+
raise ContractError(
|
|
559
|
+
"unknown_pin",
|
|
560
|
+
f"Symbol {symbol!r} carries no pin {pin!r}.",
|
|
561
|
+
)
|
|
562
|
+
cell, pin_map = _linkage_for(store, symbol)
|
|
563
|
+
net = None
|
|
564
|
+
for row in net_pins or []:
|
|
565
|
+
if (
|
|
566
|
+
isinstance(row, dict) and row.get("instance") == instance
|
|
567
|
+
and row.get("pin") == pin
|
|
568
|
+
):
|
|
569
|
+
net = row.get("net")
|
|
570
|
+
break
|
|
571
|
+
return {
|
|
572
|
+
"instance": instance, "pin": pin, "symbol": symbol,
|
|
573
|
+
"cell": cell, "cell_pin": pin_map.get(pin, pin),
|
|
574
|
+
"net": net, "net_known": net is not None,
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def plot_export(store, format="svg", sheet=None):
|
|
579
|
+
"""Export the schematic drawing (plot/export op).
|
|
580
|
+
|
|
581
|
+
Pure emission — no solver, no display server: SVG frames
|
|
582
|
+
each sheet with instance boxes plus ref/symbol labels, and
|
|
583
|
+
JSON carries the same drawing as structured data.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
store: Entity store serving the ENGINE-ECS seam.
|
|
587
|
+
format: Drawing format ("svg" or "json").
|
|
588
|
+
sheet: Sheet entity id to scope to, or None for all.
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
Mapping with format, content (text or drawing
|
|
592
|
+
mapping), and sheet/instance counts.
|
|
593
|
+
|
|
594
|
+
Raises:
|
|
595
|
+
ContractError: invalid_format for an unknown format.
|
|
596
|
+
"""
|
|
597
|
+
if format not in EXPORT_FORMATS:
|
|
598
|
+
raise ContractError(
|
|
599
|
+
"invalid_format",
|
|
600
|
+
f"Export format must be one of {EXPORT_FORMATS}.",
|
|
601
|
+
)
|
|
602
|
+
instances = [
|
|
603
|
+
item for item in _all_instances(store)
|
|
604
|
+
if sheet is None or item.get("sheet") == sheet
|
|
605
|
+
]
|
|
606
|
+
sheets = [
|
|
607
|
+
node for node in connection_graph(store, sheet)["nodes"]
|
|
608
|
+
if node["kind"] == "sheet"
|
|
609
|
+
]
|
|
610
|
+
drawing = {
|
|
611
|
+
"sheets": sheets,
|
|
612
|
+
"instances": [
|
|
613
|
+
{"id": item["id"], "sheet": item.get("sheet"),
|
|
614
|
+
"symbol": item.get("symbol"), "ref": item.get("ref"),
|
|
615
|
+
"x": item.get("x"), "y": item.get("y"),
|
|
616
|
+
"rotation": item.get("rotation"),
|
|
617
|
+
"mirror": item.get("mirror")}
|
|
618
|
+
for item in instances
|
|
619
|
+
],
|
|
620
|
+
}
|
|
621
|
+
if format == "json":
|
|
622
|
+
return {
|
|
623
|
+
"format": format, "content": drawing,
|
|
624
|
+
"sheets": len(sheets), "instances": len(instances),
|
|
625
|
+
}
|
|
626
|
+
parts = [
|
|
627
|
+
'<svg xmlns="http://www.w3.org/2000/svg">',
|
|
628
|
+
f"<!-- sheets={len(sheets)} instances={len(instances)} -->",
|
|
629
|
+
]
|
|
630
|
+
for order, item in enumerate(drawing["instances"]):
|
|
631
|
+
x = 20 + (order % 8) * 90
|
|
632
|
+
y = 20 + (order // 8) * 70
|
|
633
|
+
label = item["ref"] or item["symbol"]
|
|
634
|
+
parts.append(
|
|
635
|
+
f'<g id="{item["id"]}">'
|
|
636
|
+
f'<rect x="{x}" y="{y}" width="70" height="50" '
|
|
637
|
+
'fill="none" stroke="black"/>'
|
|
638
|
+
f'<text x="{x + 4}" y="{y + 20}">{label}</text>'
|
|
639
|
+
f"<title>{item['symbol']} {item['ref']}</title></g>"
|
|
640
|
+
)
|
|
641
|
+
parts.append("</svg>")
|
|
642
|
+
return {
|
|
643
|
+
"format": format, "content": "\n".join(parts),
|
|
644
|
+
"sheets": len(sheets), "instances": len(instances),
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def flow_gate(tool):
|
|
649
|
+
"""Report the availability verdict for a binary-gated flow tool.
|
|
650
|
+
|
|
651
|
+
Absent tools report loud BLOCKED-needs-host with a remedy —
|
|
652
|
+
never a faked green verdict.
|
|
653
|
+
|
|
654
|
+
Args:
|
|
655
|
+
tool: Tool name ("sta" or "librelane").
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
Mapping with tool, available flag, verdict ("ready" or
|
|
659
|
+
"BLOCKED-needs-host"), and remedy text when blocked.
|
|
660
|
+
|
|
661
|
+
Raises:
|
|
662
|
+
ContractError: unknown_tool for an unlisted tool.
|
|
663
|
+
"""
|
|
664
|
+
if tool not in GATED_TOOLS:
|
|
665
|
+
raise ContractError(
|
|
666
|
+
"unknown_tool", f"No flow gate for tool {tool!r}."
|
|
667
|
+
)
|
|
668
|
+
if tool == "sta":
|
|
669
|
+
available = shutil.which("sta") is not None
|
|
670
|
+
remedy = (
|
|
671
|
+
"Install OpenSTA on PATH (or provision the timing "
|
|
672
|
+
"profile) and rerun; run_timing stays unwired until then."
|
|
673
|
+
)
|
|
674
|
+
else:
|
|
675
|
+
available = importlib.util.find_spec("librelane") is not None
|
|
676
|
+
remedy = (
|
|
677
|
+
"Provision the digital-flow profile (librelane) and "
|
|
678
|
+
"rerun; run_digital_flow stays unwired until then."
|
|
679
|
+
)
|
|
680
|
+
if available:
|
|
681
|
+
return {"tool": tool, "available": True, "verdict": "ready"}
|
|
682
|
+
return {
|
|
683
|
+
"tool": tool, "available": False,
|
|
684
|
+
"verdict": "BLOCKED-needs-host", "remedy": remedy,
|
|
685
|
+
}
|