freeroute/tests/geometry/test_point_vector.py
Ryan Malloy 33cb196bc8 Add exact planar points and vectors
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.
2026-07-11 18:01:21 -06:00

162 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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))