moonlight-planar-1.1.0.0: src-build/Moonlight/Planar/Internal/Canonical.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | Canonical publication: the same triangulation, renumbered so that its
-- representation is a function of its geometry alone.
module Moonlight.Planar.Internal.Canonical
( canonicalize
, canonicalizeKnownAscending
) where
import Control.Monad (when)
import Control.Monad.ST (ST, runST)
import qualified Data.Vector.Algorithms.Intro as Intro
import qualified Data.Vector.Unboxed as U
import qualified Data.Vector.Unboxed.Mutable as MUV
import Moonlight.Planar.Dcel (numFaces, numUndirectedEdges, numVertices, vertexData)
import Moonlight.Planar.Internal.HandleDefs (VertexId (..))
import Moonlight.Planar.Internal.Validation (coordinatesAscend)
import Moonlight.Planar.Internal.Mutable
import Moonlight.Planar.Internal.Paged (pagedUnsafeIndex)
import Moonlight.Planar.Internal.Representation (CanonicalAdmission (..), Triangulation (..), authoringElementDefaults)
import Moonlight.Planar.Internal.Types (BuildError)
-- | Whether the caller has already established the authoritative vertex-order
-- section. The canonicalizer still owns every edge, directed-edge, face,
-- anchor, and constraint normalization; this witness affects only its vertex
-- rank entrance.
data VertexOrderAdmission
= VertexOrderUnknown
| VertexOrderKnownAscending
-- | Renumber a triangulation into its canonical representation.
--
-- Two triangulations of the same sites are the same triangulation — Delaunay
-- uniqueness says so, and the tie-break on an exactly cocircular quadrilateral
-- is keyed on coordinates rather than on identifiers so that it stays true.
-- What differs between two builds of one site set is only /numbering/: which
-- vertex got index 0, which half-edge got the even slot, where a face's cycle
-- was anchored. All of that records the schedule the value was constructed by,
-- and none of it is geometry.
--
-- This is what removes it. Every identifier is assigned from the geometry:
--
-- * vertices in lexicographic coordinate rank;
-- * undirected edges in lexicographic rank of their endpoint pair, each
-- taken low first, so the pair is an unordered pair by construction;
-- * of a pair's two half-edges, the even slot is the one leaving the
-- lower-ranked endpoint, which keeps twinning an @xor@ with one;
-- * inner faces in order of the least half-edge on their boundary, which
-- is a single index rather than a vertex tuple and so needs no special
-- case for a cycle that is not a triangle;
-- * every anchor — each vertex's outgoing edge, each face's edge — set to
-- the least admissible half-edge.
--
-- Construction therefore need not run in canonical order. Local insertion and
-- seam fusion may preserve their cheaper schedule-specific numbering; callers
-- invoke this operation only when they require the construction-independent
-- physical representative used to observe the finite-set laws.
--
-- Element payloads are @()@ because a renumbering is a bijection and could
-- carry them, but nothing that wants that exists; the writers a general form
-- would need were retired when their last caller went. Vertex payloads travel
-- with their vertices, and constraint flags with their edges, so a constrained
-- triangulation canonicalizes as readily as an unconstrained one.
canonicalize
:: Triangulation mode vertex () () ()
-> Either BuildError (Triangulation mode vertex () () ())
canonicalize = canonicalizeWithVertexOrder VertexOrderUnknown
-- | Canonicalize a triangulation whose vertices were appended in strict
-- lexicographic coordinate order by the admitted SiteSet bulk entrance.
-- This is an execution witness, not a second numbering implementation.
canonicalizeKnownAscending
:: Triangulation mode vertex () () ()
-> Either BuildError (Triangulation mode vertex () () ())
canonicalizeKnownAscending = canonicalizeWithVertexOrder VertexOrderKnownAscending
-- A value already published canonically is its own canonical
-- representative: every thaw forgets the witness, and nothing else
-- renumbers, so honouring it is the identity and not a cache.
canonicalizeWithVertexOrder
:: VertexOrderAdmission
-> Triangulation mode vertex () () ()
-> Either BuildError (Triangulation mode vertex () () ())
canonicalizeWithVertexOrder vertexOrderAdmission source =
case triCanonical source of
CanonicalKnown -> Right source
CanonicalUnknown ->
(\published -> published{triCanonical = CanonicalKnown})
<$> renumber vertexOrderAdmission source
renumber
:: VertexOrderAdmission
-> Triangulation mode vertex () () ()
-> Either BuildError (Triangulation mode vertex () () ())
renumber vertexOrderAdmission source = runST $ do
mutable <-
newMutableDcel
(authoringElementDefaults source)
(exactDcelCapacity vertexTotal directedTotal faceTotal)
forRange 0 vertexTotal $ \canonical -> do
let !old = vertexOrder `U.unsafeIndex` canonical
_ <-
appendVertexCoordinates
mutable
(coordinateX `pagedUnsafeIndex` old)
(coordinateY `pagedUnsafeIndex` old)
(vertexData source (VertexId (fromIntegral old)))
pure ()
_ <- addEdgeBlock mutable edgeTotal
_ <- addFaceBlock mutable (faceTotal - 1)
forRange 0 directedTotal $ \canonical -> do
let !old = directedFromCanonical `U.unsafeIndex` canonical
writeOrigin mutable canonical (vertexRank `U.unsafeIndex` originOf old)
writeNext mutable canonical (directedToCanonical `U.unsafeIndex` topology (4 * old + 1))
writePrevious mutable canonical (directedToCanonical `U.unsafeIndex` topology (4 * old + 2))
writeFace mutable canonical (faceRank `U.unsafeIndex` topology (4 * old + 3))
forRange 0 vertexTotal $ \canonical -> do
let !least = leastOutgoing `U.unsafeIndex` canonical
if least == absent
then writeVertexOut mutable canonical (-1)
else markConnected mutable canonical least
forRange 0 faceTotal $ \canonical -> do
let !least = leastOnFace `U.unsafeIndex` oldFaceOf canonical
writeFaceEdge mutable canonical (if least == absent then -1 else least)
forRange 0 edgeTotal $ \canonicalUndirected ->
when (constraintFlag `pagedUnsafeIndex` (edgeOrder `U.unsafeIndex` canonicalUndirected) /= 0) $
() <$ setConstraint mutable (2 * canonicalUndirected)
freezeTriangulation mutable
where
!vertexTotal = numVertices source
!edgeTotal = numUndirectedEdges source
!faceTotal = numFaces source
!directedTotal = 2 * edgeTotal
-- Read the arenas rather than the handle accessors. Every one of these is
-- indexed a few times per element, and the accessors would box a t'Point' or
-- an identifier newtype at each of them.
!coordinateX = triPointX source
!coordinateY = triPointY source
!topologyArena = triHalfTopology source
!constraintFlag = triConstraint source
topology slot = fromIntegral (topologyArena `pagedUnsafeIndex` slot) :: Int
originOf directed = topology (4 * directed)
-- Vertices, in lexicographic coordinate rank. Sorting a vector of keys with
-- the type's own ordering rather than an index vector under a closure: the
-- comparison then specializes instead of being an unknown call per step.
--
-- Sortedness is checked first, because a great many of the meshes handed to
-- this function already have it and the check costs one linear scan against a
-- sort's @n log n@. A seam merge is the reason: it copies two canonically
-- numbered operands into one arena, lower abscissa first, and the sites of
-- the result are then already in rank order by construction. Nothing about
-- the schedule is assumed here — the coordinates are simply read and
-- believed, so a mesh that arrives sorted for any other reason is served just
-- as well.
!alreadyRanked =
case vertexOrderAdmission of
VertexOrderUnknown -> coordinatesAscend vertexTotal coordinateX coordinateY
VertexOrderKnownAscending -> True
!vertexOrder
| alreadyRanked = U.enumFromN 0 vertexTotal
| otherwise =
thirdColumn $
sortedVector
( U.generate
vertexTotal
( \index ->
( coordinateX `pagedUnsafeIndex` index
, coordinateY `pagedUnsafeIndex` index
, index
)
)
)
!vertexRank
| alreadyRanked = vertexOrder
| otherwise = invertPermutation vertexTotal vertexOrder
-- Undirected edges, in lexicographic rank of their endpoint pair.
--
-- Both components of the key are vertex ranks, so they are already dense
-- indices into a range this function knows: comparing them is a counting
-- sort's job, not a comparison sort's. Two stable passes — the high endpoint
-- first, then the low one — leave the pairs in lexicographic order, in time
-- linear in the edges and the vertices rather than @E log E@.
!edgeLow =
U.generate edgeTotal $ \index ->
min
(vertexRank `U.unsafeIndex` originOf (2 * index))
(vertexRank `U.unsafeIndex` originOf (2 * index + 1))
!edgeHigh =
U.generate edgeTotal $ \index ->
max
(vertexRank `U.unsafeIndex` originOf (2 * index))
(vertexRank `U.unsafeIndex` originOf (2 * index + 1))
!edgeOrder =
countingSortOn vertexTotal edgeLow $
countingSortOn vertexTotal edgeHigh (U.enumFromN 0 edgeTotal)
-- The even half of each canonical pair leaves the lower-ranked endpoint.
!directedFromCanonical =
U.generate directedTotal $ \canonical ->
let !oldEdge = edgeOrder `U.unsafeIndex` (canonical `quot` 2)
!evenHalf = 2 * oldEdge
!leavesLower =
vertexRank `U.unsafeIndex` originOf evenHalf
<= vertexRank `U.unsafeIndex` originOf (evenHalf + 1)
in if even canonical == leavesLower then evenHalf else evenHalf + 1
!directedToCanonical = invertPermutation directedTotal directedFromCanonical
-- The least canonical half-edge on each old face and leaving each canonical
-- vertex, in one pass. Anchors have to be a function of the geometry too, or
-- two builds of one site set would publish the same cycles anchored in
-- different places.
(!leastOnFace, !leastOutgoing) = runST $ do
faces <- MUV.replicate (max 1 faceTotal) absent
vertices <- MUV.replicate (max 1 vertexTotal) absent
forRange 0 directedTotal $ \canonical -> do
let !old = directedFromCanonical `U.unsafeIndex` canonical
!face = topology (4 * old + 3)
!rank = vertexRank `U.unsafeIndex` originOf old
onFace <- MUV.unsafeRead faces face
when (canonical < onFace) (MUV.unsafeWrite faces face canonical)
leaving <- MUV.unsafeRead vertices rank
when (canonical < leaving) (MUV.unsafeWrite vertices rank canonical)
(,) <$> U.unsafeFreeze faces <*> U.unsafeFreeze vertices
-- Inner faces, in order of the least canonical half-edge on their boundary.
-- The outer face keeps index zero, which the arena reserves for it anyway.
--
-- No two faces share a least half-edge, so this key is injective and the
-- ordering can be read off by inverting it: mark each face at its own least
-- half-edge, then scan the half-edges in order. That is one linear pass and
-- no comparisons at all. A face the scan never reaches has no boundary — only
-- reachable in a mesh with no edges — and follows in old index order so that
-- the result stays a permutation whatever it is handed.
!innerFaceOrder = U.create $ do
(owner :: MUV.MVector state Int) <- MUV.replicate (max 1 directedTotal) absent
forRange 1 faceTotal $ \face -> do
let !least = leastOnFace `U.unsafeIndex` face
when (least /= absent) (MUV.unsafeWrite owner least face)
emitted <- MUV.replicate (max 1 faceTotal) False
out <- MUV.new (max 0 (faceTotal - 1))
let scan :: Int -> Int -> ST state Int
scan !slot !filled
| slot >= directedTotal = pure filled
| otherwise = do
!face <- MUV.unsafeRead owner slot
if face == absent
then scan (slot + 1) filled
else do
MUV.unsafeWrite out filled face
MUV.unsafeWrite emitted face True
scan (slot + 1) (filled + 1)
sweep :: Int -> Int -> ST state ()
sweep !face !filled
| face >= faceTotal = pure ()
| otherwise = do
!done <- MUV.unsafeRead emitted face
if done
then sweep (face + 1) filled
else do
MUV.unsafeWrite out filled face
sweep (face + 1) (filled + 1)
scan 0 0 >>= sweep 1
pure out
!faceRank = U.create $ do
ranks <- MUV.replicate (max 1 faceTotal) 0
U.iforM_ innerFaceOrder $ \rank old -> MUV.unsafeWrite ranks old (rank + 1)
pure ranks
oldFaceOf canonical
| canonical == 0 = 0
| otherwise = innerFaceOrder `U.unsafeIndex` (canonical - 1)
-- | No half-edge reaches this face or vertex. A one-site mesh has such a
-- vertex and an edgeless outer face; the arena spells the same absence as a
-- packed sentinel, which is not a value an index may take.
absent :: Int
absent = maxBound
-- | @[from, to)@, without materializing the range as a list.
forRange :: Monad m => Int -> Int -> (Int -> m ()) -> m ()
forRange from to action = go from
where
go !index
| index >= to = pure ()
| otherwise = action index >> go (index + 1)
{-# INLINE forRange #-}
sortedVector :: (U.Unbox key, Ord key) => U.Vector key -> U.Vector key
sortedVector = U.modify Intro.sort
{-# INLINE sortedVector #-}
-- | Stably reorder @items@ by a key that is already a dense index below
-- @range@, in time linear in both.
--
-- Applied least-significant key first, repeated application leaves the items in
-- lexicographic order of the whole key — which is what makes a two-component
-- ordering over vertex ranks cost @O(V + E)@ instead of @O(E log E)@.
countingSortOn :: Int -> U.Vector Int -> U.Vector Int -> U.Vector Int
countingSortOn range keys items = U.create $ do
(counts :: MUV.MVector state Int) <- MUV.replicate (range + 1) 0
U.forM_ items $ \item ->
MUV.unsafeModify counts (+ 1) (keys `U.unsafeIndex` item)
let prefix :: Int -> Int -> ST state ()
prefix !key !running
| key > range = pure ()
| otherwise = do
!count <- MUV.unsafeRead counts key
MUV.unsafeWrite counts key running
prefix (key + 1) (running + count)
prefix 0 0
out <- MUV.new (max 1 (U.length items))
U.forM_ items $ \item -> do
let !key = keys `U.unsafeIndex` item
!slot <- MUV.unsafeRead counts key
MUV.unsafeWrite counts key (slot + 1)
MUV.unsafeWrite out slot item
pure (MUV.slice 0 (U.length items) out)
thirdColumn :: (U.Unbox a, U.Unbox b) => U.Vector (a, b, Int) -> U.Vector Int
thirdColumn = U.map (\(_, _, index) -> index)
{-# INLINE thirdColumn #-}
-- | @inverse ! (order ! i) == i@: the rank each element was given.
invertPermutation :: Int -> U.Vector Int -> U.Vector Int
invertPermutation count order = U.create $ do
inverse <- MUV.replicate (max 1 count) 0
U.iforM_ order $ \rank element -> MUV.unsafeWrite inverse element rank
pure inverse