moonlight-triangulation-1.2.0.0: test/algebra/Moonlight/Triangulation/MinkowskiSpec.hs
-- | Exact convex convolution and residual-morphology laws.
module Moonlight.Triangulation.MinkowskiSpec (tests) where
import Moonlight.Triangulation.AlgebraFixtures
( annulusRegion
, polygonRegion
, rectangleComponent
, rectangleRegion
)
import Moonlight.Triangulation.Minkowski
( MinkowskiOperation (..)
, closeWith
, convexMinkowskiSum
, convexPolygon
, erodeBy
, minkowskiGeneratedPieces
, minkowskiOperation
, minkowskiOverlayPasses
, minkowskiSum
, openWith
, polygonOffset
, structuringElement
)
import Moonlight.Triangulation.Region
( PlanarRegion
, emptyPlanarRegion
, exactLoopPoints
, planarRegion
, planarRegionComponents
, polygonOuterLoop
)
import Moonlight.Triangulation.Valuation
( exactAreaValue
, regionValuations
, valuationArea
)
import Support (assertEqual, requireRight)
tests :: IO ()
tests = do
testConvexConvolution
testGeneralAddition
testConvexMorphology
testGeneralErosion
testHoledAndNeckedErosion
putStrLn "minkowski: ok"
testConvexConvolution :: IO ()
testConvexConvolution = do
leftComponent <- rectangleComponent 0 0 1 1
rightComponent <- rectangleComponent 0 0 1 1
left <-
requireRight
"left convex polygon"
(convexPolygon (exactLoopPoints (polygonOuterLoop leftComponent)))
right <-
requireRight
"right convex polygon"
(convexPolygon (exactLoopPoints (polygonOuterLoop rightComponent)))
let result = convexMinkowskiSum left right
assertRegionArea "convex square sum" 4 result
assertEqual
"convex convolution is commutative after canonical publication"
result
(convexMinkowskiSum right left)
testGeneralAddition :: IO ()
testGeneralAddition = do
first <- rectangleComponent 0 0 1 1
second <- rectangleComponent 3 0 4 1
disconnected <- requireRight "disconnected source" (planarRegion [first, second])
kernel <- rectangleRegion 0 0 1 1
(sumRegion, receipt) <-
requireRight "general disconnected sum" (minkowskiSum disconnected kernel)
assertEqual "general sum operation receipt" MinkowskiAddition (minkowskiOperation receipt)
assertEqual "general sum generated one piece per source component" 2 (minkowskiGeneratedPieces receipt)
assertEqual "general sum remains disconnected" 2 (length (planarRegionComponents sumRegion))
assertRegionArea "general disconnected sum" 8 sumRegion
(swapped, _) <- requireRight "swapped disconnected sum" (minkowskiSum kernel disconnected)
assertEqual "general Minkowski sum is commutative" sumRegion swapped
(annihilated, _) <-
requireRight "empty Minkowski annihilator" (minkowskiSum disconnected emptyPlanarRegion)
assertEqual "empty region annihilates Minkowski addition" emptyPlanarRegion annihilated
firstRegion <- requireRight "first distributive operand" (planarRegion [first])
secondRegion <- requireRight "second distributive operand" (planarRegion [second])
(firstSum, _) <- requireRight "first distributed sum" (minkowskiSum firstRegion kernel)
(secondSum, _) <- requireRight "second distributed sum" (minkowskiSum secondRegion kernel)
distributed <-
requireRight
"distributed union"
(planarRegion (planarRegionComponents firstSum <> planarRegionComponents secondSum))
assertEqual "Minkowski addition distributes over disjoint union" distributed sumRegion
associativityThird <- rectangleRegion (-2) 0 0 1
(leftPair, _) <- requireRight "associative left pair" (minkowskiSum firstRegion kernel)
(leftAssociated, _) <-
requireRight "left-associated Minkowski sum" (minkowskiSum leftPair associativityThird)
(rightPair, _) <- requireRight "associative right pair" (minkowskiSum kernel associativityThird)
(rightAssociated, _) <-
requireRight "right-associated Minkowski sum" (minkowskiSum firstRegion rightPair)
assertEqual "Minkowski addition is associative" leftAssociated rightAssociated
concave <-
polygonRegion
[ (0, 0), (3, 0), (3, 1), (1, 1), (1, 3), (0, 3) ]
(concaveSum, concaveReceipt) <-
requireRight "concave triangulated sum" (minkowskiSum concave kernel)
expectedConcaveSum <-
polygonRegion
[ (0, 0), (4, 0), (4, 2), (2, 2), (2, 4), (0, 4) ]
assertEqual "triangulated nonconvex sum" expectedConcaveSum concaveSum
if minkowskiOverlayPasses concaveReceipt > 0
then pure ()
else fail "nonconvex addition bypassed CDT decomposition and overlay union"
testConvexMorphology :: IO ()
testConvexMorphology = do
source <- rectangleRegion (-2) (-2) 2 2
kernelComponent <- rectangleComponent (-1) (-1) 1 1
kernelPolygon <-
requireRight
"centred square kernel"
(convexPolygon (exactLoopPoints (polygonOuterLoop kernelComponent)))
element <- requireRight "centred structuring element" (structuringElement kernelPolygon)
(eroded, erosionReceipt) <- requireRight "convex erosion" (erodeBy element source)
expectedErosion <- rectangleRegion (-1) (-1) 1 1
assertEqual "convex support-half-plane erosion" expectedErosion eroded
assertEqual "convex erosion needs no overlay" 0 (minkowskiOverlayPasses erosionReceipt)
exactFitSource <- rectangleRegion (-1) (-1) 1 1
(lowerDimensionalResidual, _) <-
requireRight "exact-fit regularized erosion" (erodeBy element exactFitSource)
assertEqual
"point-only erosion residual regularizes to the empty polygonal region"
emptyPlanarRegion
lowerDimensionalResidual
(emptyResidual, _) <- requireRight "empty source erosion" (erodeBy element emptyPlanarRegion)
assertEqual "empty source erodes to empty" emptyPlanarRegion emptyResidual
(expanded, _) <- requireRight "convex offset" (polygonOffset element source)
expectedExpansion <- rectangleRegion (-3) (-3) 3 3
assertEqual "convex offset" expectedExpansion expanded
(opened, _) <- requireRight "convex opening" (openWith element source)
assertEqual "convex opening is idempotent on the square" source opened
(openedTwice, _) <- requireRight "second convex opening" (openWith element opened)
assertEqual "opening is idempotent" opened openedTwice
(closed, _) <- requireRight "convex closing" (closeWith element source)
assertEqual "convex closing is idempotent on the square" source closed
(closedTwice, _) <- requireRight "second convex closing" (closeWith element closed)
assertEqual "closing is idempotent" closed closedTwice
cornerKernelComponent <- rectangleComponent 0 0 1 1
cornerKernel <-
requireRight
"corner-anchored kernel"
(convexPolygon (exactLoopPoints (polygonOuterLoop cornerKernelComponent)))
cornerElement <- requireRight "corner-anchored element" (structuringElement cornerKernel)
cornerSource <- rectangleRegion 0 0 4 4
(cornerErosion, _) <- requireRight "corner-anchored erosion" (erodeBy cornerElement cornerSource)
expectedCornerErosion <- rectangleRegion 0 0 3 3
assertEqual "kernel origin controls erosion translation" expectedCornerErosion cornerErosion
testGeneralErosion :: IO ()
testGeneralErosion = do
left <- rectangleComponent 0 0 4 4
right <- rectangleComponent 6 0 10 4
source <- requireRight "two-component erosion source" (planarRegion [left, right])
kernelComponent <- rectangleComponent (-1) (-1) 1 1
kernelPolygon <-
requireRight
"general erosion kernel"
(convexPolygon (exactLoopPoints (polygonOuterLoop kernelComponent)))
element <- requireRight "general erosion element" (structuringElement kernelPolygon)
(eroded, receipt) <- requireRight "general erosion" (erodeBy element source)
expectedLeft <- rectangleComponent 1 1 3 3
expectedRight <- rectangleComponent 7 1 9 3
expected <- requireRight "expected general erosion" (planarRegion [expectedLeft, expectedRight])
assertEqual "general residual erosion" expected eroded
if minkowskiOverlayPasses receipt > 0
then pure ()
else fail "general erosion bypassed the overlay candidate arrangement"
testHoledAndNeckedErosion :: IO ()
testHoledAndNeckedErosion = do
kernelComponent <- rectangleComponent (-1) (-1) 1 1
kernelPolygon <-
requireRight
"holed erosion kernel"
(convexPolygon (exactLoopPoints (polygonOuterLoop kernelComponent)))
element <- requireRight "holed erosion element" (structuringElement kernelPolygon)
sourceAnnulus <- annulusRegion (0, 0, 10, 10) (4, 4, 6, 6)
expectedAnnulus <- annulusRegion (1, 1, 9, 9) (3, 3, 7, 7)
(erodedAnnulus, _) <- requireRight "annulus erosion" (erodeBy element sourceAnnulus)
assertEqual "erosion shrinks the outer boundary and expands holes" expectedAnnulus erodedAnnulus
necked <-
polygonRegion
[ (0, 0)
, (8, 0)
, (8, 3)
, (12, 3)
, (12, 0)
, (20, 0)
, (20, 8)
, (12, 8)
, (12, 5)
, (8, 5)
, (8, 8)
, (0, 8)
]
wideKernelComponent <- rectangleComponent (-2) (-2) 2 2
wideKernelPolygon <-
requireRight
"neck erosion kernel"
(convexPolygon (exactLoopPoints (polygonOuterLoop wideKernelComponent)))
wideElement <- requireRight "neck erosion element" (structuringElement wideKernelPolygon)
(separated, _) <- requireRight "narrow-neck erosion" (erodeBy wideElement necked)
expectedLeft <- rectangleComponent 2 2 6 6
expectedRight <- rectangleComponent 14 2 18 6
expectedSeparated <-
requireRight "expected separated erosion" (planarRegion [expectedLeft, expectedRight])
assertEqual "erosion removes a neck narrower than the kernel" expectedSeparated separated
assertRegionArea :: String -> Integer -> PlanarRegion -> IO ()
assertRegionArea label expected region = do
valuations <- requireRight label (regionValuations region)
assertEqual
label
(fromInteger expected)
(exactAreaValue (valuationArea valuations))