packages feed

lazyppl-1.1.0: test/Replay.hs

module Main where

import Control.Concurrent (forkFinally, newEmptyMVar, putMVar, takeMVar)
import Control.Exception (evaluate, throwIO)
import Control.Monad (forM, forM_, unless)
import Data.List (nub)
import Data.Word (Word64)
import GHC.Float (castDoubleToWord64)
import LazyPPL.Internal.History (History, appendMutation, labelAt, newHistory)
import LazyPPL.Internal.Sampler (randomLabel)
import System.Exit (exitFailure)
import System.Random (StdGen, mkStdGen, split)
import Test.QuickCheck

-- Reference randomTree and mutateTree, as in LazyPPL 1.0.1, built without replay or the seed
-- store.
data RefTree = RefTree Double [RefTree]

randomRefTree :: StdGen -> RefTree
randomRefTree generator =
  let (value, next) = randomLabel generator
  in RefTree value (randomRefTrees next)

randomRefTrees :: StdGen -> [RefTree]
randomRefTrees generator =
  let (left, right) = split generator
  in randomRefTree left : randomRefTrees right

mutateRefTree :: Double -> StdGen -> RefTree -> RefTree
mutateRefTree rate generator (RefTree value children) =
  let (decision, next) = randomLabel generator
      (replacement, rest) = randomLabel next
  in RefTree (if decision < rate then replacement else value)
       (mutateRefTrees rate rest children)

mutateRefTrees :: Double -> StdGen -> [RefTree] -> [RefTree]
mutateRefTrees rate generator (tree:trees) =
  let (left, right) = split generator
  in mutateRefTree rate left tree : mutateRefTrees rate right trees
mutateRefTrees _ _ [] = []

referenceLabel :: [Int] -> RefTree -> Double
referenceLabel [] (RefTree value _) = value
referenceLabel (index:path) (RefTree _ children) =
  referenceLabel path (children !! index)

data ReplayCase = ReplayCase
  { initialSeed :: Int
  , proposalSeed :: Int
  , prefixSteps :: Int
  , laterSteps :: Int
  , intermediateRate :: Double
  , demandedPaths :: [[Int]]
  } deriving Show

instance Arbitrary ReplayCase where
  arbitrary = do
    initial <- chooseInt (minBound, maxBound)
    proposals <- chooseInt (minBound, maxBound)
    prefix <- oneof [chooseInt (0, 256), elements
      [31,32,33,63,64,65,95,96,97,127,128,129,4095,4096,4097]]
    later <- chooseInt (1, 96)
    rate <- choose (0.0001, 0.9999)
    paths <- listOf1 $ do
      depth <- chooseInt (1, 4)
      vectorOf depth (chooseInt (0, 5))
    pure (ReplayCase initial proposals prefix later rate (nub ([] : take 6 paths)))
  shrink input =
    [input {prefixSteps=n} | n <- shrink (prefixSteps input), n >= 0] ++
    [input {initialSeed=n} | n <- shrink (initialSeed input)] ++
    [input {proposalSeed=n} | n <- shrink (proposalSeed input)] ++
    [input {demandedPaths=[]} | not (null (demandedPaths input))]

proposalGenerators :: StdGen -> [StdGen]
proposalGenerators generator =
  let (left, right) = split generator
  in left : proposalGenerators right

appendAll :: History -> Int -> [StdGen] -> IO ()
appendAll history start generators =
  forM_ (zip [start..] generators) $ \(index, generator) ->
    appendMutation history index generator

replayedLabels :: Double -> History -> Int -> [[Int]] -> IO [Word64]
replayedLabels rate history count paths = do
  -- Each path gets its own thread, so with -N2 labels replay concurrently.
  slots <- forM paths $ \path -> do
    slot <- newEmptyMVar
    _ <- forkFinally
      (evaluate (castDoubleToWord64 (labelAt rate history count (reverse path))))
      (putMVar slot)
    pure slot
  answers <- mapM takeMVar slots
  mapM (either throwIO pure) answers

prop_replayPreservesDelayedLabels :: ReplayCase -> Property
prop_replayPreservesDelayedLabels input =
  classify (prefixSteps input >= 96) "at least 96 appended records" $
  classify (prefixSteps input >= 4095) "near the 4096-record growth" $
  classify (any ((> 1) . length) (demandedPaths input)) "nested paths" $
  ioProperty $ do
    let initial = mkStdGen (initialSeed input)
        generators = proposalGenerators (mkStdGen (proposalSeed input))
        (prefix, suffix) = splitAt (prefixSteps input) generators
        paths = nub ([] : demandedPaths input)
    results <- forM [0, intermediateRate input, 1] $ \rate -> do
      history <- newHistory initial
      appendAll history 0 prefix
      -- Later records are appended before any label is demanded, and replay must ignore them.
      appendAll history (prefixSteps input) (take (laterSteps input) suffix)
      actual <- replayedLabels rate history (prefixSteps input) paths
      let reference = foldl (\tree generator -> mutateRefTree rate generator tree)
                        (randomRefTree initial) prefix
          expected = map (castDoubleToWord64 . (`referenceLabel` reference)) paths
      pure $ counterexample ("rate=" ++ show rate ++ ", paths=" ++ show paths ++
                             ", actual=" ++ show actual ++ ", expected=" ++ show expected)
              (actual === expected)
    pure $ conjoin results

main :: IO ()
main = do
  result <- quickCheckResult prop_replayPreservesDelayedLabels
  unless (isSuccess result) exitFailure