packages feed

moonlight-triangulation-1.3.0.0: bench/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Suite.hs

{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DerivingStrategies #-}

-- | Fixture descent, compatibility gluing, and the authoritative benchmark view.
module Moonlight.Triangulation.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.Triangulation.Bench.DelaunayCompare.Domain
import Moonlight.Triangulation.Bench.DelaunayCompare.Native
import Moonlight.Triangulation.BulkLoad (delaunayGeometry)
import Moonlight.Triangulation.Dcel (numInnerFaces, numVertices)
import Moonlight.Triangulation.Types (BuildError, 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"