From 50998ec8f8bda794ac7954fea11b50cc1d4fba76 Mon Sep 17 00:00:00 2001 From: Uri Shaked Date: Sun, 14 Jun 2026 16:50:19 +0300 Subject: [PATCH] feat(precheck/gf180mcuD): run KLayout DRC via PDK run_drc.py - Density related rules skipped (irrelevant for user projects) - Add docopt dependecy (required by the PDK's run_drc.py) --- precheck/precheck.py | 53 ++++++++++++++++++++++++++++++++----- precheck/pyproject.toml | 1 + precheck/reports/.gitignore | 2 ++ precheck/requirements.txt | 2 ++ precheck/test_precheck.py | 43 ++++++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 7 deletions(-) diff --git a/precheck/precheck.py b/precheck/precheck.py index 769ee2a..4e59248 100644 --- a/precheck/precheck.py +++ b/precheck/precheck.py @@ -4,6 +4,7 @@ import logging import os import re import subprocess +import sys import tempfile import time import traceback @@ -73,6 +74,15 @@ def magic_drc(gds: str, toplevel: str): raise PrecheckFailure("Magic DRC failed") +def check_drc_report(check: str, report_file: str): + report = rdb.ReportDatabase("DRC") + report.load(report_file) + if report.num_items() > 0: + raise PrecheckFailure( + f"Klayout {check} failed with {report.num_items()} DRC violations" + ) + + def klayout_custom_drc( check: str, script_path: str, script_vars: dict[str, str], report_vars: list[str] ): @@ -86,13 +96,7 @@ def klayout_custom_drc( if klayout.returncode != 0: raise PrecheckFailure(f"Klayout {check} failed") - report = rdb.ReportDatabase("DRC") - report.load(report_file) - - if report.num_items() > 0: - raise PrecheckFailure( - f"Klayout {check} failed with {report.num_items()} DRC violations" - ) + check_drc_report(check, report_file) def klayout_drc( @@ -135,6 +139,36 @@ def klayout_gf180mcuD_antenna(gds: str): ) +def klayout_gf180mcuD_drc(gds: str, top_module: str): + logging.info(f"Running klayout gf180mcuD DRC on {gds}") + run_dir = f"{REPORTS_PATH}/gf180mcuD_drc" + os.makedirs(run_dir, exist_ok=True) + report_file = f"{run_dir}/{os.path.splitext(os.path.basename(gds))[0]}_main.lyrdb" + if os.path.exists(report_file): + os.remove(report_file) + + run_drc = f"{PDK_ROOT}/{PDK_NAME}/libs.tech/klayout/tech/drc/run_drc.py" + subprocess.run( + [ + sys.executable, + run_drc, + f"--path={gds}", + f"--topcell={top_module}", + "--variant=D", # gf180mcuD: metal_top=11K, mim_option=B, metal_level=5LM + "--run_mode=deep", + "--thr=1", # single-threaded to work around a klayout deadlock bug + f"--run_dir={run_dir}", + ], + ) + + # run_drc.py exits non-zero on violations, so we judge the result from the + # report database it produces (matching the other klayout checks). + if not os.path.exists(report_file): + raise PrecheckFailure("Klayout gf180mcuD DRC did not produce a report") + + check_drc_report("gf180mcuD DRC", report_file) + + def klayout_checks(gds: str, expected_name: str, tech: str): layout = pya.Layout() layout.read(gds) @@ -488,6 +522,11 @@ def main(): "check": lambda: urpm_nwell_check(gds_file, top_module), "techs": ["sky130A"], }, + { + "name": "KLayout GF180MCU DRC", + "check": lambda: klayout_gf180mcuD_drc(gds_file, top_module), + "techs": ["gf180mcuD"], + }, { "name": "Antenna check", "check": lambda: klayout_gf180mcuD_antenna(gds_file), diff --git a/precheck/pyproject.toml b/precheck/pyproject.toml index 9008317..0586e3c 100644 --- a/precheck/pyproject.toml +++ b/precheck/pyproject.toml @@ -3,6 +3,7 @@ name = "tt-precheck" requires-python = ">=3.11" dynamic = ["version"] dependencies = [ + "docopt", "gdstk", "klayout>=0.28.17.post1,<0.29.0", "numpy<2", diff --git a/precheck/reports/.gitignore b/precheck/reports/.gitignore index 8481468..d120ac5 100644 --- a/precheck/reports/.gitignore +++ b/precheck/reports/.gitignore @@ -5,6 +5,8 @@ drc_pin_label_purposes_overlapping_drawing.xml drc_zero_area.xml drc_nwell_urpm.xml drc_sg13g2.xml +drc_antenna.xml +gf180mcuD_drc/ magic_drc.mag magic_drc.txt results.md diff --git a/precheck/requirements.txt b/precheck/requirements.txt index 5c8faa9..a99a8a4 100644 --- a/precheck/requirements.txt +++ b/precheck/requirements.txt @@ -6,6 +6,8 @@ # click==8.3.1 # via yowasp-yosys +docopt==0.6.2 + # via tt-precheck (pyproject.toml) gdstk==0.9.52 # via tt-precheck (pyproject.toml) iniconfig==2.0.0 diff --git a/precheck/test_precheck.py b/precheck/test_precheck.py index 8a5ae0e..87c6fcc 100644 --- a/precheck/test_precheck.py +++ b/precheck/test_precheck.py @@ -609,6 +609,35 @@ def gds_antenna_fail(tmp_path_factory: pytest.TempPathFactory): return str(gds_file) +@pytest.fixture(scope="session") +def gds_gf180_drc_pass(tmp_path_factory: pytest.TempPathFactory): + """Creates a GDS without DRC violations (density rules are not run).""" + gds_file = tmp_path_factory.mktemp("gds") / "TEST_gf180_drc_pass.gds" + layout = pya.Layout() + metal1_info = gds_layers["Metal1"] + metal1 = layout.layer(metal1_info.layer, metal1_info.data_type) + top_cell = layout.create_cell("TEST_gf180_drc_pass") + # A single wide Metal1 shape: satisfies min width, area and spacing. + top_cell.shapes(metal1).insert(pya.DBox(0, 0, 1, 1)) + layout.write(str(gds_file)) + return str(gds_file) + + +@pytest.fixture(scope="session") +def gds_gf180_drc_fail(tmp_path_factory: pytest.TempPathFactory): + """Creates a GDS with a Metal1 spacing violation (rule M1.2a).""" + gds_file = tmp_path_factory.mktemp("gds") / "TEST_gf180_drc_fail.gds" + layout = pya.Layout() + metal1_info = gds_layers["Metal1"] + metal1 = layout.layer(metal1_info.layer, metal1_info.data_type) + top_cell = layout.create_cell("TEST_gf180_drc_fail") + # Two wide Metal1 shapes spaced 0.1um apart, below the min spacing rule. + top_cell.shapes(metal1).insert(pya.DBox(0, 0, 1, 1)) + top_cell.shapes(metal1).insert(pya.DBox(1.1, 0, 2.1, 1)) + layout.write(str(gds_file)) + return str(gds_file) + + # TESTS @@ -937,6 +966,20 @@ def test_verilog_syntax_error(verilog_syntax_error: str): precheck.verilog_syntax_check(verilog_syntax_error) +@gf180mcuD_only +def test_gf180mcuD_drc_pass(gds_gf180_drc_pass: str): + precheck.klayout_gf180mcuD_drc(gds_gf180_drc_pass, "TEST_gf180_drc_pass") + + +@gf180mcuD_only +def test_gf180mcuD_drc_fail(gds_gf180_drc_fail: str): + with pytest.raises( + precheck.PrecheckFailure, + match="Klayout gf180mcuD DRC failed with 1 DRC violations", + ): + precheck.klayout_gf180mcuD_drc(gds_gf180_drc_fail, "TEST_gf180_drc_fail") + + @gf180mcuD_only def test_antenna_pass(gds_antenna_pass: str): precheck.klayout_gf180mcuD_antenna(gds_antenna_pass)