packages feed

moonlight-triangulation-1.2.0.0: bench/region/Moonlight/Triangulation/RegionBench.hs

{-# LANGUAGE NumericUnderscores #-}

-- | Exact segment-event, overlay, and grouped-region publication receipts.
-- The source families are closed data; measurement is the only effect.
module Moonlight.Triangulation.RegionBench (benchmarks) where

import BenchSupport
  ( latticeFaceBand
  , latticePoints
  , requireRight
  , timedValue
  )
import Control.DeepSeq (force)
import Control.Exception (evaluate)
import Data.Foldable (traverse_)
import qualified Data.Map.Strict as Map
import Data.List.NonEmpty (NonEmpty (..))
import Moonlight.Triangulation
  ( buildTriangulation
  , delaunay
  , unitElementDefaults
  )
import Moonlight.Triangulation.CellSet
  ( exactCellSet
  , exactCellSetEdgeCount
  , exactCellSetFaceCount
  , exactCellSetVertexCount
  )
import Moonlight.Triangulation.Dcel (numInnerFaces)
import Moonlight.Triangulation.Exact (ExactPoint, exactPoint)
import Moonlight.Triangulation.Handles.HandleDefs (VertexId (..))
import Moonlight.Triangulation.Overlay
  ( OverlayReceipt (..)
  , overlayClosedIntersection
  , overlayLayers
  , overlayReceipt
  )
import Moonlight.Triangulation.Minkowski
  ( MinkowskiReceipt
  , convexMinkowskiSum
  , convexPolygon
  , erodeBy
  , minkowskiExactCoordinateBitGrowth
  , minkowskiExactCrossings
  , minkowskiGeneratedConvolutionEdges
  , minkowskiGeneratedPieces
  , minkowskiOutputCells
  , minkowskiOverlayPasses
  , minkowskiSum
  , structuringElement
  )
import Moonlight.Triangulation.Region
  ( PlanarLayer
  , PolygonComponent
  , RegionValidationError
  , exactLoop
  , exactLoopPoints
  , labelledPlanarLayer
  , planarLayer
  , planarLayerRegions
  , planarRegion
  , planarRegionComponents
  , polygonComponent
  , polygonHoleLoops
  , polygonOuterLoop
  )
import Moonlight.Triangulation.Valuation
  ( cellValuations
  , eulerCharacteristicValue
  , exactLengthTerms
  , regionValuations
  , valuationEuler
  , valuationIntrinsic1
  , exactLengthExpression
  )

data OverlayFamily
  = DisjointFamily
  | GridCrossingFamily
  | CollinearOverlapFamily
  deriving stock (Eq, Ord, Show)

benchmarks :: IO ()
benchmarks = do
  traverse_
    (\family -> traverse_ (benchmarkOverlayFamily family) [2, 4, 8])
    [DisjointFamily, GridCrossingFamily, CollinearOverlapFamily]
  benchmarkPublicationReceipt
  traverse_ benchmarkRegionAuthoring [64, 256, 1_024]
  traverse_ benchmarkRegionValuation [64, 256, 1_024]
  traverse_ benchmarkConvexMinkowski [8, 32, 128, 512]
  traverse_
    (\family -> traverse_ (benchmarkOverlaySelectorFamily family) [2, 4, 8])
    [DisjointFamily, GridCrossingFamily, CollinearOverlapFamily]
  benchmarkGeneralMorphology

benchmarkOverlayFamily :: OverlayFamily -> Int -> IO ()
benchmarkOverlayFamily family size = do
  layers <- requireRight (familyLayers family size)
  result <-
    timedValue
      (familyName family <> "-n" <> show size)
      (requireRight (uncurry overlayLayers layers))
  let receipt = overlayReceipt result
      inputAndOutput = overlayInputSegments receipt + overlayRelationEvents receipt
      logarithmicScale = max 1 (ceilingLog2 (overlayInputSegments receipt + 1))
      totalLimit = 128 * inputAndOutput * logarithmicScale
  putStrLn
    ( familyName family
        <> "-receipt: n="
        <> show size
        <> " source-segments="
        <> show (overlayInputSegments receipt)
        <> " relation-events-k="
        <> show (overlayRelationEvents receipt)
        <> " exact-relation-checks="
        <> show (overlayTotalRelationChecks receipt)
        <> " atomic-edges="
        <> show (overlayAtomicEdges receipt)
        <> " arrangement-cells="
        <> show (overlayArrangementCells receipt)
        <> " resident-faces="
        <> show (overlayResidentFaces receipt)
        <> " avl-height="
        <> show (overlaySweepMaximumHeight receipt)
    )
  if overlayTotalRelationChecks receipt <= totalLimit
    then pure ()
    else
      fail
        ( familyName family
            <> " retained superlinear total relation work: "
            <> show (overlayTotalRelationChecks receipt, totalLimit)
        )

benchmarkOverlaySelectorFamily :: OverlayFamily -> Int -> IO ()
benchmarkOverlaySelectorFamily family size = do
  layers <- requireRight (familyLayers family size)
  result <- requireRight (uncurry overlayLayers layers)
  selectedReceipt <-
    timedValue
      (familyName family <> "-selector-n" <> show size)
      (do
         selected <-
           requireRight
             (overlayClosedIntersection (== 1) (== 1) result)
         valuations <- requireRight (cellValuations selected)
         pure
           ( exactCellSetVertexCount selected
           , exactCellSetEdgeCount selected
           , exactCellSetFaceCount selected
           , eulerCharacteristicValue (valuationEuler valuations)
           , length
               ( exactLengthTerms
                   (exactLengthExpression (valuationIntrinsic1 valuations))
               )
           ))
  putStrLn
    ( familyName family
        <> "-selector-receipt: n="
        <> show size
        <> " cells="
        <> show selectedReceipt
    )

familyName :: OverlayFamily -> String
familyName DisjointFamily = "overlay-disjoint"
familyName GridCrossingFamily = "overlay-grid-crossing"
familyName CollinearOverlapFamily = "overlay-collinear-overlap"

benchmarkRegionAuthoring :: Int -> IO ()
benchmarkRegionAuthoring size = do
  componentCount <-
    timedValue
      ("planar-region-disjoint-authoring-n" <> show size)
      (requireRight $ do
         components <-
           traverse
             rectangleComponent
             [ (3 * index, 0, 3 * index + 1, 1)
             | index <- [0 .. size - 1]
             ]
         length . planarRegionComponents <$> planarRegion components)
  if componentCount == size
    then
      putStrLn
        ( "planar-region-disjoint-authoring-receipt: n="
            <> show size
            <> " components="
            <> show componentCount
        )
    else fail ("planar region authoring lost components: " <> show componentCount)

benchmarkRegionValuation :: Int -> IO ()
benchmarkRegionValuation size = do
  components <-
    requireRight
      ( traverse
          rectangleComponent
          [ (3 * index, 0, 3 * index + 1, 1)
          | index <- [0 .. size - 1]
          ]
      )
  region <- requireRight (planarRegion components)
  valuations <-
    timedValue
      ("planar-region-valuations-n" <> show size)
      (evaluate . force =<< requireRight (regionValuations region))
  let receipt =
        ( eulerCharacteristicValue (valuationEuler valuations)
        , length
            ( exactLengthTerms
                (exactLengthExpression (valuationIntrinsic1 valuations))
            )
        )
  if receipt == (size, 1)
    then
      putStrLn
        ( "planar-region-valuations-receipt: n="
            <> show size
            <> " euler="
            <> show size
            <> " radical-terms=1"
        )
    else fail ("planar region valuation receipt mismatch: " <> show receipt)

benchmarkConvexMinkowski :: Int -> IO ()
benchmarkConvexMinkowski halfSize = do
  left <- requireRight (convexPolygon (convexLens halfSize))
  right <- requireRight (convexPolygon (convexLens halfSize))
  result <-
    timedValue
      ("convex-minkowski-n" <> show (2 * halfSize))
      (evaluate (force (convexMinkowskiSum left right)))
  let outputVertices =
        sum
          [ length (exactLoopPoints (polygonOuterLoop component))
          | component <- planarRegionComponents result
          ]
      inputVertices = 4 * halfSize
  if outputVertices <= inputVertices
    then
      putStrLn
        ( "convex-minkowski-receipt: input-vertices="
            <> show inputVertices
            <> " output-vertices="
            <> show outputVertices
        )
    else fail ("convex Minkowski output exceeded n+m: " <> show (outputVertices, inputVertices))

benchmarkGeneralMorphology :: IO ()
benchmarkGeneralMorphology = do
  concaveLoop <-
    requireRight
      ( exactLoop
          ( integerPoint 0 0
              :| [ integerPoint 6 0
                 , integerPoint 6 2
                 , integerPoint 2 2
                 , integerPoint 2 6
                 , integerPoint 0 6
                 ]
          )
      )
  concaveComponent <- requireRight (polygonComponent concaveLoop [])
  concaveRegion <- requireRight (planarRegion [concaveComponent])
  kernelComponent <- requireRight (rectangleComponent (-1, -1, 1, 1))
  kernelRegion <- requireRight (planarRegion [kernelComponent])
  kernelPolygon <-
    requireRight
      (convexPolygon (exactLoopPoints (polygonOuterLoop kernelComponent)))
  element <- requireRight (structuringElement kernelPolygon)
  (sumComponents, sumReceipt) <-
    timedValue
      "general-minkowski-concave"
      (do
         (result, receipt) <- requireRight (minkowskiSum concaveRegion kernelRegion)
         pure (length (planarRegionComponents result), receipt))
  printMorphologyReceipt "general-minkowski-concave" sumComponents sumReceipt
  erosionSourceComponents <-
    requireRight
      ( traverse
          rectangleComponent
          [(0, 0, 6, 6), (9, 0, 15, 6)]
      )
  erosionSource <- requireRight (planarRegion erosionSourceComponents)
  (erosionComponents, erosionReceipt) <-
    timedValue
      "general-erosion-disconnected"
      (do
         (result, receipt) <- requireRight (erodeBy element erosionSource)
         pure (length (planarRegionComponents result), receipt))
  printMorphologyReceipt
    "general-erosion-disconnected"
    erosionComponents
    erosionReceipt
  if minkowskiOverlayPasses sumReceipt > 0
      && minkowskiGeneratedPieces sumReceipt > 0
      && minkowskiOverlayPasses erosionReceipt > 0
    then pure ()
    else fail "general morphology bypassed its declared decomposition/overlay work"

printMorphologyReceipt :: String -> Int -> MinkowskiReceipt -> IO ()
printMorphologyReceipt label outputComponents receipt =
  putStrLn
    ( label
        <> "-receipt: output-components="
        <> show outputComponents
        <> " generated-pieces="
        <> show (minkowskiGeneratedPieces receipt)
        <> " convolution-edges="
        <> show (minkowskiGeneratedConvolutionEdges receipt)
        <> " overlay-passes="
        <> show (minkowskiOverlayPasses receipt)
        <> " exact-crossings="
        <> show (minkowskiExactCrossings receipt)
        <> " output-cells="
        <> show (minkowskiOutputCells receipt)
        <> " coordinate-bit-growth="
        <> show (minkowskiExactCoordinateBitGrowth receipt)
    )

convexLens :: Int -> NonEmpty ExactPoint
convexLens halfSize =
  let maximumIndex = max 1 (halfSize - 1)
      height = 2 * maximumIndex * maximumIndex + 1
      lower =
        [ integerPoint index (index * index)
        | index <- [1 .. maximumIndex]
        ]
      upper =
        [ integerPoint index (height - index * index)
        | index <- reverse [0 .. maximumIndex]
        ]
   in integerPoint 0 0 :| (lower <> upper)

familyLayers
  :: OverlayFamily
  -> Int
  -> Either RegionValidationError (PlanarLayer Int, PlanarLayer Int)
familyLayers family size =
  case family of
    DisjointFamily ->
      (,)
        <$> layerFromRectangles
          [ (3 * index, 0, 3 * index + 1, 1)
          | index <- [0 .. size - 1]
          ]
        <*> planarLayer 0 Map.empty
    GridCrossingFamily ->
      (,)
        <$> layerFromRectangles
          [ (3 * index, 0, 3 * index + 1, 3 * size - 1)
          | index <- [0 .. size - 1]
          ]
        <*> layerFromRectangles
          [ (0, 3 * index, 3 * size - 1, 3 * index + 1)
          | index <- [0 .. size - 1]
          ]
    CollinearOverlapFamily ->
      (,)
        <$> layerFromRectangles
          [ (3 * index, 0, 3 * index + 2, 2)
          | index <- [0 .. size - 1]
          ]
        <*> layerFromRectangles
          [ (3 * index + 1, 0, 3 * index + 3, 1)
          | index <- [0 .. size - 1]
          ]

layerFromRectangles
  :: [(Int, Int, Int, Int)]
  -> Either RegionValidationError (PlanarLayer Int)
layerFromRectangles rectangles = do
  components <- traverse rectangleComponent rectangles
  region <- planarRegion components
  planarLayer 0 (Map.singleton 1 region)

rectangleComponent
  :: (Int, Int, Int, Int)
  -> Either RegionValidationError PolygonComponent
rectangleComponent (minimumX, minimumY, maximumX, maximumY) = do
  loop <-
    exactLoop
      ( integerPoint minimumX minimumY
          :| [ integerPoint maximumX minimumY
             , integerPoint maximumX maximumY
             , integerPoint minimumX maximumY
             ]
      )
  polygonComponent loop []

integerPoint :: Int -> Int -> ExactPoint
integerPoint x y =
  exactPoint
    (fromIntegral x)
    (fromIntegral y)

benchmarkPublicationReceipt :: IO ()
benchmarkPublicationReceipt = do
  built <-
    requireRight (delaunay unitElementDefaults (latticePoints 440 272))
  triangulation <- evaluate (force (buildTriangulation built))
  published <-
    timedValue
      "labelled-planar-layer-publication"
      (requireRight (labelledPlanarLayer (-1) triangulation (latticeFaceBand triangulation)))
  let components =
        concatMap planarRegionComponents (Map.elems (planarLayerRegions published))
      holeCount = sum (map (length . polygonHoleLoops) components)
      exactCoordinateCount =
        sum
          [ length (exactLoopPoints (polygonOuterLoop component))
              + sum (map (length . exactLoopPoints) (polygonHoleLoops component))
          | component <- components
          ]
      receipt =
        ( numInnerFaces triangulation
        , length components
        , length components
        , holeCount
        , exactCoordinateCount
        )
  if receipt == (239_360, 22, 22, 0, 88)
    then
      putStrLn
        "labelled-planar-layer-publication-receipt: faces=239360 components=22 outer-loops=22 holes=0 exact-coordinates=88"
    else fail ("labelled planar layer receipt mismatch: " <> show receipt)
  sparseReceipt <-
    timedValue
      "exact-cell-set-sparse-selection"
      (do
         selected <- requireRight (exactCellSet triangulation [VertexId 0] [] [])
         pure
           ( exactCellSetVertexCount selected
           , exactCellSetEdgeCount selected
           , exactCellSetFaceCount selected
           ))
  if sparseReceipt == (1, 0, 0)
    then putStrLn "exact-cell-set-sparse-selection-receipt: vertices=1 edges=0 faces=0"
    else fail ("sparse exact cell selection receipt mismatch: " <> show sparseReceipt)

ceilingLog2 :: Int -> Int
ceilingLog2 target = length (takeWhile (< target) (iterate (* 2) 1))