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.
Files changed (68) hide show
  1. layout_canvas/__init__.py +14 -0
  2. layout_canvas/blocks/__init__.py +25 -0
  3. layout_canvas/blocks/base.py +184 -0
  4. layout_canvas/blocks/cells.py +70 -0
  5. layout_canvas/blocks/gds_cell.py +222 -0
  6. layout_canvas/blocks/generic.py +949 -0
  7. layout_canvas/blocks/ihp_sg13g2/__init__.py +17 -0
  8. layout_canvas/blocks/ihp_sg13g2/current_mirror.py +224 -0
  9. layout_canvas/blocks/ihp_sg13g2/diff_pair.py +244 -0
  10. layout_canvas/blocks/ihp_sg13g2/geom.py +41 -0
  11. layout_canvas/blocks/ihp_sg13g2/guard_ring.py +100 -0
  12. layout_canvas/blocks/ihp_sg13g2/layers.py +35 -0
  13. layout_canvas/blocks/ihp_sg13g2/ota_5t.py +413 -0
  14. layout_canvas/blocks/sky130/__init__.py +25 -0
  15. layout_canvas/blocks/sky130/cap_array.py +137 -0
  16. layout_canvas/blocks/sky130/current_mirror.py +269 -0
  17. layout_canvas/blocks/sky130/diff_pair.py +306 -0
  18. layout_canvas/blocks/sky130/geom.py +58 -0
  19. layout_canvas/blocks/sky130/guard_ring.py +244 -0
  20. layout_canvas/blocks/sky130/layers.py +41 -0
  21. layout_canvas/blocks/sky130/ota_5t.py +476 -0
  22. layout_canvas/blocks/sky130/strongarm.py +497 -0
  23. layout_canvas/cli.py +457 -0
  24. layout_canvas/compiler/__init__.py +6 -0
  25. layout_canvas/compiler/compile.py +233 -0
  26. layout_canvas/compiler/hierarchy.py +61 -0
  27. layout_canvas/compiler/netlist.py +182 -0
  28. layout_canvas/compiler/netlist_import.py +635 -0
  29. layout_canvas/compiler/optimizer.py +59 -0
  30. layout_canvas/compiler/ppa.py +101 -0
  31. layout_canvas/compiler/render.py +120 -0
  32. layout_canvas/compiler/router.py +533 -0
  33. layout_canvas/compiler/virtuoso.py +297 -0
  34. layout_canvas/derived/__init__.py +5 -0
  35. layout_canvas/derived/connectivity.py +100 -0
  36. layout_canvas/engine/__init__.py +15 -0
  37. layout_canvas/engine/edits.py +281 -0
  38. layout_canvas/engine/envelope.py +61 -0
  39. layout_canvas/engine/optimize.py +335 -0
  40. layout_canvas/engine/session.py +117 -0
  41. layout_canvas/ir/__init__.py +37 -0
  42. layout_canvas/ir/model.py +228 -0
  43. layout_canvas/mcp/__init__.py +12 -0
  44. layout_canvas/mcp/bridge.py +172 -0
  45. layout_canvas/mcp/server.py +1275 -0
  46. layout_canvas/pdk/__init__.py +27 -0
  47. layout_canvas/pdk/descriptor.py +639 -0
  48. layout_canvas/protocol/__init__.py +19 -0
  49. layout_canvas/protocol/project.py +157 -0
  50. layout_canvas/tools/__init__.py +6 -0
  51. layout_canvas/tools/backends.py +294 -0
  52. layout_canvas/tools/drc.py +290 -0
  53. layout_canvas/tools/extract.py +405 -0
  54. layout_canvas/tools/lvs.py +334 -0
  55. layout_canvas/tools/sim.py +827 -0
  56. layout_canvas/tools/testbench.py +264 -0
  57. layout_canvas/tools/verify.py +53 -0
  58. layout_canvas/tools/virtuoso_check.py +844 -0
  59. layout_canvas/web/__init__.py +5 -0
  60. layout_canvas/web/app.py +472 -0
  61. layout_canvas/web/gallery.py +229 -0
  62. layout_canvas/web/page.py +893 -0
  63. layout_canvas-0.1.0.dist-info/METADATA +184 -0
  64. layout_canvas-0.1.0.dist-info/RECORD +68 -0
  65. layout_canvas-0.1.0.dist-info/WHEEL +4 -0
  66. layout_canvas-0.1.0.dist-info/entry_points.txt +2 -0
  67. layout_canvas-0.1.0.dist-info/licenses/LICENSE +21 -0
  68. 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
+ }