packages feed

moonlight-triangulation-1.4.0.2: bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Delaunay.hs

{-# LANGUAGE DataKinds #-}

-- | Bulk-load, incremental, nearest, and refinement lanes and their gates.
module Moonlight.Triangulation.Bench.SpadeCompare.Delaunay where

import Control.Exception (evaluate)
import Data.Foldable (traverse_)
import System.Exit (exitWith, ExitCode (ExitFailure))
import System.FilePath ((</>))
import System.IO (hPutStrLn, stderr)
import qualified Data.Vector as V
import Moonlight.Triangulation
import Moonlight.Triangulation.BulkLoad
import Moonlight.Triangulation.Dcel
import Moonlight.Triangulation.HintGenerator
import Moonlight.Triangulation.Session
import Moonlight.Triangulation.Types
import Moonlight.Triangulation.Bench.SpadeCompare.Support
import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)

writeBulkLoadGate :: FilePath -> Int -> IO ()
writeBulkLoadGate directory count = do
  triangulation <-
    require
      ( delaunayGeometry
          (V.fromList (randomPoints 0x9e3779b97f4a7c15 count))
      )
  writeFile
    (directory </> ("bulk-load-" <> show count <> "-edges.txt"))
    (unlines (canonicalEdges triangulation))

writeIncrementalGate :: FilePath -> Int -> IO ()
writeIncrementalGate directory count = do
  triangulation <-
    insertAllIncrementally
      (V.fromList (randomPoints 0x9e3779b97f4a7c15 count))
  writeFile
    (directory </> ("incremental-" <> show count <> "-edges.txt"))
    (unlines (canonicalEdges triangulation))

writeNearestGate :: FilePath -> IO ()
writeNearestGate directory = do
  let pointCount = 20000
      queryCount = 5000
  triangulation <-
    buildTriangulation
      <$> require
        ( delaunay
            unitElementDefaults
            (V.fromList (randomPoints 0x0123456789abcdef pointCount))
        )
  hierarchy <- require (buildHierarchyHint defaultHierarchyBranchFactor triangulation)
  queries <- requireQueryPoints (randomPoints 0x3141592653589793 queryCount)
  writeFile
    (directory </> "nearest-20000-5000.txt")
    (unlines (map (nearestAnswer triangulation hierarchy) queries))

writeRefinementGate :: FilePath -> Int -> IO ()
writeRefinementGate directory steinerBudget = do
  let (points, constraints) = refinementInput
      parameters =
        defaultRefinementParameters
          { refineMaxAdditionalVertices = Just steinerBudget
          , refineMaxArea = Just 0.5
          , refineMaxRadiusEdgeRatio = Just 1.0
          , refineExcludeOuterFaces = True
          , refineKeepConstraintEdges = False
          }
  cdtBuild <- require (constrainedDelaunay unitElementDefaults points constraints)
  refined <-
    require
      ( refine
          id
          parameters
          (buildTriangulation cdtBuild :: ConstrainedDelaunayTriangulation Point)
      )
  let triangulation = refinedTriangulation refined
      prefix = directory </> ("refine-" <> show steinerBudget)
      completion = if refinementComplete refined then "1" else "0"
      summary =
        [ "added " <> show (refinementAddedVertices refined)
        , "vertices " <> show (numVertices triangulation)
        , "edges " <> show (numUndirectedEdges triangulation)
        , "inner-faces " <> show (numInnerFaces triangulation)
        , "excluded " <> show (V.length (refinementExcludedFaces refined))
        , "complete " <> completion
        ]
  writeFile (prefix <> "-summary.txt") (unlines summary)
  writeFile (prefix <> "-edges.txt") (unlines (canonicalEdges triangulation))
  writeFile
    (prefix <> "-constraints.txt")
    (unlines (canonicalConstraintEdges triangulation))

-- | Arrival-order insertion through immutable snapshots. The artifact proves
-- final-state agreement with Spade; it is not a cross-language timing row.
writePersistentGate :: FilePath -> Int -> IO ()
writePersistentGate directory count = do
  triangulation <-
    insertAllPersistently (V.fromList (randomPoints 0x9e3779b97f4a7c15 count))
  writeFile
    (directory </> ("persistent-" <> show count <> "-edges.txt"))
    (unlines (canonicalEdges triangulation))

writeSweepAngleGate :: FilePath -> Int -> IO ()
writeSweepAngleGate directory count = do
  triangulation <-
    buildTriangulation
      <$> require
        ( delaunay
            unitElementDefaults
            (V.fromList (nearCollinearPoints 0x9e3779b97f4a7c15 count))
        )
  writeFile
    (directory </> ("sweep-angle-collapse-" <> show count <> "-edges.txt"))
    (unlines (canonicalEdges triangulation))

-- | The degenerate chain, pinned by the only thing it has: its edges.
--
-- There are no faces to compare and no circumcentres to argue about, so unlike
-- every other gate here this one has no per-side half and no exclusion. The
-- answer is @n - 1@ consecutive segments and the two sides either both produce
-- it or one of them is wrong.
writeDegenerateLineGate :: FilePath -> Int -> IO ()
writeDegenerateLineGate directory count = do
  triangulation <-
    buildTriangulation
      <$> require
        (delaunay unitElementDefaults (V.fromList (exactlyCollinearPoints count)))
  writeFile
    (directory </> ("degenerate-line-" <> show count <> "-edges.txt"))
    (unlines (canonicalEdges triangulation))

