From def8ce6a7f73cb95fa69e5a9d94ee000e1990f78 Mon Sep 17 00:00:00 2001 From: euler Date: Sat, 22 Aug 2026 06:08:03 -0500 Subject: [PATCH 1/2] mimcap: build with extensions again `mimcap()` raises with its default arguments, on both options: File "src/glayout/primitives/mimcap.py", line 293, in mimcap appendix_pos[cord_ref] = appendix_pos[cord_ref] + ... TypeError: 'tuple' object does not support item assignment `Port.center` is a tuple, and the line below indexes into it to shift the appendix. Take a list. The branch is only reached when `min_via_distance < 0.4`, so it depends on the requested size -- 5x5 hits it. No CI cell caught this because every tested cell goes through `mimcap_array`, which passes `with_extension=False` and skips the path. A direct `mimcap(pdk, size)` -- the obvious way to use the primitive -- fails. Verified on gf180: option A, option B, a non-square 10x4, and mimcap_array all build and write GDS. Option A draws met2/FuseTop/via2/met3, option B met4/FuseTop/via4/met5. --- src/glayout/primitives/mimcap.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/glayout/primitives/mimcap.py b/src/glayout/primitives/mimcap.py index 5d6e24d2..c7c4464a 100644 --- a/src/glayout/primitives/mimcap.py +++ b/src/glayout/primitives/mimcap.py @@ -289,7 +289,8 @@ def mimcap( layer=pdk.get_glayer(capmetbottom), centered=True ) - appendix_pos=target_port.center + # Port.center is a tuple; the component below indexes into it. + appendix_pos=list(target_port.center) appendix_pos[cord_ref] = appendix_pos[cord_ref] + appendix_size[cord_ref]/2 if direction=="N" or direction=="E" else appendix_pos[cord_ref] - appendix_size[cord_ref]/2 appendix.move(appendix_pos) From 55e512b670a411da899dc7a20449977f0b6aabc9 Mon Sep 17 00:00:00 2001 From: euler Date: Mon, 24 Aug 2026 05:01:50 -0500 Subject: [PATCH 2/2] tests: cover the mimcap primitive on its own Every cell in the suite reaches mimcap through mimcap_array, which passes `with_extension=False` and skips the branch that regressed. So the suite stayed green while a direct `mimcap(pdk, size)` raised -- and anyone checking the primitive by hand through the array saw it build fine. Four cases: the default path with extensions, the array path, the no-extension path kept so a fix that trades one branch for the other does not read as a pass, and a check that each option draws the stack it names (A on met2/met3, B on met4/met5) rather than merely building. Against `main` this fails 6 of them with the TypeError; with the fix, 4 tests and 8 subtests pass. --- tests/test_mimcap_build.py | 84 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/test_mimcap_build.py diff --git a/tests/test_mimcap_build.py b/tests/test_mimcap_build.py new file mode 100644 index 00000000..065774fa --- /dev/null +++ b/tests/test_mimcap_build.py @@ -0,0 +1,84 @@ +"""The mimcap primitive builds on its own, not only through mimcap_array. + +`mimcap_array` passes `with_extension=False`, so every cell in the suite +reaches the primitive through the one path that skips the appendix code. A +direct `mimcap(pdk, size)` -- the obvious way to use it -- goes down the +other branch, and that is where it raised `TypeError: 'tuple' object does +not support item assignment`. + +The appendix branch is entered only when the requested size leaves +`min_via_distance < 0.4`, so the size matters: 5x5 reaches it. Both MIM +options are covered because they build different stacks (A on +met2/FuseTop/met3, B on met4/FuseTop/met5) around the same code. +""" +import unittest + + +class MimcapBuildTests(unittest.TestCase): + SIZES = ((5.0, 5.0), (10.0, 4.0)) # square hits the appendix branch + + def _pdk(self): + from glayout.pdk.gf180_mapped.gf180_mapped import gf180_mapped_pdk + return gf180_mapped_pdk + + def test_mimcap_builds_with_extensions(self): + """The default path. This is the one that regressed.""" + from glayout.primitives.mimcap import mimcap + pdk = self._pdk() + for option in ("A", "B"): + for size in self.SIZES: + with self.subTest(option=option, size=size): + comp = mimcap(pdk, size=size, option=option) + self.assertTrue(comp.get_ports_list()) + + def test_mimcap_builds_without_extensions(self): + """The path mimcap_array takes, which never regressed. + + Kept so a future change that fixes one branch by breaking the other + does not look like a pass. + """ + from glayout.primitives.mimcap import mimcap + pdk = self._pdk() + for option in ("A", "B"): + with self.subTest(option=option): + comp = mimcap(pdk, size=(5.0, 5.0), option=option, + with_extension=False) + self.assertTrue(comp.get_ports_list()) + + def test_mimcap_array_builds(self): + from glayout.primitives.mimcap import mimcap_array + comp = mimcap_array(self._pdk(), rows=2, columns=2, size=(5.0, 5.0)) + self.assertTrue(comp.get_ports_list()) + + def test_plates_land_on_the_layers_the_option_names(self): + """Option A is a met2/met3 stack, option B a met4/met5 one. + + Building is not enough: the two options differ only in which metals + they draw, so a fix that builds both while drawing the same stack + would pass every check above. + + Read back from the written GDS rather than from the Component, so + this does not depend on which backend is active. + """ + import gdstk + import tempfile + from glayout.primitives.mimcap import mimcap + pdk = self._pdk() + expected = {"A": ("met2", "met3"), "B": ("met4", "met5")} + for option, glayers in expected.items(): + with self.subTest(option=option): + comp = mimcap(pdk, size=(5.0, 5.0), option=option) + with tempfile.TemporaryDirectory() as tmp: + path = tmp + "/mimcap.gds" + comp.write_gds(path) + top = gdstk.read_gds(path).top_level()[0] + top.flatten() + drawn = {(p.layer, p.datatype) for p in top.polygons} + for glayer in glayers: + layer, datatype = pdk.get_glayer(glayer) + self.assertIn((int(layer), int(datatype)), drawn, + "option %s should draw %s" % (option, glayer)) + + +if __name__ == "__main__": + unittest.main()