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,1023 @@
|
|
|
1
|
+
"""Layer stackup + preset serving for the viewer and 3D (LAYER-SERVING).
|
|
2
|
+
|
|
3
|
+
The engine is the queryable rendering authority: per-PDK Stackup
|
|
4
|
+
(ordered layers, metal/tech grouping, enabled set, colors, Z
|
|
5
|
+
heights) served read-only from PDK truth, plus LAYER_PRESET
|
|
6
|
+
records mirroring KiCad's `{layers, renderLayers, flip, active}`
|
|
7
|
+
with built-ins (All/None/All-metal/Top/Bottom), user
|
|
8
|
+
save/overwrite/delete, and explicit apply. Viewer rows derive
|
|
9
|
+
from the same schemas.
|
|
10
|
+
|
|
11
|
+
PDK truth (sky130A in MVP): the registered store entry's KLayout
|
|
12
|
+
tech files — `.lyp` (layer order, colors, valid/visible flags),
|
|
13
|
+
`.lyt` (tech name, database unit), and the xsection `.xs` script
|
|
14
|
+
(film thicknesses for Z heights). Files resolve through
|
|
15
|
+
PDK-STORE's hash-checked `resolve_deck`, so drift fails loudly
|
|
16
|
+
instead of rendering stale fab data. Missing xsect serves
|
|
17
|
+
`z_complete=False` with a reason — the 3D adapter must refuse to
|
|
18
|
+
render heights, never invent them. The PDK remains truth: every
|
|
19
|
+
call re-parses and returns fresh objects, and no API mutates a
|
|
20
|
+
stackup (no user fork in MVP).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import ast
|
|
24
|
+
import copy
|
|
25
|
+
import re
|
|
26
|
+
import xml.etree.ElementTree as ET
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from ..interfaces.errors import ContractError
|
|
30
|
+
from ..interfaces.paths import join_contained, slugify
|
|
31
|
+
from ..interfaces.pdks import PdkError, list_pdks, resolve_deck
|
|
32
|
+
|
|
33
|
+
STACKUP_VERSION = 1
|
|
34
|
+
PRESET_VERSION = 1
|
|
35
|
+
|
|
36
|
+
BUILTIN_PRESETS = ("All", "None", "All-metal", "Top", "Bottom")
|
|
37
|
+
|
|
38
|
+
GROUPS = ("metal", "via", "frontend", "tech", "other")
|
|
39
|
+
|
|
40
|
+
# PDK slug -> store relpaths for the stackup sources. Only sky130A in
|
|
41
|
+
# MVP; a PDK without an entry raises unknown_stackup (loud, with the
|
|
42
|
+
# remedy to extend this table — never a silent empty stackup).
|
|
43
|
+
STACKUP_FILES = {
|
|
44
|
+
"sky130a": {
|
|
45
|
+
"lyp": "libs.tech/klayout/tech/sky130A.lyp",
|
|
46
|
+
"lyt": "libs.tech/klayout/tech/sky130A.lyt",
|
|
47
|
+
"xsect": "libs.tech/klayout/tech/xsect/sky130.xs",
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_METAL_BASES = frozenset({"li1", "met1", "met2", "met3", "met4", "met5"})
|
|
52
|
+
_VIA_BASES = frozenset({"licon1", "mcon", "via", "via2", "via3", "via4"})
|
|
53
|
+
_FRONTEND_BASES = frozenset({
|
|
54
|
+
"diff", "tap", "nwell", "pwell", "dnwell", "poly", "nsdm", "psdm",
|
|
55
|
+
"rpm", "urpm", "npc", "hvntm", "hvi", "hvtr", "hvtp", "lvtn",
|
|
56
|
+
"tunm", "ldntm", "pwbm", "pwde",
|
|
57
|
+
})
|
|
58
|
+
_TECH_BASES = frozenset({"capm", "cap2m", "pad", "nsm", "prBoundary"})
|
|
59
|
+
|
|
60
|
+
# Top-side preset: the top metal plus back-end tech layers.
|
|
61
|
+
_TOP_BASES = frozenset({"met5", "capm", "cap2m", "pad", "nsm", "prBoundary"})
|
|
62
|
+
|
|
63
|
+
# Viewer aids carried on every preset (KiCad's renderLayers analogue:
|
|
64
|
+
# non-stackup toggles the viewer owns). Unknown keys pass through so
|
|
65
|
+
# the viewer can evolve without an engine change.
|
|
66
|
+
_RENDER_DEFAULT = {"grid": True, "cellframe": True, "labels": True, "rulers": True}
|
|
67
|
+
|
|
68
|
+
# Idents that must resolve for z_complete (conductors + contacts).
|
|
69
|
+
_Z_REQUIRED = (
|
|
70
|
+
"poly", "li1", "met1", "met2", "met3", "met4", "met5",
|
|
71
|
+
"licon1", "mcon", "via", "via2", "via3", "via4",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
_XS_INPUT_RE = re.compile(
|
|
75
|
+
r"""^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*layer\(\s*"(\d+)\s*/\s*(\d+)"\s*\)"""
|
|
76
|
+
)
|
|
77
|
+
_XS_ASSIGN_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+?)\s*$")
|
|
78
|
+
_XS_ARITH_CHARS = re.compile(r"[0-9A-Za-z_+\-*/(). \t]+")
|
|
79
|
+
|
|
80
|
+
_BINOPS = (ast.Add, ast.Sub, ast.Mult, ast.Div)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class LayerServingError(ContractError):
|
|
84
|
+
"""Layer-serving violation with a stable machine code.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
code: Stable machine-readable error code.
|
|
88
|
+
message: Human-readable detail.
|
|
89
|
+
fields: Itemized fields, when the error names any.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _group_of(base):
|
|
94
|
+
if base in _METAL_BASES:
|
|
95
|
+
return "metal"
|
|
96
|
+
if base in _VIA_BASES:
|
|
97
|
+
return "via"
|
|
98
|
+
if base in _FRONTEND_BASES:
|
|
99
|
+
return "frontend"
|
|
100
|
+
if base in _TECH_BASES:
|
|
101
|
+
return "tech"
|
|
102
|
+
return "other"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _child_text(node, tag, default=None):
|
|
106
|
+
child = node.find(tag)
|
|
107
|
+
if child is None or child.text is None:
|
|
108
|
+
return default
|
|
109
|
+
return child.text.strip()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def parse_lyp(text):
|
|
113
|
+
"""Parse a KLayout .lyp layer-properties file.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
text: Raw .lyp XML text.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Mapping with file-order entries (name, label, base,
|
|
120
|
+
purpose, layer, datatype, frame, fill, valid,
|
|
121
|
+
visible, order) plus the entry count.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
LayerServingError: invalid_stackup for malformed XML, a
|
|
125
|
+
missing name/source, a bad source, or zero entries.
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
root = ET.fromstring(text)
|
|
129
|
+
except ET.ParseError as exc:
|
|
130
|
+
raise LayerServingError(
|
|
131
|
+
"invalid_stackup", f"Cannot parse .lyp layer properties: {exc}."
|
|
132
|
+
) from exc
|
|
133
|
+
if root.tag != "layer-properties":
|
|
134
|
+
raise LayerServingError(
|
|
135
|
+
"invalid_stackup",
|
|
136
|
+
f".lyp root must be layer-properties, got {root.tag!r}.",
|
|
137
|
+
)
|
|
138
|
+
entries = []
|
|
139
|
+
for order, node in enumerate(root.findall("properties")):
|
|
140
|
+
label = _child_text(node, "name")
|
|
141
|
+
if not label:
|
|
142
|
+
raise LayerServingError(
|
|
143
|
+
"invalid_stackup", ".lyp entry without a name."
|
|
144
|
+
)
|
|
145
|
+
source = _child_text(node, "source")
|
|
146
|
+
if not source:
|
|
147
|
+
raise LayerServingError(
|
|
148
|
+
"invalid_stackup", f".lyp entry without a source: {label}."
|
|
149
|
+
)
|
|
150
|
+
try:
|
|
151
|
+
layer_s, rest = source.split("@", 1)[0].split("/", 1)
|
|
152
|
+
layer, datatype = int(layer_s), int(rest.split("@", 1)[0])
|
|
153
|
+
except (ValueError, IndexError):
|
|
154
|
+
raise LayerServingError(
|
|
155
|
+
"invalid_stackup",
|
|
156
|
+
f".lyp entry has a bad source {source!r}: {label}.",
|
|
157
|
+
) from None
|
|
158
|
+
head = label.rsplit(" - ", 1)[0]
|
|
159
|
+
if "." in head:
|
|
160
|
+
base, purpose = head.split(".", 1)
|
|
161
|
+
else:
|
|
162
|
+
base, purpose = head, ""
|
|
163
|
+
entries.append({
|
|
164
|
+
"name": f"{base}.{purpose}" if purpose else base,
|
|
165
|
+
"label": label,
|
|
166
|
+
"base": base,
|
|
167
|
+
"purpose": purpose,
|
|
168
|
+
"layer": layer,
|
|
169
|
+
"datatype": datatype,
|
|
170
|
+
"frame": _child_text(node, "frame-color", "") or "",
|
|
171
|
+
"fill": _child_text(node, "fill-color", "") or "",
|
|
172
|
+
"valid": _child_text(node, "valid", "true") == "true",
|
|
173
|
+
"visible": _child_text(node, "visible", "true") == "true",
|
|
174
|
+
"order": order,
|
|
175
|
+
})
|
|
176
|
+
if not entries:
|
|
177
|
+
raise LayerServingError(
|
|
178
|
+
"invalid_stackup", ".lyp carries no <properties> entries."
|
|
179
|
+
)
|
|
180
|
+
return {"entries": entries, "count": len(entries)}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def parse_lyt(text):
|
|
184
|
+
"""Parse a KLayout .lyt technology file (tech name + dbu).
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
text: Raw .lyt XML text.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Mapping with tech, dbu_um, and layer_properties.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
LayerServingError: invalid_stackup for malformed XML, a
|
|
194
|
+
missing tech name, or a missing/bad dbu.
|
|
195
|
+
"""
|
|
196
|
+
try:
|
|
197
|
+
root = ET.fromstring(text)
|
|
198
|
+
except ET.ParseError as exc:
|
|
199
|
+
raise LayerServingError(
|
|
200
|
+
"invalid_stackup", f"Cannot parse .lyt technology: {exc}."
|
|
201
|
+
) from exc
|
|
202
|
+
if root.tag != "technology":
|
|
203
|
+
raise LayerServingError(
|
|
204
|
+
"invalid_stackup",
|
|
205
|
+
f".lyt root must be technology, got {root.tag!r}.",
|
|
206
|
+
)
|
|
207
|
+
tech = _child_text(root, "name")
|
|
208
|
+
if not tech:
|
|
209
|
+
raise LayerServingError("invalid_stackup", ".lyt carries no tech name.")
|
|
210
|
+
try:
|
|
211
|
+
dbu = float(_child_text(root, "dbu", ""))
|
|
212
|
+
except (TypeError, ValueError):
|
|
213
|
+
raise LayerServingError(
|
|
214
|
+
"invalid_stackup", ".lyt carries no numeric dbu."
|
|
215
|
+
) from None
|
|
216
|
+
return {
|
|
217
|
+
"tech": tech,
|
|
218
|
+
"dbu_um": dbu,
|
|
219
|
+
"layer_properties": _child_text(root, "layer-properties_file"),
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _strip_xs_comment(line):
|
|
224
|
+
in_string = False
|
|
225
|
+
for index, char in enumerate(line):
|
|
226
|
+
if char == '"':
|
|
227
|
+
in_string = not in_string
|
|
228
|
+
elif char == "#" and not in_string:
|
|
229
|
+
return line[:index]
|
|
230
|
+
return line
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _eval_xs_arith(node, values, where):
|
|
234
|
+
if isinstance(node, ast.Expression):
|
|
235
|
+
return _eval_xs_arith(node.body, values, where)
|
|
236
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
|
|
237
|
+
return float(node.value)
|
|
238
|
+
if isinstance(node, ast.Name):
|
|
239
|
+
if node.id not in values:
|
|
240
|
+
raise LayerServingError(
|
|
241
|
+
"invalid_stackup",
|
|
242
|
+
f".xs references unknown variable {node.id!r} in {where}.",
|
|
243
|
+
)
|
|
244
|
+
return values[node.id]
|
|
245
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
|
|
246
|
+
operand = _eval_xs_arith(node.operand, values, where)
|
|
247
|
+
return operand if isinstance(node.op, ast.UAdd) else -operand
|
|
248
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, _BINOPS):
|
|
249
|
+
left = _eval_xs_arith(node.left, values, where)
|
|
250
|
+
right = _eval_xs_arith(node.right, values, where)
|
|
251
|
+
if isinstance(node.op, ast.Add):
|
|
252
|
+
return left + right
|
|
253
|
+
if isinstance(node.op, ast.Sub):
|
|
254
|
+
return left - right
|
|
255
|
+
if isinstance(node.op, ast.Mult):
|
|
256
|
+
return left * right
|
|
257
|
+
try:
|
|
258
|
+
return left / right
|
|
259
|
+
except ZeroDivisionError:
|
|
260
|
+
raise LayerServingError(
|
|
261
|
+
"invalid_stackup", f".xs divides by zero in {where}."
|
|
262
|
+
) from None
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _xs_spans(values):
|
|
267
|
+
"""Z spans from evaluated xsect variables (sky130 recipe).
|
|
268
|
+
|
|
269
|
+
shortcut: the recipe assumes the klayoutmatthias sky130
|
|
270
|
+
xsection variable shape (li/mN bottoms, ild/mN thicknesses);
|
|
271
|
+
generalize when a second PDK ships xsect.
|
|
272
|
+
"""
|
|
273
|
+
need = values.get
|
|
274
|
+
spans = {}
|
|
275
|
+
|
|
276
|
+
def _put(ident, base, top, thick, source):
|
|
277
|
+
if base is None or top is None or thick is None:
|
|
278
|
+
return
|
|
279
|
+
spans[ident] = {
|
|
280
|
+
"z_base_um": base,
|
|
281
|
+
"z_top_um": top,
|
|
282
|
+
"thickness_um": thick,
|
|
283
|
+
"z_source": f"xsect:{source}",
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
gox, poly_t = need("gox_thickness"), need("poly_thickness")
|
|
287
|
+
_put("poly", gox,
|
|
288
|
+
gox + poly_t if gox is not None and poly_t is not None else None,
|
|
289
|
+
poly_t, "gox_thickness..+poly_thickness")
|
|
290
|
+
li_bottom, li_t = need("li_bottom"), need("li_thickness")
|
|
291
|
+
_put("li1", li_bottom,
|
|
292
|
+
li_bottom + li_t if li_bottom is not None and li_t is not None else None,
|
|
293
|
+
li_t, "li_bottom..+li_thickness")
|
|
294
|
+
bottoms = {n: need(f"m{n}_bottom") for n in range(1, 6)}
|
|
295
|
+
thick = {n: need(f"m{n}_thickness") for n in range(1, 6)}
|
|
296
|
+
for n in range(1, 6):
|
|
297
|
+
ident = f"met{n}"
|
|
298
|
+
base, thick_n = bottoms[n], thick[n]
|
|
299
|
+
_put(ident, base,
|
|
300
|
+
base + thick_n if base is not None and thick_n is not None else None,
|
|
301
|
+
thick_n, f"m{n}_bottom..+m{n}_thickness")
|
|
302
|
+
_put("licon1", 0.0, li_bottom, li_bottom, "0..li_bottom")
|
|
303
|
+
chain = [
|
|
304
|
+
("mcon", li_bottom, bottoms[1], "li_bottom..m1_bottom"),
|
|
305
|
+
("via", bottoms[1], bottoms[2], "m1_bottom..m2_bottom"),
|
|
306
|
+
("via2", bottoms[2], bottoms[3], "m2_bottom..m3_bottom"),
|
|
307
|
+
("via3", bottoms[3], bottoms[4], "m3_bottom..m4_bottom"),
|
|
308
|
+
("via4", bottoms[4], bottoms[5], "m4_bottom..m5_bottom"),
|
|
309
|
+
]
|
|
310
|
+
for ident, base, top, source in chain:
|
|
311
|
+
_put(ident, base, top,
|
|
312
|
+
top - base if base is not None and top is not None else None,
|
|
313
|
+
source)
|
|
314
|
+
capm_depth, capm_t = need("capm_depth"), need("capm_thickness")
|
|
315
|
+
if capm_depth is not None and capm_t is not None and bottoms[4] is not None:
|
|
316
|
+
top = bottoms[4] - capm_depth
|
|
317
|
+
_put("capm", top - capm_t, top, capm_t,
|
|
318
|
+
"m4_bottom-capm_depth-capm_thickness..")
|
|
319
|
+
cap2m_depth, cap2m_t = need("cap2m_depth"), need("cap2m_thickness")
|
|
320
|
+
if cap2m_depth is not None and cap2m_t is not None and bottoms[5] is not None:
|
|
321
|
+
top = bottoms[5] - cap2m_depth
|
|
322
|
+
_put("cap2m", top - cap2m_t, top, cap2m_t,
|
|
323
|
+
"m5_bottom-cap2m_depth-cap2m_thickness..")
|
|
324
|
+
return spans
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def parse_xsect(text):
|
|
328
|
+
"""Parse a KLayout xsection script (inputs + thicknesses + spans).
|
|
329
|
+
|
|
330
|
+
Single-line statements only: `ident = layer("L/D")` inputs and
|
|
331
|
+
pure-arithmetic assignments evaluate; process calls (mask,
|
|
332
|
+
grow, deposit, output, ...) are skipped, never executed.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
text: Raw .xs script text.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Mapping with inputs ({(layer, datatype): ident}),
|
|
339
|
+
values ({variable: float}), and spans ({ident: Z
|
|
340
|
+
span record}).
|
|
341
|
+
|
|
342
|
+
Raises:
|
|
343
|
+
LayerServingError: invalid_stackup when compound
|
|
344
|
+
arithmetic references an unknown variable or divides
|
|
345
|
+
by zero (bare aliases to undefined process handles
|
|
346
|
+
are skipped, never numbers).
|
|
347
|
+
"""
|
|
348
|
+
inputs = {}
|
|
349
|
+
pending = []
|
|
350
|
+
for raw in (text or "").splitlines():
|
|
351
|
+
line = _strip_xs_comment(raw).strip()
|
|
352
|
+
if not line:
|
|
353
|
+
continue
|
|
354
|
+
match = _XS_INPUT_RE.match(line)
|
|
355
|
+
if match:
|
|
356
|
+
ident, layer, datatype = match.groups()
|
|
357
|
+
inputs.setdefault((int(layer), int(datatype)), ident)
|
|
358
|
+
continue
|
|
359
|
+
assign = _XS_ASSIGN_RE.match(line)
|
|
360
|
+
if not assign or _XS_INPUT_RE.match(line):
|
|
361
|
+
continue
|
|
362
|
+
ident, expr = assign.groups()
|
|
363
|
+
if "(" in expr or ")" in expr:
|
|
364
|
+
continue # process code (calls), not arithmetic
|
|
365
|
+
# Bare attribute reads (no call) fall through to the AST shape
|
|
366
|
+
# check below, which skips them as non-arithmetic; float
|
|
367
|
+
# literals legitimately contain dots, so dots never skip.
|
|
368
|
+
if _XS_ARITH_CHARS.fullmatch(expr) is None:
|
|
369
|
+
continue
|
|
370
|
+
try:
|
|
371
|
+
tree = ast.parse(expr, mode="eval")
|
|
372
|
+
except SyntaxError:
|
|
373
|
+
continue
|
|
374
|
+
pending.append((ident, expr, tree))
|
|
375
|
+
values = {}
|
|
376
|
+
remaining = list(pending)
|
|
377
|
+
while remaining:
|
|
378
|
+
progressed = False
|
|
379
|
+
still = []
|
|
380
|
+
for ident, expr, tree in remaining:
|
|
381
|
+
try:
|
|
382
|
+
value = _eval_xs_arith(tree, values, f"{ident} = {expr}")
|
|
383
|
+
except LayerServingError:
|
|
384
|
+
still.append((ident, expr, tree))
|
|
385
|
+
continue
|
|
386
|
+
if value is None:
|
|
387
|
+
continue # non-arithmetic shape; process code
|
|
388
|
+
values[ident] = value
|
|
389
|
+
progressed = True
|
|
390
|
+
remaining = still
|
|
391
|
+
if not progressed:
|
|
392
|
+
compound = [line for line in still
|
|
393
|
+
if not isinstance(line[2].body, ast.Name)]
|
|
394
|
+
if not compound:
|
|
395
|
+
break # bare aliases to process handles; not numbers
|
|
396
|
+
ident, expr, tree = compound[0]
|
|
397
|
+
_eval_xs_arith(tree, values, f"{ident} = {expr}")
|
|
398
|
+
raise LayerServingError( # pragma: no cover (the eval raises)
|
|
399
|
+
"invalid_stackup", f".xs cannot evaluate {ident} = {expr}."
|
|
400
|
+
)
|
|
401
|
+
return {"inputs": inputs, "values": values, "spans": _xs_spans(values)}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def build_stackup(pdk_id, revision, lyp_text, lyt_text=None,
|
|
405
|
+
xsect_text=None, source=None):
|
|
406
|
+
"""Assemble one read-only Stackup record from PDK file texts.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
pdk_id: Canonical PDK display id (echoed in the record).
|
|
410
|
+
revision: PDK revision pin (echoed in the record).
|
|
411
|
+
lyp_text: Raw .lyp layer-properties text (required).
|
|
412
|
+
lyt_text: Raw .lyt technology text; tech/dbu stay None
|
|
413
|
+
when omitted.
|
|
414
|
+
xsect_text: Raw .xs xsection text; heights stay None
|
|
415
|
+
with z_complete=False when omitted.
|
|
416
|
+
source: Provenance mapping (kind + relpaths/hashes);
|
|
417
|
+
{"kind": "inline"} when omitted.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Fresh Stackup record (ordered layers, enabled set,
|
|
421
|
+
groups, z_complete + reason); every call returns
|
|
422
|
+
independent objects — mutating a record never
|
|
423
|
+
affects later calls.
|
|
424
|
+
|
|
425
|
+
Raises:
|
|
426
|
+
LayerServingError: invalid_stackup for a bad id/rev, a
|
|
427
|
+
duplicate layer name, or any unparseable file.
|
|
428
|
+
"""
|
|
429
|
+
if not isinstance(pdk_id, str) or not pdk_id.strip():
|
|
430
|
+
raise LayerServingError("invalid_stackup", "Stackup needs a PDK id.")
|
|
431
|
+
if not isinstance(revision, str) or not revision.strip():
|
|
432
|
+
raise LayerServingError("invalid_stackup", "Stackup needs a revision.")
|
|
433
|
+
entries = parse_lyp(lyp_text)["entries"]
|
|
434
|
+
# Same head on different GDS (e.g. sky130A's four `boundary`
|
|
435
|
+
# entries) disambiguates with @L/D on every colliding entry;
|
|
436
|
+
# same head on the SAME GDS is a corrupt duplicate, loud.
|
|
437
|
+
by_head = {}
|
|
438
|
+
for entry in entries:
|
|
439
|
+
by_head.setdefault(entry["name"], []).append(entry)
|
|
440
|
+
for head, group in by_head.items():
|
|
441
|
+
if len(group) == 1:
|
|
442
|
+
continue
|
|
443
|
+
gdss = {(entry["layer"], entry["datatype"]) for entry in group}
|
|
444
|
+
if len(gdss) != len(group):
|
|
445
|
+
raise LayerServingError(
|
|
446
|
+
"invalid_stackup",
|
|
447
|
+
f"Duplicate layer name in .lyp: {head}.",
|
|
448
|
+
)
|
|
449
|
+
for entry in group:
|
|
450
|
+
entry["name"] = (
|
|
451
|
+
f"{head}@{entry['layer']}/{entry['datatype']}"
|
|
452
|
+
)
|
|
453
|
+
tech = parse_lyt(lyt_text) if lyt_text is not None else {
|
|
454
|
+
"tech": None, "dbu_um": None, "layer_properties": None,
|
|
455
|
+
}
|
|
456
|
+
parsed_xs = parse_xsect(xsect_text) if xsect_text is not None else None
|
|
457
|
+
spans = (parsed_xs or {}).get("spans") or {}
|
|
458
|
+
inputs = (parsed_xs or {}).get("inputs") or {}
|
|
459
|
+
layers = []
|
|
460
|
+
for entry in entries:
|
|
461
|
+
ident = inputs.get((entry["layer"], entry["datatype"]))
|
|
462
|
+
span = spans.get(ident) if ident else None
|
|
463
|
+
layers.append({
|
|
464
|
+
"name": entry["name"],
|
|
465
|
+
"label": entry["label"],
|
|
466
|
+
"base": entry["base"],
|
|
467
|
+
"purpose": entry["purpose"],
|
|
468
|
+
"gds": [entry["layer"], entry["datatype"]],
|
|
469
|
+
"group": _group_of(entry["base"]),
|
|
470
|
+
"color": {"frame": entry["frame"], "fill": entry["fill"]},
|
|
471
|
+
"visible": entry["visible"],
|
|
472
|
+
"enabled": entry["valid"],
|
|
473
|
+
"z_base_um": span["z_base_um"] if span else None,
|
|
474
|
+
"z_top_um": span["z_top_um"] if span else None,
|
|
475
|
+
"thickness_um": span["thickness_um"] if span else None,
|
|
476
|
+
"z_source": span["z_source"] if span else None,
|
|
477
|
+
"stack_index": None,
|
|
478
|
+
})
|
|
479
|
+
ranked = sorted(
|
|
480
|
+
(index for index, layer in enumerate(layers)
|
|
481
|
+
if layer["z_base_um"] is not None),
|
|
482
|
+
key=lambda i: (layers[i]["z_base_um"], i),
|
|
483
|
+
)
|
|
484
|
+
for rank, index in enumerate(ranked):
|
|
485
|
+
layers[index]["stack_index"] = rank
|
|
486
|
+
missing = [ident for ident in _Z_REQUIRED if ident not in spans]
|
|
487
|
+
if xsect_text is None:
|
|
488
|
+
z_reason = "no xsect served: 3D heights unavailable (no guess served)"
|
|
489
|
+
elif missing:
|
|
490
|
+
z_reason = f"incomplete xsect spans for: {', '.join(missing)}"
|
|
491
|
+
else:
|
|
492
|
+
z_reason = ""
|
|
493
|
+
groups = {
|
|
494
|
+
group: [layer["name"] for layer in layers if layer["group"] == group]
|
|
495
|
+
for group in GROUPS
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
"stackup_version": STACKUP_VERSION,
|
|
499
|
+
"pdk_id": pdk_id,
|
|
500
|
+
"revision": revision,
|
|
501
|
+
"source": copy.deepcopy(source) if source is not None else {"kind": "inline"},
|
|
502
|
+
"tech": tech["tech"],
|
|
503
|
+
"dbu_um": tech["dbu_um"],
|
|
504
|
+
"layers": layers,
|
|
505
|
+
"order": "file",
|
|
506
|
+
"enabled": [layer["name"] for layer in layers if layer["enabled"]],
|
|
507
|
+
"groups": groups,
|
|
508
|
+
"z_complete": not missing and xsect_text is not None,
|
|
509
|
+
"z_missing": missing,
|
|
510
|
+
"z_reason": z_reason,
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def get_stackup_from_dir(pdk_id, revision, pdk_dir):
|
|
515
|
+
"""Serve a Stackup by parsing a plain PDK tree directory.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
pdk_id: Canonical PDK display id (selects the relpath map).
|
|
519
|
+
revision: Revision label echoed in the record (the dir is
|
|
520
|
+
not a store entry, so nothing verifies it).
|
|
521
|
+
pdk_dir: PDK tree root holding the mapped relpaths.
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
Fresh Stackup record with pdk-dir provenance.
|
|
525
|
+
|
|
526
|
+
Raises:
|
|
527
|
+
LayerServingError: unknown_stackup for an unmapped PDK or
|
|
528
|
+
a missing .lyp; invalid_stackup for unparseable files.
|
|
529
|
+
"""
|
|
530
|
+
mapping = STACKUP_FILES.get(slugify(pdk_id))
|
|
531
|
+
if mapping is None:
|
|
532
|
+
raise LayerServingError(
|
|
533
|
+
"unknown_stackup",
|
|
534
|
+
f"No layer stackup mapping for PDK {pdk_id!r}; mapped: "
|
|
535
|
+
f"{sorted(STACKUP_FILES)}.",
|
|
536
|
+
)
|
|
537
|
+
base = Path(pdk_dir)
|
|
538
|
+
lyp_path = join_contained(base, mapping["lyp"])
|
|
539
|
+
if not lyp_path.is_file():
|
|
540
|
+
raise LayerServingError(
|
|
541
|
+
"unknown_stackup",
|
|
542
|
+
f"No stackup .lyp for PDK {pdk_id!r}: {mapping['lyp']} not found.",
|
|
543
|
+
)
|
|
544
|
+
texts = {"lyp_text": lyp_path.read_text(encoding="utf-8")}
|
|
545
|
+
source = {"kind": "pdk-dir", "dir": str(base), "lyp": mapping["lyp"]}
|
|
546
|
+
for key in ("lyt", "xsect"):
|
|
547
|
+
path = join_contained(base, mapping[key])
|
|
548
|
+
if path.is_file():
|
|
549
|
+
texts[f"{key}_text"] = path.read_text(encoding="utf-8")
|
|
550
|
+
source[key] = mapping[key]
|
|
551
|
+
return build_stackup(pdk_id, revision, source=source, **texts)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def get_stackup(pdk_id, revision=None, engine_root=None):
|
|
555
|
+
"""Serve a Stackup from a registered PDK store entry.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
pdk_id: Canonical display id (either spelling resolves).
|
|
559
|
+
revision: Revision pin; the newest registered revision
|
|
560
|
+
when omitted (echoed in the record — explicit pins
|
|
561
|
+
stay recommended for runs).
|
|
562
|
+
engine_root: Explicit store root; resolved when omitted.
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
Fresh Stackup record with pdk-store provenance (relpaths
|
|
566
|
+
plus manifest-verified sha256).
|
|
567
|
+
|
|
568
|
+
Raises:
|
|
569
|
+
PdkError: unknown_pdk for a missing store/id/revision or
|
|
570
|
+
.lyp deck, pdk_drift for a tampered file.
|
|
571
|
+
LayerServingError: unknown_stackup for a PDK with no
|
|
572
|
+
stackup mapping.
|
|
573
|
+
"""
|
|
574
|
+
try:
|
|
575
|
+
entries = list_pdks(engine_root=engine_root)
|
|
576
|
+
except ContractError as exc:
|
|
577
|
+
raise PdkError(
|
|
578
|
+
"unknown_pdk", f"Unknown PDK: {pdk_id}@{revision} ({exc})."
|
|
579
|
+
) from exc
|
|
580
|
+
slug = slugify(pdk_id)
|
|
581
|
+
candidates = [e for e in entries if slugify(e.get("id") or "") == slug]
|
|
582
|
+
if revision is None:
|
|
583
|
+
revisions = sorted(e["revision"] for e in candidates)
|
|
584
|
+
if not revisions:
|
|
585
|
+
raise PdkError(
|
|
586
|
+
"unknown_pdk", f"Unknown PDK: {pdk_id} (no store entry)."
|
|
587
|
+
)
|
|
588
|
+
revision = revisions[-1]
|
|
589
|
+
else:
|
|
590
|
+
candidates = [e for e in candidates if e.get("revision") == revision]
|
|
591
|
+
if not candidates:
|
|
592
|
+
raise PdkError(
|
|
593
|
+
"unknown_pdk", f"Unknown PDK: {pdk_id}@{revision} (no store entry)."
|
|
594
|
+
)
|
|
595
|
+
mapping = STACKUP_FILES.get(slug)
|
|
596
|
+
if mapping is None:
|
|
597
|
+
raise LayerServingError(
|
|
598
|
+
"unknown_stackup",
|
|
599
|
+
f"No layer stackup mapping for PDK {pdk_id!r}; mapped: "
|
|
600
|
+
f"{sorted(STACKUP_FILES)}.",
|
|
601
|
+
)
|
|
602
|
+
entry = candidates[0]
|
|
603
|
+
lyp_hit = resolve_deck(entry["id"], revision, mapping["lyp"],
|
|
604
|
+
engine_root=engine_root)
|
|
605
|
+
texts = {"lyp_text": Path(lyp_hit["path"]).read_text(encoding="utf-8")}
|
|
606
|
+
sha256 = {mapping["lyp"]: lyp_hit["sha256"]}
|
|
607
|
+
source = {
|
|
608
|
+
"kind": "pdk-store",
|
|
609
|
+
"store_path": entry["path"],
|
|
610
|
+
"lyp": mapping["lyp"],
|
|
611
|
+
}
|
|
612
|
+
for key in ("lyt", "xsect"):
|
|
613
|
+
try:
|
|
614
|
+
hit = resolve_deck(entry["id"], revision, mapping[key],
|
|
615
|
+
engine_root=engine_root)
|
|
616
|
+
except PdkError as exc:
|
|
617
|
+
if exc.code != "unknown_pdk":
|
|
618
|
+
raise
|
|
619
|
+
continue # optional source absent: served degraded, with reason
|
|
620
|
+
texts[f"{key}_text"] = Path(hit["path"]).read_text(encoding="utf-8")
|
|
621
|
+
sha256[mapping[key]] = hit["sha256"]
|
|
622
|
+
source[key] = mapping[key]
|
|
623
|
+
source["sha256"] = sha256
|
|
624
|
+
return build_stackup(entry["id"], revision, source=source, **texts)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _extreme_drawing(layers, visible, want_max):
|
|
628
|
+
drawing = [layer for layer in layers
|
|
629
|
+
if layer["purpose"] == "drawing"
|
|
630
|
+
and layer["stack_index"] is not None
|
|
631
|
+
and visible.get(layer["name"], False)]
|
|
632
|
+
if not drawing:
|
|
633
|
+
return None
|
|
634
|
+
key = (lambda layer: (layer["stack_index"], layer["name"]))
|
|
635
|
+
return (max if want_max else min)(drawing, key=key)["name"]
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def builtin_preset(stackup, name):
|
|
639
|
+
"""Compute one built-in LAYER_PRESET from a Stackup.
|
|
640
|
+
|
|
641
|
+
All covers every PDK-enabled layer, None hides all, All-metal
|
|
642
|
+
covers the metal + via groups, Top covers the top metal plus
|
|
643
|
+
back-end tech (flip=False, active = topmost visible drawing),
|
|
644
|
+
Bottom covers the frontend group (flip=True, active = lowest
|
|
645
|
+
visible drawing). Without heights the Top/Bottom active stays
|
|
646
|
+
None.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
stackup: Stackup record the preset resolves against.
|
|
650
|
+
name: Built-in preset name (see BUILTIN_PRESETS).
|
|
651
|
+
|
|
652
|
+
Returns:
|
|
653
|
+
Fresh preset record (preset_version, name, builtin,
|
|
654
|
+
stackup_ref, full layers map, renderLayers, flip,
|
|
655
|
+
active).
|
|
656
|
+
|
|
657
|
+
Raises:
|
|
658
|
+
LayerServingError: unknown_preset for a non-built-in name.
|
|
659
|
+
"""
|
|
660
|
+
if name not in BUILTIN_PRESETS:
|
|
661
|
+
raise LayerServingError(
|
|
662
|
+
"unknown_preset",
|
|
663
|
+
f"Unknown built-in preset {name!r}; built-ins: "
|
|
664
|
+
f"{', '.join(BUILTIN_PRESETS)}.",
|
|
665
|
+
)
|
|
666
|
+
layers = stackup["layers"]
|
|
667
|
+
if name == "All":
|
|
668
|
+
visible = {layer["name"]: layer["enabled"] for layer in layers}
|
|
669
|
+
elif name == "None":
|
|
670
|
+
visible = {layer["name"]: False for layer in layers}
|
|
671
|
+
elif name == "All-metal":
|
|
672
|
+
visible = {
|
|
673
|
+
layer["name"]: layer["enabled"] and layer["group"] in ("metal", "via")
|
|
674
|
+
for layer in layers
|
|
675
|
+
}
|
|
676
|
+
elif name == "Top":
|
|
677
|
+
visible = {
|
|
678
|
+
layer["name"]: layer["enabled"] and layer["base"] in _TOP_BASES
|
|
679
|
+
for layer in layers
|
|
680
|
+
}
|
|
681
|
+
else:
|
|
682
|
+
visible = {
|
|
683
|
+
layer["name"]: layer["enabled"] and layer["group"] == "frontend"
|
|
684
|
+
for layer in layers
|
|
685
|
+
}
|
|
686
|
+
if name == "Top":
|
|
687
|
+
active = _extreme_drawing(layers, visible, True)
|
|
688
|
+
elif name == "Bottom":
|
|
689
|
+
active = _extreme_drawing(layers, visible, False)
|
|
690
|
+
else:
|
|
691
|
+
active = None
|
|
692
|
+
return {
|
|
693
|
+
"preset_version": PRESET_VERSION,
|
|
694
|
+
"name": name,
|
|
695
|
+
"builtin": True,
|
|
696
|
+
"stackup_ref": {
|
|
697
|
+
"pdk_id": stackup["pdk_id"], "revision": stackup["revision"],
|
|
698
|
+
},
|
|
699
|
+
"layers": visible,
|
|
700
|
+
"renderLayers": dict(_RENDER_DEFAULT),
|
|
701
|
+
"flip": name == "Bottom",
|
|
702
|
+
"active": active,
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
class PresetStore:
|
|
707
|
+
"""In-memory user layer presets plus the computed built-ins.
|
|
708
|
+
|
|
709
|
+
Built-ins compute from the stackup on every apply (never stale,
|
|
710
|
+
never overwritten, never deleted). User presets validate
|
|
711
|
+
against the stackup at save and at apply: stale names fail
|
|
712
|
+
loudly instead of half-applying.
|
|
713
|
+
"""
|
|
714
|
+
|
|
715
|
+
def __init__(self):
|
|
716
|
+
self._user = {}
|
|
717
|
+
|
|
718
|
+
def save(self, stackup, name, layers, render_layers=None,
|
|
719
|
+
flip=False, active=None, overwrite=False):
|
|
720
|
+
"""Save a user preset (sparse visibility map + view state).
|
|
721
|
+
|
|
722
|
+
Missing names default to hidden (KiCad LSET semantics: the
|
|
723
|
+
map is the visible set); apply() resolves the full map.
|
|
724
|
+
|
|
725
|
+
Args:
|
|
726
|
+
stackup: Stackup record names validate against.
|
|
727
|
+
name: Preset name (stored stripped; built-in names
|
|
728
|
+
are reserved).
|
|
729
|
+
layers: Sparse {layer name: visible} map.
|
|
730
|
+
render_layers: {toggle: bool} viewer aids; the
|
|
731
|
+
default set when omitted (unknown keys pass
|
|
732
|
+
through for viewer evolution).
|
|
733
|
+
flip: View-from-bottom flag.
|
|
734
|
+
active: Active layer name, or None.
|
|
735
|
+
overwrite: Replace an existing user preset of the
|
|
736
|
+
same name instead of conflicting.
|
|
737
|
+
|
|
738
|
+
Returns:
|
|
739
|
+
Stored preset record (builtin False).
|
|
740
|
+
|
|
741
|
+
Raises:
|
|
742
|
+
LayerServingError: invalid_preset for a bad name,
|
|
743
|
+
unknown layer names, or a bad render/flip/active
|
|
744
|
+
shape; preset_conflict for a built-in name or an
|
|
745
|
+
existing user preset without overwrite.
|
|
746
|
+
"""
|
|
747
|
+
known = {layer["name"] for layer in stackup["layers"]}
|
|
748
|
+
if not isinstance(name, str) or not name.strip():
|
|
749
|
+
raise LayerServingError("invalid_preset", "Preset needs a name.")
|
|
750
|
+
stored = name.strip()
|
|
751
|
+
if stored in BUILTIN_PRESETS:
|
|
752
|
+
raise LayerServingError(
|
|
753
|
+
"preset_conflict",
|
|
754
|
+
f"Built-in preset {stored!r} cannot be overwritten.",
|
|
755
|
+
)
|
|
756
|
+
if not isinstance(layers, dict):
|
|
757
|
+
raise LayerServingError("invalid_preset", "Preset layers must be a map.")
|
|
758
|
+
unknown = sorted(n for n in layers if n not in known)
|
|
759
|
+
if unknown:
|
|
760
|
+
raise LayerServingError(
|
|
761
|
+
"invalid_preset",
|
|
762
|
+
f"Preset {stored!r} names unknown layers: "
|
|
763
|
+
f"{', '.join(unknown)}.",
|
|
764
|
+
)
|
|
765
|
+
for layer_name, value in layers.items():
|
|
766
|
+
if type(value) is not bool:
|
|
767
|
+
raise LayerServingError(
|
|
768
|
+
"invalid_preset",
|
|
769
|
+
f"Preset {stored!r} layer {layer_name!r} must be a bool.",
|
|
770
|
+
)
|
|
771
|
+
if render_layers is None:
|
|
772
|
+
render = dict(_RENDER_DEFAULT)
|
|
773
|
+
else:
|
|
774
|
+
if not isinstance(render_layers, dict):
|
|
775
|
+
raise LayerServingError(
|
|
776
|
+
"invalid_preset", "Preset renderLayers must be a map."
|
|
777
|
+
)
|
|
778
|
+
for key, value in render_layers.items():
|
|
779
|
+
if not isinstance(key, str) or not key or type(value) is not bool:
|
|
780
|
+
raise LayerServingError(
|
|
781
|
+
"invalid_preset",
|
|
782
|
+
f"Preset {stored!r} renderLayers must map names to bools.",
|
|
783
|
+
)
|
|
784
|
+
render = dict(render_layers)
|
|
785
|
+
if type(flip) is not bool:
|
|
786
|
+
raise LayerServingError(
|
|
787
|
+
"invalid_preset", f"Preset {stored!r} flip must be a bool."
|
|
788
|
+
)
|
|
789
|
+
if active is not None and active not in known:
|
|
790
|
+
raise LayerServingError(
|
|
791
|
+
"invalid_preset",
|
|
792
|
+
f"Preset {stored!r} names unknown active layer {active!r}.",
|
|
793
|
+
)
|
|
794
|
+
if stored in self._user and not overwrite:
|
|
795
|
+
raise LayerServingError(
|
|
796
|
+
"preset_conflict",
|
|
797
|
+
f"Preset {stored!r} already exists (overwrite to replace).",
|
|
798
|
+
)
|
|
799
|
+
record = {
|
|
800
|
+
"preset_version": PRESET_VERSION,
|
|
801
|
+
"name": stored,
|
|
802
|
+
"builtin": False,
|
|
803
|
+
"stackup_ref": {
|
|
804
|
+
"pdk_id": stackup["pdk_id"], "revision": stackup["revision"],
|
|
805
|
+
},
|
|
806
|
+
"layers": dict(layers),
|
|
807
|
+
"renderLayers": render,
|
|
808
|
+
"flip": flip,
|
|
809
|
+
"active": active,
|
|
810
|
+
}
|
|
811
|
+
self._user[stored] = record
|
|
812
|
+
return copy.deepcopy(record)
|
|
813
|
+
|
|
814
|
+
def delete(self, name):
|
|
815
|
+
"""Delete one user preset (built-ins are undeletable).
|
|
816
|
+
|
|
817
|
+
Args:
|
|
818
|
+
name: Preset name to delete.
|
|
819
|
+
|
|
820
|
+
Returns:
|
|
821
|
+
Deletion record with the deleted name.
|
|
822
|
+
|
|
823
|
+
Raises:
|
|
824
|
+
LayerServingError: preset_conflict for a built-in
|
|
825
|
+
name; unknown_preset for a missing user preset.
|
|
826
|
+
"""
|
|
827
|
+
if name in BUILTIN_PRESETS:
|
|
828
|
+
raise LayerServingError(
|
|
829
|
+
"preset_conflict", f"Built-in preset {name!r} cannot be deleted."
|
|
830
|
+
)
|
|
831
|
+
if name not in self._user:
|
|
832
|
+
raise LayerServingError("unknown_preset", f"Unknown preset {name!r}.")
|
|
833
|
+
del self._user[name]
|
|
834
|
+
return {"deleted": name}
|
|
835
|
+
|
|
836
|
+
def list_presets(self):
|
|
837
|
+
"""List built-in plus user preset names.
|
|
838
|
+
|
|
839
|
+
Returns:
|
|
840
|
+
Name records (name + builtin), built-ins first in
|
|
841
|
+
pinned order, then user presets sorted.
|
|
842
|
+
"""
|
|
843
|
+
records = [{"name": name, "builtin": True} for name in BUILTIN_PRESETS]
|
|
844
|
+
records += [
|
|
845
|
+
{"name": name, "builtin": False} for name in sorted(self._user)
|
|
846
|
+
]
|
|
847
|
+
return records
|
|
848
|
+
|
|
849
|
+
def apply(self, stackup, name):
|
|
850
|
+
"""Explicitly apply one preset to a Stackup.
|
|
851
|
+
|
|
852
|
+
Args:
|
|
853
|
+
stackup: Stackup record the preset resolves against
|
|
854
|
+
(echoed as stackup_ref).
|
|
855
|
+
name: Built-in or user preset name.
|
|
856
|
+
|
|
857
|
+
Returns:
|
|
858
|
+
Full preset record with the complete layers map,
|
|
859
|
+
renderLayers, flip, and active.
|
|
860
|
+
|
|
861
|
+
Raises:
|
|
862
|
+
LayerServingError: unknown_preset for a missing name;
|
|
863
|
+
invalid_preset when a user preset names layers
|
|
864
|
+
the stackup no longer serves.
|
|
865
|
+
"""
|
|
866
|
+
if name in BUILTIN_PRESETS:
|
|
867
|
+
return builtin_preset(stackup, name)
|
|
868
|
+
if name not in self._user:
|
|
869
|
+
raise LayerServingError("unknown_preset", f"Unknown preset {name!r}.")
|
|
870
|
+
stored = self._user[name]
|
|
871
|
+
known = {layer["name"] for layer in stackup["layers"]}
|
|
872
|
+
unknown = sorted(n for n in stored["layers"] if n not in known)
|
|
873
|
+
if unknown:
|
|
874
|
+
raise LayerServingError(
|
|
875
|
+
"invalid_preset",
|
|
876
|
+
f"Preset {name!r} names layers this stackup no longer "
|
|
877
|
+
f"serves: {', '.join(unknown)}.",
|
|
878
|
+
)
|
|
879
|
+
if stored["active"] is not None and stored["active"] not in known:
|
|
880
|
+
raise LayerServingError(
|
|
881
|
+
"invalid_preset",
|
|
882
|
+
f"Preset {name!r} names an active layer this stackup no "
|
|
883
|
+
f"longer serves: {stored['active']!r}.",
|
|
884
|
+
)
|
|
885
|
+
return {
|
|
886
|
+
"preset_version": PRESET_VERSION,
|
|
887
|
+
"name": name,
|
|
888
|
+
"builtin": False,
|
|
889
|
+
"stackup_ref": {
|
|
890
|
+
"pdk_id": stackup["pdk_id"], "revision": stackup["revision"],
|
|
891
|
+
},
|
|
892
|
+
"layers": {
|
|
893
|
+
layer["name"]: stored["layers"].get(layer["name"], False)
|
|
894
|
+
for layer in stackup["layers"]
|
|
895
|
+
},
|
|
896
|
+
"renderLayers": copy.deepcopy(stored["renderLayers"]),
|
|
897
|
+
"flip": stored["flip"],
|
|
898
|
+
"active": stored["active"],
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def viewer_rows(stackup, visibility=None, active=None):
|
|
903
|
+
"""Render viewer rows for a Stackup (visibility + color + active).
|
|
904
|
+
|
|
905
|
+
Args:
|
|
906
|
+
stackup: Stackup record rows derive from.
|
|
907
|
+
visibility: Sparse {layer name: visible} overrides over
|
|
908
|
+
the PDK defaults; all defaults when omitted.
|
|
909
|
+
active: Active layer name, or None.
|
|
910
|
+
|
|
911
|
+
Returns:
|
|
912
|
+
Page with stackup_ref, flip False, the active name, and
|
|
913
|
+
file-order rows (name, label, gds, group, purpose,
|
|
914
|
+
color, visible, active, enabled, stack_index, Z).
|
|
915
|
+
|
|
916
|
+
Raises:
|
|
917
|
+
LayerServingError: invalid_visibility for unknown layer
|
|
918
|
+
names or a non-bool/non-map shape.
|
|
919
|
+
"""
|
|
920
|
+
known = {layer["name"] for layer in stackup["layers"]}
|
|
921
|
+
if visibility is None:
|
|
922
|
+
overrides = {}
|
|
923
|
+
else:
|
|
924
|
+
if not isinstance(visibility, dict):
|
|
925
|
+
raise LayerServingError(
|
|
926
|
+
"invalid_visibility", "Visibility overrides must be a map."
|
|
927
|
+
)
|
|
928
|
+
unknown = sorted(n for n in visibility if n not in known)
|
|
929
|
+
if unknown:
|
|
930
|
+
raise LayerServingError(
|
|
931
|
+
"invalid_visibility",
|
|
932
|
+
f"Unknown layers in visibility: {', '.join(unknown)}.",
|
|
933
|
+
)
|
|
934
|
+
for layer_name, value in visibility.items():
|
|
935
|
+
if type(value) is not bool:
|
|
936
|
+
raise LayerServingError(
|
|
937
|
+
"invalid_visibility",
|
|
938
|
+
f"Visibility for {layer_name!r} must be a bool.",
|
|
939
|
+
)
|
|
940
|
+
overrides = visibility
|
|
941
|
+
if active is not None and active not in known:
|
|
942
|
+
raise LayerServingError(
|
|
943
|
+
"invalid_visibility", f"Unknown active layer {active!r}."
|
|
944
|
+
)
|
|
945
|
+
rows = []
|
|
946
|
+
for layer in stackup["layers"]:
|
|
947
|
+
rows.append({
|
|
948
|
+
"name": layer["name"],
|
|
949
|
+
"label": layer["label"],
|
|
950
|
+
"gds": list(layer["gds"]),
|
|
951
|
+
"group": layer["group"],
|
|
952
|
+
"purpose": layer["purpose"],
|
|
953
|
+
"color": dict(layer["color"]),
|
|
954
|
+
"visible": overrides.get(layer["name"], layer["visible"]),
|
|
955
|
+
"active": layer["name"] == active,
|
|
956
|
+
"enabled": layer["enabled"],
|
|
957
|
+
"stack_index": layer["stack_index"],
|
|
958
|
+
"z_base_um": layer["z_base_um"],
|
|
959
|
+
"z_top_um": layer["z_top_um"],
|
|
960
|
+
})
|
|
961
|
+
return {
|
|
962
|
+
"stackup_ref": {
|
|
963
|
+
"pdk_id": stackup["pdk_id"], "revision": stackup["revision"],
|
|
964
|
+
},
|
|
965
|
+
"flip": False,
|
|
966
|
+
"active": active,
|
|
967
|
+
"rows": rows,
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def viewer_rows_for_preset(store, stackup, name, overrides=None, active=None):
|
|
972
|
+
"""Render viewer rows with one preset explicitly applied.
|
|
973
|
+
|
|
974
|
+
Args:
|
|
975
|
+
store: PresetStore holding user presets.
|
|
976
|
+
stackup: Stackup record rows derive from.
|
|
977
|
+
name: Built-in or user preset name.
|
|
978
|
+
overrides: Sparse {layer name: visible} wins over the
|
|
979
|
+
preset map; never clears the preset flip.
|
|
980
|
+
active: Active layer override; the preset active when
|
|
981
|
+
omitted.
|
|
982
|
+
|
|
983
|
+
Returns:
|
|
984
|
+
Page with stackup_ref, preset name, flip, active, and
|
|
985
|
+
file-order rows.
|
|
986
|
+
|
|
987
|
+
Raises:
|
|
988
|
+
LayerServingError: unknown_preset for a missing preset;
|
|
989
|
+
invalid_preset for a stale user preset;
|
|
990
|
+
invalid_visibility for bad overrides/active.
|
|
991
|
+
"""
|
|
992
|
+
preset = store.apply(stackup, name)
|
|
993
|
+
merged = dict(preset["layers"])
|
|
994
|
+
if overrides:
|
|
995
|
+
merged.update(overrides)
|
|
996
|
+
page = viewer_rows(
|
|
997
|
+
stackup,
|
|
998
|
+
visibility=merged,
|
|
999
|
+
active=preset["active"] if active is None else active,
|
|
1000
|
+
)
|
|
1001
|
+
page["flip"] = preset["flip"]
|
|
1002
|
+
page["preset"] = name
|
|
1003
|
+
return page
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
__all__ = [
|
|
1007
|
+
"BUILTIN_PRESETS",
|
|
1008
|
+
"GROUPS",
|
|
1009
|
+
"PRESET_VERSION",
|
|
1010
|
+
"STACKUP_FILES",
|
|
1011
|
+
"STACKUP_VERSION",
|
|
1012
|
+
"LayerServingError",
|
|
1013
|
+
"PresetStore",
|
|
1014
|
+
"build_stackup",
|
|
1015
|
+
"builtin_preset",
|
|
1016
|
+
"get_stackup",
|
|
1017
|
+
"get_stackup_from_dir",
|
|
1018
|
+
"parse_lyp",
|
|
1019
|
+
"parse_lyt",
|
|
1020
|
+
"parse_xsect",
|
|
1021
|
+
"viewer_rows",
|
|
1022
|
+
"viewer_rows_for_preset",
|
|
1023
|
+
]
|