Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions maths/points_are_collinear_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ def are_collinear(a: Point3d, b: Point3d, c: Point3d, accuracy: int = 10) -> boo
... (-3.546599383667157, -4.630005261513976, 3.208784032924246),
... (-2.564606140206386, 3.937845170672183, 7))
False

Nearly collinear points: the result depends on ``accuracy``, because the cross
product is rounded to that many decimal places before comparing with zero.

>>> are_collinear((0, 0, 0), (1, 0, 0), (2, 1e-6, 0))
False
>>> are_collinear((0, 0, 0), (1, 0, 0), (2, 1e-6, 0), accuracy=5)
True
>>> are_collinear((0, 0, 0), (1, 0, 0), (2, 1e-9, 0), accuracy=8)
True
>>> are_collinear((0, 0, 0), (1, 0, 0), (2, 1e-9, 0), accuracy=10)
False
"""
ab = create_vector(a, b)
ac = create_vector(a, c)
Expand Down
16 changes: 16 additions & 0 deletions work/R23-T051/PROPERTIES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Properties tested: maths/points_are_collinear_3d.py

Run with: `python -m pytest work/R23-T051/ -v` (Hypothesis, 2000 examples per test).

## Properties

1. **Exact collinearity (integers).** For integer A, v, t, s, the points A, B = A + t*v and C = A + s*v lie on one line, so the cross product of AB and AC is exactly zero and `are_collinear` must return True. Integer arithmetic is exact, so no rounding is involved.
2. **Float collinearity.** The same property with float inputs (coordinates in [-100, 100], scalars in [-10, 10]). Floating-point error makes the cross product only approximately zero, so this test uses `accuracy=6`.
3. **Perturbation.** Moving C off the line by a vector w that is not parallel to v (cross(v, w) != 0, t != 0) gives a non-zero cross product, so `are_collinear` must return False. Integer inputs are used so the cross product is an exact non-zero integer vector.

## Findings

- All three properties passed over 2000 random examples each; Hypothesis found no failing input.
- The result depends on `accuracy`, because `is_zero_vector` rounds each cross-product component to that many decimals before comparing with zero. A deviation that is real but smaller than 10**-accuracy is treated as collinear. For example, with A=(0,0,0), B=(1,0,0), C=(2,1e-6,0) the cross product is (0, 0, 1e-6): `are_collinear` returns False at the default accuracy=10 but True at accuracy=5. Likewise a 1e-9 deviation is True at accuracy=8 and False at accuracy=10.
- These boundary cases are now documented as doctests in `are_collinear` (18 doctests pass: the original 14 plus 4 new).
- Limitation: the check uses an absolute tolerance, not a relative one, so for very large coordinates floating-point error can exceed the tolerance, and for very small coordinates real deviations can be rounded away.
44 changes: 44 additions & 0 deletions work/R23-T051/test_points_are_collinear_3d_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from hypothesis import assume, given, settings, strategies as st

from maths.points_are_collinear_3d import are_collinear

coord = st.integers(-1000, 1000)
point = st.tuples(coord, coord, coord)
scalar = st.integers(-100, 100)
fcoord = st.floats(-100, 100, allow_nan=False, allow_infinity=False)
fpoint = st.tuples(fcoord, fcoord, fcoord)
fscalar = st.floats(-10, 10, allow_nan=False, allow_infinity=False)


def cross(u, w):
return (
u[1] * w[2] - u[2] * w[1],
u[2] * w[0] - u[0] * w[2],
u[0] * w[1] - u[1] * w[0],
)


@settings(max_examples=2000)
@given(a=point, v=point, t=scalar, s=scalar)
def test_points_on_a_line_are_collinear_exact(a, v, t, s):
b = tuple(a[i] + t * v[i] for i in range(3))
c = tuple(a[i] + s * v[i] for i in range(3))
assert are_collinear(a, b, c)


@settings(max_examples=2000)
@given(a=fpoint, v=fpoint, t=fscalar, s=fscalar)
def test_points_on_a_line_are_collinear_float(a, v, t, s):
b = tuple(a[i] + t * v[i] for i in range(3))
c = tuple(a[i] + s * v[i] for i in range(3))
assert are_collinear(a, b, c, accuracy=6)


@settings(max_examples=2000)
@given(a=point, v=point, w=point, t=scalar, s=scalar)
def test_perturbed_points_are_not_collinear(a, v, w, t, s):
assume(cross(v, w) != (0, 0, 0))
assume(t != 0)
b = tuple(a[i] + t * v[i] for i in range(3))
c = tuple(a[i] + s * v[i] + w[i] for i in range(3))
assert not are_collinear(a, b, c)