moonlight-triangulation-1.3.0.2: src-dcel/Moonlight/Triangulation/Alpha.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
-- | Exact alpha births over one resident Delaunay triangulation. Geometry
-- remains owned by the DCEL and 'ExactCellSet'; this module supplies the
-- handle-indexed filtration section used by topology consumers.
module Moonlight.Triangulation.Alpha
( AlphaBirth
, alphaBirthNumerator
, alphaBirthDenominator
, alphaBirthToDouble
, alphaBirthFromRadiusSquared
, AlphaFiltration
, AlphaFiltrationError (..)
, alphaFiltration
, alphaFiltrationCellSet
, alphaVertexBirth
, alphaEdgeBirth
, alphaFaceBirth
, alphaFiltrationCriticalBirths
, alphaComplexAtBirth
, alphaComplexAtRadius
, alphaShapeContainsFace
) where
import Control.DeepSeq (NFData)
import Data.Bifunctor (first)
import Data.Bits (shiftL)
import Data.List.NonEmpty qualified as NonEmpty
import Data.Set qualified as Set
import Data.Vector (Vector)
import Data.Vector qualified as Vector
import Data.Word (Word64)
import GHC.Generics (Generic)
import Moonlight.Triangulation.CellSet
( CellSelectionError
, exactCellSet
)
import Moonlight.Triangulation.Dcel qualified as Dcel
import Moonlight.Triangulation.Handles.HandleDefs
( FaceId (..)
, UndirectedEdgeId (..)
, VertexId (..)
, directedPair
)
import Moonlight.Triangulation.Internal.CellSet
( ExactCellSet (..)
, residentExactCellSet
)
import Moonlight.Triangulation.Internal.Dyadic
( exactCircumradiusSquaredRational
, exactQuarterSquaredDistanceRational
, integerBitLength
, integerRatioToDouble
)
import Moonlight.Triangulation.Internal.ExactRational
( ExactArithmeticError (..)
, ExactRational
, exactRationalDenominator
, exactRationalFromFiniteDouble
, exactRationalNumerator
)
import Moonlight.Triangulation.Internal.Representation
( DelaunayTriangulation
, Triangulation
)
import Moonlight.Triangulation.Math (inDiametralCircle)
import Moonlight.Triangulation.Scalar (circumradiusSquaredWithinCoordinates)
import Moonlight.Triangulation.Types
( ConstraintMode (Unconstrained)
, Point (..)
, RadiusSquared
, radiusSquaredValue
)
-- | One exact squared-radius birth. The constructor is withheld so negative
-- exact rationals cannot masquerade as geometric radii.
data AlphaBirth = AlphaBirth !AlphaOrderKey !ExactRational
deriving stock (Generic)
deriving anyclass (NFData)
data AlphaOrderKey
= NonPositiveAlphaOrder
| PositiveAlphaOrder !Int !Word64
deriving stock (Eq, Ord, Generic)
deriving anyclass (NFData)
instance Eq AlphaBirth where
AlphaBirth _ leftValue == AlphaBirth _ rightValue = leftValue == rightValue
instance Ord AlphaBirth where
compare (AlphaBirth leftKey leftValue) (AlphaBirth rightKey rightValue) =
case compare leftKey rightKey of
EQ -> compare leftValue rightValue
distinctKeyOrder -> distinctKeyOrder
instance Show AlphaBirth where
showsPrec precedence (AlphaBirth _ value) =
showParen
(precedence > 10)
(showString "AlphaBirth " . showsPrec 11 value)
-- | Numerator of the reduced exact squared-radius birth.
alphaBirthNumerator :: AlphaBirth -> Integer
alphaBirthNumerator (AlphaBirth _ value) = exactRationalNumerator value
-- | Positive denominator of the reduced exact squared-radius birth.
alphaBirthDenominator :: AlphaBirth -> Integer
alphaBirthDenominator (AlphaBirth _ value) = exactRationalDenominator value
-- | Binary64 display projection. Equality and filtration order must use the
-- 'AlphaBirth' itself, not this rounded view.
alphaBirthToDouble :: AlphaBirth -> Double
alphaBirthToDouble birth =
integerRatioToDouble
(alphaBirthNumerator birth)
(alphaBirthDenominator birth)
-- | Embed an admitted binary64 threshold into the exact alpha order without
-- loss. Every finite binary64 value is a dyadic rational.
alphaBirthFromRadiusSquared :: RadiusSquared -> AlphaBirth
alphaBirthFromRadiusSquared =
alphaBirthFromExact . exactRationalFromFiniteDouble . radiusSquaredValue
-- | The exact handle-indexed filtration of one Delaunay DCEL. The cell set is
-- the only geometry inventory; the maps are derived birth sections over those
-- resident handles.
data AlphaFiltration = AlphaFiltration
{ alphaFiltrationCells :: !ExactCellSet
, alphaVertexBirths :: !(Vector AlphaBirth)
, alphaEdgeBirths :: !(Vector AlphaBirth)
, alphaFaceBirths :: !(Vector AlphaBirth)
}
data FaceAlphaSection = FaceAlphaSection
{ faceAlphaBirth :: !AlphaBirth
, faceAlphaVertices :: !(VertexId, VertexId, VertexId)
}
-- | Typed obstruction to deriving the exact birth section from a resident
-- Delaunay triangulation. Every incidence failure retains the affected handle.
data AlphaFiltrationError
= AlphaFiltrationCellSelection !CellSelectionError
| AlphaFaceVerticesUnavailable !FaceId
| AlphaFaceDegenerate !FaceId
| AlphaCircumradiusDivisionFailed !FaceId !ExactArithmeticError
| AlphaFaceEdgeIncidenceInvalid !FaceId !UndirectedEdgeId ![VertexId]
| AlphaIncidentFaceBirthMissing !UndirectedEdgeId !FaceId
| AlphaEdgeBirthWitnessMissing !UndirectedEdgeId
deriving stock (Eq, Show, Generic)
deriving anyclass (NFData)
-- | Construct every exact alpha birth once. Faces descend first because a
-- non-Gabriel edge is born at the least incident-face circumradius.
alphaFiltration
:: DelaunayTriangulation vertex
-> Either AlphaFiltrationError AlphaFiltration
alphaFiltration triangulation = do
cellSet <-
first AlphaFiltrationCellSelection
(residentExactCellSet triangulation)
let vertexBirths = Vector.replicate (Dcel.numVertices triangulation) zeroAlphaBirth
faceSections <-
Vector.imapM
(\faceIndex -> exactFaceSection triangulation (FaceId (fromIntegral (faceIndex + 1))))
(Dcel.innerFaceVertexTriples triangulation)
edgeBirths <-
Vector.generateM
(Dcel.numUndirectedEdges triangulation)
(exactEdgeBirth faceSections triangulation . UndirectedEdgeId . fromIntegral)
let faceBirths = fmap faceAlphaBirth faceSections
pure
AlphaFiltration
{ alphaFiltrationCells = cellSet
, alphaVertexBirths = vertexBirths
, alphaEdgeBirths = edgeBirths
, alphaFaceBirths = faceBirths
}
-- | The authoritative resident cells underlying this filtration. This is a
-- read-only projection rather than an exported record field, so callers cannot
-- splice births from one triangulation onto the cells of another.
alphaFiltrationCellSet :: AlphaFiltration -> ExactCellSet
alphaFiltrationCellSet = alphaFiltrationCells
-- | Exact birth of a resident vertex, when the handle belongs to this
-- filtration.
alphaVertexBirth :: AlphaFiltration -> VertexId -> Maybe AlphaBirth
alphaVertexBirth filtration (VertexId rawVertex) =
alphaVertexBirths filtration Vector.!? fromIntegral rawVertex
-- | Exact birth of a resident undirected edge, when the handle belongs to this
-- filtration.
alphaEdgeBirth :: AlphaFiltration -> UndirectedEdgeId -> Maybe AlphaBirth
alphaEdgeBirth filtration (UndirectedEdgeId rawEdge) =
alphaEdgeBirths filtration Vector.!? fromIntegral rawEdge
-- | Exact birth of a resident bounded face, when the handle belongs to this
-- filtration.
alphaFaceBirth :: AlphaFiltration -> FaceId -> Maybe AlphaBirth
alphaFaceBirth filtration =
faceBirthAt (alphaFaceBirths filtration)
-- | Distinct critical births in exact ascending order.
alphaFiltrationCriticalBirths :: AlphaFiltration -> [AlphaBirth]
alphaFiltrationCriticalBirths filtration =
Set.toAscList
( Set.fromList
( Vector.toList (alphaVertexBirths filtration)
<> Vector.toList (alphaEdgeBirths filtration)
<> Vector.toList (alphaFaceBirths filtration)
)
)
-- | Select the closed alpha subcomplex at one exact birth. The resident DCEL
-- is retained; only its downward-closed cell section changes.
alphaComplexAtBirth
:: AlphaBirth
-> AlphaFiltration
-> Either CellSelectionError ExactCellSet
alphaComplexAtBirth threshold filtration =
case alphaFiltrationCells filtration of
ExactCellSet triangulation _ _ _ ->
exactCellSet
triangulation
(selectedHandles (VertexId . fromIntegral) (alphaVertexBirths filtration))
(selectedHandles (UndirectedEdgeId . fromIntegral) (alphaEdgeBirths filtration))
(selectedHandles (FaceId . fromIntegral . (+ 1)) (alphaFaceBirths filtration))
where
selectedHandles :: (Int -> handle) -> Vector AlphaBirth -> [handle]
selectedHandles handleAt =
Vector.ifoldr
(\indexValue birthValue selected -> if birthValue <= threshold then handleAt indexValue : selected else selected)
[]
alphaComplexAtRadius
:: RadiusSquared
-> AlphaFiltration
-> Either CellSelectionError ExactCellSet
alphaComplexAtRadius radius =
alphaComplexAtBirth (alphaBirthFromRadiusSquared radius)
-- | Membership of a bounded face in the closed alpha shape. Exact dyadic
-- comparison makes equality independent of circumcenter rounding. This
-- specialized observation does not construct the full filtration.
alphaShapeContainsFace
:: RadiusSquared
-> Triangulation 'Unconstrained vertex directed undirected face
-> FaceId
-> Bool
alphaShapeContainsFace radius triangulation =
maybe False withinRadius . Dcel.innerFaceVertices triangulation
where
threshold = radiusSquaredValue radius
withinRadius (firstVertex, secondVertex, thirdVertex) =
let Point ax ay = Dcel.vertexPoint triangulation firstVertex
Point bx by = Dcel.vertexPoint triangulation secondVertex
Point cx cy = Dcel.vertexPoint triangulation thirdVertex
in circumradiusSquaredWithinCoordinates threshold ax ay bx by cx cy
zeroAlphaBirth :: AlphaBirth
zeroAlphaBirth = alphaBirthFromExact 0
exactFaceSection
:: DelaunayTriangulation vertex
-> FaceId
-> (VertexId, VertexId, VertexId)
-> Either AlphaFiltrationError FaceAlphaSection
exactFaceSection triangulation face verticesValue@(firstVertex, secondVertex, thirdVertex) =
let Point ax ay = Dcel.vertexPoint triangulation firstVertex
Point bx by = Dcel.vertexPoint triangulation secondVertex
Point cx cy = Dcel.vertexPoint triangulation thirdVertex
in case exactCircumradiusSquaredRational ax ay bx by cx cy of
Left ExactZeroDivisor -> Left (AlphaFaceDegenerate face)
Left arithmeticError -> Left (AlphaCircumradiusDivisionFailed face arithmeticError)
Right exactBirth ->
Right
FaceAlphaSection
{ faceAlphaBirth = alphaBirthFromExact exactBirth
, faceAlphaVertices = verticesValue
}
exactEdgeBirth
:: Vector FaceAlphaSection
-> DelaunayTriangulation vertex
-> UndirectedEdgeId
-> Either AlphaFiltrationError AlphaBirth
exactEdgeBirth faceSections triangulation edge = do
let (fromVertex, toVertex) = Dcel.undirectedEndpoints triangulation edge
fromPoint = Dcel.vertexPoint triangulation fromVertex
toPoint = Dcel.vertexPoint triangulation toVertex
incidentFaces = innerIncidentFaces triangulation edge
oppositeVertices <-
traverse
(oppositeVertexAcross faceSections triangulation edge fromVertex toVertex)
incidentFaces
let oppositePoints = fmap (Dcel.vertexPoint triangulation) oppositeVertices
if all (diametralWitnessOutside fromPoint toPoint) oppositePoints
then pure (alphaBirthFromExact (quarterSquaredPointDistance fromPoint toPoint))
else leastIncidentFaceBirth edge faceSections incidentFaces
innerIncidentFaces
:: DelaunayTriangulation vertex
-> UndirectedEdgeId
-> [FaceId]
innerIncidentFaces triangulation edge =
let (forward, backward) = directedPair edge
forwardFace = Dcel.incidentFace triangulation forward
backwardFace = Dcel.incidentFace triangulation backward
in case (forwardFace == Dcel.outerFace, backwardFace == Dcel.outerFace) of
(True, True) -> []
(False, True) -> [forwardFace]
(True, False) -> [backwardFace]
(False, False)
| forwardFace == backwardFace -> [forwardFace]
| forwardFace < backwardFace -> [forwardFace, backwardFace]
| otherwise -> [backwardFace, forwardFace]
oppositeVertexAcross
:: Vector FaceAlphaSection
-> DelaunayTriangulation vertex
-> UndirectedEdgeId
-> VertexId
-> VertexId
-> FaceId
-> Either AlphaFiltrationError VertexId
oppositeVertexAcross faceSections triangulation edge fromVertex toVertex face =
case faceSectionAt faceSections face of
Nothing ->
Left
(AlphaFaceEdgeIncidenceInvalid face edge (Dcel.faceVertices triangulation face))
Just faceSection ->
let verticesValue@(firstVertex, secondVertex, thirdVertex) = faceAlphaVertices faceSection
outsideEdge vertex = vertex /= fromVertex && vertex /= toVertex
in case
( outsideEdge firstVertex
, outsideEdge secondVertex
, outsideEdge thirdVertex
)
of
(True, False, False) -> Right firstVertex
(False, True, False) -> Right secondVertex
(False, False, True) -> Right thirdVertex
_ ->
Left
( AlphaFaceEdgeIncidenceInvalid
face
edge
(filter outsideEdge (triangleVertices verticesValue))
)
diametralWitnessOutside :: Point -> Point -> Point -> Bool
diametralWitnessOutside firstPoint secondPoint =
not . inDiametralCircle firstPoint secondPoint
quarterSquaredPointDistance :: Point -> Point -> ExactRational
quarterSquaredPointDistance (Point ax ay) (Point bx by) =
exactQuarterSquaredDistanceRational ax ay bx by
leastIncidentFaceBirth
:: UndirectedEdgeId
-> Vector FaceAlphaSection
-> [FaceId]
-> Either AlphaFiltrationError AlphaBirth
leastIncidentFaceBirth edge faceSections incidentFaces = do
births <-
traverse
(\face -> maybe (Left (AlphaIncidentFaceBirthMissing edge face)) (Right . faceAlphaBirth) (faceSectionAt faceSections face))
incidentFaces
case NonEmpty.nonEmpty births of
Nothing -> Left (AlphaEdgeBirthWitnessMissing edge)
Just nonEmptyBirths -> Right (minimum nonEmptyBirths)
faceSectionAt :: Vector FaceAlphaSection -> FaceId -> Maybe FaceAlphaSection
faceSectionAt faceSections (FaceId rawFace)
| rawFace == 0 = Nothing
| otherwise = faceSections Vector.!? (fromIntegral rawFace - 1)
faceBirthAt :: Vector AlphaBirth -> FaceId -> Maybe AlphaBirth
faceBirthAt faceBirths (FaceId rawFace)
| rawFace == 0 = Nothing
| otherwise = faceBirths Vector.!? (fromIntegral rawFace - 1)
triangleVertices :: (VertexId, VertexId, VertexId) -> [VertexId]
triangleVertices (firstVertex, secondVertex, thirdVertex) =
[firstVertex, secondVertex, thirdVertex]
alphaBirthFromExact :: ExactRational -> AlphaBirth
alphaBirthFromExact exactValue =
AlphaBirth (alphaOrderKey exactValue) exactValue
-- | A monotone exact fixed-point projection of the positive rational. Distinct
-- keys prove order; collisions descend to the authoritative rational. One
-- division at birth replaces repeated cross-products during every downstream
-- ordered-map comparison.
alphaOrderKey :: ExactRational -> AlphaOrderKey
alphaOrderKey exactValue =
let numerator = exactRationalNumerator exactValue
denominator = exactRationalDenominator exactValue
in if numerator <= 0
then NonPositiveAlphaOrder
else
let candidateExponent = integerBitLength numerator - integerBitLength denominator
binaryExponent =
if candidateExponent >= 0
then
if numerator < denominator `shiftL` candidateExponent
then candidateExponent - 1
else candidateExponent
else
if numerator `shiftL` negate candidateExponent < denominator
then candidateExponent - 1
else candidateExponent
scale = alphaOrderFractionBits - binaryExponent
scaledMantissa =
if scale >= 0
then (numerator `shiftL` scale) `quot` denominator
else numerator `quot` (denominator `shiftL` negate scale)
in PositiveAlphaOrder binaryExponent (fromIntegral scaledMantissa)
alphaOrderFractionBits :: Int
alphaOrderFractionBits = 16