feat(precheck/gf180mcuD): add test for antenna check

This commit is contained in:
htfab
2026-05-03 09:26:53 +02:00
committed by Uri Shaked
parent c904c49f53
commit d4626aaed7
4 changed files with 177 additions and 33 deletions
@@ -0,0 +1,54 @@
name: Precheck Tests (gf180mcu)
on:
push:
pull_request:
workflow_dispatch:
jobs:
test-precheck:
runs-on: ubuntu-latest
env:
PDK_ROOT: /home/runner/pdk
PDK_VERSION: ff08c23db8359afce3f134c454e7930586d0641c
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
working-directory: precheck
run: pip install -r requirements.txt
- name: Install Gf180mcu PDK
uses: TinyTapeout/ciel-action@v1
with:
pdk_name: gf180mcu
pdk_version: ${{ env.PDK_VERSION }}
pdk_root: ${{ env.PDK_ROOT }}
- name: Install Nix
uses: rikhuijzer/cache-install@v1.1.4
with:
key: nix-${{ hashFiles('precheck/default.nix') }}
nix_file: 'precheck/default.nix'
- name: Build Nix packages
working-directory: precheck
run: nix-build
- name: Run tests
working-directory: precheck
run: nix-shell --run "PDK=gf180mcuD pytest --junit-xml=test_results.xml"
- name: Test Summary
uses: test-summary/action@v2.4
with:
paths: 'precheck/test_results.xml'
if: always()
@@ -1,4 +1,4 @@
name: Precheck Tests name: Precheck Tests (sky130)
on: on:
push: push:
@@ -45,7 +45,7 @@ jobs:
- name: Run tests - name: Run tests
working-directory: precheck working-directory: precheck
run: nix-shell --run "pytest --junit-xml=test_results.xml" run: nix-shell --run "PDK=sky130A pytest --junit-xml=test_results.xml"
- name: Test Summary - name: Test Summary
uses: test-summary/action@v2.4 uses: test-summary/action@v2.4
+1 -1
View File
@@ -131,7 +131,7 @@ def klayout_gf180mcuD_antenna(gds: str):
gds, gds,
"antenna", "antenna",
"antenna.drc", "antenna.drc",
script_dir=f"{PDK_ROOT}/{PDK_NAME}/libs.tech/klayout/drc/rule_decks", script_dir=f"{PDK_ROOT}/{PDK_NAME}/libs.tech/klayout/tech/drc/rule_decks",
) )
+120 -30
View File
@@ -9,10 +9,21 @@ import pytest
import precheck import precheck
PDK_ROOT = os.getenv("PDK_ROOT") PDK_ROOT = os.getenv("PDK_ROOT")
PDK_NAME = os.getenv("PDK_NAME") or "sky130A" PDK_NAME = os.getenv("PDK") or "sky130A"
LYP_FILE = f"{PDK_ROOT}/{PDK_NAME}/libs.tech/klayout/tech/{PDK_NAME}.lyp" LYP_NAME = "gf180mcu" if PDK_NAME == "gf180mcuD" else PDK_NAME
LYP_FILE = f"{PDK_ROOT}/{PDK_NAME}/libs.tech/klayout/tech/{LYP_NAME}.lyp"
gds_layers = klayout_tools.parse_lyp_layers(LYP_FILE) gds_layers = klayout_tools.parse_lyp_layers(LYP_FILE)
sky130A_only = pytest.mark.skipif(
PDK_NAME != "sky130A", reason="test only valid for PDK=sky130A"
)
gf180mcuD_only = pytest.mark.skipif(
PDK_NAME != "gf180mcuD", reason="test only valid for PDK=gf180mcuD"
)
# FIXTURES
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def gds_valid(tmp_path_factory: pytest.TempPathFactory): def gds_valid(tmp_path_factory: pytest.TempPathFactory):
@@ -339,7 +350,7 @@ def generate_analog_example(
tcl_append( tcl_append(
f""" f"""
def read ../tech/sky130A/def/analog/tt_analog_1x2.def def read ../tech/{PDK_NAME}/def/analog/tt_analog_1x2.def
cellname rename tt_um_template {toplevel} cellname rename tt_um_template {toplevel}
""" """
) )
@@ -562,50 +573,98 @@ def verilog_syntax_error(tmp_path_factory: pytest.TempPathFactory):
return str(verilog_file) return str(verilog_file)
@pytest.fixture(scope="session")
def gds_antenna_pass(tmp_path_factory: pytest.TempPathFactory):
"""Creates a GDS without antenna violation."""
gds_file = tmp_path_factory.mktemp("gds") / "TEST_antenna_pass.gds"
layout = pya.Layout()
poly2_info = gds_layers["Poly2"]
poly2 = layout.layer(poly2_info.layer, poly2_info.data_type)
comp_info = gds_layers["COMP"]
comp = layout.layer(comp_info.layer, comp_info.data_type)
top_cell = layout.create_cell("TEST_antenna_pass")
poly2_rect = pya.DBox(0, 0, 1, 3)
top_cell.shapes(poly2).insert(poly2_rect)
comp_rect = pya.DBox(-1, 1, 2, 2)
top_cell.shapes(comp).insert(comp_rect)
layout.write(str(gds_file))
return str(gds_file)
@pytest.fixture(scope="session")
def gds_antenna_fail(tmp_path_factory: pytest.TempPathFactory):
"""Creates a GDS with antenna violation."""
gds_file = tmp_path_factory.mktemp("gds") / "TEST_antenna_fail.gds"
layout = pya.Layout()
poly2_info = gds_layers["Poly2"]
poly2 = layout.layer(poly2_info.layer, poly2_info.data_type)
comp_info = gds_layers["COMP"]
comp = layout.layer(comp_info.layer, comp_info.data_type)
top_cell = layout.create_cell("TEST_antenna_fail")
poly2_rect = pya.DBox(0, 0, 1, 500)
top_cell.shapes(poly2).insert(poly2_rect)
comp_rect = pya.DBox(-1, 1, 2, 2)
top_cell.shapes(comp).insert(comp_rect)
layout.write(str(gds_file))
return str(gds_file)
# TESTS
@sky130A_only
def test_magic_drc_pass(gds_valid: str): def test_magic_drc_pass(gds_valid: str):
precheck.magic_drc(gds_valid, "TEST_valid") precheck.magic_drc(gds_valid, "TEST_valid")
@sky130A_only
def test_magic_drc_fail(gds_fail_met1_poly: str): def test_magic_drc_fail(gds_fail_met1_poly: str):
with pytest.raises(precheck.PrecheckFailure): with pytest.raises(precheck.PrecheckFailure):
precheck.magic_drc(gds_fail_met1_poly, "TEST_met1_error") precheck.magic_drc(gds_fail_met1_poly, "TEST_met1_error")
@sky130A_only
def test_klayout_feol_pass(gds_valid: str): def test_klayout_feol_pass(gds_valid: str):
precheck.klayout_drc(gds_valid, "feol") precheck.klayout_drc(gds_valid, "feol")
@sky130A_only
def test_klayout_feol_fail(gds_fail_nwell_poly: str): def test_klayout_feol_fail(gds_fail_nwell_poly: str):
with pytest.raises(precheck.PrecheckFailure): with pytest.raises(precheck.PrecheckFailure):
precheck.klayout_drc(gds_fail_nwell_poly, "feol") precheck.klayout_drc(gds_fail_nwell_poly, "feol")
@sky130A_only
def test_klayout_beol_pass(gds_valid: str): def test_klayout_beol_pass(gds_valid: str):
precheck.klayout_drc(gds_valid, "beol") precheck.klayout_drc(gds_valid, "beol")
@sky130A_only
def test_klayout_beol_fail(gds_fail_met1_poly: str): def test_klayout_beol_fail(gds_fail_met1_poly: str):
with pytest.raises(precheck.PrecheckFailure): with pytest.raises(precheck.PrecheckFailure):
precheck.klayout_drc(gds_fail_met1_poly, "beol") precheck.klayout_drc(gds_fail_met1_poly, "beol")
@sky130A_only
def test_klayout_checks_pass(gds_valid: str): def test_klayout_checks_pass(gds_valid: str):
precheck.klayout_checks(gds_valid, "TEST_valid", "sky130A") precheck.klayout_checks(gds_valid, "TEST_valid", PDK_NAME)
@sky130A_only
def test_klayout_checks_fail_metal5(gds_fail_metal5_poly: str): def test_klayout_checks_fail_metal5(gds_fail_metal5_poly: str):
with pytest.raises( with pytest.raises(
precheck.PrecheckFailure, match=r"Forbidden layer met5\.drawing found in .+" precheck.PrecheckFailure, match=r"Forbidden layer met5\.drawing found in .+"
): ):
precheck.klayout_checks(gds_fail_metal5_poly, "TEST_met5_error", "sky130A") precheck.klayout_checks(gds_fail_metal5_poly, "TEST_met5_error", PDK_NAME)
@sky130A_only
def test_klayout_checks_fail_pr_boundary(gds_no_pr_boundary: str): def test_klayout_checks_fail_pr_boundary(gds_no_pr_boundary: str):
with pytest.raises( with pytest.raises(
precheck.PrecheckFailure, precheck.PrecheckFailure,
match=r"prBoundary.boundary \(235/4\) layer not found in .+", match=r"prBoundary.boundary \(235/4\) layer not found in .+",
): ):
precheck.klayout_checks(gds_no_pr_boundary, "TEST_no_prboundary", "sky130A") precheck.klayout_checks(gds_no_pr_boundary, "TEST_no_prboundary", PDK_NAME)
def test_klayout_top_module_name(gds_invalid_macro_name: str): def test_klayout_top_module_name(gds_invalid_macro_name: str):
@@ -614,22 +673,25 @@ def test_klayout_top_module_name(gds_invalid_macro_name: str):
match="Top macro name mismatch: expected TEST_invalid_macro_name, got wrong_name", match="Top macro name mismatch: expected TEST_invalid_macro_name, got wrong_name",
): ):
precheck.klayout_checks( precheck.klayout_checks(
gds_invalid_macro_name, "TEST_invalid_macro_name", "sky130A" gds_invalid_macro_name, "TEST_invalid_macro_name", PDK_NAME
) )
@sky130A_only
def test_klayout_zero_area_drc_pass(gds_valid: str): def test_klayout_zero_area_drc_pass(gds_valid: str):
precheck.klayout_zero_area(gds_valid) precheck.klayout_zero_area(gds_valid)
@sky130A_only
def test_klayout_zero_area_drc_fail(gds_zero_area: str): def test_klayout_zero_area_drc_fail(gds_zero_area: str):
with pytest.raises(precheck.PrecheckFailure, match="Klayout zero_area failed"): with pytest.raises(precheck.PrecheckFailure, match="Klayout zero_area failed"):
precheck.klayout_zero_area(gds_zero_area) precheck.klayout_zero_area(gds_zero_area)
@sky130A_only
def test_shapes_outside_area(gds_shapes_outside_area: str): def test_shapes_outside_area(gds_shapes_outside_area: str):
with pytest.raises(precheck.PrecheckFailure, match="Shapes outside project area"): with pytest.raises(precheck.PrecheckFailure, match="Shapes outside project area"):
precheck.boundary_check(gds_shapes_outside_area, "sky130A") precheck.boundary_check(gds_shapes_outside_area, PDK_NAME)
def test_wrong_power_pins_1(verilog_lef_wrong_power_pins: tuple[str, str]): def test_wrong_power_pins_1(verilog_lef_wrong_power_pins: tuple[str, str]):
@@ -664,7 +726,7 @@ def test_missing_use_ground(verilog_lef_missing_use_ground: tuple[str, str]):
def test_invalid_layer(gds_invalid_layer: str): def test_invalid_layer(gds_invalid_layer: str):
with pytest.raises(precheck.PrecheckFailure, match="Invalid layers in GDS"): with pytest.raises(precheck.PrecheckFailure, match="Invalid layers in GDS"):
precheck.layer_check(gds_invalid_layer, "sky130A") precheck.layer_check(gds_invalid_layer, PDK_NAME)
def test_invalid_cell_name(gds_invalid_cell_name: str): def test_invalid_cell_name(gds_invalid_cell_name: str):
@@ -675,6 +737,7 @@ def test_invalid_cell_name(gds_invalid_cell_name: str):
precheck.cell_name_check(gds_invalid_cell_name) precheck.cell_name_check(gds_invalid_cell_name)
@sky130A_only
def test_urpm_nwell_too_close(gds_urpm_nwell_too_close: str): def test_urpm_nwell_too_close(gds_urpm_nwell_too_close: str):
with pytest.raises( with pytest.raises(
precheck.PrecheckFailure, precheck.PrecheckFailure,
@@ -683,30 +746,33 @@ def test_urpm_nwell_too_close(gds_urpm_nwell_too_close: str):
precheck.urpm_nwell_check(gds_urpm_nwell_too_close, "TEST_urpm_nwell_too_close") precheck.urpm_nwell_check(gds_urpm_nwell_too_close, "TEST_urpm_nwell_too_close")
@sky130A_only
def test_pin_analog_example(gds_lef_analog_example: tuple[str, str]): def test_pin_analog_example(gds_lef_analog_example: tuple[str, str]):
gds_file, lef_file = gds_lef_analog_example gds_file, lef_file = gds_lef_analog_example
precheck.pin_check( precheck.pin_check(
gds_file, gds_file,
lef_file, lef_file,
"../tech/sky130A/def/analog/tt_analog_1x2.def", f"../tech/{PDK_NAME}/def/analog/tt_analog_1x2.def",
"TEST_analog_example", "TEST_analog_example",
False, False,
"sky130A", PDK_NAME,
) )
@sky130A_only
def test_pin_analog_power_compat(gds_lef_analog_power_compat: tuple[str, str]): def test_pin_analog_power_compat(gds_lef_analog_power_compat: tuple[str, str]):
gds_file, lef_file = gds_lef_analog_power_compat gds_file, lef_file = gds_lef_analog_power_compat
precheck.pin_check( precheck.pin_check(
gds_file, gds_file,
lef_file, lef_file,
"../tech/sky130A/def/analog/tt_analog_1x2.def", f"../tech/{PDK_NAME}/def/analog/tt_analog_1x2.def",
"TEST_analog_power_compat", "TEST_analog_power_compat",
False, False,
"sky130A", PDK_NAME,
) )
@sky130A_only
def test_pin_analog_wrong_vgnd(gds_lef_analog_wrong_vgnd: tuple[str, str]): def test_pin_analog_wrong_vgnd(gds_lef_analog_wrong_vgnd: tuple[str, str]):
with pytest.raises( with pytest.raises(
precheck.PrecheckFailure, precheck.PrecheckFailure,
@@ -716,13 +782,14 @@ def test_pin_analog_wrong_vgnd(gds_lef_analog_wrong_vgnd: tuple[str, str]):
precheck.pin_check( precheck.pin_check(
gds_file, gds_file,
lef_file, lef_file,
"../tech/sky130A/def/analog/tt_analog_1x2.def", f"../tech/{PDK_NAME}/def/analog/tt_analog_1x2.def",
"TEST_analog_wrong_vgnd", "TEST_analog_wrong_vgnd",
False, False,
"sky130A", PDK_NAME,
) )
@sky130A_only
def test_pin_analog_overlapping_vgnd(gds_lef_analog_overlapping_vgnd: tuple[str, str]): def test_pin_analog_overlapping_vgnd(gds_lef_analog_overlapping_vgnd: tuple[str, str]):
with pytest.raises( with pytest.raises(
precheck.PrecheckFailure, precheck.PrecheckFailure,
@@ -732,37 +799,40 @@ def test_pin_analog_overlapping_vgnd(gds_lef_analog_overlapping_vgnd: tuple[str,
precheck.pin_check( precheck.pin_check(
gds_file, gds_file,
lef_file, lef_file,
"../tech/sky130A/def/analog/tt_analog_1x2.def", f"../tech/{PDK_NAME}/def/analog/tt_analog_1x2.def",
"TEST_analog_overlapping_vgnd", "TEST_analog_overlapping_vgnd",
False, False,
"sky130A", PDK_NAME,
) )
@sky130A_only
def test_pin_analog_compound_vgnd(gds_lef_analog_compound_vgnd: tuple[str, str]): def test_pin_analog_compound_vgnd(gds_lef_analog_compound_vgnd: tuple[str, str]):
gds_file, lef_file = gds_lef_analog_compound_vgnd gds_file, lef_file = gds_lef_analog_compound_vgnd
precheck.pin_check( precheck.pin_check(
gds_file, gds_file,
lef_file, lef_file,
"../tech/sky130A/def/analog/tt_analog_1x2.def", f"../tech/{PDK_NAME}/def/analog/tt_analog_1x2.def",
"TEST_analog_compound_vgnd", "TEST_analog_compound_vgnd",
False, False,
"sky130A", PDK_NAME,
) )
@sky130A_only
def test_pin_analog_example_3v3(gds_lef_analog_example_3v3: tuple[str, str]): def test_pin_analog_example_3v3(gds_lef_analog_example_3v3: tuple[str, str]):
gds_file, lef_file = gds_lef_analog_example_3v3 gds_file, lef_file = gds_lef_analog_example_3v3
precheck.pin_check( precheck.pin_check(
gds_file, gds_file,
lef_file, lef_file,
"../tech/sky130A/def/analog/tt_analog_1x2.def", f"../tech/{PDK_NAME}/def/analog/tt_analog_1x2.def",
"TEST_analog_example_3v3", "TEST_analog_example_3v3",
True, True,
"sky130A", PDK_NAME,
) )
@sky130A_only
def test_pin_analog_3v3_mismatch1(gds_lef_analog_example: tuple[str, str]): def test_pin_analog_3v3_mismatch1(gds_lef_analog_example: tuple[str, str]):
with pytest.raises( with pytest.raises(
precheck.PrecheckFailure, precheck.PrecheckFailure,
@@ -772,13 +842,14 @@ def test_pin_analog_3v3_mismatch1(gds_lef_analog_example: tuple[str, str]):
precheck.pin_check( precheck.pin_check(
gds_file, gds_file,
lef_file, lef_file,
"../tech/sky130A/def/analog/tt_analog_1x2.def", f"../tech/{PDK_NAME}/def/analog/tt_analog_1x2.def",
"TEST_analog_example", "TEST_analog_example",
True, True,
"sky130A", PDK_NAME,
) )
@sky130A_only
def test_pin_analog_3v3_mismatch2(gds_lef_analog_example_3v3: tuple[str, str]): def test_pin_analog_3v3_mismatch2(gds_lef_analog_example_3v3: tuple[str, str]):
with pytest.raises( with pytest.raises(
precheck.PrecheckFailure, precheck.PrecheckFailure,
@@ -788,20 +859,22 @@ def test_pin_analog_3v3_mismatch2(gds_lef_analog_example_3v3: tuple[str, str]):
precheck.pin_check( precheck.pin_check(
gds_file, gds_file,
lef_file, lef_file,
"../tech/sky130A/def/analog/tt_analog_1x2.def", f"../tech/{PDK_NAME}/def/analog/tt_analog_1x2.def",
"TEST_analog_example_3v3", "TEST_analog_example_3v3",
False, False,
"sky130A", PDK_NAME,
) )
@sky130A_only
def test_analog_exact_pins(gds_lef_analog_pin_example: tuple[str, str]): def test_analog_exact_pins(gds_lef_analog_pin_example: tuple[str, str]):
gds_file, lef_file = gds_lef_analog_pin_example gds_file, lef_file = gds_lef_analog_pin_example
precheck.analog_pin_check( precheck.analog_pin_check(
gds_file, "sky130A", True, False, 2, {"ua[0]": "x", "ua[1]": "x"} gds_file, PDK_NAME, True, False, 2, {"ua[0]": "x", "ua[1]": "x"}
) )
@sky130A_only
def test_analog_less_pins(gds_lef_analog_pin_example: tuple[str, str]): def test_analog_less_pins(gds_lef_analog_pin_example: tuple[str, str]):
gds_file, lef_file = gds_lef_analog_pin_example gds_file, lef_file = gds_lef_analog_pin_example
with pytest.raises( with pytest.raises(
@@ -809,10 +882,11 @@ def test_analog_less_pins(gds_lef_analog_pin_example: tuple[str, str]):
match="Analog pin `ua\\[1\\]` is connected to some metal but `analog_pins` is set to 1 .*", match="Analog pin `ua\\[1\\]` is connected to some metal but `analog_pins` is set to 1 .*",
): ):
precheck.analog_pin_check( precheck.analog_pin_check(
gds_file, "sky130A", True, False, 1, {"ua[0]": "x", "ua[1]": "x"} gds_file, PDK_NAME, True, False, 1, {"ua[0]": "x", "ua[1]": "x"}
) )
@sky130A_only
def test_analog_more_pins(gds_lef_analog_pin_example: tuple[str, str]): def test_analog_more_pins(gds_lef_analog_pin_example: tuple[str, str]):
gds_file, lef_file = gds_lef_analog_pin_example gds_file, lef_file = gds_lef_analog_pin_example
with pytest.raises( with pytest.raises(
@@ -820,19 +894,21 @@ def test_analog_more_pins(gds_lef_analog_pin_example: tuple[str, str]):
match="Analog pin `ua\\[2\\]` is not connected to any adjacent metal but `analog_pins` is set to 3 .*", match="Analog pin `ua\\[2\\]` is not connected to any adjacent metal but `analog_pins` is set to 3 .*",
): ):
precheck.analog_pin_check( precheck.analog_pin_check(
gds_file, "sky130A", True, False, 3, {"ua[0]": "x", "ua[1]": "x"} gds_file, PDK_NAME, True, False, 3, {"ua[0]": "x", "ua[1]": "x"}
) )
@sky130A_only
def test_analog_less_ua_entries(gds_lef_analog_pin_example: tuple[str, str]): def test_analog_less_ua_entries(gds_lef_analog_pin_example: tuple[str, str]):
gds_file, lef_file = gds_lef_analog_pin_example gds_file, lef_file = gds_lef_analog_pin_example
with pytest.raises( with pytest.raises(
precheck.PrecheckFailure, precheck.PrecheckFailure,
match="Analog pin `ua\\[1\\]` is connected to some metal but the description of `ua\\[1\\]` .*", match="Analog pin `ua\\[1\\]` is connected to some metal but the description of `ua\\[1\\]` .*",
): ):
precheck.analog_pin_check(gds_file, "sky130A", True, False, 2, {"ua[0]": "x"}) precheck.analog_pin_check(gds_file, PDK_NAME, True, False, 2, {"ua[0]": "x"})
@sky130A_only
def test_analog_more_ua_entries(gds_lef_analog_pin_example: tuple[str, str]): def test_analog_more_ua_entries(gds_lef_analog_pin_example: tuple[str, str]):
gds_file, lef_file = gds_lef_analog_pin_example gds_file, lef_file = gds_lef_analog_pin_example
with pytest.raises( with pytest.raises(
@@ -841,7 +917,7 @@ def test_analog_more_ua_entries(gds_lef_analog_pin_example: tuple[str, str]):
): ):
precheck.analog_pin_check( precheck.analog_pin_check(
gds_file, gds_file,
"sky130A", PDK_NAME,
True, True,
False, False,
2, 2,
@@ -859,3 +935,17 @@ def test_verilog_syntax_error(verilog_syntax_error: str):
match="Verilog syntax check failed", match="Verilog syntax check failed",
): ):
precheck.verilog_syntax_check(verilog_syntax_error) precheck.verilog_syntax_check(verilog_syntax_error)
@gf180mcuD_only
def test_antenna_pass(gds_antenna_pass: str):
precheck.klayout_gf180mcuD_antenna(gds_antenna_pass)
@gf180mcuD_only
def test_antenna_fail(gds_antenna_fail: str):
with pytest.raises(
precheck.PrecheckFailure,
match="Klayout antenna failed with 1 DRC violations",
):
precheck.klayout_gf180mcuD_antenna(gds_antenna_fail)