From c4f9168963828380fd110f02847a99b9f468bb06 Mon Sep 17 00:00:00 2001 From: jeffhsu3 Date: Sun, 30 Aug 2026 06:58:09 -0700 Subject: [PATCH 1/4] two_transistor_interdigitized: drop .subckt default params for gf180 LVS KLayout's SPICE reader folds a subcircuit's default parameters into its NAME: `l=0.28u w=3u m=1` turns the circuit into `TWO_TRANSISTOR_INTERDIGITIZED(L=0.28U,M=1,W=3U)`, after which the gf180 klayout deck cannot match it to the layout's top cell and aborts with "Can't find a schematic counterpart for the top cell". The device lines carry l/w/m explicitly, so the defaults on the .subckt line were never used. sky130 goes through magic+netgen and is unaffected, which is why this only ever showed up on gf180. Co-Authored-By: Claude Opus 5 (1M context) --- src/glayout/placement/two_transistor_interdigitized.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/glayout/placement/two_transistor_interdigitized.py b/src/glayout/placement/two_transistor_interdigitized.py index 0686df73..e35d5f3f 100644 --- a/src/glayout/placement/two_transistor_interdigitized.py +++ b/src/glayout/placement/two_transistor_interdigitized.py @@ -93,7 +93,14 @@ def two_tran_interdigitized_netlist( # X-prefix is sky130's magic+netgen tech expectation; klayout decks # that classify primitive MOSFETs by SPICE prefix get their netlist # X→M-rewritten by the LVS runner. - source_netlist = """.subckt {circuit_name} {nodes} """ + f'l={length} w={width} m={1} '+ f""" + # No parameters on the top-level .subckt line. KLayout's SPICE reader folds a + # subcircuit's default parameters into its *name* -- `l=0.28u w=3u m=1` turns the + # circuit into `TWO_TRANSISTOR_INTERDIGITIZED(L=0.28U,M=1,W=3U)` -- so the gf180 + # klayout deck can no longer match it to the layout's top cell and aborts with + # "Can't find a schematic counterpart for the top cell". The device lines below + # carry l/w/m explicitly, so these defaults were never used. sky130 is unaffected + # (magic+netgen), which is why this only ever showed up on gf180. + source_netlist = """.subckt {circuit_name} {nodes}""" + f""" XA VDD1 VG1 VSS1 VB {model} l={length} w={width} m={mtop} XB VDD2 VG2 VSS2 VB {model} l={length} w={width} m={mtop}""" if with_dummy: From ec918a8ed44721415dafba10632b1b4115c01a3e Mon Sep 17 00:00:00 2001 From: jeffhsu3 Date: Sun, 30 Aug 2026 06:58:09 -0700 Subject: [PATCH 2/4] tests: rebuild stale venvs in the local CI bootstrap Gate the cached venv on a `.installed` marker written after a successful install, rather than on the python binary existing: a half-built venv used to look like a cache hit forever. Also stop discarding `uv pip install` output so failures are visible. --- tests/run_ci_locally.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/run_ci_locally.py b/tests/run_ci_locally.py index ae8d8ca2..dc56c57a 100755 --- a/tests/run_ci_locally.py +++ b/tests/run_ci_locally.py @@ -52,10 +52,12 @@ uv python install 3.10 >/dev/null 2>&1 PYTHON310="$(uv python find 3.10)" -if [ ! -x /work/.drc-cache/venv/bin/python ]; then +if [ ! -f /work/.drc-cache/venv/.installed ]; then + rm -rf /work/.drc-cache/venv "$PYTHON310" -m venv /work/.drc-cache/venv . /work/.drc-cache/venv/bin/activate - uv pip install -e . >/dev/null + uv pip install -e . + touch /work/.drc-cache/venv/.installed else # Cache hit: glayout's .pth points to /work which is the same on every # run, so no refresh is needed (matches the workflows). From 4d5bbcc60eddf41d4aa90ab7ca3067bf0d60a8a9 Mon Sep 17 00:00:00 2001 From: jeffhsu3 Date: Sun, 30 Aug 2026 09:39:19 -0700 Subject: [PATCH 3/4] differential_to_single_ended_converter: drop .subckt default params Same defect as the previous commit, second instance. KLayout's SPICE reader folds a subcircuit's default parameters into its *name*, so .subckt DIFF_TO_SINGLE VIN VOUT VSS VSS2 l=1.0 w=6.0 mt=8 mb=10 is read back as the circuit `DIFF_TO_SINGLE(L=1.0,MB=10,MT=8,W=6.0)`. The gf180 klayout deck then cannot match it to the layout's top cell and aborts with "Can't find a schematic counterpart for the top cell", which the runner reports as the catch-all "LVS inconclusive (0 mismatches)" -- zero comparisons, not zero errors. Unlike two_transistor_interdigitized the device lines here referenced the subckt parameters as l={l} w={w} m={mt}, so the values are inlined instead. The emitted netlist is electrically identical; only the parameterisation moves from the subckt header onto the device lines. gf180 now produces a real comparison: before LVS inconclusive (0 mismatches) after Netlists do not match (11 mismatches) - extra top-level pin(s) in layout: SUB That remaining mismatch is a genuine discrepancy this change makes visible for the first time; it is not fixed here. sky130 is unchanged at 5 mismatches (Magic's PMOS-bulk mis-extraction, the reason this cell is on the default --skip-cells list). CI is unaffected for the same reason. --- .../differential_to_single_ended_converter.py | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/glayout/cells/composite/differential_to_single_ended_converter/differential_to_single_ended_converter.py b/src/glayout/cells/composite/differential_to_single_ended_converter/differential_to_single_ended_converter.py index f623d898..9f2ade83 100644 --- a/src/glayout/cells/composite/differential_to_single_ended_converter/differential_to_single_ended_converter.py +++ b/src/glayout/cells/composite/differential_to_single_ended_converter/differential_to_single_ended_converter.py @@ -196,22 +196,29 @@ def differential_to_single_ended_converter_netlist(pdk: MappedPDK, half_pload: t return Netlist( circuit_name="DIFF_TO_SINGLE", nodes=['VIN', 'VOUT', 'VSS', 'VSS2'], - source_netlist=""".subckt {circuit_name} {nodes} """ + f'l={half_pload[1]} w={half_pload[0]} mt={4*2} mb={2 * half_pload[2]} ' + """ -XTOP1 VOUT VIN VSS VSS {model} l={{l}} w={{w}} m={{mt}} -XTOP2 VSS2 VIN VSS VSS {model} l={{l}} w={{w}} m={{mt}} -XBOT1 VIN VIN VOUT VSS {model} l={{l}} w={{w}} m={{mb}} -XBOT2 VOUT VIN VSS2 VSS {model} l={{l}} w={{w}} m={{mb}} -XDUMMY1 VSS VSS VSS VSS {model} l={{l}} w={{w}} -XDUMMY2 VSS VSS VSS VSS {model} l={{l}} w={{w}} -XDUMMY3 VSS VSS VSS VSS {model} l={{l}} w={{w}} -XDUMMY4 VSS VSS VSS VSS {model} l={{l}} w={{w}} -XDUMMY5 VSS VSS VSS VSS {model} l={{l}} w={{w}} -XDUMMY6 VSS VSS VSS VSS {model} l={{l}} w={{w}} -XDUMMY7 VSS VSS VSS VSS {model} l={{l}} w={{w}} -XDUMMY8 VSS VSS VSS VSS {model} l={{l}} w={{w}} -XDUMMY9 VSS VSS VSS VSS {model} l={{l}} w={{w}} -XDUMMY10 VSS VSS VSS VSS {model} l={{l}} w={{w}} -.ends {circuit_name}""", + # No parameters on the top-level .subckt line. KLayout's SPICE reader folds a + # subcircuit's defaults into its NAME -- `l=1.0 w=3.0 mt=8 mb=4` made this + # circuit `DIFF_TO_SINGLE(L=1.0,MT=8,MB=4,W=3.0)` -- so the gf180 deck could + # not match it to the layout's top cell and aborted with "Can't find a + # schematic counterpart for the top cell", which the runner reported as the + # catch-all "LVS inconclusive". Unlike two_transistor_interdigitized the body + # here referenced {l}/{w}/{mt}/{mb}, so those are inlined below. + source_netlist=""".subckt {circuit_name} {nodes}""" + f""" +XTOP1 VOUT VIN VSS VSS {{model}} l={half_pload[1]} w={half_pload[0]} m={4*2} +XTOP2 VSS2 VIN VSS VSS {{model}} l={half_pload[1]} w={half_pload[0]} m={4*2} +XBOT1 VIN VIN VOUT VSS {{model}} l={half_pload[1]} w={half_pload[0]} m={2*half_pload[2]} +XBOT2 VOUT VIN VSS2 VSS {{model}} l={half_pload[1]} w={half_pload[0]} m={2*half_pload[2]} +XDUMMY1 VSS VSS VSS VSS {{model}} l={half_pload[1]} w={half_pload[0]} +XDUMMY2 VSS VSS VSS VSS {{model}} l={half_pload[1]} w={half_pload[0]} +XDUMMY3 VSS VSS VSS VSS {{model}} l={half_pload[1]} w={half_pload[0]} +XDUMMY4 VSS VSS VSS VSS {{model}} l={half_pload[1]} w={half_pload[0]} +XDUMMY5 VSS VSS VSS VSS {{model}} l={half_pload[1]} w={half_pload[0]} +XDUMMY6 VSS VSS VSS VSS {{model}} l={half_pload[1]} w={half_pload[0]} +XDUMMY7 VSS VSS VSS VSS {{model}} l={half_pload[1]} w={half_pload[0]} +XDUMMY8 VSS VSS VSS VSS {{model}} l={half_pload[1]} w={half_pload[0]} +XDUMMY9 VSS VSS VSS VSS {{model}} l={half_pload[1]} w={half_pload[0]} +XDUMMY10 VSS VSS VSS VSS {{model}} l={half_pload[1]} w={half_pload[0]} +.ends {{circuit_name}}""", instance_format="X{name} {nodes} {circuit_name} l={length} w={width} mt={mult_top} mb={mult_bot}", parameters={ 'model': pdk.models['pfet'], From 8af2e63cdc00071cb75127fc52cd12874fd273e4 Mon Sep 17 00:00:00 2001 From: jeffhsu3 Date: Sun, 30 Aug 2026 09:39:19 -0700 Subject: [PATCH 4/4] lvs: default the gf180 MIM stack to option B, matching what glayout builds gf180 has two mutually exclusive MIM stacks. glayout builds option B -- `mimcap` and `mimcap_array` both default to option="B" (primitives/mimcap.py:56, :333), which is the met4/FuseTop/met5 stack -- but the LVS runner defaulted to option A (met2/FuseTop/met3). Extracting A against a B layout yields no capacitor at all, so every MIM in the cell showed up as missing: [FAIL] opamp: Netlists do not match (3 mismatches) 1 schematic device(s) missing from layout, first 1.2.0.1 [CAP_MIM_2F0FF] The capacitors were always there: opamp.gds carries FuseTop (75/0) x6 and MIM_L_MK (117/10) x6 alongside Metal4 and Metal5, six of each matching the six MIMCap instances in the netlist. With the default corrected, gf180 LVS is green across every cell, opamp included. $GF180_MIM_OPTION still overrides for a layout genuinely built on the met2/met3 stack. Note the --mim-option flag already existed and was documented, but .github/workflows/lvs.yml never passed it, so CI has been extracting the wrong stack for every gf180 cell with a MIM. Fixing the default means the workflow needs no change. --- tests/lvs/klayout_gf180.py | 9 +++++++-- tests/lvs/run_cell_lvs.py | 5 +++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/lvs/klayout_gf180.py b/tests/lvs/klayout_gf180.py index bb5bbebd..2150f01f 100644 --- a/tests/lvs/klayout_gf180.py +++ b/tests/lvs/klayout_gf180.py @@ -301,7 +301,12 @@ def run_lvs_klayout_gf180( FuseTop / met3) or "B" (met4 / FuseTop / met5). The two are mutually exclusive at process level, so the wrong one extracts no capacitor at all and every MIM shows up as missing from the layout. Defaults to - ``$GF180_MIM_OPTION``, then to "A". + ``$GF180_MIM_OPTION``, then to "B" -- which is what glayout actually builds: + both ``mimcap`` and ``mimcap_array`` default to ``option="B"`` + (primitives/mimcap.py). This used to default to "A", so every gf180 cell with + a MIM extracted zero capacitors and LVS reported "N schematic device(s) + missing from layout [CAP_MIM_2F0FF]". Set $GF180_MIM_OPTION=A for a layout + genuinely built on the met2/met3 stack. """ layout_path = Path(layout) netlist_path = Path(netlist) @@ -310,7 +315,7 @@ def run_lvs_klayout_gf180( rpt_dir.mkdir(parents=True, exist_ok=True) pdk_root = pdk_root or os.environ.get("PDK_ROOT", "/foss/pdks") - mim_option = (mim_option or os.environ.get("GF180_MIM_OPTION") or "A").upper() + mim_option = (mim_option or os.environ.get("GF180_MIM_OPTION") or "B").upper() if mim_option not in ("A", "B"): raise ValueError(f"mim_option must be 'A' or 'B', got {mim_option!r}") deck_dir = _resolve_deck_dir(pdk_root) diff --git a/tests/lvs/run_cell_lvs.py b/tests/lvs/run_cell_lvs.py index 238cad83..c74a17a1 100644 --- a/tests/lvs/run_cell_lvs.py +++ b/tests/lvs/run_cell_lvs.py @@ -227,8 +227,9 @@ def main() -> int: "gf180 MIM stack the deck should extract: A (met2/FuseTop/met3) " "or B (met4/FuseTop/met5). They are mutually exclusive at process " "level, so picking the wrong one extracts no capacitor and every " - "MIM reports as missing from the layout. Default: $GF180_MIM_OPTION, " - "else A." + "MIM reports as missing from the layout. glayout builds option B " + "(mimcap/mimcap_array default to option=\"B\"), so that is the " + "default here too. Default: $GF180_MIM_OPTION, else B." ), ) parser.add_argument(