master-plan-0.3.1: test/MasterPlan/DataSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE UnicodeSyntax #-}
module MasterPlan.DataSpec (spec) where
import Control.Monad.State
import Data.Bool (bool)
import qualified Data.List.NonEmpty as NE
import MasterPlan.Arbitrary ()
import MasterPlan.Data
import System.Random
import System.Random.Shuffle (shuffle')
import Test.Hspec
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck hiding (sample)
-- |Sample the simulation model of the execution of a project.
-- It's a stateful computation with the random generator, which computes
-- a 2-tuple with a Boolean: whether the execution was successful (A Bernoulli
-- sample from trust), and the total actual cost incurred.
simulate ∷ RandomGen g ⇒ Project a → State g (Bool, Cost)
simulate (Atomic _ (Cost c) (Trust t) (Progress p)) =
do r <- state $ randomR (0, 1)
let remainingProgress = 1 - p
effectiveTrust = p + t * remainingProgress
effectiveCost = c * remainingProgress
pure (effectiveTrust > r, Cost effectiveCost)
simulate (Annotated _) = pure (True, 0)
simulate (Sequence _ ps) = simulateConjunction $ NE.toList ps
simulate (Product _ ps) = simulateConjunction $ NE.toList ps
simulate (Sum _ ps) =
simulate' $ NE.toList ps
where
simulate' ∷ RandomGen g ⇒ [Project a] → State g (Bool, Cost)
simulate' [] = pure (False, 0)
simulate' (p:rest) = do (success, c) <- simulate p
if success then
pure (True, c)
else
do (success', c') <- simulate' rest
pure (success', c + c')
-- |Helper function that samples from a sequence of projects to be executed in
-- order, and which all must be successful for the end result to be succesful.
-- This is the case for sequences, and products (in a particular permutation).
simulateConjunction ∷ RandomGen g ⇒ [Project a] → State g (Bool, Cost)
simulateConjunction [] = pure (True, 0)
simulateConjunction (p:rest) = do (success, c) <- simulate p
if success then do
(success', c') <- simulateConjunction rest
pure (success', c + c')
else
pure (False, c)
-- |Compute a project's trust and cost via a Monte Carlo method of computing
-- the average of a handful of samples.
monteCarloTrustAndCost ∷ RandomGen g ⇒ Int → ProjectExpr → State g (Trust, Cost)
monteCarloTrustAndCost n p = do results <- replicateM n $ simulate p
let trusts = map (bool 0 1 . fst) results
costs = map snd results
pure (sum trusts / fromIntegral n,
sum costs / fromIntegral n)
aproximatelyEqual ∷ (Show a, Real a, Fractional a) => a -> a -> a → a -> Property
aproximatelyEqual alpha beta x y =
counterexample (show x ++ " /= " ++ show y) $ diff <= max relError beta
where
relError = alpha * max (abs x) (abs y)
diff = abs $ x - y
spec ∷ Spec
spec = do
describe "trust and cost" $ do
let g = mkStdGen 837183
let eq :: (Show a, Real a, Fractional a) => a -> a -> Property
eq = aproximatelyEqual 0.05 0.05
prop "monte-carlo and analytical implementations should agree" $ do
let monteCarloAndAnalyticalMustAgree ∷ ProjectExpr -> Property
monteCarloAndAnalyticalMustAgree p =
counterexample "disagree on cost" (cost' `eq` cost p) .&&.
counterexample "disagree on trust" (trust' `eq` trust p)
where
(trust', cost') = evalState (monteCarloTrustAndCost 50000 p) g
monteCarloAndAnalyticalMustAgree
describe "simplification" $ do
let eq :: (Show a, Real a, Fractional a) => a -> a -> Property
eq = aproximatelyEqual 0.005 0.005
prop "is irreductible" $ do
let simplificationIsIrreductible :: ProjectExpr -> Property
simplificationIsIrreductible p =
let p' = simplify p
p'' = simplify p'
in p /= p' ==> p' == p''
simplificationIsIrreductible
prop "is stable" $ do
let simplifyIsStable :: ProjectExpr -> Property
simplifyIsStable p =
let p' = simplify p
in cost p `eq` cost p' .&&. trust p `eq` trust p'
simplifyIsStable
describe "prioritization" $ do
let shuffleProjs :: NE.NonEmpty (Project a) -> IO (NE.NonEmpty (Project a))
shuffleProjs ps = do ps' <- NE.toList <$> mapM shuffleProj ps
g <- newStdGen
pure $ NE.fromList $ shuffle' ps' (length ps') g
shuffleProj :: Project a -> IO (Project a)
shuffleProj (Sum r ps) = Sum r <$> shuffleProjs ps
shuffleProj (Product r ps) = Product r <$> shuffleProjs ps
shuffleProj p = pure p
prop "minimize cost and keep trust stable" $ do
-- This test verifies that for any arbitrary project tree, the
-- prioritized version of it will have the minimum cost.
let eq = aproximatelyEqual 0.005 0.005
let prioritizeMinimizesCost :: ProjectExpr -> Property
prioritizeMinimizesCost p =
let op = prioritize p
ocost = cost op
otrust = trust op
costIsLessOrEqual p' =
counterexample ("variation has smaller cost: " ++ show p') $ ocost <= cost p'
trustIsSame p' = otrust `eq` trust p'
in ioProperty $ do variations <- replicateM 10 (shuffleProj p)
return $ conjoin (map costIsLessOrEqual variations) .&.
conjoin (map trustIsSame variations)
prioritizeMinimizesCost