moonlight-planar-1.1.0.0: src-dual/Moonlight/Planar/Interpolation.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
-- | Nearest, barycentric, and natural-neighbor interpolation over one
-- authoritative triangulation.
module Moonlight.Planar.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.Planar.Dcel
( destination
, counterClockwise
, incidentFace
, innerFaceDirectedEdges
, innerFaceVertices
, isBoundaryEdge
, next
, numConstraints
, numDirectedEdges
, numVertices
, origin
, outerFace
, vertexOutEdge
, vertexOutgoingEdges
, vertexPoint
)
import Moonlight.Planar.Handles.Iterators.FixedIterators (foldVertices')
import Moonlight.Planar.Internal.HandleDefs
( DirectedEdgeId (..)
, asUndirected
, FaceId (..)
, faceIdIndex
, VertexId (..)
, reverseEdge
, vertexIdIndex
)
import Moonlight.Planar.Internal.InterpolationWorkspace
import Moonlight.Planar.Internal.Predicates (compareSquaredDistances, inCircle)
import Moonlight.Planar.Math (barycentricCoordinates, circumcenter, projectionFactor)
import Moonlight.Planar.Scalar (isFinite)
import Moonlight.Planar.PointLocation (locatePointWithHint)
import Moonlight.Planar.Scalar (scalarEpsilon)
import Moonlight.Planar.Types (Location (..), LocationHint, LocationStats, NearestStats (..), Triangulation)
import Moonlight.Planar.Point (Point (..), QueryPoint, queryPointValue)
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)
-- | Work performed by one natural-neighbor query.
data InterpolationStats = InterpolationStats
{ interpolationCavityFaces :: {-# UNPACK #-} !Int
-- ^ Faces in the query insertion cavity.
, interpolationNaturalNeighbors :: {-# UNPACK #-} !Int
-- ^ Sites contributing nonzero Sibson weight.
, interpolationFaceTests :: {-# UNPACK #-} !Int
-- ^ Faces tested while discovering the cavity.
-- | '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)
-- | Weights and receipts produced by one natural-neighbor query.
data NaturalNeighborResult = NaturalNeighborResult
{ naturalNeighborValues :: !(V.Vector (VertexId, Double))
-- ^ Nonzero weights keyed by source vertex.
, naturalNeighborLocationStats :: !LocationStats
-- ^ Work performed while locating the query.
, naturalNeighborStats :: !InterpolationStats
-- ^ Work performed while constructing the Sibson coordinates.
}
deriving stock (Eq, Show, Generic)
deriving anyclass (NFData)
-- | Minimum resident vertex by exact squared distance, then by resident handle,
-- using an optional admitted vertex as the descent seed. Empty triangulations
-- have no nearest vertex. Handle ties are deterministic within this mesh, not
-- an identity invariant across differently constructed meshes.
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 0 0)
| otherwise = Just (completeArgmin start 0 0)
where
!query = queryPointValue queryPoint
!start = validateHint hint
validateHint (Just vertex)
| vertexIdIndex vertex < numVertices triangulation = vertex
validateHint _ = VertexId 0
compareDistance firstSite secondSite =
compareSquaredDistances
query
(vertexPoint triangulation firstSite)
(vertexPoint triangulation secondSite)
-- A Delaunay Voronoi cell is the intersection of its neighbour half-planes.
-- Outside it, some neighbour is strictly closer; the first such witness is
-- sufficient. Exact strict improvement is well-founded on the finite site
-- inventory, so no arbitrary walk budget can publish an unproved incumbent.
descend !current !steps !tests =
foldr
(inspectStar current steps)
(settle current steps)
(vertexOutgoingEdges triangulation current)
False
tests
inspectStar current steps edge remaining tied !tests =
let !candidate = destination triangulation edge
!nextTests = tests + 1
in case compareDistance candidate current of
LT -> descend candidate (steps + 1) nextTests
EQ -> remaining True nextTests
GT -> remaining tied nextTests
-- A local lower-handle tie is not the global minimum: a cocircular plateau
-- can hide it behind larger handles. Resolve ties by the complete inventory,
-- which is also the required schedule when constraints break the Voronoi
-- neighbour argument.
settle current steps tied tests
| tied = completeArgmin current steps tests
| otherwise = (current, NearestStats steps tests)
completeArgmin seed steps tests =
( foldVertices' triangulation (chooseNearest seed) seed
, NearestStats steps (tests + numVertices triangulation - 1)
)
chooseNearest seed !best candidate
| candidate == seed = best
| otherwise =
case compareDistance candidate best of
LT -> candidate
EQ -> min candidate best
GT -> best
-- | Barycentric weights at a validated query, plus point-location work.
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 !factor = edgeDestinationWeight triangulation edge query
in TwoWeights (origin triangulation edge) (1 - factor) (destination triangulation edge) 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 !factor = edgeDestinationWeight triangulation edge query
writeWeight workspace 0 (origin triangulation edge) (1 - factor)
writeWeight workspace 1 (destination triangulation edge) 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 startFace@(FaceId rawStart) = do
MUV.unsafeWrite (nnFaceSeenMarks workspace) (faceIdIndex startFace) 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) (faceIdIndex face) 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 = faceIdIndex adjacent
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 = incidentFace triangulation (reverseEdge edge)
outside <- if adjacent == outerFace
then pure True
else (/= generation) <$> MUV.unsafeRead (nnFaceMarks workspace) (faceIdIndex adjacent)
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 = vertexIdIndex (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 = vertexIdIndex (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 = 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 = faceIdIndex face
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)
-- | Interpolate from reusable Sibson weights.
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)
-- | Estimate one vertex gradient from its Delaunay neighbors.
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)
-- | Estimate gradients for every vertex in handle order.
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))
-- | Natural-neighbor interpolation with nodal-gradient correction.
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
)
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 #-}
-- | Weight of a directed edge's destination for a query on that edge: the
-- projection factor clamped into the segment, so the origin carries the rest.
edgeDestinationWeight
:: Triangulation mode vertex directed undirected face
-> DirectedEdgeId
-> Point
-> Double
edgeDestinationWeight triangulation edge query =
clamp
0
1
( projectionFactor
(vertexPoint triangulation (origin triangulation edge))
(vertexPoint triangulation (destination triangulation edge))
query
)
clamp :: Ord value => value -> value -> value -> value
clamp low high = max low . min high
{-# INLINE clamp #-}