Ports the point/vector foundation of geometry/planar: Side and Signum (three-valued signs), Limits, FloatPoint (approximate), and the exact IntPoint/IntVector plus projective RationalPoint/RationalVector. Arithmetic model per type is documented in each module. The key simplification over the Java source: Python's unbounded int makes the exact orientation determinants and rational (x,y,z) coordinates trivial, so side_of is kept exact (upstream uses a double for speed) and no BigInteger or CRIT_INT overflow promotion is needed. Rational points use the projective triple with z=0 denoting the point at infinity. Tests cover collinearity, determinants beyond Java long range, rational equality/reduction, and integer/rational promotion.
162 lines
5.2 KiB
Python
162 lines
5.2 KiB
Python
"""Exact-arithmetic tests for points and vectors.
|
||
|
||
Oracles are hand-derived from the geometry defined in FreeRouting's
|
||
``geometry/planar/{IntPoint,IntVector,RationalPoint,RationalVector}.java``.
|
||
Emphasis is on exactness: collinearity, large coordinates that would overflow a
|
||
Java ``long``, and rational equality.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from freeroute.geometry import (
|
||
IntPoint,
|
||
IntVector,
|
||
RationalPoint,
|
||
RationalVector,
|
||
Side,
|
||
Signum,
|
||
point,
|
||
rational_point,
|
||
)
|
||
|
||
# --- IntVector determinant / side_of ----------------------------------------
|
||
|
||
|
||
def test_determinant_exact():
|
||
# determinant of (1,0),(0,1) = 1
|
||
assert IntVector(1, 0).determinant(IntVector(0, 1)) == 1
|
||
assert IntVector(0, 1).determinant(IntVector(1, 0)) == -1
|
||
assert IntVector(2, 3).determinant(IntVector(4, 6)) == 0 # parallel
|
||
|
||
|
||
def test_determinant_exact_beyond_java_long():
|
||
# 10**12 * 10**12 = 10**24 overflows a Java long (~9.2e18); Python int is exact.
|
||
a = IntVector(10**12, 0)
|
||
b = IntVector(0, 10**12)
|
||
assert a.determinant(b) == 10**24
|
||
|
||
|
||
def test_side_of_collinear_left_right():
|
||
# line from origin toward (1,0): +y is left, -y is right, on axis collinear.
|
||
right_dir = IntVector(1, 0)
|
||
assert IntVector(0, 1).side_of(right_dir) == Side.ON_THE_LEFT
|
||
assert IntVector(0, -1).side_of(right_dir) == Side.ON_THE_RIGHT
|
||
assert IntVector(5, 0).side_of(right_dir) == Side.COLLINEAR
|
||
|
||
|
||
def test_side_of_exact_near_collinear():
|
||
# (10**9, 10**9) vs (10**9, 10**9 + 1): a hair off collinear, exact int detects it.
|
||
base = IntVector(10**9, 10**9)
|
||
assert IntVector(10**9, 10**9 + 1).side_of(base) != Side.COLLINEAR
|
||
assert IntVector(2 * 10**9, 2 * 10**9).side_of(base) == Side.COLLINEAR
|
||
|
||
|
||
def test_vector_add_and_negate():
|
||
assert IntVector(1, 2).add(IntVector(3, 4)) == IntVector(4, 6)
|
||
assert IntVector(1, 2).negate() == IntVector(-1, -2)
|
||
|
||
|
||
def test_turn_90_degree():
|
||
v = IntVector(1, 0)
|
||
assert v.turn_90_degree(1) == IntVector(0, 1)
|
||
assert v.turn_90_degree(2) == IntVector(-1, 0)
|
||
assert v.turn_90_degree(3) == IntVector(0, -1)
|
||
assert v.turn_90_degree(4) == IntVector(1, 0)
|
||
assert v.turn_90_degree(-1) == IntVector(0, -1)
|
||
|
||
|
||
def test_projection_signum():
|
||
assert IntVector(1, 0).projection(IntVector(1, 0)) == Signum.POSITIVE
|
||
assert IntVector(1, 0).projection(IntVector(-1, 0)) == Signum.NEGATIVE
|
||
assert IntVector(1, 0).projection(IntVector(0, 1)) == Signum.ZERO
|
||
|
||
|
||
def test_orthogonal_diagonal():
|
||
assert IntVector(3, 0).is_orthogonal()
|
||
assert IntVector(0, -4).is_orthogonal()
|
||
assert IntVector(2, 2).is_diagonal()
|
||
assert IntVector(-5, 5).is_diagonal()
|
||
assert IntVector(1, 2).is_multiple_of_45_degree() is False
|
||
|
||
|
||
# --- IntPoint ---------------------------------------------------------------
|
||
|
||
|
||
def test_point_difference_and_translate():
|
||
p = IntPoint(3, 4)
|
||
q = IntPoint(1, 1)
|
||
d = p.difference_by(q)
|
||
assert isinstance(d, IntVector)
|
||
assert d == IntVector(2, 3)
|
||
assert q.translate_by(d) == p
|
||
|
||
|
||
def test_point_side_of_directed_line():
|
||
p0 = IntPoint(0, 0)
|
||
p1 = IntPoint(2, 2)
|
||
assert IntPoint(1, 1).side_of(p0, p1) == Side.COLLINEAR
|
||
assert IntPoint(0, 1).side_of(p0, p1) == Side.ON_THE_LEFT
|
||
assert IntPoint(1, 0).side_of(p0, p1) == Side.ON_THE_RIGHT
|
||
|
||
|
||
def test_point_compare():
|
||
assert IntPoint(1, 5).compare_x(IntPoint(3, 2)) == -1
|
||
assert IntPoint(3, 5).compare_x(IntPoint(3, 2)) == 0
|
||
assert IntPoint(3, 5).compare_y(IntPoint(3, 2)) == 1
|
||
assert IntPoint(3, 2).compare_x_y(IntPoint(3, 5)) == -1 # x tie, y smaller
|
||
|
||
|
||
def test_point_turn_90_around_pole():
|
||
pole = IntPoint(1, 1)
|
||
# (2,1) rotated 90 about (1,1) -> (1,2)
|
||
assert IntPoint(2, 1).turn_90_degree(1, pole) == IntPoint(1, 2)
|
||
|
||
|
||
# --- RationalPoint / factory ------------------------------------------------
|
||
|
||
|
||
def test_rational_point_equality_scale_invariant():
|
||
assert RationalPoint(3, 2, 2) == RationalPoint(6, 4, 4)
|
||
assert RationalPoint(3, 2, 2) != RationalPoint(3, 2, 3)
|
||
|
||
|
||
def test_rational_point_negative_denominator_normalized_via_factory():
|
||
p = rational_point(3, 2, -2) # -> (−3/−2)=... factory flips sign -> (-3,-2,2)
|
||
assert isinstance(p, RationalPoint)
|
||
assert p == RationalPoint(-3, -2, 2)
|
||
|
||
|
||
def test_rational_point_reduces_to_int_when_divisible():
|
||
p = rational_point(6, 4, 2)
|
||
assert isinstance(p, IntPoint)
|
||
assert p == IntPoint(3, 2)
|
||
|
||
|
||
def test_rational_point_infinite():
|
||
inf = rational_point(1, 1, 0)
|
||
assert isinstance(inf, RationalPoint)
|
||
assert inf.is_infinite()
|
||
|
||
|
||
def test_rational_point_contained_in_box_exact():
|
||
from freeroute.geometry import IntBox
|
||
|
||
box = IntBox(0, 0, 2, 2)
|
||
assert RationalPoint(3, 2, 2).is_contained_in(box) # (1.5, 1.0) inside
|
||
assert not RationalPoint(5, 2, 2).is_contained_in(box) # (2.5, 1.0) outside
|
||
|
||
|
||
def test_int_and_rational_difference_promotes():
|
||
p = IntPoint(2, 2)
|
||
q = RationalPoint(3, 2, 2) # (1.5, 1.0)
|
||
d = p.difference_by(q)
|
||
assert isinstance(d, RationalVector)
|
||
# (2,2) - (1.5,1.0) = (0.5, 1.0) == (1,2,2)
|
||
assert d == RationalVector(1, 2, 2)
|
||
|
||
|
||
def test_factory_point_is_intpoint_even_for_huge_coords():
|
||
p = point(10**15, -(10**15))
|
||
assert isinstance(p, IntPoint)
|
||
assert (p.x, p.y) == (10**15, -(10**15))
|