diff --git a/precheck/pin_check.py b/precheck/pin_check.py index b7fac07..f9d42c3 100644 --- a/precheck/pin_check.py +++ b/precheck/pin_check.py @@ -1,6 +1,7 @@ import logging import re from collections.abc import Iterable +from dataclasses import dataclass from functools import partial from itertools import combinations from numbers import Real @@ -111,17 +112,17 @@ def parse_dbu_to_nm(value: str, dbu_per_micron: int): return (val_dbu * 1000) // dbu_per_micron -def pin_check( - gds: str, lef: str, template_def: str, toplevel: str, uses_vapwr: bool, tech: str -): - logging.info("Running pin check...") - logging.info(f"* gds: {gds}") - logging.info(f"* lef: {lef}") - logging.info(f"* template_def: {template_def}") - logging.info(f"* toplevel: {toplevel}") - logging.info(f"* uses_vapwr: {uses_vapwr}") +@dataclass +class DefTemplate: + die_width: int + die_height: int + dbu_per_micron: int + pins: dict - # parse pins from template def + +def parse_def(template_def: str) -> DefTemplate: + + # parse die size & pins from template def # def syntax: https://coriolis.lip6.fr/doc/lefdef/lefdefref/DEFSyntax.html units_re = re.compile(r"UNITS DISTANCE MICRONS (\S+) ;") @@ -131,10 +132,10 @@ def pin_check( layer_re = re.compile(r" *\+ LAYER (\S+) \( (\S+) (\S+) \) \( (\S+) (\S+) \)") placed_re = re.compile(r" *\+ PLACED \( (\S+) (\S+) \) (\S+) ;") - def_pins = {} - dbu_per_micron = 0 die_width = 0 die_height = 0 + dbu_per_micron = 0 + pins = {} with open(template_def) as f: for line in f: @@ -181,10 +182,10 @@ def pin_check( ox, oy, direction = match.groups() ox, oy = map(parse_nm, (ox, oy)) - if pin_name in def_pins: + if pin_name in pins: raise PrecheckFailure("Duplicate pin in template DEF") - def_pins[pin_name] = (layer, ox + lx, oy + by, ox + rx, oy + ty) + pins[pin_name] = (layer, ox + lx, oy + by, ox + rx, oy + ty) line = next(f) if not line.startswith("END PINS"): @@ -192,6 +193,27 @@ def pin_check( f"Unexpected token in template DEF: PINS {pin_count} section does not end after {pin_count} pins" ) + return DefTemplate( + die_width=die_width, + die_height=die_height, + dbu_per_micron=dbu_per_micron, + pins=pins, + ) + + +def pin_check( + gds: str, lef: str, template_def: str, toplevel: str, uses_vapwr: bool, tech: str +): + logging.info("Running pin check...") + logging.info(f"* gds: {gds}") + logging.info(f"* lef: {lef}") + logging.info(f"* template_def: {template_def}") + logging.info(f"* toplevel: {toplevel}") + logging.info(f"* uses_vapwr: {uses_vapwr}") + + # parse template def + template = parse_def(template_def) + # parse pins from user lef # lef syntax: https://coriolis.lip6.fr/doc/lefdef/lefdefref/LEFSyntax.html @@ -206,7 +228,7 @@ def pin_check( compat_pins = {"VPWR": "VDPWR"} macro_active = False - pins_expected = set(def_pins).union(power_pins).union(compat_pins) + pins_expected = set(template.pins).union(power_pins).union(compat_pins) pins_seen = set() current_pin = None pin_rects = 0 @@ -232,9 +254,9 @@ def pin_check( elif line.startswith("SIZE "): match = size_re.match(line) rx, ty = map(parse_fp3, match.groups()) - if (rx, ty) != (die_width, die_height): + if (rx, ty) != (template.die_width, template.die_height): raise PrecheckFailure( - f"Inconsistent die area between LEF and template DEF: ({rx}, {ty}) != ({die_width}, {die_height})" + f"Inconsistent die area between LEF and template DEF: ({rx}, {ty}) != ({template.die_width}, {template.die_height})" ) elif line.startswith("PIN "): if current_pin is not None: @@ -300,7 +322,7 @@ def pin_check( lef_ports[new_pin].extend(lef_ports[old_pin]) del lef_ports[old_pin] - for current_pin in def_pins: + for current_pin in template.pins: if current_pin not in lef_ports: logging.error(f"Pin {current_pin} not found in {lef}") lef_errors += 1 @@ -309,7 +331,7 @@ def pin_check( logging.error(f"Too many rectangles for pin {current_pin} in {lef}") lef_errors += 1 lef_layer, *lef_rect = lef_ports[current_pin][0] - def_layer, *def_rect = def_pins[current_pin] + def_layer, *def_rect = template.pins[current_pin] if lef_layer != def_layer: logging.error( f"Port {current_pin} on layer {lef_layer} in {lef} but on layer {def_layer} in {template_def}" @@ -345,12 +367,17 @@ def pin_check( f"Port {current_pin} is too far from bottom edge of module: {by/1000} > 10 um" ) lef_errors += 1 - if die_height - ty > 10000: + if template.die_height - ty > 10000: logging.error( - f"Port {current_pin} is too far from top edge of module: {(die_height-ty)/1000} > 10 um" + f"Port {current_pin} is too far from top edge of module: {(template.die_height-ty)/1000} > 10 um" ) lef_errors += 1 - if lx < 0 or rx > die_width or by < 0 or ty > die_height: + if ( + lx < 0 + or rx > template.die_width + or by < 0 + or ty > template.die_height + ): logging.error( f"Port {current_pin} not entirely within project area in {lef}" ) diff --git a/precheck/precheck.py b/precheck/precheck.py index ca15ea2..104ce87 100644 --- a/precheck/precheck.py +++ b/precheck/precheck.py @@ -14,7 +14,7 @@ import klayout.db as pya import klayout.rdb as rdb import yaml from klayout_tools import parse_lyp_layers -from pin_check import pin_check +from pin_check import parse_def, pin_check from precheck_failure import PrecheckFailure from tech_data import ( analog_pin_rects, @@ -190,21 +190,43 @@ def klayout_checks(gds: str, expected_name: str, tech: str): raise PrecheckFailure(f"{layer_name} ({calma_index}) layer not found in {gds}") -def boundary_check(gds: str, tech: str): +def boundary_check(gds: str, template_def: str, tech: str): """Ensure that there are no shapes outside the project area.""" + template = parse_def(template_def) lib = gdstk.read_gds(gds) tops = lib.top_level() if len(tops) != 1: raise PrecheckFailure("GDS top level not unique") top = tops[0] - boundary = top.copy("test_boundary") - + ((lx, by), (rx, ty)) = top.bounding_box() + lx = int(lx * 1000) + by = int(by * 1000) + rx = int(rx * 1000) + ty = int(ty * 1000) + if ((lx, by), (rx, ty)) != ((0, 0), (template.die_width, template.die_height)): + if lx < 0 or by < 0 or rx > template.die_width or ty > template.die_height: + raise PrecheckFailure("Shapes outside project area") + else: + raise PrecheckFailure("Boundary layer doesn't cover project area") layers = load_layers(tech) layer_name = boundary_layer[tech] layer_info = layers[layer_name] - boundary.filter([(layer_info.layer, layer_info.data_type)], False) - if top.bounding_box() != boundary.bounding_box(): - raise PrecheckFailure("Shapes outside project area") + found_boundary = False + for p in top.polygons: + if (p.layer, p.datatype) == (layer_info.layer, layer_info.data_type): + ((lx, by), (rx, ty)) = p.bounding_box() + lx = int(lx * 1000) + by = int(by * 1000) + rx = int(rx * 1000) + ty = int(ty * 1000) + if ((lx, by), (rx, ty)) == ( + (0, 0), + (template.die_width, template.die_height), + ): + found_boundary = True + break + if not found_boundary: + raise PrecheckFailure("Missing top-level prBoundary rectangle with right size") def power_pin_check(verilog: str, lef: str, uses_vapwr: bool): @@ -501,7 +523,10 @@ def main(): gds_file, lef_file, template_def, top_module, uses_vapwr, tech ), }, - {"name": "Boundary check", "check": lambda: boundary_check(gds_file, tech)}, + { + "name": "Boundary check", + "check": lambda: boundary_check(gds_file, template_def, tech), + }, { "name": "Power pin check", "check": lambda: power_pin_check(verilog_file, lef_file, uses_vapwr), diff --git a/precheck/test_precheck.py b/precheck/test_precheck.py index 92be7bf..7e32631 100644 --- a/precheck/test_precheck.py +++ b/precheck/test_precheck.py @@ -119,17 +119,147 @@ def gds_invalid_macro_name(tmp_path_factory: pytest.TempPathFactory): @pytest.fixture(scope="session") -def gds_shapes_outside_area(tmp_path_factory: pytest.TempPathFactory): - """Creates a GDS with shapes outside the project area.""" - gds_file = tmp_path_factory.mktemp("gds") / "TEST_shapes_outside_area.gds" +def gds_def_boundary_ok(tmp_path_factory: pytest.TempPathFactory): + """Creates a GDS & DEF with boundary matching the project size.""" + gds_file = tmp_path_factory.mktemp("gds") / "TEST_boundary_ok.gds" + layout = pya.Layout() + top_cell = layout.create_cell("TEST_boundary_ok") + met1_info = gds_layers["met1.drawing"] + met1 = layout.layer(met1_info.layer, met1_info.data_type) + rect = pya.DBox(0, 0, 1, 1) + top_cell.shapes(met1).insert(rect) + boundary_info = gds_layers["prBoundary.boundary"] + boundary = layout.layer(boundary_info.layer, boundary_info.data_type) + rect = pya.DBox(0, 0, 10, 10) + top_cell.shapes(boundary).insert(rect) + layout.write(str(gds_file)) + def_file = tmp_path_factory.mktemp("def") / "TEST_boundary_ok.def" + def_data = """ + VERSION 5.8 ; + DIVIDERCHAR "/" ; + BUSBITCHARS "[]" ; + DESIGN tt_um_template ; + UNITS DISTANCE MICRONS 1000 ; + DIEAREA ( 0 0 ) ( 10000 10000 ) ; + COMPONENTS 0 ; + END COMPONENTS + PINS 0 ; + END PINS + SPECIALNETS 0 ; + END SPECIALNETS + NETS 0 ; + END NETS + END DESIGN + """ + open(def_file, "w").write(textwrap.dedent(def_data)) + return str(gds_file), str(def_file) + + +@pytest.fixture(scope="session") +def gds_def_boundary_wrong_1(tmp_path_factory: pytest.TempPathFactory): + """Creates a GDS & DEF with a bounding box smaller than the project size.""" + gds_file = tmp_path_factory.mktemp("gds") / "TEST_boundary_wrong_1.gds" layout = pya.Layout() met1_info = gds_layers["met1.drawing"] met1 = layout.layer(met1_info.layer, met1_info.data_type) + top_cell = layout.create_cell("TEST_boundary_wrong_1") + rect = pya.DBox(0, 0, 1, 1) + top_cell.shapes(met1).insert(rect) + boundary_info = gds_layers["prBoundary.boundary"] + boundary = layout.layer(boundary_info.layer, boundary_info.data_type) + rect = pya.DBox(0, 0, 2, 2) + top_cell.shapes(boundary).insert(rect) + layout.write(str(gds_file)) + def_file = tmp_path_factory.mktemp("def") / "TEST_boundary_wrong_1.def" + def_data = """ + VERSION 5.8 ; + DIVIDERCHAR "/" ; + BUSBITCHARS "[]" ; + DESIGN tt_um_template ; + UNITS DISTANCE MICRONS 1000 ; + DIEAREA ( 0 0 ) ( 10000 10000 ) ; + COMPONENTS 0 ; + END COMPONENTS + PINS 0 ; + END PINS + SPECIALNETS 0 ; + END SPECIALNETS + NETS 0 ; + END NETS + END DESIGN + """ + open(def_file, "w").write(textwrap.dedent(def_data)) + return str(gds_file), str(def_file) + + +@pytest.fixture(scope="session") +def gds_def_boundary_wrong_2(tmp_path_factory: pytest.TempPathFactory): + """Creates a GDS & DEF where the bounding box is ok but the boundary layer is wrong.""" + gds_file = tmp_path_factory.mktemp("gds") / "TEST_boundary_wrong_2.gds" + layout = pya.Layout() + met1_info = gds_layers["met1.drawing"] + met1 = layout.layer(met1_info.layer, met1_info.data_type) + top_cell = layout.create_cell("TEST_boundary_wrong_2") + rect = pya.DBox(0, 0, 10, 10) + top_cell.shapes(met1).insert(rect) + boundary_info = gds_layers["prBoundary.boundary"] + boundary = layout.layer(boundary_info.layer, boundary_info.data_type) + rect = pya.DBox(0, 0, 2, 2) + top_cell.shapes(boundary).insert(rect) + layout.write(str(gds_file)) + def_file = tmp_path_factory.mktemp("def") / "TEST_boundary_wrong_2.def" + def_data = """ + VERSION 5.8 ; + DIVIDERCHAR "/" ; + BUSBITCHARS "[]" ; + DESIGN tt_um_template ; + UNITS DISTANCE MICRONS 1000 ; + DIEAREA ( 0 0 ) ( 10000 10000 ) ; + COMPONENTS 0 ; + END COMPONENTS + PINS 0 ; + END PINS + SPECIALNETS 0 ; + END SPECIALNETS + NETS 0 ; + END NETS + END DESIGN + """ + open(def_file, "w").write(textwrap.dedent(def_data)) + return str(gds_file), str(def_file) + + +@pytest.fixture(scope="session") +def gds_def_shapes_outside_area(tmp_path_factory: pytest.TempPathFactory): + """Creates a GDS & DEF with shapes outside the project area.""" + gds_file = tmp_path_factory.mktemp("gds") / "TEST_shapes_outside_area.gds" + layout = pya.Layout() top_cell = layout.create_cell("TEST_shapes_outside_area") + met1_info = gds_layers["met1.drawing"] + met1 = layout.layer(met1_info.layer, met1_info.data_type) rect = pya.DBox(-1, 0, 0, 1) top_cell.shapes(met1).insert(rect) layout.write(str(gds_file)) - return str(gds_file) + def_file = tmp_path_factory.mktemp("def") / "TEST_shapes_outside_area.def" + def_data = """ + VERSION 5.8 ; + DIVIDERCHAR "/" ; + BUSBITCHARS "[]" ; + DESIGN tt_um_template ; + UNITS DISTANCE MICRONS 1000 ; + DIEAREA ( 0 0 ) ( 10000 10000 ) ; + COMPONENTS 0 ; + END COMPONENTS + PINS 0 ; + END PINS + SPECIALNETS 0 ; + END SPECIALNETS + NETS 0 ; + END NETS + END DESIGN + """ + open(def_file, "w").write(textwrap.dedent(def_data)) + return str(gds_file), str(def_file) @pytest.fixture(scope="session") @@ -718,9 +848,37 @@ def test_klayout_zero_area_drc_fail(gds_zero_area: str): @sky130A_only -def test_shapes_outside_area(gds_shapes_outside_area: str): +def test_boundary_ok(gds_def_boundary_ok: tuple[str, str]): + gds_boundary_ok, def_boundary_ok = gds_def_boundary_ok + precheck.boundary_check(gds_boundary_ok, def_boundary_ok, PDK_NAME) + + +@sky130A_only +def test_boundary_wrong_1(gds_def_boundary_wrong_1: tuple[str, str]): + gds_boundary_wrong_1, def_boundary_wrong_1 = gds_def_boundary_wrong_1 + with pytest.raises( + precheck.PrecheckFailure, match="Boundary layer doesn't cover project area" + ): + precheck.boundary_check(gds_boundary_wrong_1, def_boundary_wrong_1, PDK_NAME) + + +@sky130A_only +def test_boundary_wrong_2(gds_def_boundary_wrong_2: tuple[str, str]): + gds_boundary_wrong_2, def_boundary_wrong_2 = gds_def_boundary_wrong_2 + with pytest.raises( + precheck.PrecheckFailure, + match="Missing top-level prBoundary rectangle with right size", + ): + precheck.boundary_check(gds_boundary_wrong_2, def_boundary_wrong_2, PDK_NAME) + + +@sky130A_only +def test_shapes_outside_area(gds_def_shapes_outside_area: tuple[str, str]): + gds_shapes_outside_area, def_shapes_outside_area = gds_def_shapes_outside_area with pytest.raises(precheck.PrecheckFailure, match="Shapes outside project area"): - precheck.boundary_check(gds_shapes_outside_area, PDK_NAME) + precheck.boundary_check( + gds_shapes_outside_area, def_shapes_outside_area, PDK_NAME + ) def test_wrong_power_pins_1(verilog_lef_wrong_power_pins: tuple[str, str]):