From fe0c90c3a1cc291be76121dadcfcfe4a35c8ade5 Mon Sep 17 00:00:00 2001 From: euler Date: Sat, 22 Aug 2026 04:12:38 -0500 Subject: [PATCH 1/6] tests/lvs: let the reference netlist decide which labels are pins Cell LVS on main fails on diff_pair, diff_pair_ibias and opamp, all with the same shape: "extra top-level pin(s) in layout" for SUB and VTAIL. A pin label is not a property of a cell, it is a property of how the cell is used. VTAIL is a top-level pin of a standalone diff_pair and an internal net inside diff_pair_ibias. Elementary cells emit labels so they can be LVS'd on their own, and a parent that flattens them inherits those names. Fixing that in the generator means every composite has to suppress its children's labels at every level of the hierarchy. `with_pin_labels` does this for diff_pair, but the parameter stops there: diff_pair_ibias does not accept it and emits labels of its own, so opamp cannot suppress anything. Each new composite is one more place that has to remember, and one that forgets fails silently -- which is how these three got here. The reference netlist already states which names are pins. Honour that when staging inputs: drop labels whose text is not a port of the top `.subckt`. No cell has to cooperate, it works at any depth and whether or not the parent flattened, and it needs no ambient state -- relevant because `@cell` keys its cache on arguments, so a context manager or env var can hand back a stale component built under the opposite setting. Only the staged copy is filtered; the GDS a cell ships keeps its labels, so LEF and macro flows are unaffected. Matching is case-insensitive: generators and schematics disagree on capitalisation in practice (`vdd` vs `Vdd`). Dropping every label is reported as a warning rather than passed over: that is not inheritance but a naming mismatch, and silently leaving the layout with no pins would turn it into a confusing LVS failure downstream. With no readable ports, nothing is dropped. Measured on nine real cells, 5-12 labels over 49-4913 polygons: 0.1-1.0 ms to filter, ~27 ms including the GDS round trip, against 1-8 s per cell of LVS. --- tests/lvs/klayout_gf180.py | 85 ++++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 9 deletions(-) diff --git a/tests/lvs/klayout_gf180.py b/tests/lvs/klayout_gf180.py index 94653017..908b6eee 100644 --- a/tests/lvs/klayout_gf180.py +++ b/tests/lvs/klayout_gf180.py @@ -22,7 +22,9 @@ import subprocess import tempfile from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional, Tuple + +import gdstk try: from lvsdb_report import analyze, render @@ -59,6 +61,24 @@ def _resolve_deck_dir(pdk_root: str) -> Path: return deck +def _top_level_ports(spice_path: Path, top_cell: str) -> List[str]: + """Port names on the reference netlist's top `.subckt`, in order. + + This is the schematic's own statement of what the cell's pins are, so it + is what decides which layout labels are pins -- see _filter_pin_labels. + """ + try: + text = spice_path.read_text(errors="ignore") + except OSError: + return [] + pat = re.compile(r"^\.subckt\s+" + re.escape(top_cell) + r"\s+(.+)$", + re.MULTILINE | re.IGNORECASE) + m = pat.search(text) + if not m: + return [] + return [tok for tok in m.group(1).split() if "=" not in tok] + + def _detect_substrate_name(spice_path: Path, top_cell: str) -> str: """Pick the schematic's bulk port name to pass as klayout's --lvs_sub. @@ -70,15 +90,9 @@ def _detect_substrate_name(spice_path: Path, top_cell: str) -> str: pick B). Falls back to the last positional port, then to the deck default. """ - try: - text = spice_path.read_text(errors="ignore") - except OSError: + tokens = _top_level_ports(spice_path, top_cell) + if not tokens: return "gf180mcu_gnd" - pat = re.compile(r"^\.subckt\s+" + re.escape(top_cell) + r"\s+(.+)$", re.MULTILINE | re.IGNORECASE) - m = pat.search(text) - if not m: - return "gf180mcu_gnd" - tokens = [t for t in m.group(1).split() if "=" not in t] upper = {t.upper(): t for t in tokens} for cand in ("B", "VBULK", "VSUB", "GND", "VSS"): if cand in upper: @@ -126,6 +140,46 @@ def _rewrite_x_to_m_for_primitives(cdl_text: str) -> str: return cap_pat.sub(r"C\1\2", cdl_text) +def _filter_pin_labels(gds_path: Path, ports: List[str]) -> Tuple[List[str], bool]: + """Drop layout labels the reference netlist does not declare as pins. + + A pin label is not a property of a cell, it is a property of how the cell + is used: a diff_pair's VTAIL is a top-level pin standalone and an internal + net inside a composite. Elementary cells emit labels so they can be LVS'd + on their own, and a parent that flattens them inherits those names -- + klayout extracts them as extra top-level pins and LVS fails. + + Deciding this in the generator means every composite has to suppress its + children's labels, at every level, and one that forgets fails silently. + Deciding it here needs no cooperation from any cell: the reference netlist + already states which names are pins, and that statement is honoured. + + Only the staged copy used for extraction is filtered, so the GDS a cell + ships keeps its labels and LEF/macro flows are unaffected. + + Matching is case-insensitive: SPICE is case-insensitive and generators do + not always agree with the schematic on capitalisation (`vdd` vs `Vdd`). + + Returns the dropped texts, and whether every label was dropped -- that is + not label inheritance but a naming mismatch, worth reporting. + """ + if not ports: + return [], False + keep = {port.upper() for port in ports} + lib = gdstk.read_gds(str(gds_path)) + dropped: List[str] = [] + total = 0 + for cell in lib.cells: + for label in list(cell.labels): + total += 1 + if label.text.upper() not in keep: + cell.remove(label) + dropped.append(label.text) + if dropped: + lib.write_gds(str(gds_path)) + return dropped, bool(total) and len(dropped) == total + + def _stage_inputs(workdir: Path, cell: str, gds_src: Path, netlist_src: Path) -> Path: """Copy GDS + reference netlist into the temp dir, normalize, and return the staged spice path. Normalizations (mirror `.run_ci_lvs_v2.sh`): @@ -138,6 +192,8 @@ def _stage_inputs(workdir: Path, cell: str, gds_src: Path, netlist_src: Path) -> generator code stays PDK-agnostic and emits X-prefix everywhere. * Prepend `.include` of the bundled reference spice so any std-cell subckt the test netlist references can be resolved. + * Drop labels the reference netlist does not declare as pins, so a + composite does not inherit its children's standalone pin names. """ layout_dst = workdir / f"{cell}.gds" cdl_dst = workdir / f"{cell}.cdl" @@ -163,6 +219,17 @@ def _stage_inputs(workdir: Path, cell: str, gds_src: Path, netlist_src: Path) -> parts.append(f".include {_REF_SPICE}\n") parts.append(cdl_text) spice_dst.write_text("".join(parts)) + + ports = _top_level_ports(spice_dst, cell) + dropped, all_gone = _filter_pin_labels(layout_dst, ports) + if all_gone: + print(f"[{cell}] WARNING: no layout label matches a port of " + f".subckt {cell} ({' '.join(ports)}); dropped {dropped}. " + f"Layout pin names and reference netlist pin names do not " + f"agree, so LVS will see the layout as having no pins.") + elif dropped: + print(f"[{cell}] dropped {len(dropped)} inherited label(s) not " + f"declared as pins: {' '.join(sorted(set(dropped)))}") return spice_dst From 359dae41b5650199da31cf0ed6aea1e1cd11d958 Mon Sep 17 00:00:00 2001 From: euler Date: Sat, 22 Aug 2026 04:44:25 -0500 Subject: [PATCH 2/6] tests: cover the pin-label filter Pure Python -- no PDK, no klayout, no GDS toolchain -- so it runs wherever pytest does. That matters here: the LVS workflow is triggered by `workflow_run` off Cell DRC, which only fires on the default branch, so LVS never runs on a pull request. A PR that fixes or breaks LVS cannot be seen either way until after it merges, which is how main went red. Six cases, including the two that are easy to get wrong: matching has to ignore case, because generators and schematics disagree in practice (`vdd` vs `Vdd`) and a case-sensitive compare would strip real pins; and an unreadable port list must drop nothing rather than leave the layout with no pins at all. Both are checked by mutation: making the compare case-sensitive, or removing the empty-ports guard, each fails exactly one test. --- tests/test_lvs_pin_filter.py | 101 +++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 tests/test_lvs_pin_filter.py diff --git a/tests/test_lvs_pin_filter.py b/tests/test_lvs_pin_filter.py new file mode 100644 index 00000000..9d349669 --- /dev/null +++ b/tests/test_lvs_pin_filter.py @@ -0,0 +1,101 @@ +"""The reference netlist decides which layout labels are pins. + +Covers the staging step that stops a composite from inheriting its children's +standalone pin names. Pure Python: no PDK, no klayout, no GDS toolchain, so it +runs on every PR -- unlike the LVS workflow, which is triggered by `workflow_run` +and therefore only runs on the default branch, after a merge. +""" +import importlib.util +import tempfile +import unittest +from pathlib import Path + +_MODULE = Path(__file__).resolve().parent / "lvs" / "klayout_gf180.py" + + +def _load(): + """tests/lvs is not a package, so load the runner by path.""" + spec = importlib.util.spec_from_file_location("klayout_gf180", _MODULE) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +class PinFilterTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.mod = _load() + + def _gds(self, workdir, labels): + import gdstk + lib = gdstk.Library() + cell = lib.new_cell("cell_under_test") + for text in labels: + cell.add(gdstk.Label(text, (0, 0))) + path = Path(workdir) / "cell_under_test.gds" + lib.write_gds(str(path)) + return path + + def _labels(self, path): + import gdstk + return sorted(l.text for c in gdstk.read_gds(str(path)).cells for l in c.labels) + + def test_ports_come_from_the_top_subckt(self): + with tempfile.TemporaryDirectory() as d: + spice = Path(d) / "c.spice" + spice.write_text( + ".subckt other A B\n.ends\n" + ".subckt cell_under_test Vdd Vss Iin spike\n.ends\n" + ) + self.assertEqual( + self.mod._top_level_ports(spice, "cell_under_test"), + ["Vdd", "Vss", "Iin", "spike"], + ) + + def test_inherited_label_is_dropped_and_pins_are_kept(self): + # VTAIL is a diff_pair pin standalone and an internal net in a parent. + with tempfile.TemporaryDirectory() as d: + gds = self._gds(d, ["Vdd", "Vss", "VTAIL", "spike"]) + dropped, all_gone = self.mod._filter_pin_labels(gds, ["Vdd", "Vss", "spike"]) + self.assertEqual(dropped, ["VTAIL"]) + self.assertFalse(all_gone) + self.assertEqual(self._labels(gds), ["Vdd", "Vss", "spike"]) + + def test_matching_ignores_case(self): + # Generators and schematics disagree in practice: `vdd` vs `Vdd`. + with tempfile.TemporaryDirectory() as d: + gds = self._gds(d, ["vdd", "vss"]) + dropped, all_gone = self.mod._filter_pin_labels(gds, ["Vdd", "Vss"]) + self.assertEqual(dropped, []) + self.assertFalse(all_gone) + self.assertEqual(self._labels(gds), ["vdd", "vss"]) + + def test_dropping_every_label_is_flagged(self): + # Not inheritance but a naming mismatch: the caller warns instead of + # leaving the layout silently pinless. + with tempfile.TemporaryDirectory() as d: + gds = self._gds(d, ["n1", "n2"]) + dropped, all_gone = self.mod._filter_pin_labels(gds, ["Vdd", "Vss"]) + self.assertEqual(sorted(dropped), ["n1", "n2"]) + self.assertTrue(all_gone) + + def test_a_layout_without_labels_is_not_flagged(self): + with tempfile.TemporaryDirectory() as d: + gds = self._gds(d, []) + dropped, all_gone = self.mod._filter_pin_labels(gds, ["Vdd", "Vss"]) + self.assertEqual(dropped, []) + self.assertFalse(all_gone) + + def test_unreadable_ports_drop_nothing(self): + # Safety net: without a port list, keep every label rather than + # stripping the layout of all its pins. + with tempfile.TemporaryDirectory() as d: + gds = self._gds(d, ["Vdd", "VTAIL"]) + dropped, all_gone = self.mod._filter_pin_labels(gds, []) + self.assertEqual(dropped, []) + self.assertFalse(all_gone) + self.assertEqual(self._labels(gds), ["VTAIL", "Vdd"]) + + +if __name__ == "__main__": + unittest.main() From e92dda855b1505b60f149e0c36038150f33281bb Mon Sep 17 00:00:00 2001 From: euler Date: Sat, 22 Aug 2026 06:36:51 -0500 Subject: [PATCH 3/6] ci: run the unit tests on pull requests The repo has 14 tests under tests/ and no workflow executes any of them. That matters more than it looks: the DRC and LVS workflows run the gdsfactory backend (CPython 3.10, gdsfactory 7.7), so gdstk has no CI coverage at all today, and conftest.py pins the test suite to gdstk. This job is the first thing that exercises it. It also gives pull requests a check they can currently fail. lvs.yml is triggered by `workflow_run` off Cell DRC, which only fires on the default branch: LVS has runs on main and none on any fork branch, so a PR that fixes or breaks LVS cannot be seen either way until after it merges. Deliberately not `pip install -e .`: install_requires pins gdsfactory<=7.7.0 and numpy<=1.24.0, which forces CPython 3.10 and pulls the whole layout stack. None of it is needed -- verified in a clean venv with only pytest, gdstk, numpy, pandas, pydantic and docopt: 19 passed in 7.2 s. tests/test_cells_layout.py is left out. It builds every cell in both backends, takes ~85 s, and carries five documented xfails, so green there would mean "nothing changed" rather than "everything works". Cell-build coverage deserves its own argument, and its own PR. --- .github/workflows/tests.yml | 55 +++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..86e5ead6 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,55 @@ +name: Unit tests + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +# Cancel superseded runs on the same branch. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + unit: + name: Unit tests (gdstk) + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + + # Deliberately NOT `pip install -e .`. install_requires pins + # gdsfactory<=7.7.0 and numpy<=1.24.0, which forces CPython 3.10 and + # pulls the whole layout stack; none of it is needed here. conftest.py + # sets GLAYOUT_BACKEND=gdstk precisely so tests that import glayout + # directly do not require gdsfactory, and these files build on gdstk + # only. Keeping the environment small is what makes this job fast + # enough to run on every push. + - name: Install test dependencies + run: pip install pytest gdstk numpy pandas pydantic docopt + + # The heavier DRC/LVS workflows run the gdsfactory backend (Python + # 3.10, gdsfactory 7.7), so gdstk currently has no CI coverage at all. + # tests/test_cells_layout.py is left out on purpose: it builds every + # cell in both backends, takes ~85 s, and carries five documented + # xfails, so a green result there would mean "nothing changed" rather + # than "everything works". It belongs to whoever argues for cell-build + # coverage. + - name: Run tests + env: + PYTHONPATH: src + GLAYOUT_BACKEND: gdstk + PDK_ROOT: /tmp + run: | + pytest -q \ + tests/test_lvs_pin_filter.py \ + tests/test_import_paths.py \ + tests/test_repo_layout.py \ + tests/test_gdstk_backend.py From 9043944bf85396a91da03d7a0dfefb6ba84c7d8f Mon Sep 17 00:00:00 2001 From: euler Date: Sat, 22 Aug 2026 06:54:43 -0500 Subject: [PATCH 4/6] ci: key the pip cache off setup.py setup-python's `cache: pip` looks for requirements.txt or pyproject.toml to hash and fails the job when it finds neither. This repo ships setup.py. --- .github/workflows/tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 86e5ead6..cf499f5a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,6 +24,9 @@ jobs: with: python-version: "3.11" cache: pip + # setup-python hashes this to key the cache; the repo has no + # requirements.txt or pyproject.toml, so point it at setup.py. + cache-dependency-path: setup.py # Deliberately NOT `pip install -e .`. install_requires pins # gdsfactory<=7.7.0 and numpy<=1.24.0, which forces CPython 3.10 and From ad5a149fb5e5b9c5af074b6fb8bf47cbbbcabb79 Mon Sep 17 00:00:00 2001 From: euler Date: Sat, 22 Aug 2026 07:04:41 -0500 Subject: [PATCH 5/6] ci: select tests by exclusion, not by listing them Listing the four files reproduces the problem this job exists to fix: a test added later is not run until someone remembers to edit the workflow. #100 adds tests/test_narrow_fets.py, which the explicit list would have silently skipped. Everything under tests/ now runs except test_cells_layout.py, which stays out for the reason already documented above the step. --- .github/workflows/tests.yml | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cf499f5a..0dc2f1a2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,19 +40,15 @@ jobs: # The heavier DRC/LVS workflows run the gdsfactory backend (Python # 3.10, gdsfactory 7.7), so gdstk currently has no CI coverage at all. - # tests/test_cells_layout.py is left out on purpose: it builds every - # cell in both backends, takes ~85 s, and carries five documented - # xfails, so a green result there would mean "nothing changed" rather - # than "everything works". It belongs to whoever argues for cell-build - # coverage. + # Everything under tests/ except test_cells_layout.py, which is left + # out on purpose: it builds every cell in both backends, takes ~85 s, + # and carries five documented xfails, so a green result there would + # mean "nothing changed" rather than "everything works". Excluding + # rather than listing files means a test added later is picked up + # without having to remember to edit this workflow. - name: Run tests env: PYTHONPATH: src GLAYOUT_BACKEND: gdstk PDK_ROOT: /tmp - run: | - pytest -q \ - tests/test_lvs_pin_filter.py \ - tests/test_import_paths.py \ - tests/test_repo_layout.py \ - tests/test_gdstk_backend.py + run: pytest -q tests --ignore=tests/test_cells_layout.py From 11de1e349e5fbf83b6512f4fa4e2c3b6a5e85e30 Mon Sep 17 00:00:00 2001 From: euler Date: Sat, 22 Aug 2026 13:22:31 -0500 Subject: [PATCH 6/6] tests/lvs: let the caller pick the gf180 MIM option The runner hardcoded option A, so a cell drawing its MIM on option B (met4/FuseTop/met5) extracted no capacitor at all and every MIM reported as missing from the layout. The two options are mutually exclusive at process level, so the deck can only be told one of them. Adds --mim-option / $GF180_MIM_OPTION, defaulting to A as before, and applies the option-A deck repair only when option A is selected -- the deck's option B branch already connects all three plates. --- tests/lvs/klayout_gf180.py | 30 +++++++++++++++++++++--------- tests/lvs/run_cell_lvs.py | 12 ++++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/tests/lvs/klayout_gf180.py b/tests/lvs/klayout_gf180.py index 908b6eee..bb5bbebd 100644 --- a/tests/lvs/klayout_gf180.py +++ b/tests/lvs/klayout_gf180.py @@ -287,6 +287,7 @@ def run_lvs_klayout_gf180( netlist: str, output_file_path: str, pdk_root: Optional[str] = None, + mim_option: Optional[str] = None, ) -> Dict[str, Any]: """Run gf180mcu klayout LVS for one cell. @@ -295,6 +296,12 @@ def run_lvs_klayout_gf180( verbatim — `_parse_lvs_report` recognises the "Netlists match" / "Netlists do not match" lines), and stashes the extracted .cir, .lvsdb, and lvs_run_*.log alongside it for inspection. + + ``mim_option`` selects the MIM stack the deck extracts: "A" (met2 / + 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". """ layout_path = Path(layout) netlist_path = Path(netlist) @@ -303,6 +310,9 @@ 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() + 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) run_lvs = deck_dir / "run_lvs.py" @@ -312,24 +322,26 @@ def run_lvs_klayout_gf180( sub_name = _detect_substrate_name(spice_staged, design_name) # The deck is called directly rather than through run_lvs.py, whose - # four presets are all wrong here: glayout draws the MIM on option A - # (met2 / FuseTop / met3) and routes up to met5, and no preset pairs - # option A with 5 metal levels. That combination is a real process -- - # the DRM documents 1P5M (TM 6KA with MIM) -- and the deck accepts the - # options individually. + # four presets are all wrong here: a cell can draw its MIM on option A + # (met2 / FuseTop / met3) and still route up to met5, and no preset + # pairs option A with 5 metal levels. That combination is a real + # process -- the DRM documents 1P5M (TM 6KA with MIM) -- and the deck + # accepts the options individually. # # GF180_LVS_DECK points at an already-fixed deck; otherwise the runner - # patches its own copy. + # patches its own copy. Only option A needs the patch; the deck's + # option B branch already connects all three plates. + deck_src = run_lvs.parent / "gf180mcu.lvs" lvs_deck = Path(os.environ.get("GF180_LVS_DECK") - or _deck_with_option_a_fixed(run_lvs.parent / "gf180mcu.lvs", - tmpdir / "deck")) + or (_deck_with_option_a_fixed(deck_src, tmpdir / "deck") + if mim_option == "A" else deck_src)) sws = { "input": str(layout_path), "schematic": str(spice_staged), "topcell": design_name, "target_netlist": str(tmpdir / f"{design_name}.cir"), "report": str(tmpdir / f"{design_name}.lvsdb"), - "mim_option": "A", + "mim_option": mim_option, "metal_level": "5LM", "metal_top": "11K", "poly_res": "1k", diff --git a/tests/lvs/run_cell_lvs.py b/tests/lvs/run_cell_lvs.py index 05f47ed4..238cad83 100644 --- a/tests/lvs/run_cell_lvs.py +++ b/tests/lvs/run_cell_lvs.py @@ -155,6 +155,7 @@ def _run_one_lvs(item: dict) -> dict: design_name=name, netlist=str(netlist_path), output_file_path=str(rpt_dir), + mim_option=item.get("mim_option"), ) else: pdk = _resolve_pdk(pdk_name) @@ -220,6 +221,16 @@ def main() -> int: "reporting anything useful." ), ) + parser.add_argument( + "--mim-option", choices=["A", "B"], default=None, + help=( + "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." + ), + ) parser.add_argument( "--jobs", "-j", type=int, default=max(1, (os.cpu_count() or 2) - 1), help="Worker processes for parallel LVS (default: cpu_count-1).", @@ -258,6 +269,7 @@ def main() -> int: "netlist_path": str(inputs_dir / "netlists" / f"{name}.spice"), "out_dir": str(out_dir), "rpt_dir": str(rpt_dir), + "mim_option": args.mim_option, } for name in cells ]