freeroute/tests/board/test_search_tree.py
Ryan Malloy b200ecf6f8 Add ShapeSearchTree: exact spatial index over item shapes
A bounding-box spatial hash (broad phase) with exact IntBox-intersection
narrow phase, replacing the exact router's occupancy grid. Supports
insert/remove-owner and overlap/region queries, a clearance_conflict test
(strict 2-D overlap of the clearance-expanded box, so tiles exactly a
clearance apart are allowed), and has_violation for the whole-board DRC
check. Tiles are tagged routed vs static so pad-vs-pad spacing in the
source design is not counted as a routing violation.
2026-07-13 02:16:11 -06:00

65 lines
2.7 KiB
Python

"""Tests for the exact ShapeSearchTree spatial index."""
from __future__ import annotations
from freeroute.board import ShapeSearchTree, TreeShape
from freeroute.geometry import IntBox
def shape(owner, net, layer, box, routed=False):
return TreeShape(owner, net, frozenset({layer}), box, routed=routed)
def test_insert_and_overlapping_exact():
tree = ShapeSearchTree(bucket_size=100)
tree.insert(shape(0, 1, 0, IntBox(0, 0, 50, 50)))
tree.insert(shape(1, 2, 0, IntBox(200, 200, 250, 250)))
# a query box overlapping only the first shape
hit = tree.overlapping(IntBox(40, 40, 60, 60))
assert [s.owner for s in hit] == [0]
# a query far from everything
assert tree.overlapping(IntBox(1000, 1000, 1010, 1010)) == []
def test_touching_boxes_do_not_overlap_but_do_intersect():
tree = ShapeSearchTree(bucket_size=100)
tree.insert(shape(0, 1, 0, IntBox(0, 0, 10, 10)))
# shares the edge x=10 -> intersection is 1-D (found by broad phase)
assert len(tree.overlapping(IntBox(10, 0, 20, 10))) == 1
def test_remove_owner():
tree = ShapeSearchTree(bucket_size=100)
tree.insert(shape(0, 1, 0, IntBox(0, 0, 50, 50)))
tree.insert(shape(0, 1, 0, IntBox(60, 0, 90, 50))) # same owner, 2 tiles
tree.insert(shape(1, 2, 0, IntBox(0, 0, 50, 50)))
tree.remove_owner(0)
assert all(s.owner == 1 for s in tree.all_shapes())
assert len(tree.all_shapes()) == 1
def test_clearance_conflict_respects_the_clearance_gap():
tree = ShapeSearchTree(bucket_size=1000)
tree.insert(shape(0, 1, 0, IntBox(0, 0, 100, 100), routed=True))
# a net-2 box exactly `clearance` (20) to the right -> allowed (touching)
ok = IntBox(120, 0, 200, 100)
assert not tree.clearance_conflict(ok, net_no=2, layer=0, clearance=20)
# one unit closer -> conflict
bad = IntBox(119, 0, 200, 100)
assert tree.clearance_conflict(bad, net_no=2, layer=0, clearance=20)
# same net never conflicts
assert not tree.clearance_conflict(bad, net_no=1, layer=0, clearance=20)
# different layer never conflicts
assert not tree.clearance_conflict(bad, net_no=2, layer=1, clearance=20)
def test_has_violation_ignores_static_pad_pairs():
tree = ShapeSearchTree(bucket_size=1000)
# two different-net *static* pads closer than clearance -> not a routing DRC
tree.insert(shape(0, 1, 0, IntBox(0, 0, 10, 10), routed=False))
tree.insert(shape(1, 2, 0, IntBox(11, 0, 20, 10), routed=False))
assert tree.has_violation(clearance=20) is None
# a routed trace of net 3 within clearance of pad net 1 -> violation
tree.insert(shape(2, 3, 0, IntBox(11, 0, 20, 10), routed=True))
assert tree.has_violation(clearance=20) is not None