Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/glayout/primitives/mimcap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
84 changes: 84 additions & 0 deletions tests/test_mimcap_build.py
Original file line number Diff line number Diff line change
@@ -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()
Loading