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:
htfab
2026-07-28 21:21:12 +02:00
parent d65690eeb1
commit d869909e41
3 changed files with 246 additions and 36 deletions
+49 -22
View File
@@ -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}"
)