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,1324 @@
|
|
|
1
|
+
"""Wire/junction entities + connect ops (WIRE-PLACE).
|
|
2
|
+
|
|
3
|
+
Wires are first-class entities — placeable, movable, splittable,
|
|
4
|
+
deletable, rewindable — with endpoints terminating on stable
|
|
5
|
+
pin/port/junction/net entity IDs. Junctions are explicit dots: two
|
|
6
|
+
wires join only at a shared endpoint, a T-tap, or a junction they
|
|
7
|
+
both reference; crossing interiors never connect. Nets derive from
|
|
8
|
+
CONNECT-GRAPH extraction, then materialize as Net entities keyed by
|
|
9
|
+
the shared `net_name()` authority, so emitters, ERC, and agents all
|
|
10
|
+
read the same net identity.
|
|
11
|
+
|
|
12
|
+
Every op runs through the ENGINE-ECS entity store seam as exactly
|
|
13
|
+
one attributed transaction (one revision, one description, actor on
|
|
14
|
+
every event); a no-op returns a receipt with a None revision and
|
|
15
|
+
writes nothing. Dangling endpoints (null refs) are legal transiently
|
|
16
|
+
and ERC-flagged, never silently dropped; refs to deleted entities
|
|
17
|
+
are ERC errors, and deleting a referenced junction is refused
|
|
18
|
+
loudly. This module owns no storage: `store` is any object serving
|
|
19
|
+
the seam documented in `docs/work/WIRE-PLACE/report.md` (tests carry
|
|
20
|
+
an in-memory fake; the ENGINE-ECS lane owns the durable one).
|
|
21
|
+
|
|
22
|
+
Component schemas (JSON-safe, edit-time validated):
|
|
23
|
+
|
|
24
|
+
- Wire: {segments, layer, endpoints}; segments is a non-empty list
|
|
25
|
+
of integer polylines ([x, y] points, no zero-length runs);
|
|
26
|
+
endpoints holds exactly [a, b] with xy pinned to the first and
|
|
27
|
+
last polyline points and ref a pin/port/junction/net entity ID
|
|
28
|
+
or null when dangling.
|
|
29
|
+
- Junction: {xy, layer}; the explicit dot.
|
|
30
|
+
- Net: {name, extraction {cell, net_ids}, members {pins, labels,
|
|
31
|
+
wires, junctions}, has_wires}; materialized, keyed by name.
|
|
32
|
+
|
|
33
|
+
Extraction groups that resolve to the same canonical name merge
|
|
34
|
+
into one Net entity: a name is global, so a net-ref tap and a
|
|
35
|
+
repeated label both join every group carrying the name.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from ..interfaces.errors import ContractError
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"DEFAULT_WIRE_POLICY",
|
|
42
|
+
"ENDPOINTS",
|
|
43
|
+
"JUNCTION_COMPONENT",
|
|
44
|
+
"NET_COMPONENT",
|
|
45
|
+
"RULE_DANGLING_ENDPOINT",
|
|
46
|
+
"RULE_ORPHAN_JUNCTION",
|
|
47
|
+
"RULE_STALE_GEOMETRY",
|
|
48
|
+
"RULE_UNKNOWN_REF",
|
|
49
|
+
"WIRE_COMPONENT",
|
|
50
|
+
"collect_wire_erc",
|
|
51
|
+
"connect_endpoint",
|
|
52
|
+
"delete_junction",
|
|
53
|
+
"delete_wire",
|
|
54
|
+
"materialize_nets",
|
|
55
|
+
"move_wire",
|
|
56
|
+
"place_junction",
|
|
57
|
+
"place_wire",
|
|
58
|
+
"split_wire",
|
|
59
|
+
"validate_junction",
|
|
60
|
+
"validate_label",
|
|
61
|
+
"validate_net",
|
|
62
|
+
"validate_pin",
|
|
63
|
+
"validate_wire",
|
|
64
|
+
"world_schematic",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
#: Component names as stored on entities.
|
|
68
|
+
WIRE_COMPONENT = "Wire"
|
|
69
|
+
JUNCTION_COMPONENT = "Junction"
|
|
70
|
+
NET_COMPONENT = "Net"
|
|
71
|
+
|
|
72
|
+
#: ERC rule codes for wire/junction findings.
|
|
73
|
+
RULE_DANGLING_ENDPOINT = "wire.dangling_endpoint"
|
|
74
|
+
RULE_UNKNOWN_REF = "wire.unknown_ref"
|
|
75
|
+
RULE_STALE_GEOMETRY = "wire.stale_geometry"
|
|
76
|
+
RULE_ORPHAN_JUNCTION = "wire.orphan_junction"
|
|
77
|
+
|
|
78
|
+
#: Default finding severities; unknown-ref is the only error.
|
|
79
|
+
DEFAULT_WIRE_POLICY = {
|
|
80
|
+
RULE_DANGLING_ENDPOINT: "warning",
|
|
81
|
+
RULE_UNKNOWN_REF: "error",
|
|
82
|
+
RULE_STALE_GEOMETRY: "warning",
|
|
83
|
+
RULE_ORPHAN_JUNCTION: "warning",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#: Endpoint selector order: a is the first polyline point.
|
|
87
|
+
ENDPOINTS = ("a", "b")
|
|
88
|
+
|
|
89
|
+
_PORT_DIRECTIONS = ("input", "output", "inout")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _is_int(value):
|
|
93
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _need_point(value, what, code):
|
|
97
|
+
if (
|
|
98
|
+
not isinstance(value, (list, tuple))
|
|
99
|
+
or len(value) != 2
|
|
100
|
+
or not all(_is_int(c) for c in value)
|
|
101
|
+
):
|
|
102
|
+
raise ContractError(code, f"{what} must be [x, y] ints, got {value!r}.")
|
|
103
|
+
return [value[0], value[1]]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _need_nonempty_str(value, what, code):
|
|
107
|
+
if not isinstance(value, str) or not value:
|
|
108
|
+
raise ContractError(code, f"{what} must be a non-empty string.")
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _need_actor(actor):
|
|
113
|
+
return _need_nonempty_str(actor, "actor", "invalid_actor")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _point_on_segment(point, seg_a, seg_b):
|
|
117
|
+
(px, py), (ax, ay), (bx, by) = point, seg_a, seg_b
|
|
118
|
+
if (bx - ax) * (py - ay) != (by - ay) * (px - ax):
|
|
119
|
+
return False
|
|
120
|
+
return min(ax, bx) <= px <= max(ax, bx) and min(ay, by) <= py <= max(ay, by)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _point_on_wire(point, wire):
|
|
124
|
+
for polyline in wire["segments"]:
|
|
125
|
+
for seg_a, seg_b in zip(polyline, polyline[1:]):
|
|
126
|
+
if _point_on_segment(point, seg_a, seg_b):
|
|
127
|
+
return True
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _sorted_str_list(value, what, code):
|
|
132
|
+
if not isinstance(value, list) or not all(
|
|
133
|
+
isinstance(v, str) and v for v in value
|
|
134
|
+
):
|
|
135
|
+
raise ContractError(code, f"{what} must be a non-empty-str list.")
|
|
136
|
+
if sorted(value) != value or len(set(value)) != len(value):
|
|
137
|
+
raise ContractError(code, f"{what} must be sorted and distinct.")
|
|
138
|
+
return list(value)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def validate_wire(wire):
|
|
142
|
+
"""Validate a Wire component mapping at edit time.
|
|
143
|
+
|
|
144
|
+
Checks shape only — segment geometry, layer text, endpoint
|
|
145
|
+
count, endpoint xy pinned to the polyline ends — and returns a
|
|
146
|
+
normalized copy. Ref existence is never checked here; the ops
|
|
147
|
+
resolve refs through the store and fail unknown_ref loudly.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
wire: Wire mapping with segments, layer, endpoints keys.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Normalized Wire copy (lists, storage untouched).
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ContractError: invalid_wire for any shape violation.
|
|
157
|
+
"""
|
|
158
|
+
if not isinstance(wire, dict):
|
|
159
|
+
raise ContractError(
|
|
160
|
+
"invalid_wire", f"Wire must be a mapping, got {wire!r}."
|
|
161
|
+
)
|
|
162
|
+
for key in wire:
|
|
163
|
+
if key not in ("segments", "layer", "endpoints"):
|
|
164
|
+
raise ContractError("invalid_wire", f"Unknown Wire key: {key}.")
|
|
165
|
+
segments = wire.get("segments")
|
|
166
|
+
if not isinstance(segments, list) or not segments:
|
|
167
|
+
raise ContractError("invalid_wire", "segments must be a non-empty list.")
|
|
168
|
+
clean_segments = []
|
|
169
|
+
for index, polyline in enumerate(segments):
|
|
170
|
+
if not isinstance(polyline, (list, tuple)) or len(polyline) < 2:
|
|
171
|
+
raise ContractError(
|
|
172
|
+
"invalid_wire",
|
|
173
|
+
f"segments[{index}] must hold at least 2 points.",
|
|
174
|
+
)
|
|
175
|
+
points = [
|
|
176
|
+
_need_point(p, f"segments[{index}]", "invalid_wire")
|
|
177
|
+
for p in polyline
|
|
178
|
+
]
|
|
179
|
+
for first, second in zip(points, points[1:]):
|
|
180
|
+
if first == second:
|
|
181
|
+
raise ContractError(
|
|
182
|
+
"invalid_wire",
|
|
183
|
+
f"segments[{index}] holds a zero-length run.",
|
|
184
|
+
)
|
|
185
|
+
clean_segments.append(points)
|
|
186
|
+
layer = _need_nonempty_str(wire.get("layer"), "layer", "invalid_wire")
|
|
187
|
+
endpoints = wire.get("endpoints")
|
|
188
|
+
if not isinstance(endpoints, list) or len(endpoints) != 2:
|
|
189
|
+
raise ContractError("invalid_wire", "endpoints must hold exactly [a, b].")
|
|
190
|
+
clean_endpoints = []
|
|
191
|
+
for index, endpoint in enumerate(endpoints):
|
|
192
|
+
if not isinstance(endpoint, dict):
|
|
193
|
+
raise ContractError("invalid_wire", "endpoints must be mappings.")
|
|
194
|
+
for key in endpoint:
|
|
195
|
+
if key not in ("ref", "xy"):
|
|
196
|
+
raise ContractError(
|
|
197
|
+
"invalid_wire", f"Unknown endpoint key: {key}."
|
|
198
|
+
)
|
|
199
|
+
ref = endpoint.get("ref")
|
|
200
|
+
if ref is not None and (not isinstance(ref, str) or not ref):
|
|
201
|
+
raise ContractError(
|
|
202
|
+
"invalid_wire", "endpoint ref must be an ID or null."
|
|
203
|
+
)
|
|
204
|
+
xy = _need_point(
|
|
205
|
+
endpoint.get("xy"), f"endpoints[{ENDPOINTS[index]}].xy",
|
|
206
|
+
"invalid_wire",
|
|
207
|
+
)
|
|
208
|
+
clean_endpoints.append({"ref": ref, "xy": xy})
|
|
209
|
+
if clean_endpoints[0]["xy"] != clean_segments[0][0]:
|
|
210
|
+
raise ContractError(
|
|
211
|
+
"invalid_wire", "endpoint a.xy must equal the first polyline point."
|
|
212
|
+
)
|
|
213
|
+
if clean_endpoints[1]["xy"] != clean_segments[-1][-1]:
|
|
214
|
+
raise ContractError(
|
|
215
|
+
"invalid_wire", "endpoint b.xy must equal the last polyline point."
|
|
216
|
+
)
|
|
217
|
+
return {"segments": clean_segments, "layer": layer,
|
|
218
|
+
"endpoints": clean_endpoints}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def validate_junction(junction):
|
|
222
|
+
"""Validate a Junction component mapping at edit time.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
junction: Junction mapping with xy and layer keys.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Normalized Junction copy.
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
ContractError: invalid_junction for any shape violation.
|
|
232
|
+
"""
|
|
233
|
+
if not isinstance(junction, dict):
|
|
234
|
+
raise ContractError(
|
|
235
|
+
"invalid_junction", f"Junction must be a mapping, got {junction!r}."
|
|
236
|
+
)
|
|
237
|
+
for key in junction:
|
|
238
|
+
if key not in ("xy", "layer"):
|
|
239
|
+
raise ContractError(
|
|
240
|
+
"invalid_junction", f"Unknown Junction key: {key}."
|
|
241
|
+
)
|
|
242
|
+
return {
|
|
243
|
+
"xy": _need_point(junction.get("xy"), "xy", "invalid_junction"),
|
|
244
|
+
"layer": _need_nonempty_str(
|
|
245
|
+
junction.get("layer"), "layer", "invalid_junction"
|
|
246
|
+
),
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def validate_net(net):
|
|
251
|
+
"""Validate a Net component mapping at edit time.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
net: Net mapping with name, extraction, members, has_wires.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Normalized Net copy.
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
ContractError: invalid_net for any shape violation.
|
|
261
|
+
"""
|
|
262
|
+
if not isinstance(net, dict):
|
|
263
|
+
raise ContractError(
|
|
264
|
+
"invalid_net", f"Net must be a mapping, got {net!r}."
|
|
265
|
+
)
|
|
266
|
+
for key in net:
|
|
267
|
+
if key not in ("name", "extraction", "members", "has_wires"):
|
|
268
|
+
raise ContractError("invalid_net", f"Unknown Net key: {key}.")
|
|
269
|
+
name = _need_nonempty_str(net.get("name"), "name", "invalid_net")
|
|
270
|
+
extraction = net.get("extraction")
|
|
271
|
+
if not isinstance(extraction, dict):
|
|
272
|
+
raise ContractError("invalid_net", "extraction must be a mapping.")
|
|
273
|
+
cell = _need_nonempty_str(
|
|
274
|
+
extraction.get("cell"), "extraction.cell", "invalid_net"
|
|
275
|
+
)
|
|
276
|
+
net_ids = extraction.get("net_ids")
|
|
277
|
+
if (
|
|
278
|
+
not isinstance(net_ids, list)
|
|
279
|
+
or not net_ids
|
|
280
|
+
or not all(_is_int(n) for n in net_ids)
|
|
281
|
+
or sorted(net_ids) != list(net_ids)
|
|
282
|
+
):
|
|
283
|
+
raise ContractError(
|
|
284
|
+
"invalid_net", "extraction.net_ids must be a sorted int list."
|
|
285
|
+
)
|
|
286
|
+
members = net.get("members")
|
|
287
|
+
if not isinstance(members, dict):
|
|
288
|
+
raise ContractError("invalid_net", "members must be a mapping.")
|
|
289
|
+
for key in members:
|
|
290
|
+
if key not in ("pins", "labels", "wires", "junctions"):
|
|
291
|
+
raise ContractError(
|
|
292
|
+
"invalid_net", f"Unknown members key: {key}."
|
|
293
|
+
)
|
|
294
|
+
pins = members.get("pins")
|
|
295
|
+
if not isinstance(pins, list):
|
|
296
|
+
raise ContractError("invalid_net", "members.pins must be a list.")
|
|
297
|
+
clean_pins = []
|
|
298
|
+
for entry in pins:
|
|
299
|
+
if (
|
|
300
|
+
not isinstance(entry, dict)
|
|
301
|
+
or set(entry) != {"instance", "pin"}
|
|
302
|
+
or not entry["instance"]
|
|
303
|
+
or not isinstance(entry["instance"], str)
|
|
304
|
+
or not entry["pin"]
|
|
305
|
+
or not isinstance(entry["pin"], str)
|
|
306
|
+
):
|
|
307
|
+
raise ContractError(
|
|
308
|
+
"invalid_net", "members.pins entries need instance + pin."
|
|
309
|
+
)
|
|
310
|
+
clean_pins.append(
|
|
311
|
+
{"instance": entry["instance"], "pin": entry["pin"]}
|
|
312
|
+
)
|
|
313
|
+
keys = [(p["instance"], p["pin"]) for p in clean_pins]
|
|
314
|
+
if sorted(keys) != keys or len(set(keys)) != len(keys):
|
|
315
|
+
raise ContractError(
|
|
316
|
+
"invalid_net", "members.pins must be sorted and distinct."
|
|
317
|
+
)
|
|
318
|
+
clean_members = {
|
|
319
|
+
"pins": clean_pins,
|
|
320
|
+
"labels": _sorted_str_list(
|
|
321
|
+
members.get("labels"), "members.labels", "invalid_net"
|
|
322
|
+
),
|
|
323
|
+
"wires": _sorted_str_list(
|
|
324
|
+
members.get("wires"), "members.wires", "invalid_net"
|
|
325
|
+
),
|
|
326
|
+
"junctions": _sorted_str_list(
|
|
327
|
+
members.get("junctions"), "members.junctions", "invalid_net"
|
|
328
|
+
),
|
|
329
|
+
}
|
|
330
|
+
has_wires = net.get("has_wires")
|
|
331
|
+
if not isinstance(has_wires, bool):
|
|
332
|
+
raise ContractError("invalid_net", "has_wires must be a bool.")
|
|
333
|
+
return {
|
|
334
|
+
"name": name,
|
|
335
|
+
"extraction": {"cell": cell, "net_ids": list(net_ids)},
|
|
336
|
+
"members": clean_members,
|
|
337
|
+
"has_wires": has_wires,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def validate_pin(pin):
|
|
342
|
+
"""Validate one caller-supplied pin record at edit time.
|
|
343
|
+
|
|
344
|
+
Pin entities belong to the Block lane; callers map each entity
|
|
345
|
+
to its schematic (instance, pin, xy) triple through these
|
|
346
|
+
records.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
pin: Pin mapping with entity, instance, pin, xy keys.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Normalized pin copy.
|
|
353
|
+
|
|
354
|
+
Raises:
|
|
355
|
+
ContractError: invalid_pin for any shape violation.
|
|
356
|
+
"""
|
|
357
|
+
if not isinstance(pin, dict):
|
|
358
|
+
raise ContractError(
|
|
359
|
+
"invalid_pin", f"Pin must be a mapping, got {pin!r}."
|
|
360
|
+
)
|
|
361
|
+
for key in pin:
|
|
362
|
+
if key not in ("entity", "instance", "pin", "xy"):
|
|
363
|
+
raise ContractError("invalid_pin", f"Unknown pin key: {key}.")
|
|
364
|
+
return {
|
|
365
|
+
"entity": _need_nonempty_str(
|
|
366
|
+
pin.get("entity"), "entity", "invalid_pin"
|
|
367
|
+
),
|
|
368
|
+
"instance": _need_nonempty_str(
|
|
369
|
+
pin.get("instance"), "instance", "invalid_pin"
|
|
370
|
+
),
|
|
371
|
+
"pin": _need_nonempty_str(pin.get("pin"), "pin", "invalid_pin"),
|
|
372
|
+
"xy": _need_point(pin.get("xy"), "xy", "invalid_pin"),
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def validate_label(label):
|
|
377
|
+
"""Validate one caller-supplied label record at edit time.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
label: Label mapping with name and xy keys.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Normalized label copy.
|
|
384
|
+
|
|
385
|
+
Raises:
|
|
386
|
+
ContractError: invalid_label for any shape violation.
|
|
387
|
+
"""
|
|
388
|
+
if not isinstance(label, dict):
|
|
389
|
+
raise ContractError(
|
|
390
|
+
"invalid_label", f"Label must be a mapping, got {label!r}."
|
|
391
|
+
)
|
|
392
|
+
for key in label:
|
|
393
|
+
if key not in ("name", "xy"):
|
|
394
|
+
raise ContractError("invalid_label", f"Unknown label key: {key}.")
|
|
395
|
+
return {
|
|
396
|
+
"name": _need_nonempty_str(
|
|
397
|
+
label.get("name"), "name", "invalid_label"
|
|
398
|
+
),
|
|
399
|
+
"xy": _need_point(label.get("xy"), "xy", "invalid_label"),
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _read_wire(store, wire_id):
|
|
404
|
+
try:
|
|
405
|
+
components = store.components_of(wire_id)
|
|
406
|
+
except LookupError:
|
|
407
|
+
raise ContractError(
|
|
408
|
+
"unknown_wire", f"No such wire entity: {wire_id!r}."
|
|
409
|
+
) from None
|
|
410
|
+
if WIRE_COMPONENT not in components:
|
|
411
|
+
raise ContractError(
|
|
412
|
+
"unknown_wire", f"Entity {wire_id!r} carries no Wire component."
|
|
413
|
+
)
|
|
414
|
+
return validate_wire(components[WIRE_COMPONENT])
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _check_ref(store, ref, wire_id=None):
|
|
418
|
+
"""Resolve one endpoint ref; fail loudly when unusable.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
store: Entity store serving the seam.
|
|
422
|
+
ref: Referenced entity ID.
|
|
423
|
+
wire_id: Connecting wire, rejected as a self-ref.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
The referenced entity's components.
|
|
427
|
+
|
|
428
|
+
Raises:
|
|
429
|
+
ContractError: invalid_ref for a wire/self ref, unknown_ref
|
|
430
|
+
for a missing entity.
|
|
431
|
+
"""
|
|
432
|
+
if not isinstance(ref, str) or not ref:
|
|
433
|
+
raise ContractError("invalid_ref", "endpoint ref must be an ID or null.")
|
|
434
|
+
if ref == wire_id:
|
|
435
|
+
raise ContractError(
|
|
436
|
+
"invalid_ref", f"Wire {wire_id!r} cannot terminate on itself."
|
|
437
|
+
)
|
|
438
|
+
try:
|
|
439
|
+
components = store.components_of(ref)
|
|
440
|
+
except LookupError:
|
|
441
|
+
raise ContractError(
|
|
442
|
+
"unknown_ref", f"Endpoint ref names no entity: {ref!r}."
|
|
443
|
+
) from None
|
|
444
|
+
if WIRE_COMPONENT in components:
|
|
445
|
+
raise ContractError(
|
|
446
|
+
"invalid_ref",
|
|
447
|
+
f"Endpoint ref {ref!r} names a Wire entity; endpoints "
|
|
448
|
+
"terminate on pin/port/junction/net IDs only.",
|
|
449
|
+
)
|
|
450
|
+
return components
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _receipt(op, actor, transact_receipt, **ids):
|
|
454
|
+
receipt = {
|
|
455
|
+
"op": op, "actor": actor,
|
|
456
|
+
"revision": transact_receipt["revision"],
|
|
457
|
+
"events": transact_receipt["events"],
|
|
458
|
+
}
|
|
459
|
+
receipt.update(ids)
|
|
460
|
+
return receipt
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _describe(op, detail):
|
|
464
|
+
return f"{op} {detail}"
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def place_wire(store, actor, segments, layer, endpoints):
|
|
468
|
+
"""Place one wire as a durable attributed revision.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
store: Entity store serving the seam.
|
|
472
|
+
actor: Attribution recorded on the revision and events.
|
|
473
|
+
segments: Non-empty list of integer polylines.
|
|
474
|
+
layer: Layer name for every segment.
|
|
475
|
+
endpoints: [a, b] endpoint mappings; each ref is a
|
|
476
|
+
pin/port/junction/net entity ID or null when dangling.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
Receipt with op, actor, revision, wire ID, and events.
|
|
480
|
+
|
|
481
|
+
Raises:
|
|
482
|
+
ContractError: invalid_actor for empty attribution,
|
|
483
|
+
invalid_wire for a malformed wire, unknown_ref for a
|
|
484
|
+
ref naming no entity, invalid_ref for a wire ref.
|
|
485
|
+
"""
|
|
486
|
+
_need_actor(actor)
|
|
487
|
+
value = validate_wire(
|
|
488
|
+
{"segments": segments, "layer": layer, "endpoints": endpoints}
|
|
489
|
+
)
|
|
490
|
+
for endpoint in value["endpoints"]:
|
|
491
|
+
if endpoint["ref"] is not None:
|
|
492
|
+
_check_ref(store, endpoint["ref"])
|
|
493
|
+
result = store.transact(
|
|
494
|
+
actor, _describe("place_wire", f"layer={value['layer']}"),
|
|
495
|
+
[
|
|
496
|
+
{"verb": "create_entity", "alias": "wire"},
|
|
497
|
+
{
|
|
498
|
+
"verb": "attach_component",
|
|
499
|
+
"args": {
|
|
500
|
+
"entity": {"alias": "wire"},
|
|
501
|
+
"component": WIRE_COMPONENT, "value": value,
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
)
|
|
506
|
+
return _receipt("place_wire", actor, result, wire=result["aliases"]["wire"])
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def place_junction(store, actor, xy, layer):
|
|
510
|
+
"""Place one explicit junction dot as a durable revision.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
store: Entity store serving the seam.
|
|
514
|
+
actor: Attribution recorded on the revision and events.
|
|
515
|
+
xy: Integer [x, y] dot position.
|
|
516
|
+
layer: Layer name for the dot.
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
Receipt with op, actor, revision, junction ID, and events.
|
|
520
|
+
|
|
521
|
+
Raises:
|
|
522
|
+
ContractError: invalid_actor for empty attribution,
|
|
523
|
+
invalid_junction for a malformed dot.
|
|
524
|
+
"""
|
|
525
|
+
_need_actor(actor)
|
|
526
|
+
value = validate_junction({"xy": xy, "layer": layer})
|
|
527
|
+
result = store.transact(
|
|
528
|
+
actor, _describe("place_junction", f"xy={value['xy']}"),
|
|
529
|
+
[
|
|
530
|
+
{"verb": "create_entity", "alias": "junction"},
|
|
531
|
+
{
|
|
532
|
+
"verb": "attach_component",
|
|
533
|
+
"args": {
|
|
534
|
+
"entity": {"alias": "junction"},
|
|
535
|
+
"component": JUNCTION_COMPONENT, "value": value,
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
)
|
|
540
|
+
return _receipt(
|
|
541
|
+
"place_junction", actor, result,
|
|
542
|
+
junction=result["aliases"]["junction"],
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def connect_endpoint(store, actor, wire_id, end, ref):
|
|
547
|
+
"""Point one wire endpoint at a new terminus entity.
|
|
548
|
+
|
|
549
|
+
Geometry never moves: the endpoint keeps its xy and only the
|
|
550
|
+
ref changes, so a caller that moved the target re-resolves
|
|
551
|
+
through move_wire while ERC flags the stale span. A no-change
|
|
552
|
+
call writes nothing and reports a None revision.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
store: Entity store serving the seam.
|
|
556
|
+
actor: Attribution recorded on the revision and events.
|
|
557
|
+
wire_id: Wire entity to rewire.
|
|
558
|
+
end: "a" or "b" selecting the endpoint.
|
|
559
|
+
ref: Pin/port/junction/net entity ID, or null to dangle.
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
Receipt with op, actor, revision, wire ID, and events —
|
|
563
|
+
or a noop receipt (revision None) when the ref is
|
|
564
|
+
already set.
|
|
565
|
+
|
|
566
|
+
Raises:
|
|
567
|
+
ContractError: invalid_actor for empty attribution,
|
|
568
|
+
unknown_wire for a missing wire, invalid_endpoint for
|
|
569
|
+
a bad selector, unknown_ref for a missing entity,
|
|
570
|
+
invalid_ref for a wire/self ref.
|
|
571
|
+
"""
|
|
572
|
+
_need_actor(actor)
|
|
573
|
+
if end not in ENDPOINTS:
|
|
574
|
+
raise ContractError(
|
|
575
|
+
"invalid_endpoint", f"end must be one of {ENDPOINTS}, got {end!r}."
|
|
576
|
+
)
|
|
577
|
+
if ref is not None:
|
|
578
|
+
_check_ref(store, ref, wire_id=wire_id)
|
|
579
|
+
wire = _read_wire(store, wire_id)
|
|
580
|
+
index = ENDPOINTS.index(end)
|
|
581
|
+
if wire["endpoints"][index]["ref"] == ref:
|
|
582
|
+
return {"op": "connect_endpoint", "actor": actor, "revision": None,
|
|
583
|
+
"noop": True, "wire": wire_id}
|
|
584
|
+
wire["endpoints"][index]["ref"] = ref
|
|
585
|
+
result = store.transact(
|
|
586
|
+
actor, _describe("connect_endpoint", f"wire={wire_id} end={end}"),
|
|
587
|
+
[
|
|
588
|
+
{
|
|
589
|
+
"verb": "set_component",
|
|
590
|
+
"args": {
|
|
591
|
+
"entity": wire_id, "component": WIRE_COMPONENT,
|
|
592
|
+
"value": wire,
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
],
|
|
596
|
+
)
|
|
597
|
+
return _receipt("connect_endpoint", actor, result, wire=wire_id)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def move_wire(store, actor, wire_id, dx, dy):
|
|
601
|
+
"""Translate one wire, keeping every endpoint ref attached.
|
|
602
|
+
|
|
603
|
+
Attachment is by entity ID, not geometry: refs survive the
|
|
604
|
+
move while ERC flags spans whose xy no longer meets the
|
|
605
|
+
target. A zero move writes nothing.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
store: Entity store serving the seam.
|
|
609
|
+
actor: Attribution recorded on the revision and events.
|
|
610
|
+
wire_id: Wire entity to move.
|
|
611
|
+
dx: Integer x translation.
|
|
612
|
+
dy: Integer y translation.
|
|
613
|
+
|
|
614
|
+
Returns:
|
|
615
|
+
Receipt with op, actor, revision, wire ID, and events —
|
|
616
|
+
or a noop receipt (revision None) for a zero move.
|
|
617
|
+
|
|
618
|
+
Raises:
|
|
619
|
+
ContractError: invalid_actor for empty attribution,
|
|
620
|
+
unknown_wire for a missing wire, invalid_geometry for
|
|
621
|
+
non-integer deltas.
|
|
622
|
+
"""
|
|
623
|
+
_need_actor(actor)
|
|
624
|
+
if not _is_int(dx) or not _is_int(dy):
|
|
625
|
+
raise ContractError(
|
|
626
|
+
"invalid_geometry",
|
|
627
|
+
f"dx/dy must be ints, got {(dx, dy)!r}.",
|
|
628
|
+
)
|
|
629
|
+
if dx == 0 and dy == 0:
|
|
630
|
+
return {"op": "move_wire", "actor": actor, "revision": None,
|
|
631
|
+
"noop": True, "wire": wire_id}
|
|
632
|
+
wire = _read_wire(store, wire_id)
|
|
633
|
+
wire["segments"] = [
|
|
634
|
+
[[x + dx, y + dy] for x, y in polyline]
|
|
635
|
+
for polyline in wire["segments"]
|
|
636
|
+
]
|
|
637
|
+
for endpoint in wire["endpoints"]:
|
|
638
|
+
endpoint["xy"] = [endpoint["xy"][0] + dx, endpoint["xy"][1] + dy]
|
|
639
|
+
result = store.transact(
|
|
640
|
+
actor, _describe("move_wire", f"wire={wire_id} d=({dx}, {dy})"),
|
|
641
|
+
[
|
|
642
|
+
{
|
|
643
|
+
"verb": "set_component",
|
|
644
|
+
"args": {
|
|
645
|
+
"entity": wire_id, "component": WIRE_COMPONENT,
|
|
646
|
+
"value": wire,
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
],
|
|
650
|
+
)
|
|
651
|
+
return _receipt("move_wire", actor, result, wire=wire_id)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def split_wire(store, actor, wire_id, at):
|
|
655
|
+
"""Split one single-polyline wire at an interior point.
|
|
656
|
+
|
|
657
|
+
A junction dot is placed at the split, the original wire keeps
|
|
658
|
+
its head plus endpoint a, and a new wire entity carries the
|
|
659
|
+
tail plus endpoint b; both abutting ends reference the new
|
|
660
|
+
junction. All three land in one revision.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
store: Entity store serving the seam.
|
|
664
|
+
actor: Attribution recorded on the revision and events.
|
|
665
|
+
wire_id: Wire entity to split.
|
|
666
|
+
at: Integer [x, y] point on the polyline interior (never a
|
|
667
|
+
wire end).
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
Receipt with op, actor, revision, wire, junction, and
|
|
671
|
+
new_wire IDs, plus events.
|
|
672
|
+
|
|
673
|
+
Raises:
|
|
674
|
+
ContractError: invalid_actor for empty attribution,
|
|
675
|
+
unknown_wire for a missing wire, invalid_split for a
|
|
676
|
+
point off the interior or a multi-polyline wire.
|
|
677
|
+
"""
|
|
678
|
+
_need_actor(actor)
|
|
679
|
+
point = _need_point(at, "at", "invalid_split")
|
|
680
|
+
wire = _read_wire(store, wire_id)
|
|
681
|
+
if len(wire["segments"]) != 1:
|
|
682
|
+
raise ContractError(
|
|
683
|
+
"invalid_split",
|
|
684
|
+
"split_wire needs a single-polyline wire.",
|
|
685
|
+
)
|
|
686
|
+
(polyline,) = wire["segments"]
|
|
687
|
+
if point == wire["endpoints"][0]["xy"] or point == wire["endpoints"][1]["xy"]:
|
|
688
|
+
raise ContractError(
|
|
689
|
+
"invalid_split", f"Split point {point} is a wire end."
|
|
690
|
+
)
|
|
691
|
+
cut = None
|
|
692
|
+
for index, (seg_a, seg_b) in enumerate(zip(polyline, polyline[1:])):
|
|
693
|
+
if _point_on_segment(point, seg_a, seg_b):
|
|
694
|
+
cut = index
|
|
695
|
+
break
|
|
696
|
+
if cut is None:
|
|
697
|
+
raise ContractError(
|
|
698
|
+
"invalid_split", f"Split point {point} lies off the wire."
|
|
699
|
+
)
|
|
700
|
+
head_points = polyline[:cut + 1] + [point]
|
|
701
|
+
tail_points = [point] + polyline[cut + 1:]
|
|
702
|
+
head = {
|
|
703
|
+
"segments": [head_points], "layer": wire["layer"],
|
|
704
|
+
"endpoints": [
|
|
705
|
+
dict(wire["endpoints"][0]),
|
|
706
|
+
{"ref": {"alias": "junction"}, "xy": list(point)},
|
|
707
|
+
],
|
|
708
|
+
}
|
|
709
|
+
tail = {
|
|
710
|
+
"segments": [tail_points], "layer": wire["layer"],
|
|
711
|
+
"endpoints": [
|
|
712
|
+
{"ref": {"alias": "junction"}, "xy": list(point)},
|
|
713
|
+
dict(wire["endpoints"][1]),
|
|
714
|
+
],
|
|
715
|
+
}
|
|
716
|
+
result = store.transact(
|
|
717
|
+
actor, _describe("split_wire", f"wire={wire_id} at={point}"),
|
|
718
|
+
[
|
|
719
|
+
{"verb": "create_entity", "alias": "junction"},
|
|
720
|
+
{
|
|
721
|
+
"verb": "attach_component",
|
|
722
|
+
"args": {
|
|
723
|
+
"entity": {"alias": "junction"},
|
|
724
|
+
"component": JUNCTION_COMPONENT,
|
|
725
|
+
"value": {"xy": list(point), "layer": wire["layer"]},
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
{"verb": "create_entity", "alias": "tail"},
|
|
729
|
+
{
|
|
730
|
+
"verb": "attach_component",
|
|
731
|
+
"args": {
|
|
732
|
+
"entity": {"alias": "tail"},
|
|
733
|
+
"component": WIRE_COMPONENT, "value": tail,
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
"verb": "set_component",
|
|
738
|
+
"args": {
|
|
739
|
+
"entity": wire_id, "component": WIRE_COMPONENT,
|
|
740
|
+
"value": head,
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
)
|
|
745
|
+
return _receipt(
|
|
746
|
+
"split_wire", actor, result, wire=wire_id,
|
|
747
|
+
junction=result["aliases"]["junction"],
|
|
748
|
+
new_wire=result["aliases"]["tail"],
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def delete_wire(store, actor, wire_id):
|
|
753
|
+
"""Delete one wire entity as a durable revision.
|
|
754
|
+
|
|
755
|
+
Junctions the wire referenced persist — explicit dots are
|
|
756
|
+
never cascade-deleted — and Net entities rematerialize on the
|
|
757
|
+
next materialize_nets call, which prunes names the extraction
|
|
758
|
+
no longer yields.
|
|
759
|
+
|
|
760
|
+
Args:
|
|
761
|
+
store: Entity store serving the seam.
|
|
762
|
+
actor: Attribution recorded on the revision and events.
|
|
763
|
+
wire_id: Wire entity to delete.
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
Receipt with op, actor, revision, wire ID, and events.
|
|
767
|
+
|
|
768
|
+
Raises:
|
|
769
|
+
ContractError: invalid_actor for empty attribution,
|
|
770
|
+
unknown_wire for a missing wire.
|
|
771
|
+
"""
|
|
772
|
+
_need_actor(actor)
|
|
773
|
+
_read_wire(store, wire_id)
|
|
774
|
+
result = store.transact(
|
|
775
|
+
actor, _describe("delete_wire", f"wire={wire_id}"),
|
|
776
|
+
[{"verb": "delete_entity", "args": {"entity": wire_id}}],
|
|
777
|
+
)
|
|
778
|
+
return _receipt("delete_wire", actor, result, wire=wire_id)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def delete_junction(store, actor, junction_id):
|
|
782
|
+
"""Delete one unreferenced junction as a durable revision.
|
|
783
|
+
|
|
784
|
+
Args:
|
|
785
|
+
store: Entity store serving the seam.
|
|
786
|
+
actor: Attribution recorded on the revision and events.
|
|
787
|
+
junction_id: Junction entity to delete.
|
|
788
|
+
|
|
789
|
+
Returns:
|
|
790
|
+
Receipt with op, actor, revision, junction ID, and events.
|
|
791
|
+
|
|
792
|
+
Raises:
|
|
793
|
+
ContractError: invalid_actor for empty attribution,
|
|
794
|
+
unknown_junction for a missing junction,
|
|
795
|
+
junction_in_use naming the wires still referencing it.
|
|
796
|
+
"""
|
|
797
|
+
_need_actor(actor)
|
|
798
|
+
try:
|
|
799
|
+
components = store.components_of(junction_id)
|
|
800
|
+
except LookupError:
|
|
801
|
+
raise ContractError(
|
|
802
|
+
"unknown_junction", f"No such junction entity: {junction_id!r}."
|
|
803
|
+
) from None
|
|
804
|
+
if JUNCTION_COMPONENT not in components:
|
|
805
|
+
raise ContractError(
|
|
806
|
+
"unknown_junction",
|
|
807
|
+
f"Entity {junction_id!r} carries no Junction component.",
|
|
808
|
+
)
|
|
809
|
+
users = []
|
|
810
|
+
for wire_id in store.entities_with(WIRE_COMPONENT):
|
|
811
|
+
wire = validate_wire(
|
|
812
|
+
store.get_component(wire_id, WIRE_COMPONENT)
|
|
813
|
+
)
|
|
814
|
+
if any(e["ref"] == junction_id for e in wire["endpoints"]):
|
|
815
|
+
users.append(wire_id)
|
|
816
|
+
if users:
|
|
817
|
+
raise ContractError(
|
|
818
|
+
"junction_in_use",
|
|
819
|
+
f"Junction {junction_id!r} is referenced by wires "
|
|
820
|
+
f"{sorted(users)}; rewire them first.",
|
|
821
|
+
)
|
|
822
|
+
result = store.transact(
|
|
823
|
+
actor, _describe("delete_junction", f"junction={junction_id}"),
|
|
824
|
+
[{"verb": "delete_entity", "args": {"entity": junction_id}}],
|
|
825
|
+
)
|
|
826
|
+
return _receipt("delete_junction", actor, result, junction=junction_id)
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def _index_pins(pins):
|
|
830
|
+
if not isinstance(pins, (list, tuple)):
|
|
831
|
+
raise ContractError("invalid_pin", "pins must be a list.")
|
|
832
|
+
indexed = {}
|
|
833
|
+
seen = set()
|
|
834
|
+
for pin in pins:
|
|
835
|
+
clean = validate_pin(pin)
|
|
836
|
+
if clean["entity"] in indexed:
|
|
837
|
+
raise ContractError(
|
|
838
|
+
"invalid_pin",
|
|
839
|
+
f"Duplicate pin entity: {clean['entity']!r}.",
|
|
840
|
+
)
|
|
841
|
+
key = (clean["instance"], clean["pin"])
|
|
842
|
+
if key in seen:
|
|
843
|
+
raise ContractError(
|
|
844
|
+
"invalid_pin", f"Duplicate pin: {key[0]}.{key[1]}."
|
|
845
|
+
)
|
|
846
|
+
seen.add(key)
|
|
847
|
+
indexed[clean["entity"]] = clean
|
|
848
|
+
return indexed
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _check_named_refs(named_refs):
|
|
852
|
+
if named_refs is None:
|
|
853
|
+
return {}
|
|
854
|
+
if not isinstance(named_refs, dict):
|
|
855
|
+
raise ContractError("invalid_ref", "named_refs must be a mapping.")
|
|
856
|
+
clean = {}
|
|
857
|
+
for entity, name in named_refs.items():
|
|
858
|
+
_need_nonempty_str(entity, "named_refs entity", "invalid_ref")
|
|
859
|
+
_need_nonempty_str(name, f"named_refs[{entity}]", "invalid_ref")
|
|
860
|
+
clean[entity] = name
|
|
861
|
+
return clean
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def _check_ports(ports):
|
|
865
|
+
if not isinstance(ports, (list, tuple)):
|
|
866
|
+
raise ContractError("invalid_port", "ports must be a list.")
|
|
867
|
+
clean = []
|
|
868
|
+
for index, port in enumerate(ports):
|
|
869
|
+
if not isinstance(port, dict):
|
|
870
|
+
raise ContractError(
|
|
871
|
+
"invalid_port", f"ports[{index}] must be a mapping."
|
|
872
|
+
)
|
|
873
|
+
name = port.get("name")
|
|
874
|
+
direction = port.get("direction")
|
|
875
|
+
if not isinstance(name, str) or not name:
|
|
876
|
+
raise ContractError("invalid_port", f"ports[{index}] needs a name.")
|
|
877
|
+
if direction not in _PORT_DIRECTIONS:
|
|
878
|
+
raise ContractError(
|
|
879
|
+
"invalid_port",
|
|
880
|
+
f"ports[{index}] direction must be one of "
|
|
881
|
+
f"{_PORT_DIRECTIONS}, got {direction!r}.",
|
|
882
|
+
)
|
|
883
|
+
clean.append({"name": name, "direction": direction})
|
|
884
|
+
return clean
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _read_world(store):
|
|
888
|
+
wires = {}
|
|
889
|
+
for wire_id in store.entities_with(WIRE_COMPONENT):
|
|
890
|
+
wires[wire_id] = validate_wire(
|
|
891
|
+
store.get_component(wire_id, WIRE_COMPONENT)
|
|
892
|
+
)
|
|
893
|
+
junctions = {}
|
|
894
|
+
for junction_id in store.entities_with(JUNCTION_COMPONENT):
|
|
895
|
+
junctions[junction_id] = validate_junction(
|
|
896
|
+
store.get_component(junction_id, JUNCTION_COMPONENT)
|
|
897
|
+
)
|
|
898
|
+
nets = {}
|
|
899
|
+
for net_id in store.entities_with(NET_COMPONENT):
|
|
900
|
+
nets[net_id] = validate_net(store.get_component(net_id, NET_COMPONENT))
|
|
901
|
+
return wires, junctions, nets
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def world_schematic(store, pins, labels=(), ports=(), cell="top",
|
|
905
|
+
named_refs=None):
|
|
906
|
+
"""Build the CONNECT-GRAPH schematic for the stored wires.
|
|
907
|
+
|
|
908
|
+
Wire polylines feed extraction verbatim; endpoints that
|
|
909
|
+
reference Net entities (or named_refs entries such as ports)
|
|
910
|
+
become labels carrying the referenced name at the endpoint xy,
|
|
911
|
+
so a tap joins every group already carrying that name. Stale
|
|
912
|
+
refs are reported, never silently dropped.
|
|
913
|
+
|
|
914
|
+
Args:
|
|
915
|
+
store: Entity store serving the seam.
|
|
916
|
+
pins: Caller pin records [{entity, instance, pin, xy}].
|
|
917
|
+
labels: Caller label records [{name, xy}].
|
|
918
|
+
ports: Caller port records [{name, direction}].
|
|
919
|
+
cell: Top cell name.
|
|
920
|
+
named_refs: {entity ID: net name} for port-style refs the
|
|
921
|
+
store cannot resolve to geometry.
|
|
922
|
+
|
|
923
|
+
Returns:
|
|
924
|
+
World view with the schematic mapping, the pin index by
|
|
925
|
+
entity, and unresolved [{wire, end, ref}] entries.
|
|
926
|
+
|
|
927
|
+
Raises:
|
|
928
|
+
ContractError: invalid_pin for malformed pins,
|
|
929
|
+
invalid_label for malformed labels, invalid_port for
|
|
930
|
+
malformed ports, invalid_cell for a bad cell name,
|
|
931
|
+
invalid_ref for malformed named_refs, invalid_wire /
|
|
932
|
+
invalid_junction / invalid_net for corrupt storage.
|
|
933
|
+
"""
|
|
934
|
+
_need_nonempty_str(cell, "cell", "invalid_cell")
|
|
935
|
+
indexed = _index_pins(pins)
|
|
936
|
+
if not isinstance(labels, (list, tuple)):
|
|
937
|
+
raise ContractError("invalid_label", "labels must be a list.")
|
|
938
|
+
clean_labels = [validate_label(label) for label in labels]
|
|
939
|
+
clean_ports = _check_ports(ports)
|
|
940
|
+
names = _check_named_refs(named_refs)
|
|
941
|
+
wires, _junctions, nets = _read_world(store)
|
|
942
|
+
net_names = {net_id: net["name"] for net_id, net in nets.items()}
|
|
943
|
+
known = set(indexed) | set(wires) | set(_junctions) | set(nets) | set(names)
|
|
944
|
+
schematic_labels = list(clean_labels)
|
|
945
|
+
unresolved = []
|
|
946
|
+
for wire_id in sorted(wires):
|
|
947
|
+
for index, end in enumerate(ENDPOINTS):
|
|
948
|
+
ref = wires[wire_id]["endpoints"][index]["ref"]
|
|
949
|
+
if ref is None or ref in indexed or ref in _junctions:
|
|
950
|
+
continue
|
|
951
|
+
if ref in net_names:
|
|
952
|
+
schematic_labels.append(
|
|
953
|
+
{"name": net_names[ref],
|
|
954
|
+
"xy": wires[wire_id]["endpoints"][index]["xy"]}
|
|
955
|
+
)
|
|
956
|
+
elif ref in names:
|
|
957
|
+
schematic_labels.append(
|
|
958
|
+
{"name": names[ref],
|
|
959
|
+
"xy": wires[wire_id]["endpoints"][index]["xy"]}
|
|
960
|
+
)
|
|
961
|
+
elif ref in known:
|
|
962
|
+
continue
|
|
963
|
+
else:
|
|
964
|
+
try:
|
|
965
|
+
store.components_of(ref)
|
|
966
|
+
except LookupError:
|
|
967
|
+
unresolved.append(
|
|
968
|
+
{"wire": wire_id, "end": end, "ref": ref}
|
|
969
|
+
)
|
|
970
|
+
by_instance = {}
|
|
971
|
+
for pin in indexed.values():
|
|
972
|
+
by_instance.setdefault(pin["instance"], []).append(pin["pin"])
|
|
973
|
+
schematic = {
|
|
974
|
+
"cell": cell,
|
|
975
|
+
"wires": [
|
|
976
|
+
polyline
|
|
977
|
+
for wire_id in sorted(wires)
|
|
978
|
+
for polyline in wires[wire_id]["segments"]
|
|
979
|
+
],
|
|
980
|
+
"pins": [
|
|
981
|
+
{"instance": pin["instance"], "pin": pin["pin"], "xy": pin["xy"]}
|
|
982
|
+
for pin in indexed.values()
|
|
983
|
+
],
|
|
984
|
+
"labels": schematic_labels,
|
|
985
|
+
"instances": [
|
|
986
|
+
{"name": name, "symbol": name} for name in sorted(by_instance)
|
|
987
|
+
],
|
|
988
|
+
"symbols": {
|
|
989
|
+
name: {"pins": sorted(pins)} for name, pins in by_instance.items()
|
|
990
|
+
},
|
|
991
|
+
"ports": clean_ports,
|
|
992
|
+
}
|
|
993
|
+
return {"schematic": schematic, "pins": indexed, "unresolved": unresolved}
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def _default_extract(schematic):
|
|
997
|
+
from ..workflows.connectgraph import prepare_netlist_structs
|
|
998
|
+
|
|
999
|
+
return prepare_netlist_structs(schematic)
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def _merge_by_name(connectivity):
|
|
1003
|
+
merged = {}
|
|
1004
|
+
for net in connectivity.nets:
|
|
1005
|
+
name = connectivity.net_name(net.id)
|
|
1006
|
+
slot = merged.setdefault(
|
|
1007
|
+
name, {"net_ids": [], "pins": set(), "labels": set(),
|
|
1008
|
+
"has_wires": False}
|
|
1009
|
+
)
|
|
1010
|
+
slot["net_ids"].append(net.id)
|
|
1011
|
+
slot["pins"].update(net.pins)
|
|
1012
|
+
slot["labels"].update(net.labels)
|
|
1013
|
+
slot["has_wires"] = slot["has_wires"] or net.has_wires
|
|
1014
|
+
for slot in merged.values():
|
|
1015
|
+
slot["net_ids"].sort()
|
|
1016
|
+
return merged
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def _membership(names_of_net, wires, junctions, indexed, nets, names,
|
|
1020
|
+
connectivity):
|
|
1021
|
+
"""Map every wire/junction onto its net names by ref closure."""
|
|
1022
|
+
stored_names = {net_id: net["name"] for net_id, net in nets.items()}
|
|
1023
|
+
pin_net = {}
|
|
1024
|
+
for pin in indexed.values():
|
|
1025
|
+
net_id = connectivity.pin_net(pin["instance"], pin["pin"])
|
|
1026
|
+
if net_id is not None:
|
|
1027
|
+
pin_net[pin["entity"]] = names_of_net[net_id]
|
|
1028
|
+
wire_names = {wire_id: set() for wire_id in wires}
|
|
1029
|
+
for wire_id, wire in wires.items():
|
|
1030
|
+
for endpoint in wire["endpoints"]:
|
|
1031
|
+
ref = endpoint["ref"]
|
|
1032
|
+
if ref is None:
|
|
1033
|
+
continue
|
|
1034
|
+
if ref in pin_net:
|
|
1035
|
+
wire_names[wire_id].add(pin_net[ref])
|
|
1036
|
+
elif ref in stored_names and stored_names[ref] in names_of_net.values():
|
|
1037
|
+
wire_names[wire_id].add(stored_names[ref])
|
|
1038
|
+
elif ref in names and names[ref] in names_of_net.values():
|
|
1039
|
+
wire_names[wire_id].add(names[ref])
|
|
1040
|
+
junction_names = {junction_id: set() for junction_id in junctions}
|
|
1041
|
+
changed = True
|
|
1042
|
+
while changed:
|
|
1043
|
+
changed = False
|
|
1044
|
+
for wire_id, wire in wires.items():
|
|
1045
|
+
for endpoint in wire["endpoints"]:
|
|
1046
|
+
ref = endpoint["ref"]
|
|
1047
|
+
if ref in junction_names:
|
|
1048
|
+
before = len(wire_names[wire_id])
|
|
1049
|
+
wire_names[wire_id] |= junction_names[ref]
|
|
1050
|
+
changed = changed or len(wire_names[wire_id]) != before
|
|
1051
|
+
for wire_id, wire in wires.items():
|
|
1052
|
+
for endpoint in wire["endpoints"]:
|
|
1053
|
+
ref = endpoint["ref"]
|
|
1054
|
+
if ref in junction_names:
|
|
1055
|
+
before = len(junction_names[ref])
|
|
1056
|
+
junction_names[ref] |= wire_names[wire_id]
|
|
1057
|
+
changed = changed or len(junction_names[ref]) != before
|
|
1058
|
+
for junction_id, junction in junctions.items():
|
|
1059
|
+
if junction_names[junction_id]:
|
|
1060
|
+
continue
|
|
1061
|
+
if any(
|
|
1062
|
+
wire["endpoints"][i]["ref"] == junction_id
|
|
1063
|
+
for wire in wires.values() for i in range(2)
|
|
1064
|
+
):
|
|
1065
|
+
continue
|
|
1066
|
+
for wire_id, wire in wires.items():
|
|
1067
|
+
if _point_on_wire(junction["xy"], wire):
|
|
1068
|
+
junction_names[junction_id] |= wire_names[wire_id]
|
|
1069
|
+
return wire_names, junction_names
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
def materialize_nets(store, actor, pins, labels=(), ports=(), cell="top",
|
|
1073
|
+
named_refs=None, extract=None):
|
|
1074
|
+
"""Extract connectivity, then upsert Net entities by name.
|
|
1075
|
+
|
|
1076
|
+
Extraction always rebuilds (never a cached run): the world
|
|
1077
|
+
view feeds CONNECT-GRAPH, groups sharing a canonical name
|
|
1078
|
+
merge into one Net, wires/junctions attach by ref closure,
|
|
1079
|
+
and stored Nets whose names vanish are pruned — all in one
|
|
1080
|
+
revision. An unchanged world writes nothing (noop receipt).
|
|
1081
|
+
|
|
1082
|
+
Args:
|
|
1083
|
+
store: Entity store serving the seam.
|
|
1084
|
+
actor: Attribution recorded on the revision and events.
|
|
1085
|
+
pins: Caller pin records [{entity, instance, pin, xy}].
|
|
1086
|
+
labels: Caller label records [{name, xy}].
|
|
1087
|
+
ports: Caller port records [{name, direction}].
|
|
1088
|
+
cell: Top cell name.
|
|
1089
|
+
named_refs: {entity ID: net name} for port-style refs the
|
|
1090
|
+
store cannot resolve to geometry.
|
|
1091
|
+
extract: Optional extraction callable (schematic) ->
|
|
1092
|
+
NetlistStructs; defaults to the in-tree CONNECT-GRAPH
|
|
1093
|
+
extraction.
|
|
1094
|
+
|
|
1095
|
+
Returns:
|
|
1096
|
+
Receipt with op, actor, revision, nets {name: entity ID},
|
|
1097
|
+
created/updated/pruned ID lists, unresolved refs, and
|
|
1098
|
+
events — or a noop receipt (revision None) when every
|
|
1099
|
+
Net already matches.
|
|
1100
|
+
|
|
1101
|
+
Raises:
|
|
1102
|
+
ContractError: invalid_actor for empty attribution,
|
|
1103
|
+
invalid_pin / invalid_label / invalid_port /
|
|
1104
|
+
invalid_cell / invalid_ref for malformed inputs,
|
|
1105
|
+
extraction_failed when extraction refuses the world.
|
|
1106
|
+
"""
|
|
1107
|
+
_need_actor(actor)
|
|
1108
|
+
names = _check_named_refs(named_refs)
|
|
1109
|
+
view = world_schematic(
|
|
1110
|
+
store, pins, labels=labels, ports=ports, cell=cell,
|
|
1111
|
+
named_refs=named_refs,
|
|
1112
|
+
)
|
|
1113
|
+
extractor = extract if extract is not None else _default_extract
|
|
1114
|
+
try:
|
|
1115
|
+
structs = extractor(view["schematic"])
|
|
1116
|
+
connectivity = structs.connectivity
|
|
1117
|
+
merged = _merge_by_name(connectivity)
|
|
1118
|
+
except ContractError:
|
|
1119
|
+
raise
|
|
1120
|
+
except Exception as exc:
|
|
1121
|
+
raise ContractError(
|
|
1122
|
+
"extraction_failed", f"Connectivity extraction failed: {exc}."
|
|
1123
|
+
) from exc
|
|
1124
|
+
wires, junctions, nets = _read_world(store)
|
|
1125
|
+
names_of_net = {
|
|
1126
|
+
net.id: connectivity.net_name(net.id) for net in connectivity.nets
|
|
1127
|
+
}
|
|
1128
|
+
wire_names, junction_names = _membership(
|
|
1129
|
+
names_of_net, wires, junctions, view["pins"], nets, names,
|
|
1130
|
+
connectivity,
|
|
1131
|
+
)
|
|
1132
|
+
by_name = {net["name"]: net_id for net_id, net in nets.items()}
|
|
1133
|
+
calls = []
|
|
1134
|
+
created = []
|
|
1135
|
+
updated = []
|
|
1136
|
+
nets_out = {}
|
|
1137
|
+
for name in sorted(merged):
|
|
1138
|
+
slot = merged[name]
|
|
1139
|
+
value = validate_net({
|
|
1140
|
+
"name": name,
|
|
1141
|
+
"extraction": {"cell": cell, "net_ids": slot["net_ids"]},
|
|
1142
|
+
"members": {
|
|
1143
|
+
"pins": [
|
|
1144
|
+
{"instance": instance, "pin": pin}
|
|
1145
|
+
for instance, pin in sorted(slot["pins"])
|
|
1146
|
+
],
|
|
1147
|
+
"labels": sorted(slot["labels"]),
|
|
1148
|
+
"wires": sorted(
|
|
1149
|
+
w for w, found in wire_names.items() if name in found
|
|
1150
|
+
),
|
|
1151
|
+
"junctions": sorted(
|
|
1152
|
+
j for j, found in junction_names.items() if name in found
|
|
1153
|
+
),
|
|
1154
|
+
},
|
|
1155
|
+
"has_wires": slot["has_wires"],
|
|
1156
|
+
})
|
|
1157
|
+
if name in by_name and nets[by_name[name]] == value:
|
|
1158
|
+
nets_out[name] = by_name[name]
|
|
1159
|
+
continue
|
|
1160
|
+
if name in by_name:
|
|
1161
|
+
nets_out[name] = by_name[name]
|
|
1162
|
+
updated.append(by_name[name])
|
|
1163
|
+
calls.append({
|
|
1164
|
+
"verb": "set_component",
|
|
1165
|
+
"args": {
|
|
1166
|
+
"entity": by_name[name], "component": NET_COMPONENT,
|
|
1167
|
+
"value": value,
|
|
1168
|
+
},
|
|
1169
|
+
})
|
|
1170
|
+
else:
|
|
1171
|
+
alias = f"net:{name}"
|
|
1172
|
+
calls.append({"verb": "create_entity", "alias": alias})
|
|
1173
|
+
calls.append({
|
|
1174
|
+
"verb": "attach_component",
|
|
1175
|
+
"args": {
|
|
1176
|
+
"entity": {"alias": alias}, "component": NET_COMPONENT,
|
|
1177
|
+
"value": value,
|
|
1178
|
+
},
|
|
1179
|
+
})
|
|
1180
|
+
created.append(alias)
|
|
1181
|
+
pruned = [
|
|
1182
|
+
net_id for name, net_id in sorted(by_name.items()) if name not in merged
|
|
1183
|
+
]
|
|
1184
|
+
for net_id in pruned:
|
|
1185
|
+
calls.append({
|
|
1186
|
+
"verb": "delete_entity", "args": {"entity": net_id},
|
|
1187
|
+
})
|
|
1188
|
+
if not calls:
|
|
1189
|
+
return {
|
|
1190
|
+
"op": "materialize_nets", "actor": actor, "revision": None,
|
|
1191
|
+
"noop": True, "nets": nets_out, "created": [], "updated": [],
|
|
1192
|
+
"pruned": [], "unresolved": view["unresolved"],
|
|
1193
|
+
}
|
|
1194
|
+
result = store.transact(
|
|
1195
|
+
actor, _describe("materialize_nets", f"cell={cell}"),
|
|
1196
|
+
calls,
|
|
1197
|
+
)
|
|
1198
|
+
for alias in created:
|
|
1199
|
+
name = alias[len("net:"):]
|
|
1200
|
+
nets_out[name] = result["aliases"][alias]
|
|
1201
|
+
receipt = _receipt(
|
|
1202
|
+
"materialize_nets", actor, result, nets=nets_out,
|
|
1203
|
+
unresolved=view["unresolved"],
|
|
1204
|
+
)
|
|
1205
|
+
receipt["created"] = [nets_out[alias[len("net:"):]] for alias in created]
|
|
1206
|
+
receipt["updated"] = updated
|
|
1207
|
+
receipt["pruned"] = pruned
|
|
1208
|
+
return receipt
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
def collect_wire_erc(store, pins, policy=None, named_refs=None):
|
|
1212
|
+
"""Collect wire/junction ERC findings over the stored world.
|
|
1213
|
+
|
|
1214
|
+
Dangling endpoints are warnings, refs to deleted entities are
|
|
1215
|
+
errors, spans whose xy no longer meets a pin/junction target
|
|
1216
|
+
are warnings, and junctions touching nothing are warnings.
|
|
1217
|
+
Nothing is ever silently dropped: every anomaly names its
|
|
1218
|
+
entity.
|
|
1219
|
+
|
|
1220
|
+
Args:
|
|
1221
|
+
store: Entity store serving the seam.
|
|
1222
|
+
pins: Caller pin records [{entity, instance, pin, xy}].
|
|
1223
|
+
policy: rule -> severity overrides.
|
|
1224
|
+
named_refs: {entity ID: net name} for port-style refs the
|
|
1225
|
+
store cannot resolve to geometry.
|
|
1226
|
+
|
|
1227
|
+
Returns:
|
|
1228
|
+
Findings sorted by (rule, entity, end), each with rule,
|
|
1229
|
+
severity, message, and locator keys.
|
|
1230
|
+
|
|
1231
|
+
Raises:
|
|
1232
|
+
ContractError: invalid_pin for malformed pins,
|
|
1233
|
+
invalid_policy for an unknown rule, invalid_wire /
|
|
1234
|
+
invalid_junction for corrupt storage.
|
|
1235
|
+
"""
|
|
1236
|
+
merged = dict(DEFAULT_WIRE_POLICY)
|
|
1237
|
+
for rule in (policy or {}):
|
|
1238
|
+
if rule not in merged:
|
|
1239
|
+
raise ContractError(
|
|
1240
|
+
"invalid_policy", f"Unknown ERC rule: {rule!r}."
|
|
1241
|
+
)
|
|
1242
|
+
merged[rule] = policy[rule]
|
|
1243
|
+
indexed = _index_pins(pins)
|
|
1244
|
+
_check_named_refs(named_refs)
|
|
1245
|
+
wires, junctions, _nets = _read_world(store)
|
|
1246
|
+
findings = []
|
|
1247
|
+
for wire_id in sorted(wires):
|
|
1248
|
+
wire = wires[wire_id]
|
|
1249
|
+
for index, end in enumerate(ENDPOINTS):
|
|
1250
|
+
endpoint = wire["endpoints"][index]
|
|
1251
|
+
ref = endpoint["ref"]
|
|
1252
|
+
if ref is None:
|
|
1253
|
+
findings.append({
|
|
1254
|
+
"rule": RULE_DANGLING_ENDPOINT,
|
|
1255
|
+
"severity": merged[RULE_DANGLING_ENDPOINT],
|
|
1256
|
+
"message": (
|
|
1257
|
+
f"wire {wire_id} end {end} dangles "
|
|
1258
|
+
f"at {endpoint['xy']}"
|
|
1259
|
+
),
|
|
1260
|
+
"wire": wire_id, "end": end, "xy": endpoint["xy"],
|
|
1261
|
+
})
|
|
1262
|
+
continue
|
|
1263
|
+
try:
|
|
1264
|
+
components = store.components_of(ref)
|
|
1265
|
+
except LookupError:
|
|
1266
|
+
findings.append({
|
|
1267
|
+
"rule": RULE_UNKNOWN_REF,
|
|
1268
|
+
"severity": merged[RULE_UNKNOWN_REF],
|
|
1269
|
+
"message": (
|
|
1270
|
+
f"wire {wire_id} end {end} references "
|
|
1271
|
+
f"deleted entity {ref!r}"
|
|
1272
|
+
),
|
|
1273
|
+
"wire": wire_id, "end": end, "ref": ref,
|
|
1274
|
+
})
|
|
1275
|
+
continue
|
|
1276
|
+
if WIRE_COMPONENT in components:
|
|
1277
|
+
findings.append({
|
|
1278
|
+
"rule": RULE_UNKNOWN_REF,
|
|
1279
|
+
"severity": merged[RULE_UNKNOWN_REF],
|
|
1280
|
+
"message": (
|
|
1281
|
+
f"wire {wire_id} end {end} references wire "
|
|
1282
|
+
f"entity {ref!r}; endpoints terminate on "
|
|
1283
|
+
"pin/port/junction/net IDs only"
|
|
1284
|
+
),
|
|
1285
|
+
"wire": wire_id, "end": end, "ref": ref,
|
|
1286
|
+
})
|
|
1287
|
+
continue
|
|
1288
|
+
target = None
|
|
1289
|
+
if ref in indexed:
|
|
1290
|
+
target = indexed[ref]["xy"]
|
|
1291
|
+
elif ref in junctions:
|
|
1292
|
+
target = junctions[ref]["xy"]
|
|
1293
|
+
if target is not None and endpoint["xy"] != target:
|
|
1294
|
+
findings.append({
|
|
1295
|
+
"rule": RULE_STALE_GEOMETRY,
|
|
1296
|
+
"severity": merged[RULE_STALE_GEOMETRY],
|
|
1297
|
+
"message": (
|
|
1298
|
+
f"wire {wire_id} end {end} sits at "
|
|
1299
|
+
f"{endpoint['xy']} but {ref!r} is at {target}"
|
|
1300
|
+
),
|
|
1301
|
+
"wire": wire_id, "end": end, "ref": ref,
|
|
1302
|
+
"xy": endpoint["xy"], "target": target,
|
|
1303
|
+
})
|
|
1304
|
+
referenced = {
|
|
1305
|
+
endpoint["ref"]
|
|
1306
|
+
for wire in wires.values() for endpoint in wire["endpoints"]
|
|
1307
|
+
}
|
|
1308
|
+
for junction_id in sorted(junctions):
|
|
1309
|
+
if junction_id in referenced:
|
|
1310
|
+
continue
|
|
1311
|
+
xy = junctions[junction_id]["xy"]
|
|
1312
|
+
if any(_point_on_wire(xy, wire) for wire in wires.values()):
|
|
1313
|
+
continue
|
|
1314
|
+
findings.append({
|
|
1315
|
+
"rule": RULE_ORPHAN_JUNCTION,
|
|
1316
|
+
"severity": merged[RULE_ORPHAN_JUNCTION],
|
|
1317
|
+
"message": f"junction {junction_id} at {xy} touches no wire",
|
|
1318
|
+
"junction": junction_id, "xy": xy,
|
|
1319
|
+
})
|
|
1320
|
+
findings.sort(
|
|
1321
|
+
key=lambda f: (f["rule"], f.get("wire", f.get("junction", "")),
|
|
1322
|
+
f.get("end", ""))
|
|
1323
|
+
)
|
|
1324
|
+
return findings
|