Skip to content

Fix order-dependent port renaming; close gdstk backend gaps - #102

Open
carloscl03 wants to merge 3 commits into
ReaLLMASIC:mainfrom
carloscl03:fix-port-rename-order
Open

Fix order-dependent port renaming; close gdstk backend gaps#102
carloscl03 wants to merge 3 commits into
ReaLLMASIC:mainfrom
carloscl03:fix-port-rename-order

Conversation

@carloscl03

Copy link
Copy Markdown
Contributor

Two commits. The first is a real bug in port_utils that affects the default
backend too; the second unblocks GLAYOUT_BACKEND=gdstk for the opamp.

Renaming ports depends on dict order

rename_component_ports renames in place — pops the old key and inserts the
new one on the same dict it just iterated:

for namepair in names_to_modify:
    if namepair[0] in custom_comp.ports.keys():
        portobj = custom_comp.ports.pop(namepair[0])
        custom_comp.ports[namepair[1]] = portobj

When a port renames onto a name another port still holds, that guard silently
skips it and one of the two is dropped. Which one survives depends on insertion
order.

rename_ports_by_orientation triggers this routinely: a mirrored port has
orientation 90 and legitimately renames _S -> _N onto a name still queued.
In diff_pair(gf180, width=3, fingers=4) that is 334 ports.

Same component, same port, orientation 90.0:

backend result
gdsfactory keeps bl_multiplier_0_source_S (90° is N, so this is wrong)
gdstk gives bl_multiplier_0_source_N (correct)

add_df_labels then raises KeyError on the latter. It works under gdsfactory
by accident of ordering, not by construction.

Fix builds the renamed mapping first, then swaps it in.

diff_pair(gf180, width=3, fingers=4)
  before   4530 ports    gdstk: KeyError 'bl_multiplier_0_source_S'
  after    4864 ports    both backends identical

The 334 extra ports are the ones that were being dropped.

gdstk backend gaps

With the rename fixed, the opamp got further and hit four more gaps, found one
at a time:

  1. Component.add() rejected a bare Component. gdsfactory accepts one and
    references it implicitly; several composite cells rely on that.
  2. diff_pair_stackedcmirror imported rectangle straight from gdsfactory,
    bypassing the backend abstraction — the module already imports the backend's
    rectangle at module level.
  3. ComponentReference.info held its own empty dict. mimcap_array writes
    info['netlist'] on the Component while opamp_twostage reads it back off
    the reference returned by <<; gdsfactory exposes the parent's info through
    the reference, so delegate.
  4. Component.ref() took no arguments. opamp.py:155 calls
    ref(position=...).

Results

opamp(gf180, ...), same parameters as GLayout_Cells.ipynb cell 14:

backend time ports
gdsfactory 383.0 s
gdstk 55.5 s 99090

No regression: under gdsfactory, nmos(gf180, width=3, fingers=4) produces
3328 ports and 875 polygons before and after.

Note

This makes the fast backend usable; it does not address why the slow one is
slow. Profiling the opamp under gdsfactory shows 96% of runtime inside
pydantic validation, 6.3M serialization.py calls, and 1.85M Port objects
created — 93% of a transistor's ports belong to individual vias that are never
routed to. That is a port-model question, not something a compatibility fix
reaches.

@carloscl03

Copy link
Copy Markdown
Contributor Author

Context on why this came up: #100's notebook check fails on
GLayout_Cells.ipynb, and main fails the same way — the opamp cell crosses
--cell-timeout 180 on slower runners (87 s on the runner that passed, so
roughly a 2x swing is enough to trip it).

Chasing that led here. With GLAYOUT_BACKEND=gdstk the same cell drops from
383 s to 55.5 s, which would take that check well clear of the limit rather
than leaving it near the edge. The gdstk backend could not build the opamp at
all before these fixes, so that number was not measurable until now.

So this does not fix #100's check directly, but it makes switching the notebook
job to the fast backend a viable option — likely more durable than raising the
timeout, since the current margin depends on which runner GitHub hands out.

@carloscl03
carloscl03 force-pushed the fix-port-rename-order branch from 4f83b7a to 16cf8a3 Compare August 17, 2026 02:28
@carloscl03

Copy link
Copy Markdown
Contributor Author

Measured what this unblocks, in case it helps prioritise it.

