packages feed

master-plan-0.1.0.0: test/MasterPlan/DataSpec.hs

{-# LANGUAGE UnicodeSyntax #-}
module MasterPlan.DataSpec (spec) where

import           Control.Monad.State
import           Data.Bool             (bool)
import qualified Data.List.NonEmpty    as NE
import qualified Data.Map              as M
import           Data.Maybe            (fromJust)
import           MasterPlan.Arbitrary  ()
import           MasterPlan.Data
import           System.Random
import           System.Random.Shuffle (shuffle')
import           Test.Hspec
import           Test.QuickCheck       hiding (sample)
import           Test.Hspec.QuickCheck (prop)

-- |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 ⇒ ProjectSystem → ProjectExpr → State g (Bool, Cost)
simulate sys (Reference n) =
   case M.lookup n (bindings sys) of
     Just (BindingAtomic _ c t p) ->
       do r <- state $ randomR (0, 1)
          let remainingProgress =  1 - p
              effectiveTrust = p + t * remainingProgress
              effectiveCost = c * remainingProgress
          pure (effectiveTrust > r, effectiveCost)
     Just (BindingExpr _ p)       -> simulate sys p -- TODO: avoid cyclic
     Just (BindingPlaceholder _)  -> pure (True, defaultCost)
     Nothing                      -> pure (True, defaultCost)

simulate sys (Sequence ps)   = simulateConjunction sys $ NE.toList ps
simulate sys (Product ps)    = simulateConjunction sys $ NE.toList ps
simulate sys (Sum ps)        =
  simulate' $ NE.toList ps
 where
   simulate' ∷ RandomGen g ⇒ [ProjectExpr] → State g (Bool, Cost)
   simulate' [] = pure (False, 0)
   simulate' (p:rest) = do (success, c) <- simulate sys 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 ⇒ ProjectSystem → [ProjectExpr] → State g (Bool, Cost)
simulateConjunction _ []       = pure (True, 0)
simulateConjunction sys (p:rest) = do (success, c) <- simulate sys p
                                      if success then do
                                        (success', c') <- simulateConjunction sys 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 → ProjectSystem → ProjectExpr → State g (Trust, Cost)
monteCarloTrustAndCost n sys p = do results <- replicateM n $ simulate sys p
                                    let trusts = map (bool 0 1 . fst) results
                                        costs  = map snd results
                                    pure (sum trusts / fromIntegral n,
                                          sum costs / fromIntegral n)

aproximatelyEqual ∷ Float -> Float -> Float → Float -> 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 = aproximatelyEqual 0.05 0.05

    prop "monte-carlo and analytical implementations should agree" $ do
        let p = Reference "root"
            monteCarloAndAnalyticalMustAgree ∷ ProjectSystem -> Property
            monteCarloAndAnalyticalMustAgree sys =
                counterexample "disagree on cost"  (cost'  `eq` cost  sys p) .&&.
                counterexample "disagree on trust" (trust' `eq` trust sys p)
              where
                (trust', cost') = evalState (monteCarloTrustAndCost 50000 sys p) g
        monteCarloAndAnalyticalMustAgree

  describe "simplification" $ do

    let eq = aproximatelyEqual 0.005 0.005

    prop "is irreductible" $ do
        let simplificationIsIrreductible :: ProjectExpr -> Property
            simplificationIsIrreductible p =
                let p' = simplifyProj p
                    p'' = simplifyProj p'
                 in p /= p' ==> p' == p''
        simplificationIsIrreductible

    prop "is stable" $ do
      let simplifyIsStable :: ProjectSystem -> Property
          simplifyIsStable sys =
            let sys' = simplify sys
                p    = Reference "root"
             in cost sys p `eq` cost sys' p .&&. trust sys p `eq` trust sys' p

      simplifyIsStable

  describe "prioritization" $ do

    let shuffleProjs :: NE.NonEmpty ProjectExpr -> IO (NE.NonEmpty ProjectExpr)
        shuffleProjs ps = do ps' <- NE.toList <$> mapM shuffleProj ps
                             g <- newStdGen
                             pure $ NE.fromList $ shuffle' ps' (length ps') g

        shuffleProj :: ProjectExpr -> IO ProjectExpr
        shuffleProj (Sum ps)      = Sum <$> shuffleProjs ps
        shuffleProj (Product ps)  = Product <$> 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 :: ProjectSystem -> Property
          prioritizeMinimizesCost sys =
            let (BindingExpr _ p) = fromJust $ M.lookup "root" $ bindings sys
                op = prioritizeProj sys p
                ocost = cost sys op
                otrust = trust sys op
                costIsLessOrEqual p' =
                  counterexample ("variation has smaller cost: " ++ show p') $ ocost <= cost sys p'
                trustIsSame p' = otrust `eq` trust sys p'
            in ioProperty $ do variations <- replicateM 10 (shuffleProj p)
                               return $ conjoin (map costIsLessOrEqual variations) .&.
                                        conjoin (map trustIsSame variations)
      prioritizeMinimizesCost