moonlight-planar-1.1.0.0: test/native/Moonlight/Planar/PredicateSpec.hs
-- | Predicate, binary64 format, and paraboloid lifting laws.
module Moonlight.Planar.PredicateSpec
( tests
) where
import Control.Monad ( forM_, unless )
import Moonlight.Planar.Math (inCircle, orient2d)
import Moonlight.Planar.Point (mitigateUnderflow, mkQueryPoint)
import Moonlight.Planar.Internal.Types (validatePoint)
import Moonlight.Planar.Scalar ( scalarBinaryFormat, scalarUnitRoundoff,
BinaryFormat(formatMantissaDigits, formatRadix) )
import Moonlight.Planar.Point (Point(Point))
import Support ( assertEqual, requireRight, randomPoints )
import Moonlight.Planar.Internal.Predicates qualified as Admitted
tests :: IO ()
tests =
sequence_
[ testPredicates
, testParaboloidLift
, testScalarFormat
]
testScalarFormat :: IO ()
testScalarFormat = do
let binary64 = scalarBinaryFormat
assertEqual "binary64 radix" 2 (formatRadix binary64)
assertEqual "binary64 mantissa" 53 (formatMantissaDigits binary64)
assertEqual "binary64 unit roundoff" (encodeFloat 1 (-53)) (scalarUnitRoundoff :: Double)
testPredicates :: IO ()
testPredicates = do
let rawA, rawB, rawC :: Point
rawA = Point 0 0
rawB = Point 1 0
rawC = Point 0 1
a <- requireRight "predicate point a" (mkQueryPoint rawA)
b <- requireRight "predicate point b" (mkQueryPoint rawB)
c <- requireRight "predicate point c" (mkQueryPoint rawC)
midpointQuery <- requireRight "predicate midpoint" (mkQueryPoint (Point 0.5 0))
inside <- requireRight "incircle inside point" (mkQueryPoint (Point 0.25 0.25))
boundary <- requireRight "incircle boundary point" (mkQueryPoint (Point 1 1))
outside <- requireRight "incircle outside point" (mkQueryPoint (Point 2 2))
assertEqual "orientation left" GT (orient2d a b c)
assertEqual "orientation right" LT (orient2d b a c)
assertEqual "orientation collinear" EQ (orient2d a b midpointQuery)
assertEqual "incircle inside" GT (inCircle a b c inside)
assertEqual "incircle boundary" EQ (inCircle a b c boundary)
assertEqual "incircle outside" LT (inCircle a b c outside)
assertEqual "underflow mitigation" (Point 0 1) (mitigateUnderflow (Point 1.0e-44 1 :: Point))
_ <- requireRight "point validation" (validatePoint Nothing (Point 0 1 :: Point))
let large = encodeFloat 1 180 :: Double
ulp = encodeFloat 1 128 :: Double
largeA <- requireRight "large predicate point a" (mkQueryPoint (Point large large))
largeB <- requireRight "large predicate point b" (mkQueryPoint (Point (large + ulp) large))
largeC <- requireRight "large predicate point c" (mkQueryPoint (Point large (large + ulp)))
largeD <- requireRight "large predicate point d" (mkQueryPoint (Point (large + ulp) (large + ulp)))
assertEqual
"large exact orientation"
GT
(orient2d largeA largeB largeC)
assertEqual
"large exact cocircularity"
EQ
(inCircle largeA largeB largeC largeD)
testParaboloidLift :: IO ()
testParaboloidLift = do
let lifted :: Point -> (Rational, Rational, Rational)
lifted (Point x y) =
let (rx, ry) = (toRational x, toRational y) in (rx, ry, rx * rx + ry * ry)
minor3
:: (Rational, Rational, Rational)
-> (Rational, Rational, Rational)
-> (Rational, Rational, Rational)
-> Rational
minor3 (a1, a2, a3) (b1, b2, b3) (c1, c2, c3) =
a1 * (b2 * c3 - b3 * c2) - a2 * (b1 * c3 - b3 * c1) + a3 * (b1 * c2 - b2 * c1)
-- Laplace expansion of the lifted determinant along its column of ones.
liftedOrientation
:: (Rational, Rational, Rational)
-> (Rational, Rational, Rational)
-> (Rational, Rational, Rational)
-> (Rational, Rational, Rational)
-> Rational
liftedOrientation a b c d =
negate (minor3 b c d) + minor3 a c d - minor3 a b d + minor3 a b c
predicted :: Point -> Point -> Point -> Point -> Ordering
predicted a b c d = compare (liftedOrientation (lifted a) (lifted b) (lifted c) (lifted d)) 0
measured :: Point -> Point -> Point -> Point -> Ordering
measured (Point ax ay) (Point bx by) (Point cx cy) (Point dx dy) =
Admitted.inCircleCoordinates ax ay bx by cx cy dx dy
quadruples :: [Point] -> [(Point, Point, Point, Point)]
quadruples (a : b : c : d : rest) = (a, b, c, d) : quadruples rest
quadruples _ = []
forM_ (quadruples (randomPoints 0x51ca_0b17 1600)) $ \(a, b, c, d) ->
assertEqual ("in-circle is the lifted orientation at " <> show (a, b, c, d))
(predicted a b c d) (measured a b c d)
-- Exactly cocircular quadruples, which are exactly the ones the floating
-- filter must decline to answer. Every Pythagorean point of the radius-5
-- circle is representable without rounding, so 'EQ' here is a fact about the
-- geometry rather than about the arithmetic.
let ring =
[ Point x y
| (x, y) <-
[ (5, 0), (0, 5), (-5, 0), (0, -5)
, (3, 4), (4, 3), (-3, 4), (-4, 3)
, (3, -4), (4, -3), (-3, -4), (-4, -3)
]
] :: [Point]
indexed = zip [(0 :: Int) ..] ring
cocircular =
[ (a, b, c, d)
| (i, a) <- indexed, (j, b) <- indexed, j > i
, (k, c) <- indexed, k > j, (l, d) <- indexed, l > k
]
unless (length cocircular == 495) $ fail ("expected 495 cocircular quadruples, got " <> show (length cocircular))
forM_ cocircular $ \(a, b, c, d) -> do
assertEqual ("cocircular points are cocircular at " <> show (a, b, c, d)) EQ (measured a b c d)
assertEqual ("the lift agrees on cocircularity at " <> show (a, b, c, d)) EQ (predicted a b c d)
-- The geometry the sign means, stated once against a circle anyone can read.
let (a, b, c) = (Point 1 0, Point 0 1, Point (-1) 0) :: (Point, Point, Point)
assertEqual "the centre is inside the circle" GT (measured a b c (Point 0 0))
assertEqual "the antipode is on the circle" EQ (measured a b c (Point 0 (-1)))
assertEqual "a distant point is outside" LT (measured a b c (Point 2 2))
-- | A payload labels an element, and an element is its geometry. Every rewrite
-- an insertion performs — an edge split, a face split, a Lawson flip — hands
-- some slot a different element to hold, and the label the displaced one
-- carried does not describe what took its place.
--
-- So: label every element of a triangulation by its own key, insert a point,
-- and demand that an element whose key survives still carries exactly the label
-- it was given while every element whose key is new carries the default and
-- nothing else. Both directions are checked; either alone is satisfiable by a
-- store that throws everything away.
--
-- The flip is the load-bearing case. Legalization is confluent, so which flips
-- fire and in what order is not observable in the topology that comes out. A
-- payload that rode through a flip would make it observable in the payload
-- plane, and a triangulation that is a normal form in one component and a
-- history in another is not a normal form.