-- Arrival-order insertion with one publication: the transactional comparison
-- against Spade's @&mut self@ insertion. The per-operation snapshot fold is
-- measured separately as Moonlight publication stress.
insertAllIncrementally
  :: V.Vector Point
  -> IO (DelaunayTriangulation Point)
insertAllIncrementally points =
  evaluate
    ( case either (error . show) id
        ( withSession
            (empty unitElementDefaults)
            (V.length points)
            (V.mapM_ insertVertex points)
        ) of
        (_, frozen, _) -> frozen
    )

insertAllPersistently
  :: V.Vector Point
  -> IO (DelaunayTriangulation Point)
insertAllPersistently =
  V.foldM'
    (\triangulation point ->
      insertionTriangulation <$> require (insert triangulation point)
    )
    (empty unitElementDefaults)

-- | The refinement lane's own counters, so the worklist question is answered by
-- measurement rather than by assertion.
--
-- @faceChecks@ is not a worklist counter: the terminal audit runs the complete
-- face evaluation over every final inner face without popping anything. So the
-- amplification ratio is @pops / steiner@, and @faceChecks - pops@ should equal
-- the final inner-face count less any pop discarded as excluded.
-- | The circle sweep's placement split, on the same generator the bulk lanes
-- use. A point takes the fast path when the hull edge its own angle selected
-- has the point strictly to its left; otherwise it is deferred to a generic
-- insertion after the sweep finishes. spade takes the identical branch on
-- @is_on_right_side_or_on_line@ and its own source calls that fallback "very
-- slow", so a materially non-zero skip count is wasted work in the bulk lane
-- rather than a curiosity. @seed + fast + skipped@ must exhaust the unique
-- points; this reports the identity rather than asserting it silently.
reportSweepStats :: Int -> IO ()
reportSweepStats count = do
  let points = V.fromList (randomPoints 0x9e3779b97f4a7c15 count)
  built <- require (delaunay unitElementDefaults points)
  let stats = buildStats (built :: BuildResult 'Unconstrained Point () () ())
      seeded = statSpatialSeedPoints stats
      fast = statSweepFastPoints stats
      skippedPoints = statSweepSkippedPoints stats
      unique = statUniquePoints stats
      accounted = seeded + fast + skippedPoints
      share :: Int -> String
      share value
        | unique == 0 = "n/a"
        | otherwise = show (100 * fromIntegral value / fromIntegral unique :: Double) <> "%"
  traverse_
    putStrLn
    [ "input-points " <> show (statInputPoints stats)
    , "unique-points " <> show unique
    , "duplicate-points " <> show (statDuplicatePoints stats)
    , "seed-points " <> show seeded
    , "sweep-fast-points " <> show fast
    , "sweep-skipped-points " <> show skippedPoints
    , "skipped-share " <> share skippedPoints
    , "accounted " <> show accounted
    , "identity-holds " <> show (accounted == unique)
    , "edge-flips " <> show (statEdgeFlips stats)
    , "hull-insertions " <> show (statHullInsertions stats)
    , "legalization-max-stack " <> show (statLegalizationMaxStack stats)
    ]
  if accounted == unique
    then pure ()
    else do
      hPutStrLn
        stderr
        ( "sweep-stats: seed + fast + skipped = "
            <> show accounted
            <> " but unique = "
            <> show unique
        )
      exitWith (ExitFailure 1)

reportRefinementCounters :: Int -> IO ()
reportRefinementCounters budget = do
  let (points, constraints) = refinementInput
      parameters =
        defaultRefinementParameters
          { refineMaxAdditionalVertices = Just budget
          , refineMaxArea = Just 0.5
          , refineMaxRadiusEdgeRatio = Just 1.0
          , refineExcludeOuterFaces = True
          , refineKeepConstraintEdges = False
          }
  cdt <- buildTriangulation <$> require (constrainedDelaunay unitElementDefaults points constraints)
  result <-
    require (refine id parameters (cdt :: ConstrainedDelaunayTriangulation Point))
  let stats = refinementStats result
      steiner = statSteinerPoints stats
      pops = statRefinementQueuePops stats
      checks = statRefinementFaceChecks stats
      inner = numInnerFaces (refinedTriangulation result)
      excluded = V.length (refinementExcludedFaces result)
      ratio :: Int -> Int -> String
      ratio numerator denominator
        | denominator == 0 = "n/a"
        | otherwise =
            show (fromIntegral numerator / fromIntegral denominator :: Double)
  traverse_
    putStrLn
    [ "budget " <> show budget
    , "steiner " <> show steiner
    , "queue-pops " <> show pops
    , "face-checks " <> show checks
    , "inner-faces " <> show inner
    , "excluded " <> show excluded
    , "edge-flips " <> show (statEdgeFlips stats)
    , "location-walk-steps " <> show (statLocationWalkSteps stats)
    , "complete " <> show (refinementComplete result)
    , "pops/steiner " <> ratio pops steiner
    , "checks/steiner " <> ratio checks steiner
    , "flips/steiner " <> ratio (statEdgeFlips stats) steiner
    , "walk-steps/steiner " <> ratio (statLocationWalkSteps stats) steiner
    , "checks-minus-pops " <> show (checks - pops)
    -- Every face check is now a queue pop. The terminal audit used to make the
    -- difference equal the final inner-face count; if this ever reads False
    -- again, a second whole-mesh quality pass has come back.
    , "no-second-pass " <> show (checks == pops)
    ]