dtmc-0.3.0.0: benchmark/haskell/Cases.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Cases (benchmarksFor) where
import Control.DeepSeq (NFData (rnf))
import Control.Exception (evaluate)
import Criterion.Main
import Data.Finite (Finite)
import Data.Proxy (Proxy (Proxy))
import Data.Vector.Unboxed qualified as U
import Dataset
import Dtmc.Analysis.Absorption qualified as Absorption
import Dtmc.Analysis.Classification qualified as Classification
import Dtmc.Analysis.Event (DiscreteEvent (AtMost, EqualTo, GreaterThan))
import Dtmc.Analysis.FiniteTime qualified as FiniteTime
import Dtmc.Analysis.HittingTime qualified as Hitting
import Dtmc.Analysis.ReturnTime qualified as Return
import Dtmc.Analysis.Stationary qualified as Stationary
import Dtmc.Analysis.VisitCount qualified as VisitCount
import Dtmc.Distribution.Vector qualified as Vector
import Dtmc.Dynamics qualified as Dynamics
import Dtmc.Simulation qualified as Simulation
import Dtmc.State (finiteStates)
import Dtmc.Transition.Matrix qualified as Matrix
import Force
import GHC.Exts (RealWorld)
import GHC.TypeNats (KnownNat)
import Numeric.Natural (Natural)
import System.Random.MWC qualified as MWC
newtype Prepared n = Prepared {unPrepared :: Matrix.TransitionMatrix (Finite n)}
instance NFData (Prepared n) where
rnf (Prepared matrix) = checksumMatrix matrix `seq` ()
data PreparedDynamics n = PreparedDynamics
{ dynamicsMatrix :: Matrix.TransitionMatrix (Finite n)
, dynamicsInitial :: Vector.DistributionVector (Finite n)
}
instance NFData (PreparedDynamics n) where
rnf prepared =
checksumMatrix (dynamicsMatrix prepared)
`seq` checksumVector (dynamicsInitial prepared)
`seq` ()
newtype PreparedGen = PreparedGen {unPreparedGen :: MWC.Gen RealWorld}
instance NFData PreparedGen where
rnf (PreparedGen generator) = generator `seq` ()
data PreparedScalarLookup n = PreparedScalarLookup
{ scalarLookup :: Finite n -> Double
, scalarLookupState :: Finite n
}
instance NFData (PreparedScalarLookup n) where
rnf prepared =
scalarLookup prepared
`seq` scalarLookupState prepared
`seq` ()
benchmarksFor ::
forall n.
(KnownNat n) =>
Proxy n ->
FilePath ->
Entry ->
Benchmark
benchmarksFor _ dataRoot entry =
env (loadDataset @n dataRoot entry) $ \dataset ->
bgroup
(entryId entry)
( commonBenchmarks entry dataset
++ finiteTimeBenchmarks entry dataset
++ boundedHittingBenchmarks entry dataset
++ absorptionBenchmarks entry dataset
++ occupationBenchmarks entry dataset
++ returnBenchmarks entry dataset
++ visitBenchmarks entry dataset
++ simulationBenchmarks entry dataset
)
commonBenchmarks :: forall n. (KnownNat n) => Entry -> Dataset n -> [Benchmark]
commonBenchmarks entry dataset =
[ bgroup
"construction"
[ bench "public-consumed" $
nf (checksumMatrix . matrixFromRows @n) (datasetRows dataset)
]
, env (preparedDynamics dataset) $ \ ~(PreparedDynamics matrix initial) ->
bgroup
"evolution"
[ bench "one-step" $
nf (checksumVector . Dynamics.evolveVector initial) matrix
, bench "k-10" $
nf (checksumVector . Dynamics.evolveVectorN 10 initial) matrix
, bench "k-100" $
nf (checksumVector . Dynamics.evolveVectorN 100 initial) matrix
]
]
++ powerCases
++ structureCases
++ numericalCases
where
targets = datasetTargets dataset
competing = datasetCompeting dataset
powerCases
| entrySize entry > 500 = []
| otherwise =
[ env (preparedMatrix dataset) $ \ ~(Prepared matrix) ->
bgroup
"power"
[ bench (show powerValue) $
nf (checksumMatrix . Matrix.power powerValue) matrix
| powerValue <- [2, 10, 100]
]
]
structureCases
| entryFamily entry == "dense" && entrySize entry > 500 = []
| otherwise =
[ bgroup
"structure"
( [ bench "classes-lifecycle" $
nf (classesLifecycle (Proxy @n)) (datasetRows dataset)
, bench "classes-cold" $
perRunEnv (preparedGraph dataset) $ \prepared ->
evaluate $!
checksumClasses
(Classification.communicatingClasses (unPrepared prepared))
, env (preparedWarmClasses dataset) $ \ ~(Prepared matrix) ->
bgroup
"classes-warm"
[ bench "access" $
whnf Classification.communicatingClasses matrix
, bench "consumed" $
nf
(checksumClasses . Classification.communicatingClasses)
matrix
]
, bench "irreducible-cold" $
perRunEnv (preparedGraph dataset) $ \prepared ->
evaluate $! Classification.irreducible (unPrepared prepared)
, env (preparedWarmIrreducible dataset) $ \ ~(Prepared matrix) ->
bench "irreducible-warm" $
nf Classification.irreducible matrix
]
++ periodicStructureCases
)
]
where
periodicStructureCases
| entryFamily entry /= "periodic" = []
| otherwise =
[ bench "period-cold" $
perRunEnv (preparedGraph dataset) $ \prepared ->
evaluate $!
checksumPeriod
(Classification.chainPeriod (unPrepared prepared))
, env (preparedWarmPeriod dataset) $ \ ~(Prepared matrix) ->
bench "period-warm" $
nf (checksumPeriod . Classification.chainPeriod) matrix
, bench "cyclic-classes-cold" $
perRunEnv (preparedGraph dataset) $ \prepared ->
evaluate $!
checksumCyclicClasses
(Classification.cyclicClasses (unPrepared prepared))
, env (preparedWarmCyclicClasses dataset) $ \ ~(Prepared matrix) ->
bench "cyclic-classes-warm" $
nf
(checksumCyclicClasses . Classification.cyclicClasses)
matrix
]
numericalCases
| entrySize entry > 500 = []
| otherwise =
[ bench "stationary" $
perRunEnv (preparedGraph dataset) $ \prepared ->
evaluate $!
checksumStationary
( eitherOrFail
(Stationary.stationaryDistributions (unPrepared prepared))
)
, bgroup
"hitting-probability"
[ bench "cold-all-states" $
perRunEnv (preparedGraph dataset) $ \prepared ->
evaluate $!
hittingProbabilityChecksum targets (unPrepared prepared)
, env (preparedWarmHittingProbability dataset) $ \prepared ->
bench "warm-lookup" $
whnf
(\fixture -> scalarLookup fixture (scalarLookupState fixture))
prepared
]
, bgroup
"hitting-time"
[ bench "cold-all-states" $
perRunEnv (preparedGraph dataset) $ \prepared ->
evaluate $!
hittingExpectationChecksum targets (unPrepared prepared)
, env (preparedWarmHittingExpectation dataset) $ \prepared ->
bench "warm-lookup" $
whnf
(\fixture -> scalarLookup fixture (scalarLookupState fixture))
prepared
]
]
++ raceCases
where
raceCases
| entryFamily entry `notElem` ["dense", "low-outdegree"] = []
| otherwise =
[ bench "race/forward-committor" $
perRunEnv (preparedGraph dataset) $ \prepared ->
evaluate $!
raceProbabilityChecksum
targets
competing
(unPrepared prepared)
]
finiteTimeBenchmarks :: forall n. (KnownNat n) => Entry -> Dataset n -> [Benchmark]
finiteTimeBenchmarks entry dataset
| entrySize entry > 100 = []
| otherwise =
[ bench "finite-time/step" $
perRunEnv (preparedMatrix dataset) $ \prepared ->
evaluate $!
FiniteTime.stepProbability
(unPrepared prepared)
source
target
]
++ [ bench ("finite-time/n-step/k-" ++ show steps) $
perRunEnv (preparedMatrix dataset) $ \prepared ->
evaluate $!
FiniteTime.nStepProbability
steps
(unPrepared prepared)
source
target
| steps <- [10, 100]
]
++ [ bench ("finite-time/observation/k-" ++ show time) $
perRunEnv (preparedDynamics dataset) $ \prepared ->
evaluate $!
FiniteTime.probability
(dynamicsInitial prepared)
(dynamicsMatrix prepared)
[FiniteTime.At time target]
| time <- [10, 100]
]
where
source = benchmarkState @n
target = benchmarkTarget dataset
boundedHittingBenchmarks :: forall n. (KnownNat n) => Entry -> Dataset n -> [Benchmark]
boundedHittingBenchmarks entry dataset
| entrySize entry > 100 = []
| otherwise =
[ bench ("hitting/bounded/" ++ label ++ "/k-" ++ show horizon) $
perRunEnv (preparedMatrix dataset) $ \prepared ->
evaluate $!
Hitting.probabilityGivenInitialState
(event horizon)
(unPrepared prepared)
isTarget
initial
| (label, event) <-
[ ("exact", EqualTo)
, ("at-most", AtMost)
, ("greater-than", GreaterThan)
]
, horizon <- [10, 100]
]
where
initial = benchmarkState @n
targets = datasetTargets dataset
isTarget state = state `elem` targets
absorptionBenchmarks :: forall n. (KnownNat n) => Entry -> Dataset n -> [Benchmark]
absorptionBenchmarks entry dataset
| entryFamily entry /= "absorbing" = []
| entrySize entry > 500 = []
| otherwise =
[ bench "fundamental-matrix" $
perRunEnv (preparedGraph dataset) $ \prepared ->
evaluate $!
checksumFundamental
( eitherOrFail
(Absorption.fundamentalMatrix (unPrepared prepared))
)
, bench "absorption-time" $
perRunEnv (preparedGraph dataset) $ \prepared ->
evaluate $! absorptionExpectationChecksum (unPrepared prepared)
, bench "absorption/probabilities" $
perRunEnv (preparedGraph dataset) $ \prepared ->
evaluate $!
absorptionProbabilityChecksum (unPrepared prepared)
]
occupationBenchmarks :: forall n. (KnownNat n) => Entry -> Dataset n -> [Benchmark]
occupationBenchmarks entry dataset
| entryFamily entry `notElem` ["absorbing", "reducible"] = []
| entrySize entry > 250 = []
| otherwise =
[ bench "occupation-matrix" $
perRunEnv (preparedGraph dataset) $ \prepared ->
evaluate $!
checksumOccupation
( eitherOrFail
(VisitCount.occupationMatrix (unPrepared prepared))
)
]
returnBenchmarks :: forall n. (KnownNat n) => Entry -> Dataset n -> [Benchmark]
returnBenchmarks entry dataset = meanRecurrenceCase ++ boundedCases
where
meanRecurrenceCase
| entryFamily entry `notElem` ["dense", "low-outdegree"] = []
| entrySize entry > 500 = []
| otherwise =
[ bench "return/mean-recurrence" $
perRunEnv (preparedGraph dataset) $ \prepared ->
evaluate $! returnExpectationChecksum (unPrepared prepared)
]
boundedCases
| entrySize entry > 100 = []
| otherwise =
[ bench ("return/bounded/k-" ++ show bound) $
perRunEnv (preparedMatrix dataset) $ \prepared ->
evaluate $!
Return.probabilityGivenInitialState
(AtMost bound)
(unPrepared prepared)
(benchmarkState @n)
| bound <- [10, 100]
]
visitBenchmarks :: forall n. (KnownNat n) => Entry -> Dataset n -> [Benchmark]
visitBenchmarks entry dataset
| entrySize entry > 100 = []
| otherwise =
[ bench ("visits/bounded-expectation/k-" ++ show bound) $
perRunEnv (preparedMatrix dataset) $ \prepared ->
evaluate $!
VisitCount.boundedExpectationGivenInitialState
bound
(benchmarkState @n)
(unPrepared prepared)
(== target)
| bound <- [10, 100]
]
where
target = benchmarkTarget dataset
simulationBenchmarks :: forall n. (KnownNat n) => Entry -> Dataset n -> [Benchmark]
simulationBenchmarks entry dataset
| entryFamily entry `notElem` ["dense", "low-outdegree"] = []
| otherwise =
[ env (preparedMatrix dataset) $ \ ~(Prepared matrix) ->
bgroup
"simulation"
[ bench (show transitions) $
perRunEnv (preparedGenerator (entrySeed entry)) $ \generator ->
simulationChecksum
transitions
matrix
(unPreparedGen generator)
| transitions <- [10000, 100000]
]
]
matrixFromRows :: forall n. (KnownNat n) => [[Double]] -> Matrix.TransitionMatrix (Finite n)
matrixFromRows = eitherOrFail . Matrix.fromRows
vectorFromWeights :: forall n. (KnownNat n) => [Double] -> Vector.DistributionVector (Finite n)
vectorFromWeights = eitherOrFail . Vector.fromList
preparedMatrix :: forall n. (KnownNat n) => Dataset n -> IO (Prepared n)
preparedMatrix dataset = do
let matrix = matrixFromRows @n (datasetRows dataset)
_ <- evaluate (checksumMatrix matrix)
pure (Prepared matrix)
preparedDynamics :: forall n. (KnownNat n) => Dataset n -> IO (PreparedDynamics n)
preparedDynamics dataset = do
Prepared matrix <- preparedMatrix dataset
let initial = vectorFromWeights @n (datasetInitialWeights dataset)
_ <- evaluate (checksumVector initial)
pure (PreparedDynamics matrix initial)
preparedGraph :: forall n. (KnownNat n) => Dataset n -> IO (Prepared n)
preparedGraph dataset = do
prepared <- preparedMatrix dataset
case finiteStates :: [Finite n] of
[] -> pure prepared
first : _ -> do
_ <- evaluate (Classification.supportEdge (unPrepared prepared) first first)
pure prepared
preparedWarmClasses :: forall n. (KnownNat n) => Dataset n -> IO (Prepared n)
preparedWarmClasses dataset = do
prepared <- preparedGraph dataset
_ <- evaluate (checksumClasses (Classification.communicatingClasses (unPrepared prepared)))
pure prepared
preparedWarmIrreducible :: forall n. (KnownNat n) => Dataset n -> IO (Prepared n)
preparedWarmIrreducible dataset = do
prepared <- preparedGraph dataset
_ <- evaluate (Classification.irreducible (unPrepared prepared))
pure prepared
preparedWarmPeriod :: forall n. (KnownNat n) => Dataset n -> IO (Prepared n)
preparedWarmPeriod dataset = do
prepared <- preparedGraph dataset
_ <- evaluate (checksumPeriod (Classification.chainPeriod (unPrepared prepared)))
pure prepared
preparedWarmCyclicClasses :: forall n. (KnownNat n) => Dataset n -> IO (Prepared n)
preparedWarmCyclicClasses dataset = do
prepared <- preparedGraph dataset
_ <- evaluate (checksumCyclicClasses (Classification.cyclicClasses (unPrepared prepared)))
pure prepared
preparedWarmHittingProbability ::
forall n.
(KnownNat n) =>
Dataset n ->
IO (PreparedScalarLookup n)
preparedWarmHittingProbability dataset = do
Prepared matrix <- preparedGraph dataset
let rawLookup = Hitting.eventualProbabilityGivenInitialState matrix (datasetTargets dataset)
lookupValue = eitherOrFail . rawLookup
state = benchmarkState @n
_ <- evaluate (lookupValue state)
pure (PreparedScalarLookup lookupValue state)
preparedWarmHittingExpectation ::
forall n.
(KnownNat n) =>
Dataset n ->
IO (PreparedScalarLookup n)
preparedWarmHittingExpectation dataset = do
Prepared matrix <- preparedGraph dataset
let rawLookup = Hitting.expectationGivenInitialState matrix (datasetTargets dataset)
lookupValue queryState = checksumExpectations [eitherOrFail (rawLookup queryState)]
state = benchmarkState @n
_ <- evaluate (lookupValue state)
pure (PreparedScalarLookup lookupValue state)
preparedGenerator :: Int -> IO PreparedGen
preparedGenerator seed =
PreparedGen
<$> MWC.initialize
( U.fromList
[ fromIntegral seed
, fromIntegral (seed * 1664525 + 1013904223)
, 0x9e3779b9
, 0x243f6a88
]
)
classesLifecycle :: forall n. (KnownNat n) => Proxy n -> [[Double]] -> Double
classesLifecycle _ = checksumClasses . Classification.communicatingClasses . matrixFromRows @n
hittingProbabilityChecksum ::
forall n.
(KnownNat n) =>
[Finite n] ->
Matrix.TransitionMatrix (Finite n) ->
Double
hittingProbabilityChecksum targets matrix =
weightedEither
[lookupProbability state | state <- finiteStates]
where
lookupProbability = Hitting.eventualProbabilityGivenInitialState matrix targets
hittingExpectationChecksum ::
forall n.
(KnownNat n) =>
[Finite n] ->
Matrix.TransitionMatrix (Finite n) ->
Double
hittingExpectationChecksum targets matrix =
checksumExpectations
(eitherOrFail (sequence [lookupExpectation state | state <- finiteStates]))
where
lookupExpectation = Hitting.expectationGivenInitialState matrix targets
raceProbabilityChecksum ::
forall n.
(KnownNat n) =>
[Finite n] ->
[Finite n] ->
Matrix.TransitionMatrix (Finite n) ->
Double
raceProbabilityChecksum successful competing matrix =
weightedEither
[lookupProbability state | state <- finiteStates]
where
lookupProbability =
Hitting.raceProbabilityGivenInitialState matrix successful competing
returnExpectationChecksum ::
forall n.
(KnownNat n) =>
Matrix.TransitionMatrix (Finite n) ->
Double
returnExpectationChecksum matrix =
checksumExpectations
(eitherOrFail (sequence [lookupExpectation state | state <- finiteStates]))
where
lookupExpectation = Return.expectationGivenInitialState matrix
absorptionExpectationChecksum ::
forall n.
(KnownNat n) =>
Matrix.TransitionMatrix (Finite n) ->
Double
absorptionExpectationChecksum matrix =
checksumExpectations
(eitherOrFail (sequence [lookupExpectation state | state <- finiteStates]))
where
lookupExpectation = Absorption.expectationGivenInitialState matrix
absorptionProbabilityChecksum ::
forall n.
(KnownNat n) =>
Matrix.TransitionMatrix (Finite n) ->
Double
absorptionProbabilityChecksum =
checksumAbsorption . eitherOrFail . Absorption.probabilityMatrix
benchmarkState :: forall n. (KnownNat n) => Finite n
benchmarkState =
case finiteStates of
[] -> error "benchmark requires a non-empty state space"
first : _ -> first
benchmarkTarget :: forall n. (KnownNat n) => Dataset n -> Finite n
benchmarkTarget dataset =
case datasetTargets dataset of
[] -> error "benchmark dataset requires at least one target"
target : _ -> target
checksumPeriod :: Maybe Natural -> Double
checksumPeriod = maybe 0 fromIntegral
checksumCyclicClasses :: forall n. Maybe [[Finite n]] -> Double
checksumCyclicClasses = maybe 0 (checksumStates . concat)
weightedEither :: (Show problem) => [Either problem Double] -> Double
weightedEither =
foldl'
(\total value -> total + eitherOrFail value)
0
simulationChecksum ::
forall n.
(KnownNat n) =>
Natural ->
Matrix.TransitionMatrix (Finite n) ->
MWC.Gen RealWorld ->
IO Double
simulationChecksum transitions matrix generator = do
case finiteStates :: [Finite n] of
[] -> error "benchmark simulation requires a non-empty state space"
initial : _ -> do
result <- Simulation.simulateMatrix transitions matrix initial generator
pure (checksumStates (eitherOrFail result))