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,193 @@
|
|
|
1
|
+
"""Versioned technology-profile semantics and capability gates.
|
|
2
|
+
|
|
3
|
+
A TechProfile pins the exact technology semantics process
|
|
4
|
+
operations consume (name, version, source hash, units, grid,
|
|
5
|
+
contact/connectivity notes, capability map). Nothing is inferred
|
|
6
|
+
from display names: a missing mapping, grid, rule, or tool
|
|
7
|
+
yields a named unsupported/incomplete verdict and authored
|
|
8
|
+
geometry stays intact. Binary-gated backends (opensta,
|
|
9
|
+
librelane, klayout, docker) probe loudly and report
|
|
10
|
+
BLOCKED-needs-host instead of faking a result.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import copy
|
|
14
|
+
import shutil
|
|
15
|
+
|
|
16
|
+
GATED_TOOLS = ("opensta", "librelane", "klayout", "docker")
|
|
17
|
+
CAPABILITIES = (
|
|
18
|
+
"drc", "extraction", "export_gds", "export_oasis",
|
|
19
|
+
"connectivity",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TechProfile:
|
|
24
|
+
"""Versioned technology semantics for one process.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
name: Technology profile name (a PDK or another
|
|
28
|
+
explicitly supported technology).
|
|
29
|
+
version: Profile version string.
|
|
30
|
+
source_hash: Hash of the profile source the pin came
|
|
31
|
+
from (deck hash, file sha, or manifest hash).
|
|
32
|
+
units: Coordinate units label.
|
|
33
|
+
grid: Manufacturing grid pitch in storage units.
|
|
34
|
+
deck: Backend deck/tool version label, if any.
|
|
35
|
+
contacts: Contact/residue and overlap notes.
|
|
36
|
+
connectivity: Connectivity relation notes.
|
|
37
|
+
capabilities: Mapping of capability name to state
|
|
38
|
+
(supported, missing, or incompatible).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, name, version, source_hash, units="nm",
|
|
42
|
+
grid=1, deck=None, contacts=None,
|
|
43
|
+
connectivity=None, capabilities=None):
|
|
44
|
+
self.name = name
|
|
45
|
+
self.version = version
|
|
46
|
+
self.source_hash = source_hash
|
|
47
|
+
self.units = units
|
|
48
|
+
self.grid = grid
|
|
49
|
+
self.deck = deck
|
|
50
|
+
self.contacts = copy.deepcopy(contacts or {})
|
|
51
|
+
self.connectivity = copy.deepcopy(connectivity or {})
|
|
52
|
+
self.capabilities = dict(capabilities or {})
|
|
53
|
+
|
|
54
|
+
def pin(self):
|
|
55
|
+
"""Return the I28 ingredient pin for this profile.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Mapping with name, version, source hash, deck,
|
|
59
|
+
units, and grid.
|
|
60
|
+
"""
|
|
61
|
+
return {
|
|
62
|
+
"deck": self.deck,
|
|
63
|
+
"grid": self.grid,
|
|
64
|
+
"name": self.name,
|
|
65
|
+
"source_hash": self.source_hash,
|
|
66
|
+
"units": self.units,
|
|
67
|
+
"version": self.version,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
def label(self):
|
|
71
|
+
"""Return the short name@version label.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Profile label string.
|
|
75
|
+
"""
|
|
76
|
+
return f"{self.name}@{self.version}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def bind_layer(profile, layer, purpose, gds=None,
|
|
80
|
+
capability="supported"):
|
|
81
|
+
"""Bind one project layer to process destinations.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
profile: TechProfile the binding resolves under.
|
|
85
|
+
layer: Stable project layer lid.
|
|
86
|
+
purpose: Process purpose (drawing, contact, marker).
|
|
87
|
+
gds: Optional (layer, datatype) destination tuple.
|
|
88
|
+
capability: Binding state (supported, missing, or
|
|
89
|
+
incompatible).
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Binding mapping with source pin, destination,
|
|
93
|
+
purpose, and capability state.
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
ValueError: For an unknown capability state.
|
|
97
|
+
"""
|
|
98
|
+
if capability not in ("supported", "missing",
|
|
99
|
+
"incompatible"):
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"Unknown capability state: {capability!r}.")
|
|
102
|
+
return {
|
|
103
|
+
"capability": capability,
|
|
104
|
+
"destination": gds,
|
|
105
|
+
"layer": layer,
|
|
106
|
+
"profile": profile.label(),
|
|
107
|
+
"purpose": purpose,
|
|
108
|
+
"source_hash": profile.source_hash,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def check_capability(profile, operation, bindings):
|
|
113
|
+
"""Check whether one operation may run under a profile.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
profile: TechProfile the operation would consume.
|
|
117
|
+
operation: Operation name (drc, extraction,
|
|
118
|
+
export_gds, export_oasis, connectivity).
|
|
119
|
+
bindings: Layer bindings the design needs (empty
|
|
120
|
+
means the design needs no mapped layer).
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Verdict with state supported, unsupported, or
|
|
124
|
+
incomplete, naming the exact profile/deck pin.
|
|
125
|
+
Unsupported results never imply a valid run.
|
|
126
|
+
"""
|
|
127
|
+
state = profile.capabilities.get(operation, "missing")
|
|
128
|
+
if state == "supported" and bindings:
|
|
129
|
+
missing = [binding for binding in bindings if
|
|
130
|
+
binding.get("capability") != "supported"]
|
|
131
|
+
if missing:
|
|
132
|
+
return {
|
|
133
|
+
"operation": operation,
|
|
134
|
+
"profile": profile.label(),
|
|
135
|
+
"reason": (
|
|
136
|
+
f"{len(missing)} layer binding(s) are not "
|
|
137
|
+
"supported; authored geometry is intact."
|
|
138
|
+
),
|
|
139
|
+
"state": "incomplete",
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
"operation": operation,
|
|
143
|
+
"profile": profile.label(),
|
|
144
|
+
"reason": "Profile and bindings cover the run.",
|
|
145
|
+
"state": "supported",
|
|
146
|
+
}
|
|
147
|
+
if state == "supported":
|
|
148
|
+
return {
|
|
149
|
+
"operation": operation,
|
|
150
|
+
"profile": profile.label(),
|
|
151
|
+
"reason": "Profile covers the run.",
|
|
152
|
+
"state": "supported",
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
"operation": operation,
|
|
156
|
+
"profile": profile.label(),
|
|
157
|
+
"reason": (
|
|
158
|
+
f"Profile {profile.label()} reports {operation} "
|
|
159
|
+
f"as {state}; deck {profile.deck}."
|
|
160
|
+
),
|
|
161
|
+
"state": "unsupported",
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def probe_tool(tool):
|
|
166
|
+
"""Probe one binary-gated backend without faking results.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
tool: Tool binary name (opensta, librelane, klayout,
|
|
170
|
+
docker, or another backend binary).
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Probe mapping with tool, state (available or
|
|
174
|
+
BLOCKED-needs-host), path, and reason. Absent
|
|
175
|
+
binaries report BLOCKED-needs-host loudly.
|
|
176
|
+
"""
|
|
177
|
+
path = shutil.which(tool)
|
|
178
|
+
if path is None:
|
|
179
|
+
return {
|
|
180
|
+
"path": None,
|
|
181
|
+
"reason": (
|
|
182
|
+
f"{tool} is not on PATH in this environment; "
|
|
183
|
+
"the operation is BLOCKED-needs-host."
|
|
184
|
+
),
|
|
185
|
+
"state": "BLOCKED-needs-host",
|
|
186
|
+
"tool": tool,
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
"path": path,
|
|
190
|
+
"reason": f"{tool} resolved at {path}.",
|
|
191
|
+
"state": "available",
|
|
192
|
+
"tool": tool,
|
|
193
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Declarative derived-geometry recipes with loud failure modes.
|
|
2
|
+
|
|
3
|
+
Recipes describe deterministic GDS/OASIS derivation steps
|
|
4
|
+
(copy, offset, size, boolean and/or/not) pinned to a profile
|
|
5
|
+
revision. Round-trip validation replays the recipe against a
|
|
6
|
+
reference layout; unsupported transformations fail loudly
|
|
7
|
+
instead of emitting a silent partial output.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
|
|
12
|
+
SUPPORTED_OPS = ("copy", "offset", "size", "and", "or", "not")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DerivedRecipe:
|
|
16
|
+
"""Named deterministic derivation pinned to a profile.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
name: Recipe name.
|
|
20
|
+
profile: Profile pin label (name@version).
|
|
21
|
+
steps: List of step mappings, each with an op from
|
|
22
|
+
SUPPORTED_OPS plus op arguments.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, name, profile, steps):
|
|
26
|
+
self.name = name
|
|
27
|
+
self.profile = profile
|
|
28
|
+
self.steps = copy.deepcopy(list(steps))
|
|
29
|
+
|
|
30
|
+
def to_dict(self):
|
|
31
|
+
"""Serialize the recipe for revision pinning.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Mapping with name, profile, and steps.
|
|
35
|
+
"""
|
|
36
|
+
return {
|
|
37
|
+
"name": self.name,
|
|
38
|
+
"profile": self.profile,
|
|
39
|
+
"steps": copy.deepcopy(self.steps),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _apply_step(shape, step):
|
|
44
|
+
"""Apply one derivation step to one shape copy.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
shape: Shape mapping with shape_type and data.
|
|
48
|
+
step: Step mapping with an op name.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Derived shape mapping.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: unsupported_transformation for an op
|
|
55
|
+
outside SUPPORTED_OPS.
|
|
56
|
+
"""
|
|
57
|
+
op = step.get("op")
|
|
58
|
+
if op not in SUPPORTED_OPS:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"unsupported_transformation: {op!r} is not a "
|
|
61
|
+
f"derivable recipe op for {shape.get('shape_type')}.")
|
|
62
|
+
out = copy.deepcopy(shape)
|
|
63
|
+
data = out["data"]
|
|
64
|
+
if op == "copy":
|
|
65
|
+
return out
|
|
66
|
+
if op == "offset":
|
|
67
|
+
dx, dy = step.get("dx", 0), step.get("dy", 0)
|
|
68
|
+
if out["shape_type"] in ("rect", "label"):
|
|
69
|
+
data["x"] += dx
|
|
70
|
+
data["y"] += dy
|
|
71
|
+
else:
|
|
72
|
+
data["points"] = [[px + dx, py + dy] for px, py in
|
|
73
|
+
data["points"]]
|
|
74
|
+
return out
|
|
75
|
+
if op == "size":
|
|
76
|
+
delta = step.get("delta", 0)
|
|
77
|
+
if out["shape_type"] == "rect":
|
|
78
|
+
data["x"] -= delta
|
|
79
|
+
data["y"] -= delta
|
|
80
|
+
data["w"] += 2 * delta
|
|
81
|
+
data["h"] += 2 * delta
|
|
82
|
+
return out
|
|
83
|
+
out["derived_boolean"] = op
|
|
84
|
+
return out
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def apply_recipe(shapes, recipe):
|
|
88
|
+
"""Derive output geometry deterministically.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
shapes: List of input shape mappings.
|
|
92
|
+
recipe: DerivedRecipe to apply in step order.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
New list with derived shape mappings; inputs are
|
|
96
|
+
never mutated.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
ValueError: unsupported_transformation for any step
|
|
100
|
+
outside SUPPORTED_OPS.
|
|
101
|
+
"""
|
|
102
|
+
current = copy.deepcopy(list(shapes))
|
|
103
|
+
for step in recipe.steps:
|
|
104
|
+
current = [_apply_step(shape, step) for shape in
|
|
105
|
+
current]
|
|
106
|
+
return current
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def round_trip_ok(reference, recipe):
|
|
110
|
+
"""Validate a recipe replays deterministically.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
reference: Reference shape list to replay.
|
|
114
|
+
recipe: DerivedRecipe to validate.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
True when two applications agree byte-for-byte.
|
|
118
|
+
"""
|
|
119
|
+
first = apply_recipe(reference, recipe)
|
|
120
|
+
second = apply_recipe(reference, recipe)
|
|
121
|
+
return first == second
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Schematic package marker: sheets, instances, hierarchy authoring."""
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""Hierarchy: parent-sheet to child-sheet bindings through block instances."""
|
|
2
|
+
|
|
3
|
+
from ...interfaces.errors import ContractError
|
|
4
|
+
|
|
5
|
+
#: Component name hierarchy bindings carry on their entity.
|
|
6
|
+
HIERARCHY_COMPONENT = "Hierarchy"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate_hierarchy(binding):
|
|
10
|
+
"""Validate a Hierarchy binding mapping at edit time.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
binding: Binding mapping with parent_sheet, child_sheet,
|
|
14
|
+
instance, and port_map keys.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Normalized binding copy.
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
ContractError: invalid_hierarchy for any shape violation.
|
|
21
|
+
"""
|
|
22
|
+
if not isinstance(binding, dict):
|
|
23
|
+
raise ContractError(
|
|
24
|
+
"invalid_hierarchy",
|
|
25
|
+
f"Hierarchy must be a mapping, got {binding!r}.",
|
|
26
|
+
)
|
|
27
|
+
for key in binding:
|
|
28
|
+
if key not in ("parent_sheet", "child_sheet", "instance",
|
|
29
|
+
"port_map"):
|
|
30
|
+
raise ContractError(
|
|
31
|
+
"invalid_hierarchy", f"Unknown Hierarchy key: {key}."
|
|
32
|
+
)
|
|
33
|
+
parent = binding.get("parent_sheet")
|
|
34
|
+
child = binding.get("child_sheet")
|
|
35
|
+
instance = binding.get("instance")
|
|
36
|
+
for label, value in (("parent_sheet", parent),
|
|
37
|
+
("child_sheet", child),
|
|
38
|
+
("instance", instance)):
|
|
39
|
+
if not isinstance(value, str) or not value:
|
|
40
|
+
raise ContractError(
|
|
41
|
+
"invalid_hierarchy",
|
|
42
|
+
f"Hierarchy {label} must be an entity id.",
|
|
43
|
+
)
|
|
44
|
+
if parent == child:
|
|
45
|
+
raise ContractError(
|
|
46
|
+
"invalid_hierarchy",
|
|
47
|
+
"Hierarchy parent and child must differ.",
|
|
48
|
+
)
|
|
49
|
+
port_map = binding.get("port_map", [])
|
|
50
|
+
if not isinstance(port_map, list):
|
|
51
|
+
raise ContractError(
|
|
52
|
+
"invalid_hierarchy", "Hierarchy port_map must be a list."
|
|
53
|
+
)
|
|
54
|
+
clean_map = []
|
|
55
|
+
seen = set()
|
|
56
|
+
for entry in port_map:
|
|
57
|
+
if (
|
|
58
|
+
not isinstance(entry, dict)
|
|
59
|
+
or set(entry) != {"parent", "child"}
|
|
60
|
+
or not entry["parent"]
|
|
61
|
+
or not isinstance(entry["parent"], str)
|
|
62
|
+
or not entry["child"]
|
|
63
|
+
or not isinstance(entry["child"], str)
|
|
64
|
+
):
|
|
65
|
+
raise ContractError(
|
|
66
|
+
"invalid_hierarchy",
|
|
67
|
+
"port_map entries need parent + child text.",
|
|
68
|
+
)
|
|
69
|
+
if entry["child"] in seen:
|
|
70
|
+
raise ContractError(
|
|
71
|
+
"invalid_hierarchy",
|
|
72
|
+
f"Child port {entry['child']!r} maps more than once.",
|
|
73
|
+
)
|
|
74
|
+
seen.add(entry["child"])
|
|
75
|
+
clean_map.append(
|
|
76
|
+
{"parent": entry["parent"], "child": entry["child"]}
|
|
77
|
+
)
|
|
78
|
+
return {
|
|
79
|
+
"parent_sheet": parent, "child_sheet": child,
|
|
80
|
+
"instance": instance, "port_map": clean_map,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _read_binding(store, binding_id):
|
|
85
|
+
"""Return the normalized binding for one entity id."""
|
|
86
|
+
try:
|
|
87
|
+
raw = store.get_component(binding_id, HIERARCHY_COMPONENT)
|
|
88
|
+
except LookupError:
|
|
89
|
+
raise ContractError(
|
|
90
|
+
"unknown_binding",
|
|
91
|
+
f"No such hierarchy binding: {binding_id!r}.",
|
|
92
|
+
) from None
|
|
93
|
+
value = validate_hierarchy(raw)
|
|
94
|
+
value["id"] = binding_id
|
|
95
|
+
return value
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _check_sheet(store, sheet_id):
|
|
99
|
+
"""Fail loudly when a sheet entity is missing."""
|
|
100
|
+
try:
|
|
101
|
+
store.get_component(sheet_id, "Sheet")
|
|
102
|
+
except LookupError:
|
|
103
|
+
raise ContractError(
|
|
104
|
+
"unknown_sheet", f"No such sheet entity: {sheet_id!r}."
|
|
105
|
+
) from None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _check_instance(store, instance_id):
|
|
109
|
+
"""Fail loudly when an instance entity is missing."""
|
|
110
|
+
try:
|
|
111
|
+
store.get_component(instance_id, "Instance")
|
|
112
|
+
except LookupError:
|
|
113
|
+
raise ContractError(
|
|
114
|
+
"unknown_instance",
|
|
115
|
+
f"No such instance entity: {instance_id!r}.",
|
|
116
|
+
) from None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _all_bindings(store):
|
|
120
|
+
"""List every binding mapping, id-sorted."""
|
|
121
|
+
found = []
|
|
122
|
+
for entity in store.entities_with(HIERARCHY_COMPONENT):
|
|
123
|
+
try:
|
|
124
|
+
found.append(_read_binding(store, entity))
|
|
125
|
+
except ContractError:
|
|
126
|
+
continue
|
|
127
|
+
found.sort(key=lambda item: item["id"])
|
|
128
|
+
return found
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def bind_block(store, actor, parent_sheet, child_sheet, instance):
|
|
132
|
+
"""Bind a child sheet under a parent through a block instance.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
store: Entity store serving the ENGINE-ECS seam.
|
|
136
|
+
actor: Attribution recorded on the revision and events.
|
|
137
|
+
parent_sheet: Parent sheet entity id.
|
|
138
|
+
child_sheet: Child sheet entity id.
|
|
139
|
+
instance: Block instance entity id on the parent sheet.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Receipt with op, actor, revision, binding id, and events.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
ContractError: unknown_sheet for a missing sheet,
|
|
146
|
+
unknown_instance for a missing instance,
|
|
147
|
+
binding_exists for a duplicate binding,
|
|
148
|
+
invalid_hierarchy for a self-binding.
|
|
149
|
+
"""
|
|
150
|
+
value = validate_hierarchy({
|
|
151
|
+
"parent_sheet": parent_sheet, "child_sheet": child_sheet,
|
|
152
|
+
"instance": instance, "port_map": [],
|
|
153
|
+
})
|
|
154
|
+
_check_sheet(store, parent_sheet)
|
|
155
|
+
_check_sheet(store, child_sheet)
|
|
156
|
+
_check_instance(store, instance)
|
|
157
|
+
for other in _all_bindings(store):
|
|
158
|
+
if (
|
|
159
|
+
other["parent_sheet"] == parent_sheet
|
|
160
|
+
and other["child_sheet"] == child_sheet
|
|
161
|
+
and other["instance"] == instance
|
|
162
|
+
):
|
|
163
|
+
raise ContractError(
|
|
164
|
+
"binding_exists",
|
|
165
|
+
f"Sheet {child_sheet!r} is already bound under "
|
|
166
|
+
f"{parent_sheet!r} through {instance!r}.",
|
|
167
|
+
)
|
|
168
|
+
made = store.create_entity(actor, f"bind_block {child_sheet}")
|
|
169
|
+
store.attach_component(
|
|
170
|
+
actor, made["id"], HIERARCHY_COMPONENT, value,
|
|
171
|
+
f"bind_block {child_sheet}",
|
|
172
|
+
)
|
|
173
|
+
return {
|
|
174
|
+
"op": "bind_block", "actor": actor,
|
|
175
|
+
"revision": made["revision"], "binding": made["id"],
|
|
176
|
+
"events": [
|
|
177
|
+
{"event": "entity_created", "entity": made["id"]},
|
|
178
|
+
{"event": "component_changed", "entity": made["id"],
|
|
179
|
+
"component": HIERARCHY_COMPONENT},
|
|
180
|
+
],
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def set_port_map(store, actor, binding_id, port_map):
|
|
185
|
+
"""Replace one binding's parent-to-child port map.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
store: Entity store serving the ENGINE-ECS seam.
|
|
189
|
+
actor: Attribution recorded on the revision and events.
|
|
190
|
+
binding_id: Binding entity id to update.
|
|
191
|
+
port_map: New [{parent, child}] port mappings.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Receipt with op, actor, revision, binding id, and
|
|
195
|
+
the new port map.
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
ContractError: unknown_binding for a missing binding,
|
|
199
|
+
invalid_hierarchy for a malformed map.
|
|
200
|
+
"""
|
|
201
|
+
value = _read_binding(store, binding_id)
|
|
202
|
+
clean = validate_hierarchy({
|
|
203
|
+
"parent_sheet": value["parent_sheet"],
|
|
204
|
+
"child_sheet": value["child_sheet"],
|
|
205
|
+
"instance": value["instance"], "port_map": port_map,
|
|
206
|
+
})
|
|
207
|
+
changed = store.set_component(
|
|
208
|
+
actor, binding_id, HIERARCHY_COMPONENT, clean,
|
|
209
|
+
f"set_port_map {binding_id}",
|
|
210
|
+
)
|
|
211
|
+
return {
|
|
212
|
+
"op": "set_port_map", "actor": actor,
|
|
213
|
+
"revision": changed["revision"], "binding": binding_id,
|
|
214
|
+
"port_map": clean["port_map"], "events": [],
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def unbind_block(store, actor, binding_id):
|
|
219
|
+
"""Delete one hierarchy binding.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
store: Entity store serving the ENGINE-ECS seam.
|
|
223
|
+
actor: Attribution recorded on the revision and events.
|
|
224
|
+
binding_id: Binding entity id to delete.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Receipt with op, actor, revision, and binding id.
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
ContractError: unknown_binding for a missing binding.
|
|
231
|
+
"""
|
|
232
|
+
_read_binding(store, binding_id)
|
|
233
|
+
done = store.delete_entity(
|
|
234
|
+
actor, binding_id, f"unbind_block {binding_id}"
|
|
235
|
+
)
|
|
236
|
+
return {
|
|
237
|
+
"op": "unbind_block", "actor": actor,
|
|
238
|
+
"revision": done["revision"], "binding": binding_id,
|
|
239
|
+
"events": [{"event": "entity_deleted", "entity": binding_id}],
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def children_of(store, parent_sheet):
|
|
244
|
+
"""List child sheet ids bound under one parent, id-sorted.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
store: Entity store serving the ENGINE-ECS seam.
|
|
248
|
+
parent_sheet: Parent sheet entity id.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Child sheet entity ids, id-sorted.
|
|
252
|
+
"""
|
|
253
|
+
return sorted(
|
|
254
|
+
item["child_sheet"] for item in _all_bindings(store)
|
|
255
|
+
if item["parent_sheet"] == parent_sheet
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def parent_of(store, child_sheet):
|
|
260
|
+
"""List parent sheet ids binding one child, id-sorted.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
store: Entity store serving the ENGINE-ECS seam.
|
|
264
|
+
child_sheet: Child sheet entity id.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Parent sheet entity ids, id-sorted.
|
|
268
|
+
"""
|
|
269
|
+
return sorted(
|
|
270
|
+
item["parent_sheet"] for item in _all_bindings(store)
|
|
271
|
+
if item["child_sheet"] == child_sheet
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def hierarchy_tree(store, root_sheet):
|
|
276
|
+
"""Render the hierarchy subtree rooted at one sheet.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
store: Entity store serving the ENGINE-ECS seam.
|
|
280
|
+
root_sheet: Root sheet entity id.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Nested mapping with sheet plus children subtrees.
|
|
284
|
+
"""
|
|
285
|
+
return {
|
|
286
|
+
"sheet": root_sheet,
|
|
287
|
+
"children": [
|
|
288
|
+
hierarchy_tree(store, child)
|
|
289
|
+
for child in children_of(store, root_sheet)
|
|
290
|
+
],
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def sheet_path(store, sheet):
|
|
295
|
+
"""Return the root-to-sheet chain through first parents.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
store: Entity store serving the ENGINE-ECS seam.
|
|
299
|
+
sheet: Sheet entity id to locate.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Sheet ids from the root down to the sheet itself.
|
|
303
|
+
"""
|
|
304
|
+
chain = [sheet]
|
|
305
|
+
seen = {sheet}
|
|
306
|
+
while True:
|
|
307
|
+
parents = parent_of(store, chain[0])
|
|
308
|
+
if not parents or parents[0] in seen:
|
|
309
|
+
break
|
|
310
|
+
chain.insert(0, parents[0])
|
|
311
|
+
seen.add(parents[0])
|
|
312
|
+
return chain
|