packages feed

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

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE NumericUnderscores #-}

-- | Incremental location, frozen-location reuse, and hierarchy laws.
module Moonlight.Planar.LocationSpec
  ( tests
  ) where

import Control.Monad ( unless, forM_ )
import Data.Foldable ( traverse_ )
import Moonlight.Planar.BulkLoad ( delaunay, empty, insert )
import Moonlight.Planar.Dcel ( innerFaceDirectedEdges, numInnerFaces, numVertices, origin,
  vertexData, vertexPoint )
import Moonlight.Planar.Handles.Iterators.FixedIterators ( vertices, innerFaces )
import Moonlight.Planar.HintGenerator ( buildHierarchyHint, hierarchyHint,
  updateHierarchyAfterInsertion )
import Moonlight.Planar.Internal.HandleDefs ( VertexId(VertexId), reverseEdge )
import Moonlight.Planar.Math ( midpoint )
import Moonlight.Planar.MeshFixtures ( requirePointBuild, edgeKeys )
import Moonlight.Planar.PayloadFixtures ( SampleVertex(..) )
import Moonlight.Planar.PointLocation ( locatePointWithHint, locatePoint )
import Moonlight.Planar.Session ( withSession, insertVertexAt, insertVertexAtNearVertex )
import Moonlight.Planar.Point (Point(Point))
import Moonlight.Planar.Types (unitElementDefaults, ConstraintMode(Unconstrained), InsertionDisposition(..), Location(..), LocationHint(VertexHint, FaceHint), LocationStats(LocationStats, locationUsedFallback,
  locationWalkSteps), BuildResult(buildTriangulation), DelaunayTriangulation, InsertionResult(insertionDisposition, insertionStats, insertionTriangulation, insertionVertex), Triangulation)
import Moonlight.Planar.BuildStats (BuildMetric (InputPoints, UniquePoints, ExistingPoints, DuplicatePoints, LocationWalkSteps, LocationFallbacks, LocationMaxWalk), buildStat)
import Support ( assertEqual, assertValid, requireQueryPoint, requireRight, randomPoints )
import qualified Data.Vector as V


tests :: IO ()
tests =
  sequence_
    [ testIncrementalLocationDescent
    , testPersistentInsertionReusesFrozenLocation
    , testVertexSeededSessionInsertion
    , testPointLocationAndHints
    , testHierarchyNestingLaw
    ]

