packages feed

dtmc-0.2.0.0: test/Dtmc/TestSupport.hs

{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module Dtmc.TestSupport (
    testTolerance,
    approxEq,
    approxDistributionEq,
    approxTransitionMatrixEq,
    genSimplexPoint,
    genTransitionRows,
    chunksOf,
    bumpSmallest,
    bumpSmallestInFirstRow,
    setFirstEntry,
    hitProbabilityByState,
    hitEventualProbabilityByState,
    hitRaceProbabilityByState,
    hitExpectationByState,
    returnProbabilityByState,
    returnEventualProbabilityByState,
    returnExpectationByState,
    visitTotalProbabilityByState,
    visitInfiniteProbabilityByState,
    visitTotalExpectationByState,
    absorptionProbabilityByState,
    absorptionExpectationByState,
) where

import Dtmc.Analysis.Absorption qualified as Absorption
import Dtmc.Analysis.Event (
    DiscreteEvent,
 )
import Dtmc.Analysis.Expectation (
    Expectation,
 )
import Dtmc.Analysis.HittingTime qualified as Hit
import Dtmc.Analysis.LinearSystem (
    LinearSystemError,
 )
import Dtmc.Analysis.ReturnTime qualified as Return
import Dtmc.Analysis.VisitCount qualified as Visit
import Dtmc.Distribution.Vector (
    DistributionVector,
    toList,
 )
import Dtmc.State (
    FiniteState,
    finiteStates,
 )
import Dtmc.Transition.Matrix (
    TransitionMatrix,
    toRows,
 )
import Test.QuickCheck (
    Gen,
    choose,
    frequency,
    vectorOf,
 )

hitProbabilityByState ::
    forall state.
    (FiniteState state) =>
    DiscreteEvent ->
    TransitionMatrix state ->
    [state] ->
    [Double]
hitProbabilityByState event matrix targets =
    [ Hit.probabilityGivenInitialState event matrix (`elem` targets) initial
    | initial <- finiteStates
    ]

hitEventualProbabilityByState ::
    forall state.
    (FiniteState state) =>
    TransitionMatrix state ->
    [state] ->
    Either LinearSystemError [Double]
hitEventualProbabilityByState matrix targets =
    traverse
        (Hit.eventualProbabilityGivenInitialState matrix targets)
        finiteStates

hitRaceProbabilityByState ::
    forall state.
    (FiniteState state) =>
    TransitionMatrix state ->
    [state] ->
    [state] ->
    Either LinearSystemError [Double]
hitRaceProbabilityByState matrix successful competing =
    traverse
        (Hit.raceProbabilityGivenInitialState matrix successful competing)
        finiteStates

hitExpectationByState ::
    forall state.
    (FiniteState state) =>
    TransitionMatrix state ->
    [state] ->
    Either LinearSystemError [Expectation]
hitExpectationByState matrix targets =
    traverse
        (Hit.expectationGivenInitialState matrix targets)
        finiteStates

returnProbabilityByState ::
    forall state.
    (FiniteState state) =>
    DiscreteEvent ->
    TransitionMatrix state ->
    [Double]
returnProbabilityByState event matrix =
    [ Return.probabilityGivenInitialState event matrix initial
    | initial <- finiteStates
    ]

returnEventualProbabilityByState ::
    forall state.
    (FiniteState state) =>
    TransitionMatrix state ->
    Either LinearSystemError [Double]
returnEventualProbabilityByState matrix =
    traverse
        (Return.eventualProbabilityGivenInitialState matrix)
        finiteStates

returnExpectationByState ::
    forall state.
    (FiniteState state) =>
    TransitionMatrix state ->
    Either LinearSystemError [Expectation]
returnExpectationByState matrix =
    traverse
        (Return.expectationGivenInitialState matrix)
        finiteStates

visitTotalProbabilityByState ::
    forall state.
    (FiniteState state) =>
    DiscreteEvent ->
    TransitionMatrix state ->
    state ->
    Either LinearSystemError [Double]
visitTotalProbabilityByState event matrix target =
    traverse
        (Visit.totalProbabilityGivenInitialState event matrix target)
        finiteStates

visitInfiniteProbabilityByState ::
    forall state.
    (FiniteState state) =>
    TransitionMatrix state ->
    state ->
    Either LinearSystemError [Double]
visitInfiniteProbabilityByState matrix target =
    traverse
        (Visit.infiniteProbabilityGivenInitialState matrix target)
        finiteStates

visitTotalExpectationByState ::
    forall state.
    (FiniteState state) =>
    TransitionMatrix state ->
    state ->
    Either LinearSystemError [Expectation]
visitTotalExpectationByState matrix target =
    traverse
        (Visit.totalExpectationGivenInitialState matrix target)
        finiteStates

absorptionProbabilityByState ::
    forall state.
    (FiniteState state) =>
    TransitionMatrix state ->
    state ->
    Either LinearSystemError [Double]
absorptionProbabilityByState matrix target =
    traverse
        (Absorption.probabilityGivenInitialState matrix target)
        finiteStates

absorptionExpectationByState ::
    forall state.
    (FiniteState state) =>
    TransitionMatrix state ->
    Either LinearSystemError [Expectation]
absorptionExpectationByState matrix =
    traverse
        (Absorption.expectationGivenInitialState matrix)
        finiteStates

{- | Absolute slack the tests use when comparing floating-point results. Kept
independent of the library's private validation threshold so a change there
cannot silently mask a regression here; the two happen to share a value.
-}
testTolerance :: Double
testTolerance = 1e-9

{- | Absolute-tolerance comparison of two scalar 'Double' results, matching the
@abs (x - y) <= tolerance@ convention of the vector and matrix helpers.
-}
approxEq :: Double -> Double -> Double -> Bool
approxEq tolerance left right =
    abs (left - right) <= tolerance

genSimplexPoint :: Int -> Gen [Double]
genSimplexPoint dimension = do
    entries <- vectorOf dimension genEntry
    let total = sum entries
    if total == 0
        then genSimplexPoint dimension
        else pure (map (/ total) entries)
  where
    genEntry =
        frequency
            [ (3, pure 0)
            , (7, choose (0, 1000))
            ]

{- | Generate a square grid of weights whose rows are probability vectors,
ready for 'Dtmc.Transition.Matrix.fromRows'.
-}
genTransitionRows :: Int -> Gen [[Double]]
genTransitionRows dimension =
    vectorOf dimension (genSimplexPoint dimension)

-- | Split a flat row-major list into rows of the given width.
chunksOf :: Int -> [value] -> [[value]]
chunksOf width values
    | width <= 0 || null values = []
    | otherwise = row : chunksOf width rest
  where
    (row, rest) = splitAt width values

bumpSmallest :: Double -> [Double] -> [Double]
bumpSmallest _ [] = []
bumpSmallest amount entries =
    zipWith bump [0 :: Int ..] entries
  where
    smallestIndex =
        snd (minimum (zip entries [0 :: Int ..]))

    bump index entry
        | index == smallestIndex = entry + amount
        | otherwise = entry

bumpSmallestInFirstRow ::
    Double ->
    [[Double]] ->
    [[Double]]
bumpSmallestInFirstRow _ [] = []
bumpSmallestInFirstRow amount (row : rows) =
    bumpSmallest amount row : rows

setFirstEntry ::
    Double ->
    [[Double]] ->
    [[Double]]
setFirstEntry value ((_ : rest) : rows) =
    (value : rest) : rows
setFirstEntry _ rows = rows

approxTransitionMatrixEq ::
    Double ->
    TransitionMatrix state ->
    TransitionMatrix state ->
    Bool
approxTransitionMatrixEq tolerance left right =
    and (zipWith close (entries left) (entries right))
  where
    entries = concat . toRows
    close x y = abs (x - y) <= tolerance

approxDistributionEq ::
    Double ->
    DistributionVector state ->
    DistributionVector state ->
    Bool
approxDistributionEq tolerance left right =
    and (zipWith close (entries left) (entries right))
  where
    entries = toList
    close x y = abs (x - y) <= tolerance