mirror of
https://github.com/TinyTapeout/tt-support-tools.git
synced 2026-11-03 04:36:09 +01:00
fix(precheck): rewrite the boundary check
The previous version of the boundary check was a quick heuristic where we compared the bounding box of the design to that of the prBoundary layer. Over time we came across some designs where this is insufficient. The new check compares the bounding box to the correct project size based on the .def template and verifies that there is a rectangle of this exact size in the prBoundary layer at the top level. The patch also abstracts parsing the .def template into a separate function since we now use it in two different checks.
This commit is contained in:
+49
-22
@@ -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}"
|
||||
)
|
||||
|
||||
+33
-8
@@ -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),
|
||||
|
||||
+164
-6
@@ -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]):
|
||||
|
||||
Reference in New Issue
Block a user