data IncrementalLocationEvidence = IncrementalLocationEvidence
  { incrementalTriangulation :: !(DelaunayTriangulation (Point))
  , incrementalWalkSteps :: {-# UNPACK #-} !Int
  , incrementalFallbacks :: {-# UNPACK #-} !Int
  }

testIncrementalLocationDescent :: IO ()
testIncrementalLocationDescent = do
  small <- collectIncrementalLocationEvidence 500
  large <- collectIncrementalLocationEvidence 1000
  assertEqual "incremental location fallbacks/500" 0 (incrementalFallbacks small)
  assertEqual "incremental location fallbacks/1000" 0 (incrementalFallbacks large)
  unless (2 * incrementalWalkSteps large < 7 * incrementalWalkSteps small) $
    fail
      ( "incremental location approached quadratic growth: "
          <> show (incrementalWalkSteps small, incrementalWalkSteps large)
      )
  assertValid "incremental location descent/1000" (incrementalTriangulation large)

collectIncrementalLocationEvidence :: Int -> IO IncrementalLocationEvidence
collectIncrementalLocationEvidence count =
  V.foldM' insertAndAccumulate initialEvidence (V.fromList (randomPoints 0xc1ac_10ca count))
 where
  initialEvidence =
    IncrementalLocationEvidence
      { incrementalTriangulation = empty unitElementDefaults
      , incrementalWalkSteps = 0
      , incrementalFallbacks = 0
      }

  insertAndAccumulate evidence point = do
    result <- requireRight "incremental location descent" (insert (incrementalTriangulation evidence) point)
    let stats = insertionStats result
    pure
      IncrementalLocationEvidence
        { incrementalTriangulation = insertionTriangulation result
        , incrementalWalkSteps = incrementalWalkSteps evidence + (buildStat LocationWalkSteps) stats
        , incrementalFallbacks = incrementalFallbacks evidence + (buildStat LocationFallbacks) stats
        }

-- A persistent insertion locates on the frozen mesh before opening its dense
-- transaction. The thaw preserves every extant handle, so a lawful frozen
-- location can be interpreted directly without a second mutable walk. A
-- degenerate-line outside witness lacks the terminal-edge evidence its mutable
-- interpreter requires, so that one stratum deliberately retains the mutable
-- fallback. Exercise every frozen stratum, including the singleton's edge-less
-- outside witness, rather than testing only the ordinary face case.
testPersistentInsertionReusesFrozenLocation :: IO ()
testPersistentInsertionReusesFrozenLocation = do
  let vacant = empty unitElementDefaults :: DelaunayTriangulation (Point)
  emptyLocation <- assertPersistentInsertionFromFrozenLocation "empty" Inserted vacant (Point 0 0)
  assertEqual "empty insertion frozen location" EmptyTriangulation emptyLocation

  singletonBuild <- requirePointBuild "singleton frozen location" [Point 0 0]
  singletonLocation <-
    assertPersistentInsertionFromFrozenLocation
      "singleton outside insertion"
      Inserted
      (buildTriangulation singletonBuild)
      (Point 2 0)
  assertEqual "singleton insertion frozen location" (OutsideConvexHull Nothing) singletonLocation

  lineBuild <- requirePointBuild "line frozen locations" [Point 0 0, Point 2 0, Point 4 0]
  let line = buildTriangulation lineBuild
  lineEdgeLocation <- assertPersistentInsertionFromFrozenLocation "line edge insertion" Inserted line (Point 1 0)
  case lineEdgeLocation of
    OnEdge _ -> pure ()
    other -> fail ("line edge insertion located " <> show other)
  lineOutsideLocation <- assertPersistentInsertionFromFrozenLocation "line extension" Inserted line (Point 6 0)
  case lineOutsideLocation of
    OutsideConvexHull (Just _) -> pure ()
    other -> fail ("line extension located " <> show other)

  triangleBuild <- requirePointBuild "area frozen locations" [Point 0 0, Point 4 0, Point 0 4]
  let triangle = buildTriangulation triangleBuild
  faceLocation <- assertPersistentInsertionFromFrozenLocation "face insertion" Inserted triangle (Point 1 1)
  case faceLocation of
    InFace _ -> pure ()
    other -> fail ("face insertion located " <> show other)
  edgeLocation <- assertPersistentInsertionFromFrozenLocation "area edge insertion" Inserted triangle (Point 2 0)
  case edgeLocation of
    OnEdge _ -> pure ()
    other -> fail ("area edge insertion located " <> show other)
  hullLocation <- assertPersistentInsertionFromFrozenLocation "hull insertion" Inserted triangle (Point 5 1)
  case hullLocation of
    OutsideConvexHull (Just _) -> pure ()
    other -> fail ("hull insertion located " <> show other)
  duplicateLocation <- assertPersistentInsertionFromFrozenLocation "duplicate insertion" AlreadyPresent triangle (Point 0 0)
  assertEqual "duplicate insertion frozen location" (OnVertex (VertexId 0)) duplicateLocation

assertPersistentInsertionFromFrozenLocation
  :: String
  -> InsertionDisposition
  -> DelaunayTriangulation (Point)
  -> Point
  -> IO Location
assertPersistentInsertionFromFrozenLocation label expectedDisposition source point = do
  query <- requireQueryPoint (label <> " frozen query") point
  let (located, walked) = locatePointWithHint source Nothing query
      sourceVertices = numVertices source
      usesMutableFallback =
        case located of
          OutsideConvexHull (Just _) -> numInnerFaces source == 0
          _ -> False
  result <- requireRight (label <> " insert") (insert source point)
  ((referenceVertex, referenceDisposition), reference, _) <-
    requireRight (label <> " session reference") $
      withSession source 1 (insertVertexAt point point)
  let stats = insertionStats result
      expectedVertices =
        case expectedDisposition of
          Inserted -> sourceVertices + 1
          AlreadyPresent -> sourceVertices
      expectedUnique =
        case expectedDisposition of
          Inserted -> 1
          AlreadyPresent -> 0
      expectedExisting =
        case expectedDisposition of
          Inserted -> 0
          AlreadyPresent -> 1
  assertEqual (label <> " disposition") expectedDisposition (insertionDisposition result)
  assertEqual (label <> " session disposition") referenceDisposition (insertionDisposition result)
  assertEqual (label <> " session vertex") referenceVertex (insertionVertex result)
  assertEqual
    (label <> " exact-location topology matches session")
    (edgeKeys reference)
    (edgeKeys (insertionTriangulation result))
  assertEqual (label <> " source remains unchanged") sourceVertices (numVertices source)
  assertEqual (label <> " result vertex count") expectedVertices (numVertices (insertionTriangulation result))
  assertEqual (label <> " input count") 1 ((buildStat InputPoints) stats)
  assertEqual (label <> " unique count") expectedUnique ((buildStat UniquePoints) stats)
  assertEqual (label <> " existing count") expectedExisting ((buildStat ExistingPoints) stats)
  assertEqual (label <> " duplicate count") expectedExisting ((buildStat DuplicatePoints) stats)
  if usesMutableFallback
    then do
      let assertAtLeast
            :: String
            -> Int
            -> Int
            -> IO ()
          assertAtLeast counter expected actual =
            unless
              (actual >= expected)
              ( fail
                  ( label
                      <> " "
                      <> counter
                      <> " includes frozen evidence: expected at least "
                      <> show expected
                      <> ", got "
                      <> show actual
                  )
              )
      assertAtLeast "walk steps" (locationWalkSteps walked) ((buildStat LocationWalkSteps) stats)
      assertAtLeast "walk maximum" (locationWalkSteps walked) ((buildStat LocationMaxWalk) stats)
      assertAtLeast
        "fallback count"
        (if locationUsedFallback walked then 1 else 0)
        ((buildStat LocationFallbacks) stats)
    else do
      assertEqual (label <> " frozen walk steps") (locationWalkSteps walked) ((buildStat LocationWalkSteps) stats)
      assertEqual (label <> " frozen walk maximum") (locationWalkSteps walked) ((buildStat LocationMaxWalk) stats)
      assertEqual
        (label <> " frozen fallback count")
        (if locationUsedFallback walked then 1 else 0)
        ((buildStat LocationFallbacks) stats)
  assertValid (label <> " result") (insertionTriangulation result)
  pure located

-- A vertex hint restricts to one incident face and then relinquishes
-- authority to the exact mutable walk. Valid, stale, occupied, and face-less
-- local sections must therefore glue to the same published result as the
-- unhinted entrance.
testVertexSeededSessionInsertion :: IO ()
testVertexSeededSessionInsertion = do
  area <- sampleMesh "vertex-seeded area" [Point 0 0, Point 4 0, Point 0 4]
  seed <- vertexAt "vertex-seeded area seed" area (Point 0 0)
  assertVertexSeededInsertionAgreement
    "vertex-seeded interior insertion"
    area
    seed
    (SampleVertex (Point 1 1) 40)
  assertVertexSeededInsertionAgreement
    "vertex-seeded invalid hint"
    area
    (VertexId maxBound)
    (SampleVertex (Point 3 3) 41)
  assertVertexSeededInsertionAgreement
    "vertex-seeded duplicate"
    area
    seed
    (SampleVertex (Point 4 0) 42)

  line <- sampleMesh "vertex-seeded line" [Point 0 0, Point 2 0, Point 4 0]
  lineSeed <- vertexAt "vertex-seeded line seed" line (Point 2 0)
  assertVertexSeededInsertionAgreement
    "vertex-seeded face-less fallback"
    line
    lineSeed
    (SampleVertex (Point 3 0) 43)
 where
  sampleMesh
    :: String
    -> [Point]
    -> IO (Triangulation 'Unconstrained SampleVertex () () ())
  sampleMesh label points = do
    built <-
      requireRight label $
        delaunay
          unitElementDefaults
          (V.imap (\index point -> SampleVertex point index) (V.fromList points))
    pure (buildTriangulation built)

  vertexAt
    :: String
    -> Triangulation 'Unconstrained SampleVertex () () ()
    -> Point
    -> IO VertexId
  vertexAt label triangulation point = do
    query <- requireQueryPoint label point
    case locatePoint triangulation query of
      OnVertex vertex -> pure vertex
      location -> fail (label <> " did not locate a vertex: " <> show location)

assertVertexSeededInsertionAgreement
  :: String
  -> Triangulation 'Unconstrained SampleVertex () () ()
  -> VertexId
  -> SampleVertex
  -> IO ()
assertVertexSeededInsertionAgreement label source seed payload = do
  ((referenceVertex, referenceDisposition), reference, _) <-
    requireRight (label <> " unhinted") $
      withSession source 1 (insertVertexAt (samplePosition payload) payload)
  ((hintedVertex, hintedDisposition), hinted, _) <-
    requireRight (label <> " hinted") $
      withSession source 1 (insertVertexAtNearVertex seed (samplePosition payload) payload)
  assertEqual (label <> " vertex") referenceVertex hintedVertex
  assertEqual (label <> " disposition") referenceDisposition hintedDisposition
  assertEqual (label <> " topology") (edgeKeys reference) (edgeKeys hinted)
  assertEqual (label <> " payload") payload (vertexData hinted hintedVertex)
  assertValid label hinted

-- Circle sweep must be a construction schedule, not a second topology. It is
-- compared against the arrival-order session kernel on the same exact inputs.
-- | One session, both verbs. The reason the two published sessions became one:
-- a caller who removes and inserts had to thaw twice and pay the O(n)
-- publication a session exists to delete.
--
-- Graded against the oracle that needs no second implementation — the Delaunay
-- triangulation of a point set in general position is unique, so a mixed edit
-- must land exactly where a fresh bulk load of the surviving set lands.
testPointLocationAndHints :: IO ()
testPointLocationAndHints = do
  testTriangleLocationPriority
  built <- requirePointBuild "location" (randomPoints 0x1234_5678 1200)
  let triangulation = buildTriangulation built
  queries <- traverse (requireQueryPoint "location query") (take 250 (randomPoints 0xdead_beef 250))
  let
      baseline = sum [locationWalkSteps stats | query <- queries, let (_, stats) = locatePointWithHint triangulation Nothing query]
  hierarchy <- requireRight "hierarchy build" (buildHierarchyHint 16 triangulation)
  let hinted = sum
        [ locationWalkSteps stats
        | query <- queries
        , let hint = hierarchyHint hierarchy query
              (_, stats) = locatePointWithHint triangulation hint query
        ]
  unless (hinted <= baseline) $
    fail ("hierarchy hint increased aggregate walking: " <> show (baseline, hinted))
  staleHintQuery <- requireQueryPoint "stale hint query" (Point 0.125 (-0.375))
  let staleVertexHint = VertexHint (VertexId (fromIntegral (numVertices triangulation)))
  assertEqual
    "stale vertex hint falls back to the canonical start face"
    (locatePointWithHint triangulation Nothing staleHintQuery)
    (locatePointWithHint triangulation (Just staleVertexHint) staleHintQuery)
  forM_ (vertices triangulation) $ \vertex -> do
    vertexQuery <- requireQueryPoint "vertex lookup" (vertexPoint triangulation vertex)
    assertEqual "vertex lookup" (OnVertex vertex) (locatePoint triangulation vertexQuery)
  let insertedPoint = Point 0.1234567 (-0.2345678)
  inserted <- requireRight "hierarchy incremental source" (insert triangulation insertedPoint)
  let updatedTriangulation = insertionTriangulation inserted
  updatedHierarchy <-
    requireRight
      "hierarchy incremental update"
      ( updateHierarchyAfterInsertion
          hierarchy
          insertedPoint
          (insertionVertex inserted)
          (insertionDisposition inserted)
      )
  rebuiltHierarchy <- requireRight "hierarchy reference rebuild" (buildHierarchyHint 16 updatedTriangulation)
  assertEqual "incremental hierarchy equals canonical rebuild" rebuiltHierarchy updatedHierarchy

-- The immutable triangle already owns edge cyclicity. Pin early boundary hits
-- and the LAST crossing when a corner query lies beyond two incident edges.
testTriangleLocationPriority :: IO ()
testTriangleLocationPriority = do
  built <- requirePointBuild "location priority triangle" [Point 0 0, Point 4 0, Point 0 4]
  let mesh = buildTriangulation built
  traverse_
    (\face -> case innerFaceDirectedEdges mesh face of
      Nothing -> fail "location priority fixture lost its triangular face"
      Just (e0, e1, e2) -> do
        let v0 = origin mesh e0
            v1 = origin mesh e1
            v2 = origin mesh e2
            p0 = vertexPoint mesh v0
            p1 = vertexPoint mesh v1
            p2 = vertexPoint mesh v2
            hint = Just (FaceHint face)
            check label expected point = do
              query <- requireQueryPoint label point
              assertEqual label (expected, LocationStats 1 False) (locatePointWithHint mesh hint query)
            beyondCorner (Point ax ay) (Point bx by) (Point cx cy) = Point (3 * ax - bx - cx) (3 * ay - by - cy)
        traverse_ (\(vertex, point) -> check "triangle vertex priority" (OnVertex vertex) point) [(v0, p0), (v1, p1), (v2, p2)]
        traverse_ (\(edge, edgeFrom, edgeTo) -> check "triangle edge priority" (OnEdge edge) (midpoint edgeFrom edgeTo)) [(e0, p0, p1), (e1, p1, p2), (e2, p2, p0)]
        traverse_
          (\(lastCrossing, cornerPoint, nextPoint, previousPoint) -> check "triangle last crossing" (OutsideConvexHull (Just (reverseEdge lastCrossing))) (beyondCorner cornerPoint nextPoint previousPoint))
          [(e2, p0, p1, p2), (e1, p1, p2, p0), (e2, p2, p0, p1)])
    (innerFaces mesh)

-- The hierarchy replaces its level-to-base correspondence with the arithmetic
-- claim that level-local handle @j@ names base vertex @j * branch@. Query each
-- sampled vertex with its own position: the descent must return that very
-- vertex, at every branch factor and along the whole of level 0. A stride the
-- construction does not actually obey shows up here as a named mismatch.
testHierarchyNestingLaw :: IO ()
testHierarchyNestingLaw = do
  built <- requirePointBuild "hierarchy nesting" (randomPoints 0x0f1e_2d3c 900)
  let triangulation = buildTriangulation built
  forM_ [2, 3, 16] $ \branch -> do
    hierarchy <- requireRight ("hierarchy build at branch " <> show branch) (buildHierarchyHint branch triangulation)
    let sampled = [0, branch .. numVertices triangulation - 1]
    forM_ sampled $ \index -> do
      let vertex = VertexId (fromIntegral index)
      vertexQuery <- requireQueryPoint "sampled hierarchy vertex" (vertexPoint triangulation vertex)
      assertEqual
        ("branch " <> show branch <> " descent onto sampled vertex " <> show index)
        (Just (VertexHint vertex))
        (hierarchyHint hierarchy vertexQuery)