The DRC and LVS workflows run the gdsfactory backend (CPython 3.10, gdsfactory 7.7 via install_requires), so gdstk has no CI coverage today. I tried building the gf180 cells on gdstk to see how far it is from being usable there.

On main, they do not build at all:

[ERROR] diff_pair: build failed        KeyError: 'bl_multiplier_0_source_S'
[ERROR] diff_pair_ibias: build failed  KeyError: 'br_multiplier_0_gate_S'

With this branch, the same command builds both and writes GDS and netlists — 0 errors. What is left is off-grid geometry (contact_OFFGRID, via1_OFFGRID, metal1_OFFGRID), which is what #104's snap-to-grid commit addresses.

So this is not only a fix for the alternative backend: together with #104 it is what would let the DRC/LVS workflows run on gdstk at all. Worth noting because on the runner that passed, the gdstk backend built the opamp cell in 55.5 s against 383 s on gdsfactory.

@carloscl03

Copy link
Copy Markdown
Contributor Author

Heads-up for whoever merges this and #104: the two branches conflict, and the obvious resolution is the wrong one.

Both add to ComponentReference in src/glayout/backend/_gdstk.py. Two hunks:

  1. __init__gdstk backend: close API gaps so the tutorials run #104 gives the reference its own self.info: dict = {}; this branch makes info a property delegating to the parent. Keeping both compiles and breaks silently: the assignment in __init__ calls the property setter, which does self.parent.info = {} and clears the target component's metadata on every placement. That includes info['netlist'], which mimcap_array writes and lvs_netgen reads back — so LVS stops finding the netlist, with no error to trace it by.

    Resolution: keep the property, drop the self.info = {} line, keep self._name from gdstk backend: close API gaps so the tutorials run #104.

  2. __getitem__ (gdstk backend: close API gaps so the tutorials run #104) vs pprint_ports (this branch) — two distinct methods in the same place. Keep both.

Verified with the resolution applied, on top of main with #100, #103, #112 and #113 also merged:

cell result
CapiMagics encoder builds, 0 DRC violations
CapiMagics integrator builds (fails to build without this PR), 8 violations
our LIF cell builds, Netlists match under klayout LVS

Same numbers as the tree these were originally measured on, so the resolution does not change behaviour.

Happy to rebase this branch onto main once #104 lands, if that is the preferred order.

@carloscl03

Copy link
Copy Markdown
Contributor Author

Another measurement that bears on this one, from the notebook CI on the fork.

Running the tutorial suite on the gdstk backend, GLayout_Cells raises in Component.add without this PR — the same TypeError: Component.add: unsupported type Component that this branch fixes. The notebook masks it: the failing cell assigns inside a widgets.Output() context, so what surfaces is a NameError on the next line and the real cause is swallowed.

This matters beyond the one cell. The notebooks job is why six of the eight open PRs are red right now: two notebooks sit on the 180s per-cell limit and cross it depending on the runner, so the check flips on identical content. Moving the job to gdstk takes the suite from 660s to 158s and off the limit — but it needs this PR, #104 and #113 together. Without this one, GLayout_Cells raises in Component.add.

Runs on this fork's CI, same container and runners: gdsfactory baseline · gdstk. Full table in #116.

The _gdstk.py conflict with #104 described in my previous comment still stands, and #104 has since gained one more commit in that file (add_ref taking columns/rows/spacing), which does not overlap the conflicting hunks.

msaligane pushed a commit that referenced this pull request Aug 25, 2026
Two notebooks sit on the 180s per-cell limit and cross it depending on
which runner the job lands on, so the check passes or fails on identical
content. `lvs-pin-filter` has four green runs and two red ones.

    GLayout_Cells   290s pass (#112) / 372s error (#113)
    glayout_opamp   136s pass (#112) / 198s error (#113)

The native backend builds these cells several times faster, which moves
them off the limit instead of raising it. Measured on this fork's CI --
same container, same runners -- with #102, #104 and #113 applied on both
sides, the only difference being this line:

                              gdsfactory        gdstk
    GLayout_Cells          187.8s  ERROR      14.2s  pass
    glayout_opamp          126.4s  pass       19.2s  pass
    5T_OTA_part2            90.8s  pass       16.2s  pass
    test_bjt_custom_pattern 68.2s  pass       11.4s  pass
    whole suite            660s 13/14        158s 14/14

Note the baseline already carries all three PRs and still fails: they are
what lets the notebooks run on gdstk at all, not what fixes the timeouts.

Depends on #102, #104 and #113. Without #113 `GLayout_Cells` raises in
`mimcap.py`; without #102 it raises in `Component.add`; without #104's
`add_ref(columns=)` the BJT notebook raises. This check stays red until
all three land.
carloscl03 added a commit to carloscl03/gLayout that referenced this pull request Aug 25, 2026
rename_component_ports renamed in place: pop the old key, insert the new one
on the same dict it was iterating. When a port renames onto a name another
port still holds, the guard

    if namepair[0] in custom_comp.ports.keys():

silently skips it, so one of the two is dropped -- and which one survives
depends on dict insertion order.

rename_ports_by_orientation triggers this routinely: a mirrored port has
orientation 90 and legitimately renames _S -> _N onto a name that is still
queued. In diff_pair(gf180, width=3, fingers=4) that is 334 ports.

The result differs per backend. Same component, same port, orientation 90.0:

    gdsfactory  keeps  bl_multiplier_0_source_S   (90 deg is N, so this is wrong)
    gdstk       gives  bl_multiplier_0_source_N   (correct)

and add_df_labels then raises KeyError on the latter. It works under
gdsfactory by accident of ordering, not by construction.

Build the renamed mapping first, then swap it in. Order-independent.

diff_pair(gf180, width=3, fingers=4):

    before   4530 ports   gdstk: KeyError 'bl_multiplier_0_source_S'
    after    4864 ports   both backends identical, lookup succeeds

The 334 extra ports are the ones that were being dropped.
With GLAYOUT_BACKEND=gdstk the opamp failed on gf180. Four distinct gaps,
found by fixing one and hitting the next:

1. Component.add() rejected a bare Component. gdsfactory accepts one and
   references it implicitly; several composite cells rely on that.

2. diff_pair_stackedcmirror imported rectangle straight from gdsfactory
   (`from gdsfactory.components.rectangle import rectangle as _rect`),
   bypassing the backend abstraction, so the active backend could not
   reference the result. The module already imports the backend's rectangle
   at module level.

3. ComponentReference.info did not reach the referenced component. mimcap_array
   writes info['netlist'] on the Component while opamp_twostage reads it back
   off the reference returned by '<<' -- gdsfactory exposes the parent's info
   through the reference, so delegate instead of holding an empty dict.

4. Component.ref() took no arguments. opamp.py calls ref(position=...);
   added position/rotation/x_reflection to match gdsfactory.

opamp(gf180, ...) with GLAYOUT_BACKEND=gdstk, same parameters as
GLayout_Cells.ipynb cell 14:

    gdsfactory backend   383.0 s
    gdstk backend         55.5 s      99090 ports

Requires the port rename fix in the previous commit; without it the run stops
earlier with KeyError: 'bl_multiplier_0_source_S'.
gdsfactory offers it on Component and the tutorials use it to inspect a
cell while building it. Without it a notebook that runs on the other
backend dies with AttributeError halfway through.
@carloscl03
carloscl03 force-pushed the fix-port-rename-order branch from 16cf8a3 to 60fad74 Compare August 26, 2026 09:53
@carloscl03

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (35 commits behind), so this now carries the tests.yml workflow from #112 and its unit tests run here.

Worth flagging what that surfaced. Against main the suite has 10 xfails; with this branch, three of them xpass:

XPASS  diff_pair_ibias-gdstk              (pre-existing: current_mirror_interdigitized_netlist() missing 'fingers' arg)
XPASS  low_voltage_cmirror_standalone-gdstk (pre-existing: KeyError on ref.info['netlist'])
XPASS  diff_pair_stackedcmirror-gdstk     (pre-existing: current_mirror_interdigitized_netlist() missing 'fingers' arg)

Those three cells were marked as known-broken; the port-renaming fix and the info delegation make them build. The remaining failures are identical to main's (all -gdsfactory variants, unrelated to this PR).

On merging alongside #104

There is no conflict with main — this merges clean. The only overlap is with #104, in ComponentReference in _gdstk.py, and it does not go away by rebasing since both PRs add to the same class. Details and the resolution are in my earlier comment above; the short version:

Verified on the fork's CI with that resolution plus #100/#103/#113/#114: the tutorial suite runs 14/14 in 156s. Happy to push the resolved merge here the moment #104 lands, whichever order suits you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant