packages feed

moonlight-planar-1.1.0.0: docs/delaunay-compare/Moonlight/Planar/Bench/DelaunayCompare/Suite.hs

{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DerivingStrategies #-}

-- | Fixture descent, compatibility gluing, and the authoritative benchmark view.
module Moonlight.Planar.Bench.DelaunayCompare.Suite
  ( PreparedSuite
  , withPreparedSuite
  , preflightSuite
  , suiteBenchmarks
  , suiteAgreementMessage
  ) where

import Control.DeepSeq (force)
import Control.Exception (bracket, evaluate)
import Data.Bifunctor (first)
import Data.Either (lefts, rights)
import Data.Foldable (toList, traverse_)
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Vector as Vector
import Moonlight.Planar.Bench.DelaunayCompare.Domain
import Moonlight.Planar.Bench.DelaunayCompare.Native
import Moonlight.Planar.BulkLoad (delaunayGeometry)
import Moonlight.Planar.Dcel (numInnerFaces, numVertices)
import Moonlight.Planar.Types (BuildError)
import Moonlight.Planar.Point (Point (Point))
import Test.Tasty.Bench (Benchmark, bench, bgroup, nf, nfIO)

data NativeSections value = NativeSections
  { nativeSpadeSection :: !value
  , nativeSpadeHierarchySection :: !value
  , nativeCdtSection :: !value
  , nativeDelaunatorSection :: !value
  }
  deriving stock (Functor, Foldable, Traversable)

nativeImplementationSections :: NativeSections NativeImplementation
nativeImplementationSections =
  NativeSections
    { nativeSpadeSection = NativeSpade
    , nativeSpadeHierarchySection = NativeSpadeHierarchy
    , nativeCdtSection = NativeCdt
    , nativeDelaunatorSection = NativeDelaunator
    }

nativeSectionAt :: NativeImplementation -> NativeSections value -> value
nativeSectionAt implementation sections =
  case implementation of
    NativeSpade -> nativeSpadeSection sections
    NativeSpadeHierarchy -> nativeSpadeHierarchySection sections
    NativeCdt -> nativeCdtSection sections
    NativeDelaunator -> nativeDelaunatorSection sections

data PreparedFixture = PreparedFixture
  { preparedSpec :: !FixtureSpec
  , preparedMoonlightPoints :: !(Vector.Vector Point)
  , preparedNativeFixtures :: !(NativeSections NativeFixture)
  }

newtype PreparedSuite = PreparedSuite [PreparedFixture]

withPreparedSuite
  :: NativeApi
  -> (PreparedSuite -> IO (Either CompareObstruction result))
  -> IO (Either CompareObstruction result)
withPreparedSuite api action = do
  prepared <- prepareSuite api
  case prepared of
    Left obstruction -> pure (Left obstruction)
    Right suite -> bracket (pure suite) (releaseSuite api) action

prepareSuite :: NativeApi -> IO (Either CompareObstruction PreparedSuite)
prepareSuite api = do
  outcomes <- traverse (prepareFixture api) allFixtureSpecs
  case NonEmpty.nonEmpty (lefts outcomes) of
    Just obstructions -> do
      traverse_ (releasePreparedFixture api) (rights outcomes)
      pure (Left (ComparisonObstructions obstructions))
    Nothing -> pure (Right (PreparedSuite (rights outcomes)))

prepareFixture :: NativeApi -> FixtureSpec -> IO (Either CompareObstruction PreparedFixture)
prepareFixture api fixture = do
  generated <- generateCoordinates api fixture
  case generated of
    Left status -> pure (Left (FixtureGenerationFailed fixture status))
    Right coordinates -> do
      nativeFixtures <- prepareNativeFixtures api fixture coordinates
      pure $
        PreparedFixture
          fixture
          (Vector.map (uncurry Point) (generatedCoordinatePairs coordinates))
          <$> nativeFixtures

prepareNativeFixtures
  :: NativeApi
  -> FixtureSpec
  -> GeneratedCoordinates
  -> IO (Either CompareObstruction (NativeSections NativeFixture))
prepareNativeFixtures api fixture coordinates = do
  outcomes <- traverse prepareSection nativeImplementationSections
  case sequenceA outcomes of
    Right nativeFixtures -> pure (Right nativeFixtures)
    Left obstruction -> do
      traverse_ (releaseNativeFixture api) (rights (toList outcomes))
      pure (Left obstruction)
 where
  prepareSection implementation = do
    prepared <- prepareNativeFixture api implementation coordinates
    pure $
      first
        (preparationObstruction implementation)
        prepared

  preparationObstruction implementation failure =
    case failure of
      NativePreparationStatus status -> FixturePreparationFailed fixture implementation status
      NativePreparationReturnedNull -> NativePreparedNull fixture implementation

releaseSuite :: NativeApi -> PreparedSuite -> IO ()
releaseSuite api (PreparedSuite fixtures) =
  traverse_ (releasePreparedFixture api) fixtures

releasePreparedFixture :: NativeApi -> PreparedFixture -> IO ()
releasePreparedFixture api = releasePreparedNativeFixtures api . preparedNativeFixtures

releasePreparedNativeFixtures :: NativeApi -> NativeSections NativeFixture -> IO ()
releasePreparedNativeFixtures api = traverse_ (releaseNativeFixture api)

preflightSuite :: NativeApi -> PreparedSuite -> IO (Either CompareObstruction ())
preflightSuite api (PreparedSuite fixtures) = do
  outcomes <- traverse (preflightFixture api) fixtures
  pure $
    case NonEmpty.nonEmpty (lefts outcomes) of
      Just obstructions -> Left (ComparisonObstructions obstructions)
      Nothing -> Right ()

preflightFixture :: NativeApi -> PreparedFixture -> IO (Either CompareObstruction ())
preflightFixture api fixture = do
  moonlightResult <-
    evaluate
      ( force
          (moonlightConstructionSummary (preparedMoonlightPoints fixture))
      )
  nativeOutcomes <- traverse runSection nativeImplementationSections
  pure $ do
    moonlightSummary <-
      first
        (MoonlightConstructionFailed spec)
        moonlightResult
    nativeSummaries <- sequenceA nativeOutcomes
    traverse_
      ( \implementation ->
          requireSummaryAgreement
            spec
            moonlightSummary
            ( implementationOfNative implementation
            , nativeSectionAt implementation nativeSummaries
            )
      )
      nativeImplementationSections
 where
  spec = preparedSpec fixture
  nativeFixtures = preparedNativeFixtures fixture
  runSection implementation =
    first (NativeConstructionFailed spec implementation)
      <$> runNativeFixture api (nativeSectionAt implementation nativeFixtures)

requireSummaryAgreement
  :: FixtureSpec
  -> DelaunaySummary
  -> (Implementation, DelaunaySummary)
  -> Either CompareObstruction ()
requireSummaryAgreement fixture expected (implementation, observed) =
  if observed == expected
    then Right ()
    else Left (SummaryDisagreement fixture implementation expected observed)

moonlightConstructionSummary
  :: Vector.Vector Point
  -> Either BuildError DelaunaySummary
moonlightConstructionSummary points = do
  triangulation <- delaunayGeometry points
  pure
    DelaunaySummary
      { summaryVertexCount = numVertices triangulation
      , summaryTriangleCount = numInnerFaces triangulation
      }

suiteBenchmarks :: NativeApi -> PreparedSuite -> [Benchmark]
suiteBenchmarks api (PreparedSuite fixtures) =
  fmap (sizeBandBenchmarks api fixtures) allSizeBands

sizeBandBenchmarks
  :: NativeApi
  -> [PreparedFixture]
  -> SizeBand
  -> Benchmark
sizeBandBenchmarks api fixtures sizeBand =
  bgroup
    ("comparison: creation benchmark (" <> sizeBandLabel sizeBand <> ")")
    (fmap (implementationBenchmarks api fixtures sizeBand) allImplementations)

implementationBenchmarks
  :: NativeApi
  -> [PreparedFixture]
  -> SizeBand
  -> Implementation
  -> Benchmark
implementationBenchmarks api fixtures sizeBand implementation =
  bgroup
    (implementationLabel implementation)
    ( fmap
        (distributionBenchmarks api fixtures sizeBand implementation)
        allPointDistributions
    )

distributionBenchmarks
  :: NativeApi
  -> [PreparedFixture]
  -> SizeBand
  -> Implementation
  -> PointDistribution
  -> Benchmark
distributionBenchmarks api fixtures sizeBand implementation distribution =
  bgroup
    (pointDistributionLabel distribution)
    ( fmap
        (fixtureBenchmark api implementation)
        ( filter
            (matchesFixture sizeBand distribution . preparedSpec)
            fixtures
        )
    )

matchesFixture :: SizeBand -> PointDistribution -> FixtureSpec -> Bool
matchesFixture sizeBand distribution fixture =
  fixtureSizeBand fixture == sizeBand
    && fixturePointDistribution fixture == distribution

fixtureBenchmark :: NativeApi -> Implementation -> PreparedFixture -> Benchmark
fixtureBenchmark api implementation fixture =
  bench (show (pointCountValue (fixturePointCount (preparedSpec fixture)))) $
    case implementation of
      Moonlight -> nf moonlightConstructionSummary (preparedMoonlightPoints fixture)
      Spade -> nativeBenchmark NativeSpade
      SpadeHierarchy -> nativeBenchmark NativeSpadeHierarchy
      Cdt -> nativeBenchmark NativeCdt
      Delaunator -> nativeBenchmark NativeDelaunator
 where
  nativeFixtures = preparedNativeFixtures fixture
  nativeBenchmark nativeImplementation =
    nfIO (runNativeFixture api (nativeSectionAt nativeImplementation nativeFixtures))

suiteAgreementMessage :: String
suiteAgreementMessage =
  "delaunay-compare agreement: "
    <> show (length allFixtureSpecs)
    <> " fixtures agree on vertex and triangle counts across "
    <> show (length allImplementations)
    <> " implementations"