From 7fa6cbc5f04413e995ab5afeab138ee681133bdb Mon Sep 17 00:00:00 2001 From: euler Date: Sun, 9 Aug 2026 01:08:07 -0500 Subject: [PATCH 1/4] gdstk backend: close API gaps so the tutorials run Running the tutorial notebooks under GLAYOUT_BACKEND=gdstk fails almost immediately: 4 of 14 pass. Two reasons, one on each side. The notebooks import Component, rectangle, boolean and cell straight from gdsfactory instead of glayout.backend, so they bypass the backend selection entirely. Under the default backend both names resolve to the same class and nobody notices; under gdstk, gdsfactory's add_ref() gets a gdstk Component and rejects it. Symbols glayout.backend does not re-export (text_freetype, array) are left on gdsfactory. The gdstk ComponentReference/Component are also missing pieces of the gdsfactory surface that glayout's own cells and the tutorials use: movex/movey took a bare delta; move() already accepted destination= ref.name read-only, but cells label placements (ref.name = "pfet_2") ref.x / ref.y had xmin/xmax/ymin/ymax but not the centre accessors write_gds required a filename and ignored gdsdir= Component.show absent ref.name stores the label on the reference rather than renaming the target cell, which would rename every other placement of it too. Tutorial notebooks under gdstk: 4/14 -> 10/14. The three BJT tutorials still fail (Component indexing, add_ref(columns=)) and are untouched here. Notebooks under the default gdsfactory backend are unaffected: the imports resolve to the same objects. --- src/glayout/backend/_gdstk.py | 78 +++++++++++++++++-- .../BJT_tutorials/test_bjt_gdsfactory.ipynb | 7 +- tutorial/GLayout_Cmirror.ipynb | 4 +- tutorial/GLayout_Introduction.ipynb | 4 +- tutorial/GLayout_Via.ipynb | 4 +- tutorial/glayout_tutorial_5T_OTA_part1.ipynb | 5 +- tutorial/glayout_tutorial_5T_OTA_part2.ipynb | 10 ++- tutorial/glayout_tutorial_FVF_part1.ipynb | 10 ++- tutorial/glayout_tutorial_FVF_part2.ipynb | 5 +- tutorial/glayout_tutorial_INV_part1.ipynb | 12 +-- tutorial/glayout_tutorial_INV_part2.ipynb | 5 +- 11 files changed, 110 insertions(+), 34 deletions(-) diff --git a/src/glayout/backend/_gdstk.py b/src/glayout/backend/_gdstk.py index 6345d668..0db83afa 100644 --- a/src/glayout/backend/_gdstk.py +++ b/src/glayout/backend/_gdstk.py @@ -29,6 +29,7 @@ # cell_decorator_settings, .activate()). Extra fields are allowed so callers # can pass arbitrary pdk-specific config. from pydantic import BaseModel, ConfigDict # noqa: E402 +import os as _os class _GdsWriteSettings(BaseModel): @@ -264,6 +265,8 @@ def __init__(self, parent: "Component", gref: Optional[gdstk.Reference] = None): self._ref = gref # owner is the Component this reference has been added to (not the target) self.owner: Optional["Component"] = None + # a label for this placement; falls back to the target cell's name + self._name: Optional[str] = None # `info` is used by some cells to attach netlist / hierarchy metadata. self.info: dict = {} @@ -294,14 +297,31 @@ def x_reflection(self, value: bool) -> None: self._ref.x_reflection = bool(value) # --- movement (mutate + return self) ---------------------------------- - def movex(self, dx: float = 0.0) -> "ComponentReference": + def movex( + self, + origin: float = 0.0, + destination: Optional[float] = None, + ) -> "ComponentReference": + """Move along x, mirroring ``move``'s calling conventions: + - movex(dx) — translate by dx + - movex(destination=x) — translate by x + - movex(origin=x0, + destination=x1) — translate by x1-x0 + """ + dx = float(origin) if destination is None else float(destination) - float(origin) ox, oy = self.origin - self.origin = (ox + float(dx), oy) + self.origin = (ox + dx, oy) return self - def movey(self, dy: float = 0.0) -> "ComponentReference": + def movey( + self, + origin: float = 0.0, + destination: Optional[float] = None, + ) -> "ComponentReference": + """Move along y. See :meth:`movex` for the calling conventions.""" + dy = float(origin) if destination is None else float(destination) - float(origin) ox, oy = self.origin - self.origin = (ox, oy + float(dy)) + self.origin = (ox, oy + dy) return self def move( @@ -404,6 +424,16 @@ def center(self) -> Coord: (x0, y0), (x1, y1) = self.bbox return ((x0 + x1) / 2.0, (y0 + y1) / 2.0) + @property + def x(self) -> float: + """Centre x. gdsfactory exposes this on references, not just on cells.""" + return self.center[0] + + @property + def y(self) -> float: + """Centre y. Counterpart of :attr:`x`.""" + return self.center[1] + @property def xmin(self) -> float: return self.bbox[0][0] @property @@ -415,7 +445,15 @@ def ymax(self) -> float: return self.bbox[1][1] @property def name(self) -> str: - return self.parent.name + return self._name if self._name is not None else self.parent.name + + @name.setter + def name(self, value: str) -> None: + # gdsfactory lets callers label a placement without touching the cell it + # points at (``ref.name = "pfet_2"``), and cells and tutorials do exactly + # that. Keep it on the reference: renaming the parent here would rename + # every other reference to the same cell too. + self._name = str(value) def __repr__(self) -> str: return f"ComponentReference(parent={self.parent.name!r}, origin={self.origin}, rotation={self.rotation})" @@ -454,6 +492,21 @@ def name(self) -> str: def name(self, value: str) -> None: self._cell.name = str(value) + def show(self, *args, **kwargs) -> None: + """Open the layout in KLayout, like gdsfactory's Component.show(). + + Writes a temp .gds and hands it to klive when reachable. Tutorials + call this for interactive viewing, so it must stay quiet headless. + """ + import tempfile + path = _os.path.join(tempfile.gettempdir(), f"{self.name}.gds") + self.write_gds(path) + try: + from gdsfactory.show import show as _gf_show # type: ignore + _gf_show(path) + except Exception: + pass + def __repr__(self) -> str: return f"Component(name={self.name!r}, ports={list(self.ports)}, refs={len(self._references)})" @@ -726,7 +779,20 @@ def visit(cell: gdstk.Cell) -> None: visit(self._cell) return order - def write_gds(self, filename: str, unit: float = 1e-6, precision: float = 1e-9) -> str: + def write_gds( + self, + filename: Optional[str] = None, + unit: float = 1e-6, + precision: float = 1e-9, + gdsdir: Optional[str] = None, + ) -> str: + # Match gdsfactory's write_gds(gdspath=None, gdsdir=None): the path + # may be omitted (defaults to ".gds") and a directory may be + # given on its own. Tutorials use both call styles. + if filename is None: + filename = f"{self.name}.gds" + if gdsdir is not None: + filename = str(_os.path.join(str(gdsdir), str(filename))) lib = gdstk.Library(unit=unit, precision=precision) used_names: set[str] = set() for cell in self._collect_cells(): diff --git a/tutorial/BJT_tutorials/test_bjt_gdsfactory.ipynb b/tutorial/BJT_tutorials/test_bjt_gdsfactory.ipynb index c878bdc0..687bb63b 100644 --- a/tutorial/BJT_tutorials/test_bjt_gdsfactory.ipynb +++ b/tutorial/BJT_tutorials/test_bjt_gdsfactory.ipynb @@ -47,9 +47,10 @@ "source": [ "from glayout import MappedPDK, sky130 , gf180\n", "#from gdsfactory.cell import cell\n", - "from gdsfactory import Component\n", - "from gdsfactory.geometry import boolean\n", - "from gdsfactory.components import text_freetype, rectangle, array, rectangular_ring" + "from glayout.backend import Component\n", + "from glayout.backend import boolean\n", + "from glayout.backend import rectangle, rectangular_ring\n", + "from gdsfactory.components import text_freetype, array" ] }, { diff --git a/tutorial/GLayout_Cmirror.ipynb b/tutorial/GLayout_Cmirror.ipynb index 112f8c31..14aa2be9 100644 --- a/tutorial/GLayout_Cmirror.ipynb +++ b/tutorial/GLayout_Cmirror.ipynb @@ -37,7 +37,7 @@ "from glayout.util.comp_utils import move, movex, movey, align_comp_to_port, evaluate_bbox, prec_center\n", "from glayout.routing.straight_route import straight_route\n", "from glayout.routing.c_route import c_route\n", - "from gdsfactory import Component\n", + "from glayout.backend import Component\n", "import gdstk\n", "import svgutils.transform as sg\n", "import IPython.display\n", @@ -103,7 +103,7 @@ "from glayout.util.comp_utils import evaluate_bbox, prec_center\n", "from glayout.routing.straight_route import straight_route\n", "from glayout.routing.c_route import c_route\n", - "from gdsfactory import Component\n" + "from glayout.backend import Component\n" ] }, { diff --git a/tutorial/GLayout_Introduction.ipynb b/tutorial/GLayout_Introduction.ipynb index a779df12..74011d53 100644 --- a/tutorial/GLayout_Introduction.ipynb +++ b/tutorial/GLayout_Introduction.ipynb @@ -193,8 +193,8 @@ "metadata": {}, "outputs": [], "source": [ - "from gdsfactory import Component\n", - "from gdsfactory.components import rectangle\n", + "from glayout.backend import Component\n", + "from glayout.backend import rectangle\n", "\n", "def makeMet1Rectangle(pdk, length):\n", " met1 = pdk.get_glayer(\"met1\")\n", diff --git a/tutorial/GLayout_Via.ipynb b/tutorial/GLayout_Via.ipynb index e2e30c23..7278a011 100644 --- a/tutorial/GLayout_Via.ipynb +++ b/tutorial/GLayout_Via.ipynb @@ -104,8 +104,8 @@ "outputs": [], "source": [ "from glayout import sky130, gf180\n", - "from gdsfactory import Component\n", - "from gdsfactory.components import rectangle" + "from glayout.backend import Component\n", + "from glayout.backend import rectangle" ] }, { diff --git a/tutorial/glayout_tutorial_5T_OTA_part1.ipynb b/tutorial/glayout_tutorial_5T_OTA_part1.ipynb index d66f7bd7..6a0536b8 100644 --- a/tutorial/glayout_tutorial_5T_OTA_part1.ipynb +++ b/tutorial/glayout_tutorial_5T_OTA_part1.ipynb @@ -136,8 +136,9 @@ "outputs": [], "source": [ "from glayout import MappedPDK, sky130, gf180\n", - "from gdsfactory import Component\n", - "from gdsfactory.components import text_freetype, rectangle\n", + "from glayout.backend import Component\n", + "from glayout.backend import rectangle\n", + "from gdsfactory.components import text_freetype\n", "\n", "from glayout import nmos, pmos\n", "from glayout import via_stack\n", diff --git a/tutorial/glayout_tutorial_5T_OTA_part2.ipynb b/tutorial/glayout_tutorial_5T_OTA_part2.ipynb index 1d14a4f5..16c9a1dc 100644 --- a/tutorial/glayout_tutorial_5T_OTA_part2.ipynb +++ b/tutorial/glayout_tutorial_5T_OTA_part2.ipynb @@ -95,8 +95,9 @@ "source": [ "from glayout import MappedPDK, sky130 , gf180\n", "#from gdsfactory.cell import cell\n", - "from gdsfactory import Component\n", - "from gdsfactory.components import text_freetype, rectangle" + "from glayout.backend import Component\n", + "from glayout.backend import rectangle\n", + "from gdsfactory.components import text_freetype" ] }, { @@ -236,8 +237,9 @@ "fivet_ota_code_string = \"\"\"\n", "from glayout import MappedPDK, sky130 , gf180\n", "# from gdsfactory.cell import cell\n", - "from gdsfactory import Component\n", - "from gdsfactory.components import text_freetype, rectangle\n", + "from glayout.backend import Component\n", + "from glayout.backend import rectangle\n", + "from gdsfactory.components import text_freetype\n", "\n", "from glayout import nmos, pmos\n", "from glayout import via_stack\n", diff --git a/tutorial/glayout_tutorial_FVF_part1.ipynb b/tutorial/glayout_tutorial_FVF_part1.ipynb index 3808aed2..a07cb1b2 100644 --- a/tutorial/glayout_tutorial_FVF_part1.ipynb +++ b/tutorial/glayout_tutorial_FVF_part1.ipynb @@ -171,8 +171,9 @@ "source": [ "from glayout import MappedPDK, sky130 , gf180\n", "#from gdsfactory.cell import cell\n", - "from gdsfactory import Component\n", - "from gdsfactory.components import text_freetype, rectangle" + "from glayout.backend import Component\n", + "from glayout.backend import rectangle\n", + "from gdsfactory.components import text_freetype" ] }, { @@ -863,8 +864,9 @@ "fvf_code_string = \"\"\"\n", "from glayout import MappedPDK, sky130 , gf180\n", "# from gdsfactory.cell import cell\n", - "from gdsfactory import Component\n", - "from gdsfactory.components import text_freetype, rectangle\n", + "from glayout.backend import Component\n", + "from glayout.backend import rectangle\n", + "from gdsfactory.components import text_freetype\n", "\n", "from glayout import nmos, pmos\n", "from glayout import via_stack\n", diff --git a/tutorial/glayout_tutorial_FVF_part2.ipynb b/tutorial/glayout_tutorial_FVF_part2.ipynb index 1ba93304..0c34ddb6 100644 --- a/tutorial/glayout_tutorial_FVF_part2.ipynb +++ b/tutorial/glayout_tutorial_FVF_part2.ipynb @@ -113,8 +113,9 @@ "source": [ "from glayout import MappedPDK, sky130 , gf180\n", "#from gdsfactory.cell import cell\n", - "from gdsfactory import Component\n", - "from gdsfactory.components import text_freetype, rectangle" + "from glayout.backend import Component\n", + "from glayout.backend import rectangle\n", + "from gdsfactory.components import text_freetype" ] }, { diff --git a/tutorial/glayout_tutorial_INV_part1.ipynb b/tutorial/glayout_tutorial_INV_part1.ipynb index eb3160d5..1569b0db 100644 --- a/tutorial/glayout_tutorial_INV_part1.ipynb +++ b/tutorial/glayout_tutorial_INV_part1.ipynb @@ -164,8 +164,9 @@ "source": [ "from glayout import MappedPDK, sky130 , gf180\n", "#from gdsfactory.cell import cell\n", - "from gdsfactory import Component\n", - "from gdsfactory.components import text_freetype, rectangle" + "from glayout.backend import Component\n", + "from glayout.backend import rectangle\n", + "from gdsfactory.components import text_freetype" ] }, { @@ -803,9 +804,10 @@ "source": [ "inv_code_string = \"\"\"\n", "from glayout import MappedPDK, sky130 , gf180\n", - "from gdsfactory.cell import cell\n", - "from gdsfactory import Component\n", - "from gdsfactory.components import text_freetype, rectangle\n", + "from glayout.backend import cell\n", + "from glayout.backend import Component\n", + "from glayout.backend import rectangle\n", + "from gdsfactory.components import text_freetype\n", "\n", "from glayout import nmos, pmos\n", "from glayout import via_stack\n", diff --git a/tutorial/glayout_tutorial_INV_part2.ipynb b/tutorial/glayout_tutorial_INV_part2.ipynb index bec0ebc9..7aa84f0d 100644 --- a/tutorial/glayout_tutorial_INV_part2.ipynb +++ b/tutorial/glayout_tutorial_INV_part2.ipynb @@ -105,8 +105,9 @@ "outputs": [], "source": [ "from glayout import MappedPDK, sky130 , gf180\n", - "from gdsfactory import Component\n", - "from gdsfactory.components import text_freetype, rectangle\n", + "from glayout.backend import Component\n", + "from glayout.backend import rectangle\n", + "from gdsfactory.components import text_freetype\n", "\n", "import gdsfactory as gf\n", "gf.clear_cache()" From 66019fa597d7027fdcd6c0dfb72d8f3a9e41e041 Mon Sep 17 00:00:00 2001 From: euler Date: Sun, 9 Aug 2026 21:04:25 -0500 Subject: [PATCH 2/4] gdstk backend: snap to the PDK's grid, not to 1 nm Every vertex of a gdstk-generated layout lands off-grid on gf180. The DRC reports it on all of them -- 1301 violations on the LIF cell used to check this, split across contact_OFFGRID x348, via1_OFFGRID x276, metal1_OFFGRID x252, metal2_OFFGRID x132 and comp_OFFGRID x60. snap_to_grid() took a bare `nm: int = 1` default. gdsfactory's version reads the pitch from the active PDK instead: nm = int(get_grid_size() * 1000 * grid_factor) which is 5 nm on gf180. Rounding a 5 nm process to 1 nm produces values like 10.246 where the process wants 10.245, and every one of them is a violation. Two pieces were missing. Pdk.activate() was a no-op, so nothing recorded which PDK was active; and grid_size kept the class default of 0.001 because gdsfactory used to fill it in from its own PDK database on activate. The real pitch is already in gds_write_settings.precision (5e-9 m on both gf180 and sky130), so activate() now derives grid_size from it and registers the PDK for snap_to_grid to read. `nm=` still overrides when a caller wants a specific pitch. After: 0 of 1228 vertices off-grid, same as the gdsfactory backend, and the 1301 OFFGRID violations are gone. snap_to_2xgrid(10.2463) returns 10.25 on both backends now. DRC on diff_pair, current_mirror_nfet and transmission_gate under gdsfactory is unchanged. --- src/glayout/backend/_gdstk.py | 43 +++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/src/glayout/backend/_gdstk.py b/src/glayout/backend/_gdstk.py index 0db83afa..3a22126a 100644 --- a/src/glayout/backend/_gdstk.py +++ b/src/glayout/backend/_gdstk.py @@ -44,6 +44,10 @@ class _CellDecoratorSettings(BaseModel): cache: bool = False +# The PDK whose grid snap_to_grid() should use. Set by Pdk.activate(). +_ACTIVE_PDK: Optional["Pdk"] = None + + class Pdk(BaseModel): """Minimal shim for gdsfactory.pdk.Pdk. Holds enough state for MappedPDK to function.""" @@ -53,15 +57,29 @@ class Pdk(BaseModel): name: str layers: Optional[dict] = None default_decorator: Optional[Any] = None - grid_size: float = 0.001 # microns; matches gdsfactory default + grid_size: float = 0.001 # microns; corrected in activate() from precision gds_write_settings: _GdsWriteSettings = _GdsWriteSettings() cell_decorator_settings: _CellDecoratorSettings = _CellDecoratorSettings() def activate(self) -> None: - """No-op. gdsfactory's activate() registered the PDK in a global - registry; that registry is a gdsfactory concern and isn't needed - once gdsfactory is out of the import graph.""" - return None + """Register this PDK as the active one. + + gdsfactory kept a global registry so that helpers like snap_to_grid + could look up the process grid. Most of that registry is a gdsfactory + concern, but the grid is not: snapping to the wrong pitch puts every + vertex off-grid, and the DRC reports it on all of them. + """ + # gdsfactory filled grid_size in from its PDK database here. Without + # that database the class default (1 nm) survives, and snapping a 5 nm + # process to 1 nm puts every vertex off-grid -- the DRC then flags + # comp, metal, contact and via alike. precision already carries the + # real pitch, so derive it rather than duplicating the number. + precision_um = float(self.gds_write_settings.precision) * 1e6 + if precision_um > 0 and self.grid_size != precision_um: + object.__setattr__(self, "grid_size", precision_um) + + global _ACTIVE_PDK + _ACTIVE_PDK = self def validate_layers(self, layers_required) -> None: """Mimics gdsfactory.pdk.Pdk.validate_layers — raise if any named @@ -836,16 +854,23 @@ def Polygon(points, layer=(0, 0), datatype=None) -> gdstk.Polygon: # --------------------------------------------------------------------------- -def snap_to_grid(x, nm: int = 1): - """Snap `x` (in micrometers) to an `nm`-nanometer grid. +def snap_to_grid(x, nm: Optional[int] = None, grid_factor: int = 1): + """Snap `x` (in micrometers) to the active PDK's grid. + + Mirrors gdsfactory.snap.snap_to_grid, which reads the grid from the active + PDK rather than assuming one: gf180 is on 5 nm, and snapping it to 1 nm + leaves every vertex off-grid (the DRC then flags comp, metal, contact and + via alike). `nm` overrides the lookup when a caller needs a specific pitch. - Matches gdsfactory.snap.snap_to_grid semantics used in this repo. Accepts scalars or iterables. """ if x is None: return None if isinstance(x, (list, tuple)): - return type(x)(snap_to_grid(v, nm) for v in x) + return type(x)(snap_to_grid(v, nm, grid_factor) for v in x) + if nm is None: + grid_um = _ACTIVE_PDK.grid_size if _ACTIVE_PDK is not None else 0.001 + nm = max(1, int(round(grid_um * 1000.0 * grid_factor))) return round(float(x) * 1000.0 / nm) * nm / 1000.0 From 681a8a65f9074c5b688c78e1b2e89b60c11450d3 Mon Sep 17 00:00:00 2001 From: euler Date: Sun, 9 Aug 2026 22:08:24 -0500 Subject: [PATCH 3/4] backend gdstk: arreglar move() y el orden de get_ports_list move(destination=) traducia respecto al centro del bbox en vez de (0,0), que es el default de gdsfactory. c_route coloca sus rectangulos de extension con move(destination=...) seguido de movex relativos, asi que la ruta se desplazaba medio rectangulo: la neurona LIF salia 32.4 um de ancho en vez de 29.9 y con 54 violaciones M2.2a de mas. get_ports_list devolvia el orden del dict; gdsfactory ordena clockwise (oeste, norte, este, sur). Las celdas buscan puertos por subcadena, asi que el orden cambia que puerto se enruta. Con ambos, neurona.ipynb da 1168.6 um2 y 31 violaciones en los dos backends, identico a gdsfactory. --- src/glayout/backend/_gdstk.py | 84 +++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 18 deletions(-) diff --git a/src/glayout/backend/_gdstk.py b/src/glayout/backend/_gdstk.py index 3a22126a..279d11d1 100644 --- a/src/glayout/backend/_gdstk.py +++ b/src/glayout/backend/_gdstk.py @@ -267,6 +267,41 @@ def _as_layer(layer) -> Layer: # --------------------------------------------------------------------------- +def _sort_ports_clockwise(ports: list) -> list: + """Order ports the way gdsfactory's select_ports() does. + + select_ports defaults to clockwise=True, so ports come out bucketed by + orientation and swept west, north, east, south -- west sorted south to + north, north west to east, east north to south, south east to west. + + The order is not cosmetic. Cells and notebooks pick ports out of + get_ports_list() by substring, e.g. + + next(p for p in cap.get_ports_list() if "bottom_met" in p.name) + + and a different order hands back a different port: on a 10x15 mimcap that + is array_row0_col0_bottom_met_W at (-4.250,-6.400) rather than + bottom_met_W at (-5.600,0.000), so the route lands somewhere else. + """ + buckets = {"E": [], "N": [], "W": [], "S": []} + for p in ports: + angle = (p.orientation or 0) % 360 + if angle <= 45 or angle >= 315: + buckets["E"].append(p) + elif 45 <= angle <= 135: + buckets["N"].append(p) + elif 135 <= angle <= 225: + buckets["W"].append(p) + else: + buckets["S"].append(p) + + buckets["W"].sort(key=lambda p: +p.center[1]) # south to north + buckets["N"].sort(key=lambda p: +p.center[0]) # west to east + buckets["E"].sort(key=lambda p: -p.center[1]) # north to south + buckets["S"].sort(key=lambda p: -p.center[0]) # east to west + return buckets["W"] + buckets["N"] + buckets["E"] + buckets["S"] + + class ComponentReference: """Wraps a `gdstk.Reference`. Exposes transform mutation and transformed views of the parent component's ports/bbox.""" @@ -347,24 +382,37 @@ def move( origin: Optional[Coord] = None, destination: Optional[Coord] = None, ) -> "ComponentReference": - """Move this reference. Two calling conventions: - - move((dx, dy)) — translate by offset - - move(destination=(x, y)) — move so the ref's center lands at (x, y) - - move(origin=(x0, y0), - destination=(x1, y1)) — translate by (x1-x0, y1-y0) + """Translate this reference by (destination - origin). + + `origin` defaults to (0, 0), NOT to the reference's centre -- so + move(destination=(x, y)) is a plain translation by (x, y), the same as + move((x, y)). That is gdsfactory's signature, and the difference is not + academic: c_route places its extension rectangles with + + e1_extension.move(destination=edge1.center) + e1_extension.movex(0 - evaluate_bbox(e1_extension)[0]) + + i.e. an absolute-looking call followed by relative nudges. Centring the + bbox on the destination instead injects an offset of half the + rectangle, and the route walks off: on the LIF neuron the met2 return + path ran to x=-8.125 instead of -1.250, widening the cell 8% and + raising 54 M2.2a violations that gdsfactory never produces. + + Either endpoint may be a Port (or anything exposing `.center`), which + is how callers route to a port without unpacking it. """ - if destination is None and origin is not None and not isinstance(origin, ComponentReference): - # single-arg form: treat as offset - return self.movex(origin[0]).movey(origin[1]) + def _coord(v): + c = getattr(v, "center", None) + return (float(v[0]), float(v[1])) if c is None else (float(c[0]), float(c[1])) + if destination is None: - return self - if origin is None: - # move by (destination - current center) - cx, cy = self.center - dx, dy = destination[0] - cx, destination[1] - cy - else: - dx, dy = destination[0] - origin[0], destination[1] - origin[1] - return self.movex(dx).movey(dy) + if origin is None: + return self + # single-arg form: move((dx, dy)) is a translation by that offset + destination, origin = origin, (0.0, 0.0) + ox, oy = _coord((0.0, 0.0) if origin is None else origin) + dx, dy = _coord(destination) + return self.movex(dx - ox).movey(dy - oy) def rotate(self, angle_deg: float, center: Coord = (0.0, 0.0)) -> "ComponentReference": # rotate the reference's placement about `center` @@ -431,7 +479,7 @@ def get_ports_list(self, prefix: str = "", **filters) -> list[Port]: if skip: continue out.append(p) - return out + return _sort_ports_clockwise(out) @property def bbox(self) -> tuple[Coord, Coord]: @@ -628,7 +676,7 @@ def get_ports_list(self, prefix: str = "", **filters) -> list[Port]: out.append(p.copy(name=f"{prefix}{name}")) else: out.append(p) - return out + return _sort_ports_clockwise(out) # --- geometry --------------------------------------------------------- def add_polygon(self, points, layer: Optional[Layer] = None) -> gdstk.Polygon: From d0d4763cea93819d051d2c35c2417fc9ad8f428f Mon Sep 17 00:00:00 2001 From: euler Date: Mon, 10 Aug 2026 00:28:13 -0500 Subject: [PATCH 4/4] gf180: el mimcap no tenia dielectrico, las placas iban en corto capmet apuntaba a CAP_MK (117,5), que es solo un marcador. El MIM real es FuseTop (75,0), que estaba en el archivo pero comentado. Sin dielectrico, el via_array que deberia contactar la placa superior contactaba la inferior: 187 via2 uniendo met2 con met3. Extraido con magic, el cap salia como un unico nodo flotante. Ahora salen dos placas con capacitancia entre ellas. sky130 ya apuntaba a capm, asi que gf180 era el caso desviado. Ademas CAP_MK tiene que envolver al FuseTop (regla MIM.7), asi que mimcap() dibuja el marcador cuando el pdk mapea capmet_mk. --- src/glayout/pdk/gf180_mapped/gf180_mapped.py | 10 ++++++++-- src/glayout/pdk/mappedpdk.py | 2 ++ src/glayout/primitives/mimcap.py | 6 ++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/glayout/pdk/gf180_mapped/gf180_mapped.py b/src/glayout/pdk/gf180_mapped/gf180_mapped.py index 0b170bb9..a6daca8a 100644 --- a/src/glayout/pdk/gf180_mapped/gf180_mapped.py +++ b/src/glayout/pdk/gf180_mapped/gf180_mapped.py @@ -10,7 +10,6 @@ # Actual Pin definations for GlobalFoundries 180nmMCU from the PDK manual # Ref: https://gf180mcu-pdk.readthedocs.io/en/latest/ -#LAYER["fusetop"]=(75, 0) LAYER = { "metal5": (81, 0), "via4": (41, 0), @@ -29,6 +28,12 @@ "nwell": (21, 0), "lvpwell": (204, 0), "dnwell": (12, 0), + # MIM capacitor, option A. The top plate is FuseTop; CAP_MK is only a + # marker that has to enclose it (rule MIM.7). Drawing the marker as if it + # were the plate leaves the cap with no dielectric at all, so the via2 + # array that contacts the top plate lands on the bottom plate instead and + # shorts the two together. + "fusetop": (75, 0), "CAP_MK": (117, 5), # BJT layers "drc_bjt": (127, 5), @@ -62,7 +67,8 @@ "nwell": "nwell", "pwell": "lvpwell", "dnwell": "dnwell", - "capmet": "CAP_MK", + "capmet": "fusetop", + "capmet_mk": "CAP_MK", # bjt layer "drc_bjt": "drc_bjt", "lvs_bjt": "lvs_bjt", diff --git a/src/glayout/pdk/mappedpdk.py b/src/glayout/pdk/mappedpdk.py index 05b487f9..e2872886 100644 --- a/src/glayout/pdk/mappedpdk.py +++ b/src/glayout/pdk/mappedpdk.py @@ -258,6 +258,8 @@ class MappedPDK(Pdk): "via4", "met5", "capmet", + # optional marker some processes require around the cap plate + "capmet_mk", "lvs_bjt", "drc_bjt", # _pin layers diff --git a/src/glayout/primitives/mimcap.py b/src/glayout/primitives/mimcap.py index ecf3336f..56d430d1 100644 --- a/src/glayout/primitives/mimcap.py +++ b/src/glayout/primitives/mimcap.py @@ -73,6 +73,12 @@ def mimcap( ) bottom_met_enclosure = pdk.get_grule(capmetbottom,"capmet")["min_enclosure"] mim_cap.add_padding(layers=(pdk.get_glayer(capmetbottom),),default=bottom_met_enclosure) + # Some processes require a marker enclosing the cap plate -- gf180 rule + # MIM.7 asks for CAP_MK around FuseTop. PDKs without such a marker just + # do not map the glayer. + if "capmet_mk" in pdk.glayers: + marker_enclosure = pdk.get_grule("capmet").get("mk_enclosure", bottom_met_enclosure) + mim_cap.add_padding(layers=(pdk.get_glayer("capmet_mk"),), default=marker_enclosure) # flatten and create ports mim_cap = add_ports_perimeter(mim_cap, layer=pdk.get_glayer(capmetbottom), prefix="bottom_met_") mim_cap.add_ports(top_met_ref.get_ports_list())