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.
189 lines
6.1 KiB
Python
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))
|