layout-canvas 0.1.0__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.
- layout_canvas/__init__.py +14 -0
- layout_canvas/blocks/__init__.py +25 -0
- layout_canvas/blocks/base.py +184 -0
- layout_canvas/blocks/cells.py +70 -0
- layout_canvas/blocks/gds_cell.py +222 -0
- layout_canvas/blocks/generic.py +949 -0
- layout_canvas/blocks/ihp_sg13g2/__init__.py +17 -0
- layout_canvas/blocks/ihp_sg13g2/current_mirror.py +224 -0
- layout_canvas/blocks/ihp_sg13g2/diff_pair.py +244 -0
- layout_canvas/blocks/ihp_sg13g2/geom.py +41 -0
- layout_canvas/blocks/ihp_sg13g2/guard_ring.py +100 -0
- layout_canvas/blocks/ihp_sg13g2/layers.py +35 -0
- layout_canvas/blocks/ihp_sg13g2/ota_5t.py +413 -0
- layout_canvas/blocks/sky130/__init__.py +25 -0
- layout_canvas/blocks/sky130/cap_array.py +137 -0
- layout_canvas/blocks/sky130/current_mirror.py +269 -0
- layout_canvas/blocks/sky130/diff_pair.py +306 -0
- layout_canvas/blocks/sky130/geom.py +58 -0
- layout_canvas/blocks/sky130/guard_ring.py +244 -0
- layout_canvas/blocks/sky130/layers.py +41 -0
- layout_canvas/blocks/sky130/ota_5t.py +476 -0
- layout_canvas/blocks/sky130/strongarm.py +497 -0
- layout_canvas/cli.py +457 -0
- layout_canvas/compiler/__init__.py +6 -0
- layout_canvas/compiler/compile.py +233 -0
- layout_canvas/compiler/hierarchy.py +61 -0
- layout_canvas/compiler/netlist.py +182 -0
- layout_canvas/compiler/netlist_import.py +635 -0
- layout_canvas/compiler/optimizer.py +59 -0
- layout_canvas/compiler/ppa.py +101 -0
- layout_canvas/compiler/render.py +120 -0
- layout_canvas/compiler/router.py +533 -0
- layout_canvas/compiler/virtuoso.py +297 -0
- layout_canvas/derived/__init__.py +5 -0
- layout_canvas/derived/connectivity.py +100 -0
- layout_canvas/engine/__init__.py +15 -0
- layout_canvas/engine/edits.py +281 -0
- layout_canvas/engine/envelope.py +61 -0
- layout_canvas/engine/optimize.py +335 -0
- layout_canvas/engine/session.py +117 -0
- layout_canvas/ir/__init__.py +37 -0
- layout_canvas/ir/model.py +228 -0
- layout_canvas/mcp/__init__.py +12 -0
- layout_canvas/mcp/bridge.py +172 -0
- layout_canvas/mcp/server.py +1275 -0
- layout_canvas/pdk/__init__.py +27 -0
- layout_canvas/pdk/descriptor.py +639 -0
- layout_canvas/protocol/__init__.py +19 -0
- layout_canvas/protocol/project.py +157 -0
- layout_canvas/tools/__init__.py +6 -0
- layout_canvas/tools/backends.py +294 -0
- layout_canvas/tools/drc.py +290 -0
- layout_canvas/tools/extract.py +405 -0
- layout_canvas/tools/lvs.py +334 -0
- layout_canvas/tools/sim.py +827 -0
- layout_canvas/tools/testbench.py +264 -0
- layout_canvas/tools/verify.py +53 -0
- layout_canvas/tools/virtuoso_check.py +844 -0
- layout_canvas/web/__init__.py +5 -0
- layout_canvas/web/app.py +472 -0
- layout_canvas/web/gallery.py +229 -0
- layout_canvas/web/page.py +893 -0
- layout_canvas-0.1.0.dist-info/METADATA +184 -0
- layout_canvas-0.1.0.dist-info/RECORD +68 -0
- layout_canvas-0.1.0.dist-info/WHEEL +4 -0
- layout_canvas-0.1.0.dist-info/entry_points.txt +2 -0
- layout_canvas-0.1.0.dist-info/licenses/LICENSE +21 -0
- layout_canvas-0.1.0.dist-info/licenses/NOTICE +23 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Package initialization."""
|
|
2
|
+
|
|
3
|
+
import gdsfactory as gf
|
|
4
|
+
|
|
5
|
+
# Ensure gdsfactory has an active generic PDK default for block generators and compilation
|
|
6
|
+
try:
|
|
7
|
+
gf.get_active_pdk()
|
|
8
|
+
except Exception:
|
|
9
|
+
gf.gpdk.PDK.activate()
|
|
10
|
+
|
|
11
|
+
from layout_canvas.blocks import base
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
__all__ = ["base"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Block package initialization."""
|
|
2
|
+
|
|
3
|
+
from layout_canvas.blocks.base import (
|
|
4
|
+
Block,
|
|
5
|
+
all_blocks,
|
|
6
|
+
get,
|
|
7
|
+
on_pdk_registered,
|
|
8
|
+
register,
|
|
9
|
+
unregister,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# Importing the package activates the descriptor-driven ``gen_*``
|
|
13
|
+
# generators: ``generic`` installs its PDK-registration hook on import and
|
|
14
|
+
# replays every PDK already in the registry, so block availability is
|
|
15
|
+
# load-order independent.
|
|
16
|
+
from layout_canvas.blocks import generic # noqa: F401
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Block",
|
|
20
|
+
"get",
|
|
21
|
+
"register",
|
|
22
|
+
"unregister",
|
|
23
|
+
"all_blocks",
|
|
24
|
+
"on_pdk_registered",
|
|
25
|
+
]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Block registry.
|
|
2
|
+
|
|
3
|
+
A *block* is a parametric layout generator plus its contract: parameter schema,
|
|
4
|
+
port list, constraints it guarantees internally and a SPICE netlist emitter.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import gdsfactory as gf
|
|
14
|
+
|
|
15
|
+
from layout_canvas.ir.model import BlockSpec, ParamSpec
|
|
16
|
+
|
|
17
|
+
BuildFn = Callable[..., gf.Component]
|
|
18
|
+
NetlistFn = Callable[..., str]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class Block:
|
|
23
|
+
spec: BlockSpec
|
|
24
|
+
build: BuildFn
|
|
25
|
+
netlist: NetlistFn | None = None
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def name(self) -> str:
|
|
29
|
+
return self.spec.name
|
|
30
|
+
|
|
31
|
+
def defaults(self) -> dict[str, Any]:
|
|
32
|
+
return {p.name: p.default for p in self.spec.params}
|
|
33
|
+
|
|
34
|
+
def resolve_params(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
35
|
+
"""Merge user params with defaults, coerce types and range-check."""
|
|
36
|
+
specs = {p.name: p for p in self.spec.params}
|
|
37
|
+
unknown = set(params) - set(specs)
|
|
38
|
+
if unknown:
|
|
39
|
+
raise ValueError(f"{self.name}: unknown params {sorted(unknown)}")
|
|
40
|
+
out: dict[str, Any] = {}
|
|
41
|
+
for name, ps in specs.items():
|
|
42
|
+
out[name] = _coerce(ps, params.get(name, ps.default), self.name)
|
|
43
|
+
return out
|
|
44
|
+
|
|
45
|
+
def component(self, **params: Any) -> gf.Component:
|
|
46
|
+
resolved = self.resolve_params(params)
|
|
47
|
+
key = (self.name, tuple(sorted(resolved.items())))
|
|
48
|
+
cached = _COMPONENT_CACHE.get(key)
|
|
49
|
+
if cached is not None:
|
|
50
|
+
return cached
|
|
51
|
+
component = self.build(**resolved)
|
|
52
|
+
_COMPONENT_CACHE[key] = component
|
|
53
|
+
return component
|
|
54
|
+
|
|
55
|
+
def spice(self, **params: Any) -> str:
|
|
56
|
+
if self.netlist is None:
|
|
57
|
+
raise NotImplementedError(f"{self.name} has no netlist emitter")
|
|
58
|
+
return self.netlist(**self.resolve_params(params))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _coerce(ps: ParamSpec, value: Any, block: str) -> Any:
|
|
62
|
+
try:
|
|
63
|
+
if ps.type == "int":
|
|
64
|
+
if isinstance(value, float) and not value.is_integer():
|
|
65
|
+
raise ValueError
|
|
66
|
+
v: Any = int(value)
|
|
67
|
+
elif ps.type == "float":
|
|
68
|
+
v = float(value)
|
|
69
|
+
elif ps.type == "bool":
|
|
70
|
+
v = value if isinstance(value, bool) else str(value).lower() in ("1", "true", "yes")
|
|
71
|
+
else:
|
|
72
|
+
v = str(value)
|
|
73
|
+
except (TypeError, ValueError) as e:
|
|
74
|
+
raise ValueError(f"{block}.{ps.name}: expected {ps.type}, got {value!r}") from e
|
|
75
|
+
if ps.min is not None and v < ps.min:
|
|
76
|
+
raise ValueError(f"{block}.{ps.name}={v} below min {ps.min}")
|
|
77
|
+
if ps.max is not None and v > ps.max:
|
|
78
|
+
raise ValueError(f"{block}.{ps.name}={v} above max {ps.max}")
|
|
79
|
+
if ps.choices is not None and v not in ps.choices:
|
|
80
|
+
raise ValueError(f"{block}.{ps.name}={v!r} not in {ps.choices}")
|
|
81
|
+
return v
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_REGISTRY: dict[str, Block] = {}
|
|
85
|
+
# gdsfactory stores cells in a process-wide KCLayout and rejects duplicate
|
|
86
|
+
# cell names. Repeated compilation in an MCP session must therefore reuse the
|
|
87
|
+
# immutable component for an identical block/parameter tuple.
|
|
88
|
+
_COMPONENT_CACHE: dict[tuple[str, tuple[tuple[str, Any], ...]], gf.Component] = {}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def register(spec: BlockSpec, netlist: NetlistFn | None = None) -> Callable[[BuildFn], BuildFn]:
|
|
92
|
+
def deco(fn: BuildFn) -> BuildFn:
|
|
93
|
+
_REGISTRY[spec.name] = Block(spec=spec, build=fn, netlist=netlist)
|
|
94
|
+
return fn
|
|
95
|
+
|
|
96
|
+
return deco
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def unregister(name: str) -> None:
|
|
100
|
+
"""Drop a block and its cached components (generated-block removal)."""
|
|
101
|
+
_REGISTRY.pop(name, None)
|
|
102
|
+
for key in [k for k in _COMPONENT_CACHE if k[0] == name]:
|
|
103
|
+
_COMPONENT_CACHE.pop(key, None)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _sync_external_pdks() -> None:
|
|
107
|
+
"""Re-scan the external-PDK env hooks when they changed.
|
|
108
|
+
|
|
109
|
+
External descriptors load lazily; their ``gen_*`` blocks register
|
|
110
|
+
through the ``on_pdk_registered`` hook during that scan, so a caller
|
|
111
|
+
that never touched ``get_pdk`` still sees them.
|
|
112
|
+
"""
|
|
113
|
+
from layout_canvas.pdk.descriptor import _ensure_external
|
|
114
|
+
|
|
115
|
+
_ensure_external()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def on_pdk_registered(
|
|
119
|
+
cb: Callable[[Any], None],
|
|
120
|
+
*,
|
|
121
|
+
on_removed: Callable[[str], None] | None = None,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Subscribe ``cb(pdk)`` to PDK registrations — present and future.
|
|
124
|
+
|
|
125
|
+
The external-PDK scan runs first, so ``cb`` sees every currently
|
|
126
|
+
registered PDK exactly once (import-order independent), then subscribes
|
|
127
|
+
for future ``register_pdk`` calls. ``on_removed`` (optional) fires with
|
|
128
|
+
a name when an external PDK leaves the registry on an env rescan.
|
|
129
|
+
"""
|
|
130
|
+
from layout_canvas.pdk import all_pdks
|
|
131
|
+
from layout_canvas.pdk.descriptor import (
|
|
132
|
+
add_pdk_listener,
|
|
133
|
+
add_pdk_removed_listener,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
existing = list(all_pdks().values()) # triggers the lazy external scan
|
|
137
|
+
add_pdk_listener(cb)
|
|
138
|
+
if on_removed is not None:
|
|
139
|
+
add_pdk_removed_listener(on_removed)
|
|
140
|
+
for pdk in existing:
|
|
141
|
+
cb(pdk)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get(name: str) -> Block:
|
|
145
|
+
_sync_external_pdks()
|
|
146
|
+
try:
|
|
147
|
+
return _REGISTRY[name]
|
|
148
|
+
except KeyError:
|
|
149
|
+
raise KeyError(f"unknown block {name!r}; known: {sorted(_REGISTRY)}") from None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def describe_pdk_block_gap(pdk_name: str) -> str | None:
|
|
153
|
+
"""Explain a missing block in terms of the PDK registry.
|
|
154
|
+
|
|
155
|
+
A JSON-loaded (descriptor-only) PDK has no parametric generators — the
|
|
156
|
+
honest answer names that boundary instead of a bare "unknown block".
|
|
157
|
+
Returns None when the PDK is unknown or already has blocks.
|
|
158
|
+
"""
|
|
159
|
+
from layout_canvas.pdk import get_pdk
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
get_pdk(pdk_name)
|
|
163
|
+
except KeyError:
|
|
164
|
+
return None
|
|
165
|
+
if any(b.spec.pdk == pdk_name for b in _REGISTRY.values()):
|
|
166
|
+
return None
|
|
167
|
+
return (
|
|
168
|
+
f"no blocks registered for pdk {pdk_name!r} — descriptor PDKs get "
|
|
169
|
+
"gen_diff_pair/gen_current_mirror/gen_guard_ring automatically when "
|
|
170
|
+
"extract.roles covers the contact stack; this one does not. Bring "
|
|
171
|
+
"cells in via import_gds or register_cell, or write a Python block "
|
|
172
|
+
"generator"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def all_blocks() -> dict[str, Block]:
|
|
177
|
+
_sync_external_pdks()
|
|
178
|
+
return dict(_REGISTRY)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def clear_component_cache() -> None:
|
|
182
|
+
"""Release cached Python references without invalidating the global layout."""
|
|
183
|
+
|
|
184
|
+
_COMPONENT_CACHE.clear()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Register a compiled Design as a reusable cell block.
|
|
2
|
+
|
|
3
|
+
This is the consumption half of ``compiler/hierarchy.cell_abstract``: a
|
|
4
|
+
design compiled once can be instantiated by a parent design as
|
|
5
|
+
``block: "<alias>"`` after registration. The cell's PortSpec contract is
|
|
6
|
+
derived from the child design's formal ports.
|
|
7
|
+
|
|
8
|
+
The cell's netlist emitter flattens the child's full hierarchical netlist
|
|
9
|
+
into the parent deck: every primitive subckt is inlined and the child's top
|
|
10
|
+
``.subckt`` is retargeted to this block's name, so ``simulate_design`` can
|
|
11
|
+
run real device-level simulation instead of refusing.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import gdsfactory as gf
|
|
17
|
+
|
|
18
|
+
from layout_canvas.blocks import base
|
|
19
|
+
from layout_canvas.compiler.compile import compile_design
|
|
20
|
+
from layout_canvas.compiler.netlist import compile_netlist
|
|
21
|
+
from layout_canvas.ir.model import BlockSpec, Design, PortSpec
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _flattened_netlist(design: Design, alias: str, ports: list[PortSpec]) -> str:
|
|
25
|
+
"""Inline ``design``'s complete netlist, renaming its top to ``alias``.
|
|
26
|
+
|
|
27
|
+
The emitted source keeps every primitive ``.subckt`` the child needs;
|
|
28
|
+
the parent's compiler then renames the alias-decl to the variant name.
|
|
29
|
+
"""
|
|
30
|
+
lines = compile_netlist(design).strip().splitlines()
|
|
31
|
+
port_names = " ".join(p.name for p in ports)
|
|
32
|
+
for i, line in enumerate(lines):
|
|
33
|
+
tok = line.strip().split()
|
|
34
|
+
if len(tok) > 1 and tok[0].lower() == ".subckt" and tok[1] == design.name:
|
|
35
|
+
lines[i] = f".subckt {alias} {port_names}".rstrip()
|
|
36
|
+
for j in range(i + 1, len(lines)):
|
|
37
|
+
if lines[j].strip().lower().startswith(".ends"):
|
|
38
|
+
lines[j] = f".ends {alias}"
|
|
39
|
+
break
|
|
40
|
+
break
|
|
41
|
+
return "\n".join(lines)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def register_design_cell(alias: str, design: Design) -> base.Block:
|
|
45
|
+
"""Register ``design`` under ``alias`` so parent IR can instantiate it."""
|
|
46
|
+
ports = [
|
|
47
|
+
PortSpec(name=p.name, layer="met1", direction=p.direction) for p in design.ports
|
|
48
|
+
]
|
|
49
|
+
spec = BlockSpec(
|
|
50
|
+
name=alias,
|
|
51
|
+
pdk=design.pdk,
|
|
52
|
+
level="L2",
|
|
53
|
+
summary=f"Compiled cell macro for design {design.name!r}",
|
|
54
|
+
params=[],
|
|
55
|
+
ports=ports,
|
|
56
|
+
constraints=["fixed internal layout; reuse via abstract pins"],
|
|
57
|
+
tags=["cell", "macro"],
|
|
58
|
+
version=design.ir_version,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def _build() -> gf.Component:
|
|
62
|
+
return compile_design(design)
|
|
63
|
+
|
|
64
|
+
block = base.Block(
|
|
65
|
+
spec=spec,
|
|
66
|
+
build=lambda **_: _build(),
|
|
67
|
+
netlist=lambda **_: _flattened_netlist(design, alias, ports),
|
|
68
|
+
)
|
|
69
|
+
base._REGISTRY[alias] = block
|
|
70
|
+
return block
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Register an external GDS file (e.g. a Virtuoso stream-out) as a cell block.
|
|
2
|
+
|
|
3
|
+
The layout side is closed: ``build`` re-imports the GDS verbatim through
|
|
4
|
+
``gf.import_gds``. The port contract comes from pin labels — texts on the
|
|
5
|
+
PDK's pin-label datatype (``PDK.pin_purpose``) — walked recursively through
|
|
6
|
+
the cell hierarchy so labels buried in subcells still surface.
|
|
7
|
+
|
|
8
|
+
Netlist is opt-in: pass ``spice_text`` carrying a ``.subckt <cell_name>``
|
|
9
|
+
declaration whose pin count matches the discovered labels and the block
|
|
10
|
+
gets a real transistor-level emitter (retargeted to the alias). Without it
|
|
11
|
+
the block keeps ``netlist=None`` — the existing fail-closed path makes
|
|
12
|
+
``simulate_design``/``simulate_auto`` refuse by name and LVS compares
|
|
13
|
+
against a black-box, never a false pass.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import gdsfactory as gf
|
|
23
|
+
|
|
24
|
+
from layout_canvas.blocks import base
|
|
25
|
+
from layout_canvas.ir.model import BlockSpec, PortSpec
|
|
26
|
+
from layout_canvas.pdk import get_pdk
|
|
27
|
+
|
|
28
|
+
# Datatypes considered "drawing" when mapping a pin label's layer number
|
|
29
|
+
# back to a conductor: sky130 uses 20, IHP uses 0. Vias/contacts share the
|
|
30
|
+
# layer number and are excluded by name.
|
|
31
|
+
_DRAWING_DATATYPES = (0, 20)
|
|
32
|
+
_NON_DRAWING_NAMES = ("via", "con", "mcon", "licon", "tap")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class _Pin:
|
|
37
|
+
name: str
|
|
38
|
+
layer_name: str
|
|
39
|
+
layer_tuple: tuple[int, int]
|
|
40
|
+
x: float
|
|
41
|
+
y: float
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _drawing_layer_name(pdk_layers: dict[str, tuple[int, int]], layer_number: int) -> str | None:
|
|
45
|
+
"""Resolve a pin-label layer number to its conductor layer name."""
|
|
46
|
+
candidates = [
|
|
47
|
+
name for name, (ln, dt) in pdk_layers.items()
|
|
48
|
+
if ln == layer_number and dt in _DRAWING_DATATYPES
|
|
49
|
+
and not any(k in name for k in _NON_DRAWING_NAMES)
|
|
50
|
+
]
|
|
51
|
+
return sorted(candidates)[0] if candidates else None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _pin_labels(gds_path: Path, cell_name: str | None, pdk_name: str) -> tuple[str, list[_Pin]]:
|
|
55
|
+
"""Read the GDS, pick the top cell, harvest pin-label texts (um)."""
|
|
56
|
+
import klayout.db as db
|
|
57
|
+
|
|
58
|
+
ly = db.Layout()
|
|
59
|
+
ly.read(str(gds_path))
|
|
60
|
+
tops = list(ly.top_cells())
|
|
61
|
+
if not tops:
|
|
62
|
+
raise ValueError(f"{gds_path}: GDS has no top cell")
|
|
63
|
+
if cell_name is None:
|
|
64
|
+
if len(tops) > 1:
|
|
65
|
+
names = sorted(c.name for c in tops)
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"{gds_path}: multiple top cells {names}; pass cell_name to pick one"
|
|
68
|
+
)
|
|
69
|
+
cell = tops[0]
|
|
70
|
+
else:
|
|
71
|
+
cell = ly.cell(cell_name)
|
|
72
|
+
if cell is None:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"{gds_path}: cell {cell_name!r} not found; "
|
|
75
|
+
f"tops: {sorted(c.name for c in tops)}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
pdk = get_pdk(pdk_name)
|
|
79
|
+
pins: list[_Pin] = []
|
|
80
|
+
seen: set[str] = set()
|
|
81
|
+
for li in ly.layer_indexes():
|
|
82
|
+
info = ly.get_info(li)
|
|
83
|
+
if info.datatype != pdk.pin_purpose:
|
|
84
|
+
continue
|
|
85
|
+
layer_name = _drawing_layer_name(pdk.layers, info.layer)
|
|
86
|
+
if layer_name is None:
|
|
87
|
+
continue
|
|
88
|
+
it = db.RecursiveShapeIterator(ly, cell, li)
|
|
89
|
+
while not it.at_end():
|
|
90
|
+
sh = it.shape()
|
|
91
|
+
if sh.is_text():
|
|
92
|
+
text = sh.text_string.strip()
|
|
93
|
+
pt = it.trans() * sh.text_trans * db.Point(0, 0)
|
|
94
|
+
if text and text not in seen:
|
|
95
|
+
seen.add(text)
|
|
96
|
+
pins.append(_Pin(
|
|
97
|
+
name=text,
|
|
98
|
+
layer_name=layer_name,
|
|
99
|
+
layer_tuple=(info.layer, pdk.layers[layer_name][1]),
|
|
100
|
+
x=pt.x * ly.dbu,
|
|
101
|
+
y=pt.y * ly.dbu,
|
|
102
|
+
))
|
|
103
|
+
it.next()
|
|
104
|
+
if not pins:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"{gds_path} cell {cell.name!r}: no pin labels found on "
|
|
107
|
+
f"(layer/{pdk.pin_purpose}) for pdk {pdk_name!r}. "
|
|
108
|
+
"Export from Virtuoso with pin labels (e.g. on met*/pn layers)."
|
|
109
|
+
)
|
|
110
|
+
return cell.name, pins
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _retarget_spice(spice_text: str, cell_name: str, alias: str, n_ports: int) -> str:
|
|
114
|
+
"""Validate the supplied netlist and rename its top subckt to ``alias``."""
|
|
115
|
+
lines = spice_text.strip().splitlines()
|
|
116
|
+
decl = None
|
|
117
|
+
for i, line in enumerate(lines):
|
|
118
|
+
tok = line.strip().split()
|
|
119
|
+
if len(tok) > 1 and tok[0].lower() == ".subckt" and tok[1].lower() == cell_name.lower():
|
|
120
|
+
decl = i
|
|
121
|
+
subckt_pins = [t for t in tok[2:] if "=" not in t]
|
|
122
|
+
if len(subckt_pins) != n_ports:
|
|
123
|
+
raise ValueError(
|
|
124
|
+
f"netlist .subckt {cell_name} declares {len(subckt_pins)} pins "
|
|
125
|
+
f"but the GDS exposes {n_ports} pin labels"
|
|
126
|
+
)
|
|
127
|
+
lines[i] = f".subckt {alias} {' '.join(tok[2:])}".rstrip()
|
|
128
|
+
break
|
|
129
|
+
if decl is None:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"spice_text has no '.subckt {cell_name}' declaration matching the GDS cell"
|
|
132
|
+
)
|
|
133
|
+
for j in range(decl + 1, len(lines)):
|
|
134
|
+
if lines[j].strip().lower().startswith(".ends"):
|
|
135
|
+
lines[j] = f".ends {alias}"
|
|
136
|
+
break
|
|
137
|
+
return "\n".join(lines)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def register_gds_cell(
|
|
141
|
+
alias: str,
|
|
142
|
+
gds_path: str | Path,
|
|
143
|
+
pdk: str,
|
|
144
|
+
cell_name: str | None = None,
|
|
145
|
+
spice_text: str | None = None,
|
|
146
|
+
) -> base.Block:
|
|
147
|
+
"""Register a GDS file under ``alias`` so parent IR can instantiate it."""
|
|
148
|
+
path = Path(gds_path)
|
|
149
|
+
if not path.is_file():
|
|
150
|
+
raise ValueError(f"GDS not found: {path}")
|
|
151
|
+
resolved_cell, pins = _pin_labels(path, cell_name, pdk)
|
|
152
|
+
|
|
153
|
+
ports = [
|
|
154
|
+
PortSpec(name=p.name, layer=p.layer_name, direction="inout") for p in pins
|
|
155
|
+
]
|
|
156
|
+
spec = BlockSpec(
|
|
157
|
+
name=alias,
|
|
158
|
+
pdk=pdk,
|
|
159
|
+
level="L2",
|
|
160
|
+
summary=f"External GDS cell {resolved_cell!r} imported from {path.name}",
|
|
161
|
+
params=[],
|
|
162
|
+
ports=ports,
|
|
163
|
+
constraints=["imported fixed layout"],
|
|
164
|
+
tags=["cell", "external", "gds"],
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
emitter = None
|
|
168
|
+
if spice_text is not None:
|
|
169
|
+
netlist_text = _retarget_spice(spice_text, resolved_cell, alias, len(ports))
|
|
170
|
+
emitter = lambda **_: netlist_text
|
|
171
|
+
|
|
172
|
+
pin_width = 0.2
|
|
173
|
+
try:
|
|
174
|
+
pin_width = float(get_pdk(pdk).rules.get("min_pin_width_um", 0.2))
|
|
175
|
+
except (KeyError, TypeError, ValueError):
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
def _build() -> gf.Component:
|
|
179
|
+
comp = gf.import_gds(str(path), cellname=resolved_cell)
|
|
180
|
+
for p in pins:
|
|
181
|
+
comp.add_port(
|
|
182
|
+
name=p.name,
|
|
183
|
+
center=(p.x, p.y),
|
|
184
|
+
width=pin_width,
|
|
185
|
+
orientation=0,
|
|
186
|
+
layer=p.layer_tuple,
|
|
187
|
+
)
|
|
188
|
+
return comp
|
|
189
|
+
|
|
190
|
+
block = base.Block(spec=spec, build=lambda **_: _build(), netlist=emitter)
|
|
191
|
+
base._REGISTRY[alias] = block
|
|
192
|
+
_IMPORTED[alias] = (resolved_cell, pins, path, pdk)
|
|
193
|
+
return block
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
_IMPORTED: dict[str, tuple[str, list[_Pin], Path, str]] = {}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def import_summary(block: base.Block) -> dict:
|
|
200
|
+
"""Payload shared by the web ``import_gds`` action and the MCP tool."""
|
|
201
|
+
cell_name, pins, path, pdk = _IMPORTED.get(
|
|
202
|
+
block.spec.name, (block.spec.name, [], Path(""), block.spec.pdk))
|
|
203
|
+
bb = block.component().bbox()
|
|
204
|
+
drc: dict[str, Any]
|
|
205
|
+
try:
|
|
206
|
+
from layout_canvas.tools.drc import run_drc
|
|
207
|
+
|
|
208
|
+
res = run_drc(str(path), tech=pdk, engine="pya")
|
|
209
|
+
drc = {"status": res.status, "violations": res.total_violations}
|
|
210
|
+
except Exception as exc:
|
|
211
|
+
drc = {"status": "unavailable", "violations": None, "error": str(exc)}
|
|
212
|
+
return {
|
|
213
|
+
"alias": block.spec.name,
|
|
214
|
+
"cell": cell_name,
|
|
215
|
+
"ports": [
|
|
216
|
+
{"name": p.name, "layer": p.layer_name,
|
|
217
|
+
"x": round(p.x, 4), "y": round(p.y, 4)}
|
|
218
|
+
for p in pins
|
|
219
|
+
],
|
|
220
|
+
"bbox": [float(bb.left), float(bb.bottom), float(bb.right), float(bb.top)],
|
|
221
|
+
"drc": drc,
|
|
222
|
+
}
|