moonlight-triangulation-0.1.0.0: src-dual/Moonlight/Triangulation/Interpolation.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
-- | Nearest, barycentric, and natural-neighbor interpolation over one
-- authoritative triangulation.
module Moonlight.Triangulation.Interpolation
( BarycentricWeights (..)
, InterpolationStats (..)
, NaturalNeighborResult (..)
, NaturalNeighborWorkspace
, newNaturalNeighborWorkspace
, workspaceBytes
, nearestNeighbor
, barycentricWeights
, naturalNeighborWeights
, foldNaturalNeighborWeights
, interpolateNearest
, interpolateBarycentric
, interpolateNaturalNeighbor
, estimateGradient
, estimateGradients
, interpolateNaturalNeighborGradient
) where
import Control.DeepSeq (NFData)
import Control.Monad.ST (ST)
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed.Mutable as MUV
import Data.Word (Word32)
import Moonlight.Triangulation.Dcel
( destination
, counterClockwise
, foldVertexOutgoingEdges'
, incidentFace
, innerFaceDirectedEdges
, innerFaceVertices
, isBoundaryEdge
, next
, numConstraints
, numDirectedEdges
, numVertices
, origin
, outerFace
, vertexOutEdge
, vertexPoint
)
import Moonlight.Triangulation.Handles.HandleDefs
( DirectedEdgeId (..)
, asUndirected
, FaceId (..)
, VertexId (..)
, reverseEdge
)
import Moonlight.Triangulation.Internal.InterpolationWorkspace
import Moonlight.Triangulation.Math
( barycentricCoordinates
, circumcenter
, inCircle
, isFinite
, projectionFactor
, squaredDistanceWide
)
import Moonlight.Triangulation.PointLocation (locatePointWithHint)
import Moonlight.Triangulation.Scalar (scalarEpsilon)
import Moonlight.Triangulation.Types
( Location (..)
, LocationHint
, LocationStats
, NearestStats (..)
, Point (..)
, QueryPoint
, queryPointValue
, Triangulation
)
import GHC.Generics (Generic)
-- | Barycentric weights use the mesh's binary64 coordinate domain.
data BarycentricWeights
= NoWeights
| OneWeight !VertexId
| TwoWeights !VertexId !Double !VertexId !Double
| ThreeWeights !VertexId !Double !VertexId !Double !VertexId !Double
deriving stock (Eq, Show, Generic)
deriving anyclass (NFData)
data InterpolationStats = InterpolationStats
{ interpolationCavityFaces :: {-# UNPACK #-} !Int
, interpolationNaturalNeighbors :: {-# UNPACK #-} !Int
, interpolationFaceTests :: {-# UNPACK #-} !Int
-- | 'True' exactly when the Sibson pipeline declined and the returned
-- weights are the barycentric coordinates of the located face instead.
-- A silent degradation from Sibson to barycentric is a defect that hides;
-- this bit is the announcement.
, interpolationUsedFallback :: !Bool
}
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData)
data NaturalNeighborResult = NaturalNeighborResult
{ naturalNeighborValues :: !(V.Vector (VertexId, Double))
, naturalNeighborLocationStats :: !LocationStats
, naturalNeighborStats :: !InterpolationStats
}
deriving stock (Eq, Show, Generic)
deriving anyclass (NFData)
-- | Nearest vertex and query work, using an optional admitted vertex as the
-- descent seed. Empty triangulations have no nearest vertex.
nearestNeighbor
:: Triangulation mode vertex directed undirected face
-> Maybe VertexId
-> QueryPoint
-> Maybe (VertexId, NearestStats)
nearestNeighbor triangulation hint queryPoint
| numVertices triangulation == 0 = Nothing
| numConstraints triangulation == 0 = Just (descend start startDistance 0 0)
| otherwise = Just (walk start startDistance 0 0)
where
!query = queryPointValue queryPoint
!start = validateHint hint
!startDistance = squaredDistanceWide query (vertexPoint triangulation start)
!bound = numDirectedEdges triangulation + numVertices triangulation + 1
validateHint (Just vertex@(VertexId index))
| fromIntegral index < numVertices triangulation = vertex
validateHint _ = VertexId 0
-- A vertex's Voronoi cell is the intersection of the half-planes its
-- Delaunay neighbours induce, so a query outside that cell violates one of
-- them and some neighbour is strictly closer. Descending on the /first/
-- strict improvement therefore lands on the same vertex as descending on the
-- best one, having tested about half of the star on average instead of all
-- of it. The argument needs the Delaunay property, so a constrained
-- triangulation keeps the exhaustive scan below.
descend !current !currentDistance !steps !tests
| steps >= bound = (current, NearestStats steps tests)
| otherwise =
case vertexOutEdge triangulation current of
Nothing -> (current, NearestStats steps tests)
Just ring -> revolve current currentDistance steps ring ring Nothing tests
-- Equal-distance neighbours are not an improvement, so they cannot end the
-- revolution early; the lowest such handle is carried to the end and taken
-- only if nothing strictly closer appeared. That is the same deterministic
-- tie the exhaustive scan settles on.
revolve !current !currentDistance !steps !edge !ring !tie !tests
| candidateDistance < currentDistance =
descend candidate candidateDistance (steps + 1) nextTests
| nextEdge == ring =
case nextTie of
Just settled -> descend settled currentDistance (steps + 1) nextTests
Nothing -> (current, NearestStats steps nextTests)
| otherwise = revolve current currentDistance steps nextEdge ring nextTie nextTests
where
!candidate = destination triangulation edge
!candidateDistance = squaredDistanceWide query (vertexPoint triangulation candidate)
!nextTests = tests + 1
!nextEdge = counterClockwise triangulation edge
!nextTie
| candidateDistance == currentDistance && candidate < current =
case tie of
Just held | held <= candidate -> tie
_ -> Just candidate
| otherwise = tie
walk !current !currentDistance !steps !tests
| steps >= bound = (current, NearestStats steps tests)
| otherwise =
let (!candidate, !candidateDistance, !newTests) =
foldVertexOutgoingEdges'
triangulation
current
inspect
(current, currentDistance, tests)
in if candidate == current
then (current, NearestStats steps newTests)
else walk candidate candidateDistance (steps + 1) newTests
inspect (!best, !bestDistance, !tests) edge =
let !candidate = destination triangulation edge
!candidateDistance = squaredDistanceWide query (vertexPoint triangulation candidate)
!isBetter =
candidateDistance < bestDistance
|| (candidateDistance == bestDistance && candidate < best)
in if isBetter
then (candidate, candidateDistance, tests + 1)
else (best, bestDistance, tests + 1)
barycentricWeights
:: Triangulation mode vertex directed undirected face
-> Maybe LocationHint
-> QueryPoint
-> (BarycentricWeights, LocationStats)
barycentricWeights triangulation hint queryPoint =
let !query = queryPointValue queryPoint
(!location, !stats) = locatePointWithHint triangulation hint queryPoint
in (weightsFor query location, stats)
where
weightsFor _ (OnVertex vertex) = OneWeight vertex
weightsFor query (OnEdge edge) =
let !from = origin triangulation edge
!to = destination triangulation edge
!factor = clamp 0 1 (projectionFactor (vertexPoint triangulation from) (vertexPoint triangulation to) query)
in TwoWeights from (1 - factor) to factor
weightsFor query (InFace face) =
case innerFaceVertices triangulation face of
Nothing -> NoWeights
Just (a, b, c) ->
case barycentricCoordinates
(vertexPoint triangulation a)
(vertexPoint triangulation b)
(vertexPoint triangulation c)
query of
Nothing -> NoWeights
Just (wa, wb, wc) -> ThreeWeights a wa b wb c wc
weightsFor _ EmptyTriangulation = NoWeights
weightsFor _ (OutsideConvexHull _) = NoWeights
-- | Calculate Sibson coordinates using fixed-capacity reusable scratch storage.
-- The only per-query heap object is the returned boxed vector.
naturalNeighborWeights
:: NaturalNeighborWorkspace s mode vertex directed undirected face
-> Maybe LocationHint
-> QueryPoint
-> ST s (NaturalNeighborResult)
naturalNeighborWeights workspace hint queryPoint = do
(!count, !locationStats, !stats) <- queryNaturalNeighborWorkspace workspace hint queryPoint
values <- V.generateM count $ \index -> do
rawVertex <- MUV.unsafeRead (nnWeightVertex workspace) index
weight <- MUV.unsafeRead (nnWeightValue workspace) index
pure (VertexId rawVertex, weight)
pure NaturalNeighborResult
{ naturalNeighborValues = values
, naturalNeighborLocationStats = locationStats
, naturalNeighborStats = stats
}
-- | Strictly fold the Sibson coordinates held in a reusable workspace. Unlike
-- 'naturalNeighborWeights', this does not allocate a result vector. It is the
-- canonical path for repeated interpolation and other reductions.
foldNaturalNeighborWeights
:: (accumulator -> VertexId -> Double -> accumulator)
-> accumulator
-> NaturalNeighborWorkspace s mode vertex directed undirected face
-> Maybe LocationHint
-> QueryPoint
-> ST s (accumulator, LocationStats, InterpolationStats)
foldNaturalNeighborWeights combine initial workspace hint query = do
(!count, !locationStats, !stats) <- queryNaturalNeighborWorkspace workspace hint query
value <- go 0 initial count
pure (value, locationStats, stats)
where
go !index !accumulator !count
| index >= count = pure accumulator
| otherwise = do
rawVertex <- MUV.unsafeRead (nnWeightVertex workspace) index
weight <- MUV.unsafeRead (nnWeightValue workspace) index
let !nextAccumulator = combine accumulator (VertexId rawVertex) weight
go (index + 1) nextAccumulator count
queryNaturalNeighborWorkspace
:: NaturalNeighborWorkspace s mode vertex directed undirected face
-> Maybe LocationHint
-> QueryPoint
-> ST s (Int, LocationStats, InterpolationStats)
queryNaturalNeighborWorkspace workspace hint queryPoint = do
let !triangulation = nnTriangulation workspace
!query = queryPointValue queryPoint
(!location, !locationStats) = locatePointWithHint triangulation hint queryPoint
(!count, !stats) <- case location of
OnVertex vertex -> do
writeWeight workspace 0 vertex 1
pure (1, InterpolationStats 0 1 0 False)
OnEdge edge
| isBoundaryEdge triangulation (asUndirected edge) -> do
let !from = origin triangulation edge
!to = destination triangulation edge
!factor = clamp 0 1 (projectionFactor (vertexPoint triangulation from) (vertexPoint triangulation to) query)
writeWeight workspace 0 from (1 - factor)
writeWeight workspace 1 to factor
pure (2, InterpolationStats 0 2 0 False)
| otherwise ->
let !left = incidentFace triangulation edge
!right = incidentFace triangulation (reverseEdge edge)
!start = if left /= outerFace then left else right
in sibsonQuery workspace query start
InFace face -> sibsonQuery workspace query face
EmptyTriangulation -> pure (0, InterpolationStats 0 0 0 False)
OutsideConvexHull _ -> pure (0, InterpolationStats 0 0 0 False)
pure (count, locationStats, stats)
sibsonQuery
:: NaturalNeighborWorkspace s mode vertex directed undirected face
-> Point
-> FaceId
-> ST s (Int, InterpolationStats)
sibsonQuery workspace query startFace = do
generation <- nextFaceGeneration workspace
(cavityCount, faceTests) <- discoverCavity workspace generation query startFace
boundaryCount <- collectBoundary workspace generation cavityCount
orderedCount <- orderBoundary workspace boundaryCount
if orderedCount < 3
then fallbackBarycentric workspace query startFace faceTests cavityCount
else do
cellOkay <- buildInsertionCell workspace query orderedCount
if not cellOkay
then fallbackBarycentric workspace query startFace faceTests cavityCount
else do
weightCount <- buildStolenAreas workspace generation query orderedCount
normalized <- normalizeWeights workspace weightCount
if normalized
then pure (weightCount, InterpolationStats cavityCount weightCount faceTests False)
else fallbackBarycentric workspace query startFace faceTests cavityCount
-- The query cavity is exactly the set of faces whose circumcircles contain the
-- inserted point. Generation marks make clearing O(1).
discoverCavity
:: NaturalNeighborWorkspace s mode vertex directed undirected face
-> Word32
-> Point
-> FaceId
-> ST s (Int, Int)
discoverCavity workspace generation query (FaceId rawStart) = do
MUV.unsafeWrite (nnFaceSeenMarks workspace) (fromIntegral rawStart) generation
MUV.unsafeWrite (nnFaceQueue workspace) 0 rawStart
go 1 0 0
where
!triangulation = nnTriangulation workspace
go !queueSize !cavitySize !tests
| queueSize <= 0 = pure (cavitySize, tests)
| otherwise = do
let !slot = queueSize - 1
rawFace <- MUV.unsafeRead (nnFaceQueue workspace) slot
let !face = FaceId rawFace
!inside = containsQuery triangulation query face
!tests' = tests + 1
if not inside
then go slot cavitySize tests'
else do
MUV.unsafeWrite (nnFaceMarks workspace) (fromIntegral rawFace) generation
MUV.unsafeWrite (nnCavityFaces workspace) cavitySize rawFace
nextQueue <- pushNeighbors triangulation workspace generation slot face
go nextQueue (cavitySize + 1) tests'
containsQuery
:: Triangulation mode vertex directed undirected face
-> Point
-> FaceId
-> Bool
containsQuery triangulation query face =
case innerFaceVertices triangulation face of
Nothing -> False
Just (a, b, c) ->
-- Strictly inside, matching spade's contained_in_circumference: an
-- exactly cocircular face is NOT part of the cavity, so its opposite
-- vertex is not a natural neighbour. A '/= LT' reading would include
-- such a vertex with a zero stolen area and diverge from spade's
-- neighbour set on cocircular queries.
inCircle
(vertexPoint triangulation a)
(vertexPoint triangulation b)
(vertexPoint triangulation c)
query
== GT
pushNeighbors
:: Triangulation mode vertex directed undirected face
-> NaturalNeighborWorkspace s mode vertex directed undirected face
-> Word32
-> Int
-> FaceId
-> ST s Int
pushNeighbors triangulation workspace generation start face =
case innerFaceDirectedEdges triangulation face of
Nothing -> pure start
Just (e0, e1, e2) -> do
size1 <- pushOne start e0
size2 <- pushOne size1 e1
pushOne size2 e2
where
pushOne !size edge =
let !adjacent@(FaceId raw) = incidentFace triangulation (reverseEdge edge)
!index = fromIntegral raw
in if adjacent == outerFace
then pure size
else do
seen <- MUV.unsafeRead (nnFaceSeenMarks workspace) index
if seen == generation
then pure size
else do
MUV.unsafeWrite (nnFaceSeenMarks workspace) index generation
MUV.unsafeWrite (nnFaceQueue workspace) size raw
pure (size + 1)
collectBoundary
:: NaturalNeighborWorkspace s mode vertex directed undirected face
-> Word32
-> Int
-> ST s Int
collectBoundary workspace generation cavityCount = goFaces 0 0
where
!triangulation = nnTriangulation workspace
goFaces !index !boundarySize
| index >= cavityCount = pure boundarySize
| otherwise = do
rawFace <- MUV.unsafeRead (nnCavityFaces workspace) index
nextSize <- case innerFaceDirectedEdges triangulation (FaceId rawFace) of
Nothing -> pure boundarySize
Just (e0, e1, e2) -> do
size1 <- appendIfBoundary boundarySize e0
size2 <- appendIfBoundary size1 e1
appendIfBoundary size2 e2
goFaces (index + 1) nextSize
appendIfBoundary !size edge = do
let adjacent@(FaceId rawAdjacent) = incidentFace triangulation (reverseEdge edge)
outside <- if adjacent == outerFace
then pure True
else (/= generation) <$> MUV.unsafeRead (nnFaceMarks workspace) (fromIntegral rawAdjacent)
if outside
then case edge of
DirectedEdgeId raw -> MUV.unsafeWrite (nnBoundaryEdges workspace) size raw >> pure (size + 1)
else pure size
orderBoundary
:: NaturalNeighborWorkspace s mode vertex directed undirected face
-> Int
-> ST s Int
orderBoundary _ 0 = pure 0
orderBoundary workspace count = do
generation <- nextOriginGeneration workspace
install generation 0
firstRaw <- MUV.unsafeRead (nnBoundaryEdges workspace) 0
follow generation firstRaw firstRaw 0
where
!triangulation = nnTriangulation workspace
install !generation !index
| index >= count = pure ()
| otherwise = do
rawEdge <- MUV.unsafeRead (nnBoundaryEdges workspace) index
let !from = vertexIndex (origin triangulation (DirectedEdgeId rawEdge))
MUV.unsafeWrite (nnOriginMarks workspace) from generation
MUV.unsafeWrite (nnOriginEdge workspace) from rawEdge
install generation (index + 1)
follow !generation !firstRaw !currentRaw !index
| index >= count =
if currentRaw == firstRaw then pure count else pure 0
| otherwise = do
MUV.unsafeWrite (nnOrderedEdges workspace) index currentRaw
let !to = vertexIndex (destination triangulation (DirectedEdgeId currentRaw))
marked <- MUV.unsafeRead (nnOriginMarks workspace) to
if marked /= generation
then pure 0
else do
nextRaw <- MUV.unsafeRead (nnOriginEdge workspace) to
if nextRaw == firstRaw && index + 1 == count
then pure count
else if nextRaw == firstRaw
then pure 0
else follow generation firstRaw nextRaw (index + 1)
buildInsertionCell
:: NaturalNeighborWorkspace s mode vertex directed undirected face
-> Point
-> Int
-> ST s Bool
buildInsertionCell workspace query count = go 0
where
!triangulation = nnTriangulation workspace
go !index
| index >= count = pure True
| otherwise = do
rawEdge <- MUV.unsafeRead (nnOrderedEdges workspace) index
let !edge = DirectedEdgeId rawEdge
!from = vertexPoint triangulation (origin triangulation edge)
!to = vertexPoint triangulation (destination triangulation edge)
case circumcenter (subtractPoint to query) (subtractPoint from query) (Point 0 0) of
Nothing -> pure False
Just (Point x y)
| isFinite x && isFinite y -> do
MUV.unsafeWrite (nnInsertionX workspace) index x
MUV.unsafeWrite (nnInsertionY workspace) index y
go (index + 1)
| otherwise -> pure False
buildStolenAreas
:: NaturalNeighborWorkspace s mode vertex directed undirected face
-> Word32
-> Point
-> Int
-> ST s Int
buildStolenAreas workspace generation query count = do
lastRaw <- MUV.unsafeRead (nnOrderedEdges workspace) (count - 1)
lastX <- MUV.unsafeRead (nnInsertionX workspace) (count - 1)
lastY <- MUV.unsafeRead (nnInsertionY workspace) (count - 1)
go 0 (DirectedEdgeId lastRaw) (Point lastX lastY)
where
!triangulation = nnTriangulation workspace
go !index !lastEdge !lastPoint
| index >= count = pure count
| otherwise = do
rawStop <- MUV.unsafeRead (nnOrderedEdges workspace) index
firstX <- MUV.unsafeRead (nnInsertionX workspace) index
firstY <- MUV.unsafeRead (nnInsertionY workspace) index
let !stopEdge = DirectedEdgeId rawStop
!first = Point firstX firstY
area <- stolenArea workspace generation query stopEdge first lastEdge lastPoint
case area of
Nothing -> pure 0
Just polygonArea -> do
writeWeight workspace index (origin triangulation stopEdge) polygonArea
go (index + 1) stopEdge first
stolenArea
:: NaturalNeighborWorkspace s mode vertex directed undirected face
-> Word32
-> Point
-> DirectedEdgeId
-> Point
-> DirectedEdgeId
-> Point
-> ST s (Maybe Double)
stolenArea workspace generation query stopEdge first initialEdge initialPoint =
walk initialEdge initialPoint initialPositive initialNegative 0
where
!triangulation = nnTriangulation workspace
-- The boundary loop runs counterclockwise around the cavity, so the fan
-- walk around each boundary vertex necessarily runs clockwise: the
-- shoelace sequence first, lastPoint, circumcenters... is the clockwise
-- traversal of the stolen polygon, and positive - negative is the
-- NEGATED twice-area. The result is negated here so the returned value is
-- the positive twice-area 'normalizeWeights' requires. (spade's identical
-- walk leaves the sum negative and cancels the sign in its total; its
-- "ordered ccw" comment is wrong.)
!initialPositive = pointX first * pointY initialPoint
!initialNegative = pointY first * pointX initialPoint
!target = reverseEdge stopEdge
!limit = numDirectedEdges triangulation + 1
walk !lastEdge !lastPoint !positive !negative !steps
| steps >= limit = pure Nothing
| face == outerFace = pure Nothing
| otherwise = do
center <- cachedFaceCircumcenter workspace generation query face
case center of
Nothing -> pure Nothing
Just current ->
let !positive' = positive + pointX lastPoint * pointY current
!negative' = negative + pointY lastPoint * pointX current
!nextEdge = reverseEdge (next triangulation lastEdge)
in if nextEdge == target
then
let !closedPositive = positive' + pointX current * pointY first
!closedNegative = negative' + pointY current * pointX first
in pure (Just (closedNegative - closedPositive))
else walk nextEdge current positive' negative' (steps + 1)
where
face = incidentFace triangulation lastEdge
-- Each fan turns around the vertex two consecutive boundary edges share, so a
-- cavity face is walked once per vertex it has on the boundary loop: twice for
-- two, three times for a face whose whole triangle is on the loop, which is
-- every face of a single-face cavity. Without this plane each of those visits
-- pays two divisions and a dozen multiply-adds for a value already in hand.
--
-- The cached value is the one 'faceCircumcenterRelative' returned, stored as
-- its own coordinates and handed back unchanged; nothing is recomputed from a
-- rearrangement, so a hit and a miss are the same bits. Soundness needs only
-- that the query point cannot change while an entry is readable, which the
-- generation supplies: it is minted once per Sibson query and every stamp from
-- an earlier query is strictly smaller.
--
-- A 'Nothing' is not recorded. It cannot repeat: the first one aborts this
-- fan, and 'buildStolenAreas' abandons the query on the spot.
cachedFaceCircumcenter
:: NaturalNeighborWorkspace s mode vertex directed undirected face
-> Word32
-> Point
-> FaceId
-> ST s (Maybe (Point))
cachedFaceCircumcenter workspace generation query face@(FaceId rawFace) = do
stamp <- MUV.unsafeRead (nnCircumcenterMarks workspace) slot
if stamp == generation
then do
x <- MUV.unsafeRead (nnCircumcenterX workspace) slot
y <- MUV.unsafeRead (nnCircumcenterY workspace) slot
pure (Just (Point x y))
else case faceCircumcenterRelative (nnTriangulation workspace) query face of
Nothing -> pure Nothing
Just center@(Point x y) -> do
MUV.unsafeWrite (nnCircumcenterX workspace) slot x
MUV.unsafeWrite (nnCircumcenterY workspace) slot y
MUV.unsafeWrite (nnCircumcenterMarks workspace) slot generation
pure (Just center)
where
!slot = fromIntegral rawFace
faceCircumcenterRelative
:: Triangulation mode vertex directed undirected face
-> Point
-> FaceId
-> Maybe (Point)
faceCircumcenterRelative triangulation query face = do
(a, b, c) <- innerFaceVertices triangulation face
center <- circumcenter
(subtractPoint (vertexPoint triangulation a) query)
(subtractPoint (vertexPoint triangulation b) query)
(subtractPoint (vertexPoint triangulation c) query)
if finitePoint center then Just center else Nothing
normalizeWeights
:: NaturalNeighborWorkspace s mode vertex directed undirected face
-> Int
-> ST s Bool
normalizeWeights _ 0 = pure False
normalizeWeights workspace count = do
(!total, !minimumWeight, !finite) <- firstPass 0 0 0 True
let !tolerance = max 1.0e-12 (128 * scalarEpsilon)
if not finite || total == 0 || minimumWeight < negate tolerance
then pure False
else do
clampedTotal <- clampPass 0 0
if clampedTotal <= 0 || not (isFinite clampedTotal)
then pure False
else normalizePass 0 clampedTotal >> pure True
where
firstPass !index !total !minimumWeight !finite
| index >= count = pure (total, minimumWeight, finite)
| otherwise = do
weight <- MUV.unsafeRead (nnWeightValue workspace) index
let !minimumWeight' = if index == 0 then weight else min minimumWeight weight
firstPass (index + 1) (total + weight) minimumWeight' (finite && isFinite weight)
clampPass !index !total
| index >= count = pure total
| otherwise = do
weight <- MUV.unsafeRead (nnWeightValue workspace) index
let !clamped = max 0 weight
MUV.unsafeWrite (nnWeightValue workspace) index clamped
clampPass (index + 1) (total + clamped)
normalizePass !index !total
| index >= count = pure ()
| otherwise = do
weight <- MUV.unsafeRead (nnWeightValue workspace) index
MUV.unsafeWrite (nnWeightValue workspace) index (weight / total)
normalizePass (index + 1) total
fallbackBarycentric
:: NaturalNeighborWorkspace s mode vertex directed undirected face
-> Point
-> FaceId
-> Int
-> Int
-> ST s (Int, InterpolationStats)
fallbackBarycentric workspace query face faceTests cavityCount =
case innerFaceVertices triangulation face of
Nothing -> pure (0, InterpolationStats cavityCount 0 faceTests True)
Just (a, b, c) ->
case barycentricCoordinates
(vertexPoint triangulation a)
(vertexPoint triangulation b)
(vertexPoint triangulation c)
query of
Nothing -> pure (0, InterpolationStats cavityCount 0 faceTests True)
Just (wa, wb, wc) -> do
writeWeight workspace 0 a wa
writeWeight workspace 1 b wb
writeWeight workspace 2 c wc
pure (3, InterpolationStats cavityCount 3 faceTests True)
where
triangulation = nnTriangulation workspace
writeWeight
:: NaturalNeighborWorkspace s mode vertex directed undirected face
-> Int
-> VertexId
-> Double
-> ST s ()
writeWeight workspace index (VertexId vertex) weight = do
MUV.unsafeWrite (nnWeightVertex workspace) index vertex
MUV.unsafeWrite (nnWeightValue workspace) index weight
-- | Sample the nearest vertex, or return 'Nothing' for an empty mesh.
interpolateNearest
:: (VertexId -> value)
-> Triangulation mode vertex directed undirected face
-> Maybe VertexId
-> QueryPoint
-> Maybe value
interpolateNearest sample triangulation hint query = sample . fst <$> nearestNeighbor triangulation hint query
-- | Interpolate scalar vertex samples in the cell containing the query.
interpolateBarycentric
:: (VertexId -> Double)
-> Triangulation mode vertex directed undirected face
-> Maybe LocationHint
-> QueryPoint
-> Maybe Double
interpolateBarycentric sample triangulation hint query =
case fst (barycentricWeights triangulation hint query) of
NoWeights -> Nothing
OneWeight vertex -> Just (sample vertex)
TwoWeights a wa b wb -> Just (wa * sample a + wb * sample b)
ThreeWeights a wa b wb c wc -> Just (wa * sample a + wb * sample b + wc * sample c)
interpolateNaturalNeighbor
:: (VertexId -> Double)
-> NaturalNeighborWorkspace s mode vertex directed undirected face
-> Maybe LocationHint
-> QueryPoint
-> ST s (Maybe Double, InterpolationStats)
interpolateNaturalNeighbor sample workspace hint query = do
(total, _, stats) <-
foldNaturalNeighborWeights
(\accumulator vertex weight -> accumulator + weight * sample vertex)
0
workspace
hint
query
pure (if interpolationNaturalNeighbors stats == 0 then Nothing else Just total, stats)
estimateGradient
:: (VertexId -> Double)
-> Triangulation mode vertex directed undirected face
-> VertexId
-> (Double, Double)
estimateGradient sample triangulation vertex =
case vertexOutEdge triangulation vertex of
Nothing -> (0, 0)
Just start ->
let !nextEdge = counterClockwise triangulation start
in if nextEdge == start
then (0, 0)
else
let (!nx, !ny, !nz) = walk start start 0 0 0 0
in if nz /= 0 && isFinite nz then (-nx / nz, -ny / nz) else (0, 0)
where
Point vx vy = vertexPoint triangulation vertex
!vz = sample vertex
!bound = numDirectedEdges triangulation + 1
walk !start !current !steps !sumX !sumY !sumZ
| steps >= bound = (sumX, sumY, sumZ)
| otherwise =
let !nextEdge = counterClockwise triangulation current
!leftVertex = destination triangulation current
!rightVertex = destination triangulation nextEdge
(!nextX, !nextY, !nextZ) = accumulate sumX sumY sumZ leftVertex rightVertex
in if nextEdge == start
then (nextX, nextY, nextZ)
else walk start nextEdge (steps + 1) nextX nextY nextZ
accumulate !sumX !sumY !sumZ leftVertex rightVertex =
let Point lx ly = vertexPoint triangulation leftVertex
Point rx ry = vertexPoint triangulation rightVertex
!lz = sample leftVertex
!rz = sample rightVertex
!d0x = lx - vx
!d0y = ly - vy
!d0z = lz - vz
!d1x = rx - vx
!d1y = ry - vy
!d1z = rz - vz
!normalX = d0y * d1z - d0z * d1y
!normalY = d0z * d1x - d0x * d1z
!normalZ = d0x * d1y - d0y * d1x
in if normalZ > 0
then (sumX + normalX, sumY + normalY, sumZ + normalZ)
else (sumX, sumY, sumZ)
estimateGradients
:: (VertexId -> Double)
-> Triangulation mode vertex directed undirected face
-> V.Vector (Double, Double)
estimateGradients sample triangulation =
V.generate (numVertices triangulation) $ \index ->
estimateGradient sample triangulation (VertexId (fromIntegral index))
interpolateNaturalNeighborGradient
:: (VertexId -> Double)
-> (VertexId -> (Double, Double))
-> Double
-> NaturalNeighborWorkspace s mode vertex directed undirected face
-> Maybe LocationHint
-> QueryPoint
-> ST s (Maybe Double, InterpolationStats)
interpolateNaturalNeighborGradient sample gradient flatness workspace hint queryPoint
| flatness < 0 || not (isFinite flatness) = pure (Nothing, InterpolationStats 0 0 0 False)
| otherwise = do
(!count, _, !stats) <- queryNaturalNeighborWorkspace workspace hint queryPoint
if count == 0
then pure (Nothing, stats)
else do
accumulation <- accumulate 0 count Nothing (0, 0, 0, 0, 0)
pure (finish accumulation, stats)
where
!query = queryPointValue queryPoint
!triangulation = nnTriangulation workspace
-- 'flatness' is 0.5 or 1 in practice and @**@ is an exp/log pair per natural
-- neighbour per query, so both are answered directly. The two shortcuts do
-- not stand on the same ground.
--
-- The identity is exact. @x@ raised to 1 is @x@, which is already a float,
-- and rounding an exactly representable result admits only that result, so
-- any implementation faithful to within an ulp returns it.
--
-- The square root is not exact and does not need to be. IEEE-754 mandates a
-- correctly rounded 'sqrt'; @pow@ is a recommended operation carrying no such
-- requirement. Where the two disagree it is therefore in the last ulp, and it
-- is 'sqrt' that holds the correctly rounded answer — this substitution can
-- only move the result toward it. Against this platform's libm they in fact
-- agree on every non-negative binary32 and on every non-negative binary64
-- sampled, which is a measurement and not a proof.
--
-- They part company at negative zero, where @pow@ answers @+0@ and 'sqrt'
-- answers @-0@. A sum of two squares is never negative zero, and were it one
-- the sole consumer below tests @== 0@, which both zeroes satisfy alike.
raiseToFlatness squared
| flatness == 0.5 = sqrt squared
| flatness == 1 = squared
| otherwise = squared ** flatness
accumulate !index !count !exact !totals
| index >= count = pure (exact, totals)
| otherwise = do
rawVertex <- MUV.unsafeRead (nnWeightVertex workspace) index
weight <- MUV.unsafeRead (nnWeightValue workspace) index
let !vertex = VertexId rawVertex
!point = vertexPoint triangulation vertex
!exact' =
case exact of
Just _ -> exact
Nothing
| query == point -> Just (sample vertex)
| otherwise -> Nothing
!totals' = contribution totals vertex weight
accumulate (index + 1) count exact' totals'
finish
:: (Maybe Double, (Double, Double, Double, Double, Double))
-> Maybe Double
finish (Just value, _) = Just value
finish (Nothing, (!sumC0, !sumC1, !sumC1Weights, !alphaNumerator, !beta))
| sumC1Weights == 0 = Just sumC0
| otherwise =
let !alpha = alphaNumerator / sumC1Weights
!c1 = sumC1 / sumC1Weights
!denominator = alpha + beta
in if denominator == 0 || not (isFinite denominator)
then Just sumC0
else Just ((alpha * sumC0 + beta * c1) / denominator)
contribution (!sumC0, !sumC1, !sumC1Weights, !alpha, !beta) vertex weight =
let Point qx qy = query
Point vx vy = vertexPoint triangulation vertex
!dx = qx - vx
!dy = qy - vy
!radiusSquared = dx * dx + dy * dy
!radiusPower = raiseToFlatness radiusSquared
!c1Weight = if radiusPower == 0 then 0 else weight / radiusPower
(!gx, !gy) = gradient vertex
!height = sample vertex
!zeta = height + dx * gx + dy * gy
in ( sumC0 + height * weight
, sumC1 + zeta * c1Weight
, sumC1Weights + c1Weight
, alpha + c1Weight * radiusSquared
, beta + weight * radiusSquared
)
vertexIndex :: VertexId -> Int
vertexIndex (VertexId raw) = fromIntegral raw
{-# INLINE vertexIndex #-}
subtractPoint :: Point -> Point -> Point
subtractPoint (Point ax ay) (Point bx by) = Point (ax - bx) (ay - by)
{-# INLINE subtractPoint #-}
finitePoint :: Point -> Bool
finitePoint (Point x y) = isFinite x && isFinite y
{-# INLINE finitePoint #-}
clamp :: Ord value => value -> value -> value -> value
clamp low high = max low . min high
{-# INLINE clamp #-}