Files
rpmroute/tests/geometry/test_simplex.py
T
rsp2k a6aa1991a1 Rename to rpmroute
The router began as a port of FreeRouting and has grown its own engine
(negotiated congestion, tree nets, exact checks, axis escapes), and
'freeroute' was one letter from the project it no longer follows. The
package, module and command are now rpmroute; the freeroute command stays
as an alias, and the RPMROUTE / RPMROUTE_ORACLE_JAVA variables fall back
to the old FREEROUTE names. Version 2026.10.07. Routing is unchanged: a
PCBench board routes to a byte-identical session either way.
2026-10-07 14:10:36 -06:00

189 lines
6.1 KiB
Python

"""Invariant tests for TileShape / Simplex.
There is no external oracle (no JVM; FreeRouting ships no unit tests for
``geometry.planar``), so these assert properties that must hold regardless of
implementation, per the phase-2 brief:
* corners lie exactly on their two border lines (exact ``Line.side_of == 0``),
* ``IntBox -> Simplex -> region`` preserves the region,
* ``Simplex.intersection`` is contained in both operands and a point is in the
result iff it is in both,
* ``get_instance`` normalization drops redundant lines without changing the
region.
Source: ``geometry/planar/{TileShape,Simplex}.java``.
"""
from __future__ import annotations
from rpmroute.geometry import IntBox, IntPoint, Line, Side, Simplex
def _grid(lo: int, hi: int):
for x in range(lo, hi + 1):
for y in range(lo, hi + 1):
yield IntPoint(x, y)
def right_triangle() -> Simplex:
# legs of length 10 along the axes; hypotenuse x + y = 10
return Simplex.get_instance(
[
Line.from_coords(0, 0, 10, 0),
Line.from_coords(10, 0, 0, 10),
Line.from_coords(0, 10, 0, 0),
]
)
def unit_box_simplex() -> Simplex:
return IntBox(0, 0, 10, 10).to_simplex()
# --- corners lie exactly on their border lines ------------------------------
def test_corners_are_exact_on_border_lines():
for shape in (unit_box_simplex(), right_triangle()):
n = shape.border_line_count()
for i in range(n):
corner = shape.corner(i)
prev = shape.border_line(n - 1 if i == 0 else i - 1)
curr = shape.border_line(i)
# the corner is the exact intersection of these two lines
assert curr.side_of(corner) == Side.COLLINEAR
assert prev.side_of(corner) == Side.COLLINEAR
# --- IntBox -> Simplex -> region round trip ---------------------------------
def test_box_to_simplex_preserves_region():
box = IntBox(-3, 2, 7, 11)
s = box.to_simplex()
assert s.bounding_box() == box
for p in _grid(-6, 14):
assert box.contains(p) == s.contains(p)
def test_box_to_simplex_is_a_box_and_simplifies_back():
box = IntBox(0, 0, 10, 10)
s = box.to_simplex()
assert s.is_int_box()
assert s.simplify() == box
# --- containment semantics --------------------------------------------------
def test_triangle_containment():
tri = right_triangle()
assert tri.contains(IntPoint(0, 0)) # corner
assert tri.contains(IntPoint(5, 5)) # on hypotenuse
assert tri.contains_inside(IntPoint(2, 2))
assert not tri.contains_inside(IntPoint(5, 5)) # on border, not inside
assert not tri.contains(IntPoint(6, 6)) # x+y=12 > 10
assert tri.contains_on_border(IntPoint(3, 0)) # on the bottom edge
def test_triangle_area_and_bounds():
tri = right_triangle()
assert tri.is_bounded()
assert tri.dimension() == 2
assert tri.area() == 50.0
assert tri.bounding_box() == IntBox(0, 0, 10, 10)
# --- intersection invariants ------------------------------------------------
def test_intersection_contained_in_both_and_iff():
a = IntBox(0, 0, 12, 8).to_simplex()
b = right_triangle() # x,y >= 0, x+y <= 10
inter = a.intersection(b)
assert not inter.is_empty()
for p in _grid(-3, 15):
in_both = a.contains(p) and b.contains(p)
in_inter = inter.contains(p)
assert in_inter == in_both
# every corner of the intersection lies inside both operands
for i in range(inter.border_line_count()):
c = inter.corner(i)
assert a.contains(c)
assert b.contains(c)
def test_intersection_of_two_boxes_matches_box_intersection():
a_box = IntBox(0, 0, 10, 10)
b_box = IntBox(4, -2, 20, 6)
inter = a_box.to_simplex().intersection(b_box.to_simplex())
expected = a_box.intersection(b_box)
for p in _grid(-5, 22):
assert inter.contains(p) == expected.contains(p)
def test_disjoint_intersection_is_empty():
a = IntBox(0, 0, 3, 3).to_simplex()
b = IntBox(10, 10, 13, 13).to_simplex()
assert a.intersection(b).is_empty()
assert not a.intersects(b)
def test_intersection_is_idempotent_with_self():
tri = right_triangle()
inter = tri.intersection(tri)
for p in _grid(-3, 13):
assert inter.contains(p) == tri.contains(p)
# --- normalization (get_instance drops redundant lines) ---------------------
def test_get_instance_removes_redundant_line():
# a unit box plus a far-away redundant half-plane that does not cut it.
# A DOWN-directed vertical line has its interior to the east, so this is
# x >= -100, which contains the whole box and is therefore redundant.
box_lines = list(IntBox(0, 0, 10, 10).to_simplex()._arr)
redundant = Line.from_coords(-100, 1, -100, 0) # x >= -100
s = Simplex.get_instance(box_lines + [redundant])
assert s.border_line_count() == 4 # redundant line dropped
for p in _grid(-5, 15):
assert s.contains(p) == IntBox(0, 0, 10, 10).contains(p)
def test_get_instance_detects_empty_from_opposing_halfplanes():
# x >= 5 (DOWN line, interior east) and x <= 0 (UP line, interior west)
# cannot both hold.
ge5 = Line.from_coords(5, 1, 5, 0) # DOWN -> interior x >= 5
le0 = Line.from_coords(0, 0, 0, 1) # UP -> interior x <= 0
s = Simplex.get_instance([ge5, le0])
assert s.is_empty()
def test_translate_preserves_shape():
tri = right_triangle()
from rpmroute.geometry import IntVector
moved = tri.translate_by(IntVector(100, 50))
for p in _grid(-3, 13):
assert tri.contains(p) == moved.contains(IntPoint(p.x + 100, p.y + 50))
def test_offset_outward_enlarges_region():
box = IntBox(0, 0, 10, 10).to_simplex()
bigger = box.offset(2)
# every point of the original box is still contained after outward offset
for p in _grid(0, 10):
assert bigger.contains(p)
# a point 2 outside the original right edge is now contained
assert bigger.contains(IntPoint(12, 5))
assert not box.contains(IntPoint(12, 5))
def test_empty_simplex_predicates():
e = Simplex.empty()
assert e.is_empty()
assert e.dimension() == -1
assert e.is_outside(IntPoint(0, 0))
assert not e.contains(IntPoint(0, 0))