packages feed

moonlight-planar-1.1.0.0: test/algebra/Moonlight/Planar/PowerDiagramSpec.hs

{-# LANGUAGE BangPatterns #-}

-- | Exact bounded power-cell laws against a complete half-plane oracle.
module Moonlight.Planar.PowerDiagramSpec
  ( tests
  ) where

import Control.Monad ( unless )
import Data.Foldable ( traverse_ )
import Data.List.NonEmpty ( NonEmpty(..) )
import Moonlight.Planar.Convex ( ConvexPolygon, convexPolygon, convexPolygonPoints )
import Moonlight.Planar.Exact ( ExactPoint, exactPointCoordinates, exactPointFromPoint,
  ExactClipDisposition(..), ExactClosedHalfPlane, exactAffineLine, exactClipRetainedPolygon,
  exactClosedHalfPlane, exactRetainedPolygon, exactRetainedPolygonPoints )
import Moonlight.Planar.Exact (ExactRational)
import Moonlight.Planar.PowerDiagram ( powerDiagramInputSites, powerSitePosition, powerWeight,
  powerWeightExact, boundedPowerDiagram, boundedPowerDiagramFromRegular, powerCellDisposition,
  powerCellDispositions, powerDiagramPlanarLayer, emptyRegularTriangulation, regularTriangulation,
  BoundedPowerDiagram, PowerCellDisposition(..), PowerDiagramError(DuplicatePowerSiteLabel),
  PowerDiagramReceipt(powerDiagramPublishedCells, powerDiagramMaximumCellConstraints,
  powerDiagramRegularEdges, powerDiagramSubmittedSiteConstraints, powerDiagramActiveBoundaries,
  powerDiagramCoincidentDominatedCells, powerDiagramCoincidentEquivalentCells,
  powerDiagramLowerDimensionalCells, powerDiagramEmptyCells), PowerSite(..),
  PowerWeightError(PowerWeightNonFinite), RegularTriangulation )
import Moonlight.Planar.PowerFixtures ( admittedSite, admittedWeight, admittedWeightedSite,
  squareDomain, dispositionTag, powerDistance )
import Moonlight.Planar.Region ( planarLayerRegions )
import Moonlight.Planar.Point (Point(..))
import Support ( assertEqual, integerPoint, requireRight )
import qualified Data.List as List
import qualified Data.Map.Strict as Map
import qualified Data.List.NonEmpty as NonEmpty


tests :: IO ()
tests =
  sequence_
    [ testEqualWeightsAndCommonShift
    , testThreeSiteExactPartitionCoverage
    , testBoundedPowerFromRegular
    , testSparsePowerMatchesCompleteOracle
    , testEqualCoincidentResolution
    , testDominantCoincidentResolution
    , testLowerDimensionalCell
    , testDistinctEmptyCell
    , testDyadicNearParallelBisectors
    , testBinary64PrecisionSites
    , testDuplicateLabels
    , testNonFiniteWeight
    ]

testBoundedPowerFromRegular :: IO ()
testBoundedPowerFromRegular = do
  domain <- squareDomain
  fixtures <-
    traverse
      (\(name, submitted) -> (,) name <$> traverse admittedWeightedSite submitted)
      [ ( "ordinary"
        , ("a", Point 1 1, 0.25)
            :| [("b", Point 3 1, -0.125), ("c", Point 2 3, 0.5)]
        )
      , ( "lower-dimensional"
        , ("a", Point 0 0, 0)
            :| [("b", Point 2 0, 0), ("c", Point 0 2, 0), ("center", Point 0.5 0.5, -1.5)]
        )
      , ( "hidden"
        , ("a", Point 0 0, 0)
            :| [("b", Point 2 0, 0), ("c", Point 0 2, 0), ("center", Point 0.5 0.5, -2)]
        )
      , ( "coincident"
        , ("a", Point 1 1, 0)
            :| [("b", Point 1 1, 0), ("c", Point 3 1, 0)]
        )
      ]
  traverse_ (assertPreparedPowerMatches domain) fixtures
  (emptyDiagram, emptyReceipt) <-
    requireRight
      "bounded power diagram from empty regular source"
      (boundedPowerDiagramFromRegular domain (emptyRegularTriangulation :: RegularTriangulation String))
  assertEqual "empty regular source has no cell dispositions" [] (powerCellDispositions emptyDiagram)
  assertEqual "empty regular source has no input sites" 0 (powerDiagramInputSites emptyReceipt)

assertPreparedPowerMatches
  :: ConvexPolygon
  -> (String, NonEmpty (PowerSite String))
  -> IO ()
assertPreparedPowerMatches domain (name, sites) = do
  (regular, _) <-
    requireRight (name <> " regular source") (regularTriangulation sites)
  oneShot <-
    requireRight (name <> " one-shot power diagram") (boundedPowerDiagram domain sites)
  fromRegular <-
    requireRight
      (name <> " prepared power diagram")
      (boundedPowerDiagramFromRegular domain regular)
  assertEqual (name <> " prepared clipping") oneShot fromRegular

testSparsePowerMatchesCompleteOracle :: IO ()
testSparsePowerMatchesCompleteOracle = do
  domain <- squareDomain
  sites <-
    traverse
      admittedWeightedSite
      ( ("a", Point 1.0 1.0, 0.25)
          :| [ ("b", Point 3.0 0.8, -0.125)
             , ("c", Point 5.2 1.4, 0.375)
             , ("d", Point 8.5 1.0, 0.0)
             , ("e", Point 1.7 3.8, -0.25)
             , ("f", Point 4.1 4.4, 0.125)
             , ("g", Point 7.6 3.5, -0.375)
             , ("h", Point 9.1 5.4, 0.25)
             , ("i", Point 1.0 7.8, 0.0)
             , ("j", Point 3.6 8.9, 0.5)
             , ("k", Point 6.4 7.5, -0.125)
             , ("l", Point 8.8 9.0, 0.375)
             ]
      )
  (diagram, receipt) <-
    requireRight "sparse regular-neighbour power diagram" (boundedPowerDiagram domain sites)
  traverse_
    (\site -> do
       oracle <- completeOraclePowerCell domain sites site
       assertEqual
         ("regular-neighbour cell equals complete HPI oracle for " <> powerSiteLabel site)
         (Just oracle)
         (powerCellDisposition (powerSiteLabel site) diagram))
    sites
  let siteCount = NonEmpty.length sites
      completeConstraintCount = siteCount * (siteCount - 1)
  unless (powerDiagramSubmittedSiteConstraints receipt < completeConstraintCount) $
    fail "regular-neighbour construction did not eliminate the complete pairwise cell schedule"
  unless (powerDiagramMaximumCellConstraints receipt <= powerDiagramRegularEdges receipt) $
    fail "regular-neighbour construction retained a non-topological radical axis"

data OracleScore = OracleScore
  { oracleXCoefficient :: !ExactRational
  , oracleYCoefficient :: !ExactRational
  , oracleConstant :: !ExactRational
  }

oracleScore :: PowerSite label -> IO OracleScore
oracleScore site = do
  point <- requireRight "oracle source point" (exactPointFromPoint (powerSitePosition site))
  let (coordinateX, coordinateY) = exactPointCoordinates point
  pure
    OracleScore
      { oracleXCoefficient = 2 * coordinateX
      , oracleYCoefficient = 2 * coordinateY
      , oracleConstant =
          powerWeightExact (powerSiteWeight site)
            - coordinateX * coordinateX
            - coordinateY * coordinateY
      }

completeOraclePowerCell
  :: ConvexPolygon
  -> NonEmpty (PowerSite String)
  -> PowerSite String
  -> IO (PowerCellDisposition String)
completeOraclePowerCell domain sites owner = do
  retainedDomain <-
    requireRight "oracle retained domain" (exactRetainedPolygon (convexPolygonPoints domain))
  ownerScore <- oracleScore owner
  halfPlanes <-
    traverse
      (oracleWinningHalfPlane ownerScore)
      ( filter
          ((/= powerSiteLabel owner) . powerSiteLabel)
          (NonEmpty.toList sites)
      )
  (disposition, _) <-
    requireRight
      ("complete HPI oracle for " <> powerSiteLabel owner)
      (exactClipRetainedPolygon retainedDomain halfPlanes)
  case disposition of
    ExactClipFullDimensional retained ->
      PublishedPowerCell
        <$> requireRight
          "oracle convex publication"
          (convexPolygon (exactRetainedPolygonPoints retained))
    ExactClipLowerDimensional points ->
      pure (LowerDimensionalPowerCell points)
    ExactClipEmpty -> pure EmptyPowerCell

oracleWinningHalfPlane
  :: OracleScore
  -> PowerSite label
  -> IO ExactClosedHalfPlane
oracleWinningHalfPlane owner competitor = do
  competitorScore <- oracleScore competitor
  line <-
    requireRight
      "oracle radical axis"
      ( exactAffineLine
          (oracleXCoefficient owner - oracleXCoefficient competitorScore)
          (oracleYCoefficient owner - oracleYCoefficient competitorScore)
          (oracleConstant owner - oracleConstant competitorScore)
      )
  pure (exactClosedHalfPlane line)

testEqualWeightsAndCommonShift :: IO ()
testEqualWeightsAndCommonShift = do
  domain <- squareDomain
  zero <- admittedWeight 0
  shifted <- admittedWeight 7
  zeroSites <-
    ( :| )
      <$> admittedSite "left" (Point 2 5) zero
      <*> traverse (uncurry3 admittedSite) [("right", Point 8 5, zero)]
  shiftedSites <-
    ( :| )
      <$> admittedSite "left" (Point 2 5) shifted
      <*> traverse (uncurry3 admittedSite) [("right", Point 8 5, shifted)]
  (diagram, receipt) <- requireRight "equal-weight power diagram" (boundedPowerDiagram domain zeroSites)
  (shiftedDiagram, _) <- requireRight "common-shift power diagram" (boundedPowerDiagram domain shiftedSites)
  assertEqual "common additive power shift" (powerCellDispositions diagram) (powerCellDispositions shiftedDiagram)
  assertPermutationInvariant "equal-weight power diagram" domain zeroSites diagram
  assertPermutationInvariant "common-shift power diagram" domain shiftedSites shiftedDiagram
  expectedLeft <-
    requireRight
      "expected left power cell"
      (convexPolygon (integerPoint 0 0 :| [integerPoint 5 0, integerPoint 5 10, integerPoint 0 10]))
  expectedRight <-
    requireRight
      "expected right power cell"
      (convexPolygon (integerPoint 5 0 :| [integerPoint 10 0, integerPoint 10 10, integerPoint 5 10]))
  assertEqual "equal-weight left bisector cell" (Just (PublishedPowerCell expectedLeft)) (powerCellDisposition "left" diagram)
  assertEqual "equal-weight right bisector cell" (Just (PublishedPowerCell expectedRight)) (powerCellDisposition "right" diagram)
  assertEqual "receipt input sites" 2 (powerDiagramInputSites receipt)
  assertEqual
    "one submitted site constraint per cell"
    2
    (powerDiagramSubmittedSiteConstraints receipt)
  assertEqual
    "parallel domain and site boundaries coalesce"
    8
    (powerDiagramActiveBoundaries receipt)
  assertEqual "receipt published cells" 2 (powerDiagramPublishedCells receipt)
  assertEqual "receipt empty cells" 0 (powerDiagramEmptyCells receipt)
  traverse_ (assertPublishedVerticesWin zeroSites) (powerCellDispositions diagram)
  let layer = powerDiagramPlanarLayer diagram
  assertEqual "derived layer contains both cells" [Just "left", Just "right"] (Map.keys (planarLayerRegions layer))

testThreeSiteExactPartitionCoverage :: IO ()
testThreeSiteExactPartitionCoverage = do
  domain <- squareDomain
  weight <- admittedWeight 0
  left <- admittedSite "left" (Point 2 5) weight
  middle <- admittedSite "middle" (Point 5 5) weight
  right <- admittedSite "right" (Point 8 5) weight
  let sites = left :| [middle, right]
  (diagram, receipt) <-
    requireRight "three-site exact partition" (boundedPowerDiagram domain sites)
  assertEqual "three-site partition publishes every cell" 3 (powerDiagramPublishedCells receipt)
  assertEqual
    "three-site exact cell areas cover the square"
    200
    (publishedTwiceAreaSum diagram)
  traverse_
    (\permutation -> do
       (permuted, _) <-
         requireRight
           "permuted three-site exact partition"
           (boundedPowerDiagram domain permutation)
       assertEqual
         "three-site partition is permutation invariant"
         (powerCellDispositions diagram)
         (powerCellDispositions permuted)
       assertEqual
         "permuted exact cell areas cover the square"
         200
         (publishedTwiceAreaSum permuted))
    (nonEmptyPermutations sites)

nonEmptyPermutations :: NonEmpty value -> [NonEmpty value]
nonEmptyPermutations =
  foldMap (maybe [] pure . NonEmpty.nonEmpty)
    . List.permutations
    . NonEmpty.toList

assertPermutationInvariant
  :: String
  -> ConvexPolygon
  -> NonEmpty (PowerSite String)
  -> BoundedPowerDiagram String
  -> IO ()
assertPermutationInvariant label domain sites expected =
  traverse_
    (\permutation -> do
       (permuted, _) <-
         requireRight
           (label <> " permutation")
           (boundedPowerDiagram domain permutation)
       assertEqual
         (label <> " is permutation invariant")
         (powerCellDispositions expected)
         (powerCellDispositions permuted))
    (nonEmptyPermutations sites)

publishedTwiceAreaSum :: BoundedPowerDiagram label -> ExactRational
publishedTwiceAreaSum =
  List.foldl'
    (\total (_, disposition) -> case disposition of
        PublishedPowerCell polygon -> total + exactPolygonTwiceArea (convexPolygonPoints polygon)
        LowerDimensionalPowerCell _ -> total
        EmptyPowerCell -> total
        CoincidentEquivalentTo _ -> total
        CoincidentDominatedBy _ -> total)
    0
    . powerCellDispositions

exactPolygonTwiceArea :: NonEmpty ExactPoint -> ExactRational
exactPolygonTwiceArea points =
  abs
    ( List.foldl'
        (\twiceArea (firstPoint, secondPoint) ->
           let (firstX, firstY) = exactPointCoordinates firstPoint
               (secondX, secondY) = exactPointCoordinates secondPoint
            in twiceArea + firstX * secondY - firstY * secondX)
        0
        (cyclicPairs points)
    )

cyclicPairs :: NonEmpty value -> [(value, value)]
cyclicPairs (firstValue :| remainingValues) =
  zip
    (firstValue : remainingValues)
    (remainingValues <> [firstValue])

testEqualCoincidentResolution :: IO ()
testEqualCoincidentResolution = do
  domain <- squareDomain
  weight <- admittedWeight 3
  a <- admittedSite "a" (Point 4 4) weight
  b <- admittedSite "b" (Point 4 4) weight
  (diagram, receipt) <- requireRight "equivalent coincident power sites" (boundedPowerDiagram domain (b :| [a]))
  assertPublished "coincident canonical representative" (powerCellDisposition "a" diagram)
  assertEqual "equal-function coincident alias" (Just (CoincidentEquivalentTo "a")) (powerCellDisposition "b" diagram)
  assertEqual "equivalent coincidence receipt" 1 (powerDiagramCoincidentEquivalentCells receipt)
  assertEqual "equivalent coincidence is not dominance" 0 (powerDiagramCoincidentDominatedCells receipt)
  (permuted, _) <- requireRight "permuted equivalent sites" (boundedPowerDiagram domain (a :| [b]))
  assertEqual "equivalent resolution permutation invariance" (powerCellDispositions diagram) (powerCellDispositions permuted)

testDominantCoincidentResolution :: IO ()
testDominantCoincidentResolution = do
  domain <- squareDomain
  high <- admittedWeight 3
  low <- admittedWeight 2
  winner <- admittedSite "winner" (Point 4 4) high
  dominated <- admittedSite "dominated" (Point 4 4) low
  (diagram, receipt) <- requireRight "dominant coincident power sites" (boundedPowerDiagram domain (dominated :| [winner]))
  assertPublished "dominant coincident representative" (powerCellDisposition "winner" diagram)
  assertEqual "lower-weight coincident site" (Just (CoincidentDominatedBy "winner")) (powerCellDisposition "dominated" diagram)
  assertEqual "dominant coincidence receipt" 1 (powerDiagramCoincidentDominatedCells receipt)
  assertEqual "dominance is not equivalence" 0 (powerDiagramCoincidentEquivalentCells receipt)
  assertPermutationInvariant "dominant coincident power sites" domain (dominated :| [winner]) diagram

testLowerDimensionalCell :: IO ()
testLowerDimensionalCell = do
  domain <- squareDomain
  zero <- admittedWeight 0
  suppressed <- admittedWeight (-1)
  left <- admittedSite "left" (Point 0 5) zero
  right <- admittedSite "right" (Point 2 5) zero
  middle <- admittedSite "middle" (Point 1 5) suppressed
  let sites = left :| [right, middle]
  (diagram, receipt) <- requireRight "one-dimensional bounded power cell" (boundedPowerDiagram domain sites)
  case powerCellDisposition "middle" diagram of
    Just (LowerDimensionalPowerCell points) ->
      assertEqual
        "middle cell is the exact x=1 segment"
        (List.sort [integerPoint 1 0, integerPoint 1 10])
        (List.sort (NonEmpty.toList points))
    other -> fail ("one-dimensional power cell: expected retained segment, got " <> dispositionTag other)
  assertEqual "lower-dimensional receipt" 1 (powerDiagramLowerDimensionalCells receipt)
  assertEqual "lower-dimensional cell is not empty" 0 (powerDiagramEmptyCells receipt)
  assertEqual "two full-dimensional neighbours" 2 (powerDiagramPublishedCells receipt)
  traverse_ (assertPublishedVerticesWin sites) (powerCellDispositions diagram)
  let layer = powerDiagramPlanarLayer diagram
  assertEqual "derived layer omits the one-dimensional cell" [Just "left", Just "right"] (Map.keys (planarLayerRegions layer))
  assertPermutationInvariant "one-dimensional bounded power cell" domain sites diagram

testDistinctEmptyCell :: IO ()
testDistinctEmptyCell = do
  domain <- squareDomain
  ordinary <- admittedWeight 0
  suppressed <- admittedWeight (-1000)
  winner <- admittedSite "winner" (Point 0 0) ordinary
  hidden <- admittedSite "hidden" (Point 5 5) suppressed
  (diagram, receipt) <- requireRight "distinct empty power cell" (boundedPowerDiagram domain (winner :| [hidden]))
  assertPublished "dominant distinct site" (powerCellDisposition "winner" diagram)
  assertEqual "distinct site may have empty bounded cell" (Just EmptyPowerCell) (powerCellDisposition "hidden" diagram)
  assertEqual "empty disposition receipt" 1 (powerDiagramEmptyCells receipt)
  assertPermutationInvariant "distinct empty power cell" domain (winner :| [hidden]) diagram

testDyadicNearParallelBisectors :: IO ()
testDyadicNearParallelBisectors = do
  domain <- squareDomain
  weight <- admittedWeight 0
  let epsilon = 2 ** (-20) :: Double
  origin <- admittedSite "origin" (Point 0 0) weight
  horizontal <- admittedSite "horizontal" (Point 2 0) weight
  tilted <- admittedSite "tilted" (Point 2 epsilon) weight
  let sites = origin :| [horizontal, tilted]
  (diagram, receipt) <-
    requireRight
      "dyadic near-parallel power bisectors"
      (boundedPowerDiagram domain sites)
  assertEqual "near-parallel cells retain full dimension" 3 (powerDiagramPublishedCells receipt)
  traverse_ (assertPublishedVerticesWin sites) (powerCellDispositions diagram)
  assertPermutationInvariant "dyadic near-parallel power bisectors" domain sites diagram

testBinary64PrecisionSites :: IO ()
testBinary64PrecisionSites = do
  domain <- squareDomain
  firstWeight <- admittedWeight 0.2
  secondWeight <- admittedWeight (-0.3)
  thirdWeight <- admittedWeight 0.7
  firstSite <- admittedSite "first" (Point 0.1 0.3) firstWeight
  secondSite <- admittedSite "second" (Point 9.7 0.2) secondWeight
  thirdSite <- admittedSite "third" (Point 4.9 9.6) thirdWeight
  let sites = firstSite :| [secondSite, thirdSite]
  (diagram, receipt) <-
    requireRight
      "exact binary64 power sites"
      (boundedPowerDiagram domain sites)
  assertEqual "binary64 fixture publishes every cell" 3 (powerDiagramPublishedCells receipt)
  traverse_ (assertPublishedVerticesWin sites) (powerCellDispositions diagram)
  assertPermutationInvariant "exact binary64 power sites" domain sites diagram

testDuplicateLabels :: IO ()
testDuplicateLabels = do
  domain <- squareDomain
  weight <- admittedWeight 0
  firstSite <- admittedSite "duplicate" (Point 1 1) weight
  secondSite <- admittedSite "duplicate" (Point 9 9) weight
  case boundedPowerDiagram domain (firstSite :| [secondSite]) of
    Left (DuplicatePowerSiteLabel "duplicate") -> pure ()
    other -> fail ("duplicate power-site label: expected typed refusal, got " <> show other)

testNonFiniteWeight :: IO ()
testNonFiniteWeight =
  case powerWeight (0 / 0) of
    Left (PowerWeightNonFinite _) -> pure ()
    other -> fail ("non-finite power weight: expected typed refusal, got " <> show other)

uncurry3 :: (a -> b -> c -> result) -> (a, b, c) -> result
uncurry3 function (firstValue, secondValue, thirdValue) = function firstValue secondValue thirdValue

assertPublished :: String -> Maybe (PowerCellDisposition label) -> IO ()
assertPublished _ (Just (PublishedPowerCell _)) = pure ()
assertPublished label other = fail (label <> ": expected published cell, got " <> dispositionTag other)

assertPublishedVerticesWin
  :: NonEmpty (PowerSite String)
  -> (String, PowerCellDisposition String)
  -> IO ()
assertPublishedVerticesWin sites (ownerLabel, disposition) =
  case disposition of
    PublishedPowerCell polygon ->
      assertCellPointsWin ownerLabel sites (convexPolygonPoints polygon)
    LowerDimensionalPowerCell points ->
      assertCellPointsWin ownerLabel sites points
    EmptyPowerCell -> pure ()
    CoincidentEquivalentTo _ -> pure ()
    CoincidentDominatedBy _ -> pure ()

assertCellPointsWin
  :: String
  -> NonEmpty (PowerSite String)
  -> NonEmpty ExactPoint
  -> IO ()
assertCellPointsWin ownerLabel sites points =
  case lookupOwner ownerLabel (NonEmpty.toList sites) of
    Nothing -> fail ("power-cell owner missing: " <> ownerLabel)
    Just owner ->
      traverse_
        (\point -> traverse_ (assertOwnerWinsAt point owner) sites)
        points

lookupOwner :: Eq label => label -> [PowerSite label] -> Maybe (PowerSite label)
lookupOwner label = List.find ((== label) . powerSiteLabel)

assertOwnerWinsAt
  :: ExactPoint
  -> PowerSite String
  -> PowerSite String
  -> IO ()
assertOwnerWinsAt point owner competitor = do
  ownerValue <- powerDistance point owner
  competitorValue <- powerDistance point competitor
  unless (ownerValue <= competitorValue) $
    fail
      ( "power-cell vertex violates source inequality: "
          <> show (powerSiteLabel owner, powerSiteLabel competitor, ownerValue, competitorValue)
      )