packages feed

moonlight-planar-1.1.0.0: test/native/Moonlight/Planar/DcelSpec.hs

{-# LANGUAGE LambdaCase #-}

-- | Resident handles, intersection traversal, interpolation, and Voronoi dual laws.
module Moonlight.Planar.DcelSpec
  ( tests
  ) where

import Control.Monad ( forM_, unless, when )
import Control.Monad.ST ( stToIO )
import Data.Maybe ( isJust )
import Moonlight.Planar.Dcel ( geometryTopologyBytes, innerFaceDirectedEdgeTriples,
  innerFaceVertexTriples, next, numDirectedEdges, numFaces, numInnerFaces, numUndirectedEdges,
  numVertices, origin, previous, topologyIndexBytes, undirectedEndpoints, vertexPoint, vertexPoints
  )
import Moonlight.Planar.FloodFillIterator ( circleMetric, edgesInCircle, verticesInCircle,
  verticesInRectangle, CircleMetricError(InvalidCircleRadius, InvalidCircleCenter) )
import Moonlight.Planar.Handles.Iterators.FixedIterators ( directedEdges, undirectedEdges, vertices,
  innerFaces )
import Moonlight.Planar.Internal.HandleDefs ( VertexId(VertexId), asUndirected, directedPair,
  reverseEdge )
import Moonlight.Planar.Interpolation ( newNaturalNeighborWorkspace, workspaceBytes,
  estimateGradients, foldNaturalNeighborWeights, interpolateNaturalNeighbor,
  interpolateNaturalNeighborGradient, naturalNeighborWeights,
  InterpolationStats(interpolationNaturalNeighbors), NaturalNeighborResult(naturalNeighborValues) )
import Moonlight.Planar.IntersectionIterator ( lineIntersections, Intersection(..) )
import Moonlight.Planar.Math ( segmentDistanceSquared )
import Moonlight.Planar.MeshFixtures ( requirePointBuild )
import Moonlight.Planar.Point (Point(Point), PointValidationError(InvalidPointX, InvalidPointY))
import Moonlight.Planar.Scalar (CoordinateError(CoordinateInfinite, CoordinateNaN), NonFiniteValue(ValueNegativeInfinity, ValueNaN, ValuePositiveInfinity), RadiusSquaredError(NonFiniteRadiusSquared,
  NegativeRadiusSquared))
import Moonlight.Planar.Types (BuildResult(buildTriangulation))
import Moonlight.Planar.Voronoi ( asDelaunayDirectedEdge, directedVoronoiEdges, reverseVoronoiEdge,
  undirectedVoronoiEdges, voronoiEdgeGeometry, voronoiFaceSite, voronoiFaces, voronoiIncidentFace,
  voronoiNext, voronoiPrevious )
import Support ( assertEqual, assertValid, requireQueryPoint, requireRight, requireJust )
import qualified Moonlight.Planar.Dcel as Dcel
import Moonlight.Planar.Handles.Dynamic qualified as Dynamic
import Moonlight.Planar.Handles.Iterators.DynamicIterators qualified as DynamicIterators
import qualified Data.Set as Set
import qualified Data.Vector as V
import Moonlight.Planar.Voronoi.Handles qualified as VoronoiDynamic


tests :: IO ()
tests =
  sequence_
    [ testHandlesAndFiniteDcel
    , testSibsonInterpolation
    , testVoronoiDual
    , testTraversal
    ]

reverseTraversalEvent :: Intersection -> Intersection
reverseTraversalEvent = \case
  EdgeIntersection edge -> EdgeIntersection (reverseEdge edge)
  EdgeOverlap edge -> EdgeOverlap (reverseEdge edge)
  VertexIntersection vertex -> VertexIntersection vertex

testHandlesAndFiniteDcel :: IO ()
testHandlesAndFiniteDcel = do
  built <- requirePointBuild "handle algebra" [Point 0 0, Point 3 0, Point 0 2, Point 0.4 0.7]
  let triangulation = buildTriangulation built
  assertValid "handle algebra" triangulation
  assertEqual "one outer face" (numInnerFaces triangulation + 1) (numFaces triangulation)
  forM_ (directedEdges triangulation) $ \edge -> do
    assertEqual "double reversal" edge (reverseEdge (reverseEdge edge))
    assertEqual "next/previous" edge (previous triangulation (next triangulation edge))
    assertEqual "previous/next" edge (next triangulation (previous triangulation edge))
  forM_ (undirectedEdges triangulation) $ \edge -> do
    let (forward, backward) = directedPair edge
    assertEqual "pair reversal" backward (reverseEdge forward)
    assertEqual "undirected projection" edge (asUndirected forward)
  assertEqual "vertex iterator" (numVertices triangulation) (length (vertices triangulation))
  assertEqual
    "dense vertex projection"
    (V.fromList (fmap (vertexPoint triangulation) (vertices triangulation)))
    (vertexPoints triangulation)
  assertEqual "edge iterator" (numUndirectedEdges triangulation) (length (undirectedEdges triangulation))
  assertEqual "face iterator" (numInnerFaces triangulation) (length (innerFaces triangulation))
  assertEqual
    "dense inner-face directed-edge projection"
    (Just (V.toList (innerFaceDirectedEdgeTriples triangulation)))
    (traverse (Dcel.innerFaceDirectedEdges triangulation) (innerFaces triangulation))
  assertEqual
    "dense inner-face projection"
    (Just (V.toList (innerFaceVertexTriples triangulation)))
    (traverse (Dcel.innerFaceVertices triangulation) (innerFaces triangulation))
  assertEqual "dynamic vertex iterator" (numVertices triangulation) (length (DynamicIterators.vertexHandles triangulation))
  assertEqual "dynamic edge iterator" (numDirectedEdges triangulation) (length (DynamicIterators.directedEdgeHandles triangulation))
  assertEqual "dynamic face iterator" (numInnerFaces triangulation) (length (DynamicIterators.innerFaceHandles triangulation))
  unless (geometryTopologyBytes triangulation > topologyIndexBytes triangulation) $
    fail "geometry byte accounting omitted coordinates"
  vertex0 <- requireJust "dynamic vertex handle" (Dynamic.vertexHandle triangulation (VertexId 0))
  assertEqual "dynamic vertex fix" (VertexId 0) (Dynamic.fixVertex vertex0)
  assertEqual "dynamic vertex position" (vertexPoint triangulation (VertexId 0)) (Dynamic.vertexHandlePosition vertex0)
  case Dynamic.vertexHandleOutEdge vertex0 of
    Nothing -> fail "connected dynamic vertex has no outgoing edge"
    Just edge -> do
      assertEqual "dynamic edge reversal" (Dynamic.fixDirectedEdge edge) (Dynamic.fixDirectedEdge (Dynamic.directedEdgeReverse (Dynamic.directedEdgeReverse edge)))
      assertEqual "dynamic edge next/previous" (Dynamic.fixDirectedEdge edge) (Dynamic.fixDirectedEdge (Dynamic.directedEdgePrevious (Dynamic.directedEdgeNext edge)))
      let face = Dynamic.directedEdgeFace edge
      if Dynamic.faceIsOuter face
        then pure ()
        else case Dynamic.faceAsInner face of
          Nothing -> fail "non-outer dynamic face did not refine to InnerTag"
          Just inner -> do
            _ <- requireJust "inner dynamic face vertices" (Dynamic.innerFaceVertices inner)
            case Dynamic.innerFaceCircumcenter inner of
              Nothing -> fail "inner dynamic face has no circumcenter"
              Just _ -> pure ()

testSibsonInterpolation :: IO ()
testSibsonInterpolation = do
  built <- requirePointBuild "sibson" (gridPoints 9 9)
  let triangulation = buildTriangulation built
      queryPoint = Point 3.25 4.4
      linear vertex = let Point x y = vertexPoint triangulation vertex in 2 * x - 3 * y + 5
      expected = let Point x y = queryPoint in 2 * x - 3 * y + 5
  query <- requireQueryPoint "Sibson query" queryPoint
  workspace <- stToIO (newNaturalNeighborWorkspace triangulation)
  result <- stToIO (naturalNeighborWeights workspace Nothing query)
  let weights = naturalNeighborValues result
  unless (V.length weights >= 3) $ fail "Sibson query did not discover a natural-neighbor cavity"
  assertNear "Sibson partition" 1.0e-11 1 (V.sum (V.map snd weights))
  unless (V.all ((>= (-1.0e-12)) . snd) weights) $ fail "Sibson produced a negative weight"
  (folded, _, foldedStats) <- stToIO (foldNaturalNeighborWeights
    (\total vertex weight -> total + weight * linear vertex)
    0
    workspace
    Nothing
    query)
  assertNear "allocation-free Sibson fold" 2.0e-9 expected folded
  assertEqual "Sibson fold neighbor count" (V.length weights) (interpolationNaturalNeighbors foldedStats)
  (interpolated, _) <- stToIO (interpolateNaturalNeighbor linear workspace Nothing query)
  case interpolated of
    Nothing -> fail "Sibson interpolation rejected an interior query"
    Just value -> assertNear "Sibson affine precision" 2.0e-9 expected value
  let gradients = estimateGradients linear triangulation
  V.forM_ gradients $ \(gx, gy) -> do
    assertNear "planar gradient x" 1.0e-9 2 gx
    assertNear "planar gradient y" 1.0e-9 (-3) gy
  let gradient (VertexId raw) = gradients V.! fromIntegral raw
  (gradientValue, _) <- stToIO (interpolateNaturalNeighborGradient linear gradient 0.5 workspace Nothing query)
  case gradientValue of
    Nothing -> fail "gradient natural-neighbor interpolation rejected an interior query"
    Just value -> assertNear "gradient affine precision" 2.0e-9 expected value
  unless (workspaceBytes workspace > 0) $ fail "Sibson workspace byte accounting is empty"

testVoronoiDual :: IO ()
testVoronoiDual = do
  built <- requirePointBuild "voronoi" [Point 0 0, Point 2 0, Point 0 2, Point 2 2, Point 1 1]
  let triangulation = buildTriangulation built
  assertEqual "Voronoi face count" (numVertices triangulation) (length (voronoiFaces triangulation))
  assertEqual "directed dual edge count" (numDirectedEdges triangulation) (length (directedVoronoiEdges triangulation))
  assertEqual "undirected dual edge count" (numUndirectedEdges triangulation) (length (undirectedVoronoiEdges triangulation))
  forM_ (directedVoronoiEdges triangulation) $ \edge -> do
    assertEqual "dual double reversal" edge (reverseVoronoiEdge (reverseVoronoiEdge edge))
    assertEqual "dual next/previous" edge (voronoiPrevious triangulation (voronoiNext triangulation edge))
    assertEqual
      "dual face/site"
      (origin triangulation (asDelaunayDirectedEdge edge))
      (voronoiFaceSite (voronoiIncidentFace triangulation edge))
    case voronoiEdgeGeometry triangulation edge of
      Nothing -> fail "valid dual edge has no geometry"
      fixedGeometry@(Just _) -> do
        owning <- requireJust "dynamic Voronoi edge" (VoronoiDynamic.directedVoronoiEdgeHandle triangulation edge)
        assertEqual "fixed/owning dual geometry" fixedGeometry (VoronoiDynamic.voronoiEdgeGeometryH owning)
  case directedVoronoiEdges triangulation of
    [] -> fail "Voronoi test produced no directed dual edge"
    first : _ -> do
      handle <- requireJust "dynamic Voronoi edge" (VoronoiDynamic.directedVoronoiEdgeHandle triangulation first)
      assertEqual "dynamic dual fix" first (VoronoiDynamic.fixDirectedVoronoiEdge handle)
      assertEqual "dynamic dual reversal" first (VoronoiDynamic.fixDirectedVoronoiEdge (VoronoiDynamic.voronoiEdgeReverseH (VoronoiDynamic.voronoiEdgeReverseH handle)))
      assertEqual "dynamic dual/primal conversion" (asDelaunayDirectedEdge first) (Dynamic.fixDirectedEdge (VoronoiDynamic.voronoiEdgeAsDelaunayH handle))
      let dualFace = VoronoiDynamic.voronoiEdgeFaceH handle
      assertEqual "dynamic dual face site" (origin triangulation (asDelaunayDirectedEdge first)) (Dynamic.fixVertex (VoronoiDynamic.voronoiFaceSiteH dualFace))
      let source = VoronoiDynamic.voronoiEdgeFromH handle
      case VoronoiDynamic.voronoiVertexAsDelaunayFaceH source of
        Just inner -> case VoronoiDynamic.voronoiVertexPositionH source of
          Nothing -> fail "inner dynamic Voronoi vertex has no position"
          Just voronoiPosition -> assertEqual "inner dynamic Voronoi position" (Dynamic.innerFaceCircumcenter inner) (Just voronoiPosition)
        Nothing -> unless (isJust (VoronoiDynamic.voronoiVertexAsOuterEdgeH source)) $
          fail "outer dynamic Voronoi vertex has no defining edge"

testTraversal :: IO ()
testTraversal = do
  built <- requirePointBuild "traversal" [Point (-3) 0, Point (-1) (-2), Point (-1) 2, Point 1 (-2), Point 1 2, Point 3 0]
  forwardStart <- requireQueryPoint "forward traversal start" (Point (-4) 0)
  forwardEnd <- requireQueryPoint "forward traversal end" (Point 4 0)
  let triangulation = buildTriangulation built
      forward = lineIntersections triangulation forwardStart forwardEnd
      backward = lineIntersections triangulation forwardEnd forwardStart
  when (null forward) $ fail "ordered line traversal crossed nothing"
  assertEqual
    "reversing the segment reverses crossing order and orientation"
    (map reverseTraversalEvent forward)
    (reverse backward)
  interiorEnd <- requireQueryPoint "outside traversal interior end" (Point 0.25 0.5)
  let outsideToInterior = lineIntersections triangulation forwardStart interiorEnd
      interiorToOutside = lineIntersections triangulation interiorEnd forwardStart
  when (null outsideToInterior) $ fail "outside-to-interior traversal crossed nothing"
  assertEqual
    "endpoint-directed outside traversal preserves order and orientation"
    (map reverseTraversalEvent outsideToInterior)
    (reverse interiorToOutside)
  missFrom <- requireQueryPoint "outside traversal miss from" (Point (-4) 4)
  missTo <- requireQueryPoint "outside traversal miss to" (Point 4 4)
  assertEqual
    "outside segment missing the hull reports no intersections"
    []
    (lineIntersections triangulation missFrom missTo)
  circleEdges <- Set.fromList <$> requireRight "circle edge query" (edgesInCircle triangulation (Point 0 0) 4)
  let bruteCircle = Set.fromList
        [ edge
        | edge <- undirectedEdges triangulation
        , let (a, b) = undirectedEndpoints triangulation edge
        , segmentDistanceSquared (vertexPoint triangulation a) (vertexPoint triangulation b) (Point 0 0) <= 4
        ]
  assertEqual "circle edge flood" bruteCircle circleEdges
  assertEqual
    "negative circle metric refusal"
    (Left (InvalidCircleRadius (NegativeRadiusSquared (-1))))
    (circleMetric (Point 0 0 :: Point) (-1))
  assertEqual
    "negative circle edge-query refusal"
    (Left (InvalidCircleRadius (NegativeRadiusSquared (-1))))
    (edgesInCircle triangulation (Point 0 0) (-1))
  assertEqual
    "negative circle vertex-query refusal"
    (Left (InvalidCircleRadius (NegativeRadiusSquared (-1))))
    (verticesInCircle triangulation (Point 0 0) (-1))
  assertEqual
    "NaN circle metric refusal"
    (Left (InvalidCircleRadius (NonFiniteRadiusSquared ValueNaN)))
    (circleMetric (Point 0 0 :: Point) (0 / 0))
  assertEqual
    "NaN circle center-x refusal"
    (Left (InvalidCircleCenter (InvalidPointX CoordinateNaN)))
    (circleMetric (Point (0 / 0) 0 :: Point) 1)
  assertEqual
    "positive-infinite circle center-y refusal"
    (Left (InvalidCircleCenter (InvalidPointY CoordinateInfinite)))
    (edgesInCircle triangulation (Point 0 (1 / 0)) 1)
  assertEqual
    "negative-infinite circle center-x refusal"
    (Left (InvalidCircleCenter (InvalidPointX CoordinateInfinite)))
    (verticesInCircle triangulation (Point ((-1) / 0) 0) 1)
  assertEqual
    "infinite circle edge-query refusal"
    (Left (InvalidCircleRadius (NonFiniteRadiusSquared ValuePositiveInfinity)))
    (edgesInCircle triangulation (Point 0 0) (1 / 0))
  assertEqual
    "negative-infinite circle vertex-query refusal"
    (Left (InvalidCircleRadius (NonFiniteRadiusSquared ValueNegativeInfinity)))
    (verticesInCircle triangulation (Point 0 0) ((-1) / 0))
  rectangleVertices <-
    Set.fromList
      <$> requireRight
        "rectangle query"
        (verticesInRectangle triangulation (Point (-1.1) (-2.1)) (Point 1.1 2.1))
  let bruteVertices = Set.fromList
        [ vertex
        | vertex <- vertices triangulation
        , let Point x y = vertexPoint triangulation vertex
        , x >= (-1.1), x <= 1.1, y >= (-2.1), y <= 2.1
        ]
  assertEqual "rectangle vertex flood" bruteVertices rectangleVertices

assertNear :: String -> Double -> Double -> Double -> IO ()
assertNear label tolerance expected actual =
  unless (abs (expected - actual) <= tolerance * max 1 (max (abs expected) (abs actual))) $
    fail (label <> ": expected " <> show expected <> ", got " <> show actual)

gridPoints :: Int -> Int -> [Point]
gridPoints width height =
  [ Point (fromIntegral x + jitter x y) (fromIntegral y + jitter y x)
  | y <- [0 .. height - 1]
  , x <- [0 .. width - 1]
  ]
 where
  jitter :: Int -> Int -> Double
  jitter a b = fromIntegral ((a * 17 + b * 31) `mod` 11) * 1.0e-5