moonlight-triangulation-1.4.0.3: bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Removal.hs
{-# LANGUAGE DataKinds #-}
-- | Removal lanes, their gates, and the removal diagnostics.
module Moonlight.Triangulation.Bench.SpadeCompare.Removal where
import Control.Monad (when)
import Data.Foldable (traverse_)
import Data.List (sortOn)
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.Dcel
import Moonlight.Triangulation.Internal.HandleDefs
import Moonlight.Triangulation.Math
import Moonlight.Triangulation.PointLocation
import Moonlight.Triangulation.Removal
import Moonlight.Triangulation.Session
import Moonlight.Triangulation.Types
import Moonlight.Triangulation.Bench.SpadeCompare.Support
import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
-- | The removal gate: a deterministic prefix of the input, removed by
-- coordinate, and the surviving canonical edge set. Delaunay triangulations
-- of point sets in general position are unique, so the survivor is forced
-- and both implementations must agree bit-for-bit.
writeRemovalGate :: FilePath -> Int -> Int -> IO ()
writeRemovalGate directory pointCount removalCount = do
let points = randomPoints 0x9e3779b97f4a7c15 pointCount
triangulation <- buildTriangulation <$> require (delaunay unitElementDefaults (V.fromList points))
surviving <- removeAllByCoordinate triangulation (V.fromList (take removalCount points))
writeFile
(directory </> ("removal-" <> show pointCount <> "-" <> show removalCount <> "-edges.txt"))
(unlines (canonicalEdges surviving))
-- | Remove vertices by coordinate inside one session: one O(n) dense thaw and
-- publication for the whole program, then O(degree) topology work per removal.
-- The snapshot 'locateAndRemove' fold pays that O(n) boundary per removal.
removeAllByCoordinate
:: DelaunayTriangulation Point
-> V.Vector Point
-> IO (DelaunayTriangulation Point)
removeAllByCoordinate triangulation removals =
case withSession triangulation 0 (removeManyAt removals) of
Left refusal -> fail (show refusal)
Right (outcomes, surviving, _) -> do
V.imapM_ checkRemoved outcomes
pure surviving
where
checkRemoved index outcome = case outcome of
Nothing ->
error
( "removal target is not a vertex: index="
<> show index
<> " point="
<> ( case removals V.! index of
Point x y -> hex64 x <> "," <> hex64 y
)
)
Just _ -> pure ()
-- | Removal by coordinate through the persistent entry point: a thaw and a
-- publication per removal, against the session fold's one of each for the whole
-- program. Same removals, same order, same survivor — only the price differs.
removeAllPersistently
:: DelaunayTriangulation Point
-> V.Vector Point
-> IO (DelaunayTriangulation Point)
removeAllPersistently =
V.foldM'
(\current point -> do
outcome <- require (locateAndRemove current point)
case outcome of
Nothing -> fail ("removal target is not a vertex: " <> pointHex point)
Just result -> pure (removalTriangulation result)
)
writePersistentRemovalGate :: FilePath -> Int -> Int -> IO ()
writePersistentRemovalGate directory pointCount removalCount = do
let points = randomPoints 0x9e3779b97f4a7c15 pointCount
base <- buildTriangulation <$> require (delaunay unitElementDefaults (V.fromList points))
surviving <- removeAllPersistently base (V.fromList (take removalCount points))
writeFile
( directory
</> ("persistent-removal-" <> show pointCount <> "-" <> show removalCount <> "-edges.txt")
)
(unlines (canonicalEdges surviving))
-- | Removal audited against the one oracle that needs no second implementation:
-- the Delaunay triangulation of a point set in general position is unique, so
-- removing a prefix must leave exactly the triangulation a fresh bulk load of
-- the survivors produces. Reports the first removal at which that identity
-- breaks, so a divergence is dated rather than merely observed.
--
-- Every removal also reports the handle swap-compaction relocated into the
-- freed slot, and that handle is the caller's only re-anchor after a removal
-- invalidates the rest. An out-of-range or misnamed handle is invisible to the
-- edge-set oracle, so it is checked here against the vertex the mesh moved.
reportRemovalAudit :: Int -> Int -> IO ()
reportRemovalAudit pointCount removalCount = do
let allPoints = V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount)
base <- buildTriangulation <$> require (delaunay unitElementDefaults allPoints)
let step current index
| index >= removalCount = putStrLn ("clean-through " <> show removalCount)
| otherwise =
case allPoints V.!? index of
Nothing -> putStrLn ("input-exhausted-at-removal " <> show index)
Just point -> do
outcome <- require (locateAndRemove current point)
case outcome of
Nothing -> putStrLn ("located-nothing-at-removal " <> show index)
Just result -> do
let nextTriangulation = removalTriangulation result
movedPoint =
vertexPoint current (VertexId (fromIntegral (numVertices current - 1)))
case removalOutcomeSwap (removalOutcome result) of
Nothing
| vertexPoint current (VertexId (fromIntegral (numVertices current - 1))) /= point ->
putStrLn ("swap-handle-missing-at-removal " <> show index)
| otherwise -> pure ()
Just (swappedIn@(VertexId rawSwappedIn), _)
| toInteger rawSwappedIn >= toInteger (numVertices nextTriangulation) ->
putStrLn
( "swap-handle-out-of-range-at-removal "
<> show index
<> " handle="
<> show rawSwappedIn
<> " vertices="
<> show (numVertices nextTriangulation)
)
| vertexPoint nextTriangulation swappedIn /= movedPoint ->
putStrLn
( "swap-handle-misnamed-at-removal "
<> show index
<> " handle="
<> show rawSwappedIn
)
| otherwise -> pure ()
fresh <-
buildTriangulation
<$> require (delaunay unitElementDefaults (V.drop (index + 1) allPoints))
let removalEdges = canonicalEdges nextTriangulation
freshEdges = canonicalEdges fresh
if removalEdges == freshEdges
then step nextTriangulation (index + 1)
else do
let (onlyRemoval, onlyFresh) = sortedDifference removalEdges freshEdges
putStrLn ("diverged-at-removal " <> show index)
putStrLn
( "removed-point "
<> case point of
Point x y -> hex64 x <> "," <> hex64 y
)
putStrLn ("removal-vertices " <> show (numVertices nextTriangulation))
putStrLn ("fresh-vertices " <> show (numVertices fresh))
putStrLn ("removal-edges " <> show (length removalEdges))
putStrLn ("fresh-edges " <> show (length freshEdges))
putStrLn ("only-in-removal " <> show (length onlyRemoval))
putStrLn ("only-in-fresh " <> show (length onlyFresh))
let (invertedRemoval, illegalRemoval) = meshHealth nextTriangulation
(invertedFresh, illegalFresh) = meshHealth fresh
putStrLn ("removal-inverted-faces " <> show invertedRemoval)
putStrLn ("removal-illegal-half-edges " <> show illegalRemoval)
putStrLn ("fresh-inverted-faces " <> show invertedFresh)
putStrLn ("fresh-illegal-half-edges " <> show illegalFresh)
traverse_ (putStrLn . (" removal-only " <>)) (take 8 onlyRemoval)
traverse_ (putStrLn . (" fresh-only " <>)) (take 8 onlyFresh)
step base 0
-- | The geometry around one removal, reconstructed with public API only. For
-- an interior removal the cavity border is the cyclic link of the removed
-- vertex, and nothing but a drain flip can destroy one of those non-star
-- edges: cleanup removes only the star, and swap-remove touches only the
-- removed vertex. The reported hull predicate guards that interpretation.
-- The fan origin 'remeshRing' would pick is also public: 'collectOutgoing'
-- starts at the stored out-edge, so the origin is the destination of that
-- edge.
reportRemovalContext :: Int -> Int -> IO ()
reportRemovalContext pointCount target = do
let allPoints = V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount)
case allPoints V.!? target of
Nothing -> do
hPutStrLn stderr "removal-context: target index out of range"
exitWith (ExitFailure 2)
Just removedPoint -> do
base <- buildTriangulation <$> require (delaunay unitElementDefaults allPoints)
before <- removeAllByCoordinate base (V.take target allPoints)
after <- removeAllByCoordinate before (V.singleton removedPoint)
fresh <- buildTriangulation <$> require (delaunay unitElementDefaults (V.drop (target + 1) allPoints))
lab <- buildTriangulation <$> require (delaunay unitElementDefaults (V.drop target allPoints))
removedQuery <- require (mkQueryPoint removedPoint)
putStrLn ("lab-clean " <> show (canonicalEdges before == canonicalEdges lab))
case locatePoint before removedQuery of
OnVertex removedVertex -> do
let ring = ringOf before removedVertex
ringPoints = map snd ring
ringMember point = any (== point) ringPoints
rOnHull = any (\(edge, _) -> incidentFace before edge == outerFace) ring
afterPairs = canonicalEdgePairs after
freshPairs = canonicalEdgePairs fresh
borderPairs = cyclicPairs ringPoints
destroyedBorder = [pair | pair <- borderPairs, not (any (== pair) afterPairs)]
removalOnly = [pair | pair <- afterPairs, not (any (== pair) freshPairs)]
freshOnly = [pair | pair <- freshPairs, not (any (== pair) afterPairs)]
putStrLn ("r-on-hull " <> show rOnHull)
putStrLn ("ring-size " <> show (length ringPoints))
traverse_
(\(index, (_, point)) -> putStrLn (" ring " <> show index <> " " <> pointHex point))
(zip [(0 :: Int) ..] ring)
putStrLn ("border-edges-before " <> show (length borderPairs))
putStrLn ("border-edges-destroyed " <> show (length destroyedBorder))
traverse_
(\(p, q) -> putStrLn (" destroyed " <> pointHex p <> " " <> pointHex q))
destroyedBorder
case ringPoints of
[] -> putStrLn "fan-unavailable empty-ring"
fanOriginPoint : ringTail ->
if rOnHull
then putStrLn "fan-unavailable hull-removal"
else do
let fanOrientations =
zipWith3
(\index p q -> (index, p, q, orient2d fanOriginPoint p q))
[(1 :: Int) ..]
ringTail
(drop 1 ringTail)
fanCandidates =
zipWith3
( \(index, priorPoint) current following ->
( index
, current
, orient2d priorPoint following fanOriginPoint == GT
&& orient2d following priorPoint current == GT
, inCircle current fanOriginPoint priorPoint following
)
)
(zip [(2 :: Int) ..] ringTail)
(drop 1 ringTail)
(drop 2 ringTail)
putStrLn ("fan-origin " <> pointHex fanOriginPoint)
putStrLn
( "fan-inverted "
<> show (length [() | (_, _, _, verdict) <- fanOrientations, verdict == LT])
<> " of "
<> show (length fanOrientations)
)
traverse_
( \(index, p, q, verdict) ->
when (verdict == LT) $
putStrLn
( " fan-inverted-at "
<> show index
<> " "
<> pointHex fanOriginPoint
<> " "
<> pointHex p
<> " "
<> pointHex q
)
)
fanOrientations
traverse_
( \(index, current, convex, circle) ->
putStrLn
( " fan-candidate "
<> show index
<> " "
<> pointHex current
<> "->"
<> pointHex fanOriginPoint
<> " convex="
<> show convex
<> " incircle="
<> show circle
)
)
fanCandidates
putStrLn ("only-in-removal " <> show (length removalOnly))
traverse_
( \(p, q) ->
putStrLn
( " removal-only "
<> pointHex p
<> " "
<> pointHex q
<> " ring="
<> show (ringMember p, ringMember q)
)
)
removalOnly
putStrLn ("only-in-fresh " <> show (length freshOnly))
traverse_
( \(p, q) ->
putStrLn
( " fresh-only "
<> pointHex p
<> " "
<> pointHex q
<> " ring="
<> show (ringMember p, ringMember q)
)
)
freshOnly
let inverted = invertedFaces after
putStrLn ("inverted-faces " <> show (length inverted))
traverse_
( \(p, q, r) ->
putStrLn
( " inverted-face "
<> pointHex p
<> " "
<> pointHex q
<> " "
<> pointHex r
<> " ring="
<> show (ringMember p, ringMember q, ringMember r)
)
)
inverted
other -> putStrLn ("locate-unexpected " <> show other)
where
ringOf triangulation vertex =
case vertexOutEdge triangulation vertex of
Nothing -> []
Just start -> walk start start []
where
walk start current acc =
let ringVertex = destination triangulation current
nextEdge = counterClockwise triangulation current
acc' = (current, vertexPoint triangulation ringVertex) : acc
in if nextEdge == start
then reverse acc'
else walk start nextEdge acc'
cyclicPairs (first : second : rest) =
zipWith canonicalPair (first : second : rest) ((second : rest) ++ [first])
cyclicPairs _ = []
canonicalEdgePairs triangulation =
sortOn
(\(p, q) -> (pointKey p, pointKey q))
[ canonicalPair p q
| edge <- undirectedEdges triangulation
, let (from, to) = undirectedEndpoints triangulation edge
p = vertexPoint triangulation from
q = vertexPoint triangulation to
]
canonicalPair p q =
if pointKey p <= pointKey q then (p, q) else (q, p)
pointKey (Point x y) = (x, y)
invertedFaces triangulation =
[ (vertexPoint triangulation a, vertexPoint triangulation b, vertexPoint triangulation c)
| face <- innerFaces triangulation
, Just (a, b, c) <- [innerFaceVertices triangulation face]
, orient2d
(vertexPoint triangulation a)
(vertexPoint triangulation b)
(vertexPoint triangulation c)
== LT
]
-- | Two independent health measures, which together decide what a divergence
-- means. Delaunay's theorem says a /valid/ triangulation with no locally
-- illegal edge is globally Delaunay — so a mesh that differs from the fresh
-- build while carrying no illegal edge cannot be a valid triangulation, and
-- one carrying illegal edges was simply left under-legalized.
meshHealth
:: Triangulation mode vertex directed undirected face
-> (Int, Int)
meshHealth triangulation =
( length [() | face <- innerFaces triangulation, not (isCounterClockwise face)]
, length
[ ()
| face <- innerFaces triangulation
, edge <- faceEdgeList face
, edgeIsIllegal edge
]
)
where
faceEdgeList face = case innerFaceDirectedEdges triangulation face of
Nothing -> []
Just (e0, e1, e2) -> [e0, e1, e2]
isCounterClockwise face = case innerFaceVertices triangulation face of
Nothing -> True
Just (a, b, c) ->
orient2d
(vertexPoint triangulation a)
(vertexPoint triangulation b)
(vertexPoint triangulation c)
== GT
edgeIsIllegal edge =
let twin = reverseEdge edge
in incidentFace triangulation twin /= outerFace
&& inCircle
(vertexPoint triangulation (origin triangulation edge))
(vertexPoint triangulation (destination triangulation edge))
(vertexPoint triangulation (destination triangulation (next triangulation edge)))
(vertexPoint triangulation (destination triangulation (next triangulation twin)))
== GT
-- | The Delaunay triangulation of the survivors, built from scratch. Diffed
-- against a removal result it decides whether that result is the Delaunay
-- triangulation it is required to be.
writeFreshSuffix :: Int -> Int -> FilePath -> IO ()
writeFreshSuffix pointCount dropped path = do
triangulation <-
buildTriangulation
<$> require
( delaunay
unitElementDefaults
(V.drop dropped (V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount)))
)
writeFile path (unlines (canonicalEdges triangulation))
-- | Symmetric difference of two ascending lists, by merge.
sortedDifference :: Ord a => [a] -> [a] -> ([a], [a])
sortedDifference [] right = ([], right)
sortedDifference left [] = (left, [])
sortedDifference left@(l : ls) right@(r : rs)
| l == r = sortedDifference ls rs
| l < r = let (onlyLeft, onlyRight) = sortedDifference ls right in (l : onlyLeft, onlyRight)
| otherwise = let (onlyLeft, onlyRight) = sortedDifference left rs in (onlyLeft, r : onlyRight)