yampa-test-0.14.1: tests/Test/FRP/Yampa/Random.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE ForeignFunctionInterface #-}
-- |
-- Description : Test cases for signal functions working with random values.
-- Copyright : (c) Ivan Perez, 2023
-- Authors : Ivan Perez
module Test.FRP.Yampa.Random
( tests
)
where
#if __GLASGOW_HASKELL__ < 708
import Data.Bits (bitSize)
#endif
#if __GLASGOW_HASKELL__ >= 708
import Data.Bits (bitSizeMaybe)
#endif
import Data.Bits (Bits, popCount)
import Data.Maybe (fromMaybe)
import Data.Word (Word32, Word64)
import Foreign.C (CFloat(..))
import System.Random (mkStdGen)
import Test.QuickCheck hiding (once, sample)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.QuickCheck (testProperty)
import FRP.Yampa (DTime, Event (..), embed, isEvent, noise, noiseR,
occasionally, second)
import FRP.Yampa.QuickCheck (Distribution (DistRandom), generateStream)
import FRP.Yampa.Stream (SignalSampleStream)
tests :: TestTree
tests = testGroup "Regression tests for FRP.Yampa.Random"
[ testProperty "noise (0, qc)" propNoise
, testProperty "noiseR (0, qc)" propNoiseR
, testProperty "occasionally (0, qc)" propOccasionally
]
-- * Noise (i.e. random signal generators) and stochastic processes
propNoise :: Property
propNoise =
forAll genSeed $ \seed ->
forAll myStream $ \stream ->
isRandom (embed (noise (mkStdGen seed)) (structure stream) :: [Word32])
where
-- Generator: Input stream.
--
-- We provide a number of samples; otherwise, deviations might not indicate
-- lack of randomness for the signal function.
myStream :: Gen (SignalSampleStream ())
myStream =
generateStream DistRandom (Nothing, Nothing) (Just (Left numSamples))
-- Generator: Random generator seed
genSeed :: Gen Int
genSeed = arbitrary
-- Constant: Number of samples in the stream used for testing.
--
-- This number has to be high; numbers 100 or below will likely not work.
numSamples :: Int
numSamples = 400
propNoiseR :: Property
propNoiseR =
forAll genSeed $ \seed ->
forAll myStream $ \stream ->
-- True if the noise signal is within the given bounds, and it is random
-- when constrained to that range.
let output = embed (noiseR bounds (mkStdGen seed)) (structure stream)
in all (`isInRange` bounds) output && isRandom (constrainTypes output)
where
-- Generator: Input stream.
--
-- We provide a number of samples; otherwise, deviations might not indicate
-- lack of randomness for the signal function.
myStream :: Gen (SignalSampleStream ())
myStream =
generateStream DistRandom (Nothing, Nothing) (Just (Left numSamples))
-- Generator: Random generator seed
genSeed :: Gen Int
genSeed = arbitrary
-- Constant: Bounds used for the test.
--
-- We bound the numbers generated to the 32-bit range, but express it
-- using the type of Word64.
bounds :: (Word64, Word64)
bounds = (min32, max32)
where
min32 = fromIntegral (minBound :: Word32)
max32 = fromIntegral (maxBound :: Word32)
-- Constant: Number of samples in the stream used for testing.
--
-- This number has to be high; numbers 100 or below will likely not work.
numSamples :: Int
numSamples = 400
-- Constrain the types of the argument list to the output type.
--
-- For this test to work, this type must be consistent with the bounds
-- chosen in the constant 'bounds'.
constrainTypes :: [Word64] -> [Word32]
constrainTypes = map fromIntegral
-- | True if the argument is within the given range, false otherwise.
isInRange :: Ord a => a -> (a, a) -> Bool
isInRange x (minB, maxB) = minB <= x && x <= maxB
propOccasionally :: Property
propOccasionally =
forAll genDt $ \avgDt ->
forAll genOutput $ \b ->
forAll genSeed $ \seed ->
-- We pass avgDt / 10 as max time delta to myStream to ensure that the
-- stream produces frequent samples.
forAll (myStream (avgDt / 10)) $ \stream ->
-- True if all events in the output contain the value 'b',
-- the number of events produced is roughtly as expected.
let output =
embed (occasionally (mkStdGen seed) avgDt b) (structure stream)
-- Difference between the number of samples produced and expected
diffNumSamples = abs (actualOcurrences - expectedOccurrences)
actualOcurrences = length $ filter isEvent output
expectedOccurrences = round (streamTime / avgDt)
streamTime = sum $ map fst $ snd stream
in all (== Event b) (filter isEvent output) && diffNumSamples < margin
where
-- Generator: Input stream.
--
-- We provide a number of samples; otherwise, deviations might not indicate
-- lack of randomness for the signal function.
--
-- We also provide the max dt and ensure that samples are
myStream :: DTime -> Gen (SignalSampleStream ())
myStream maxDT =
generateStream
DistRandom
(Nothing, (Just maxDT))
(Just (Left numSamples))
-- Generator: Random generator seed
genDt :: Gen Double
genDt = fmap getPositive arbitrary
-- Generator: Random generator seed
genSeed :: Gen Int
genSeed = arbitrary
-- Generator: Random value generator
genOutput :: Gen Int
genOutput = arbitrary
-- Constant: Number of samples in the stream used for testing.
--
-- This number has to be high; numbers 100 or below will likely not work.
numSamples :: Int
numSamples = 400
-- Constant: Max difference accepted between actual occurrences and
-- expected occurrences
margin :: Int
margin = round (fromIntegral numSamples * 0.05)
-- * Auxiliary definitions
-- | Check whether a list of values exhibits randomness.
--
-- This function implements the Frequence (Monobit) Test, as described in
-- Section 2.1 of "A Statistical Test Suite for Random and Pseudorandom Number
-- Generators for Cryptographic Applications", by Rukhin et al.
isRandom :: Bits a => [a] -> Bool
isRandom ls = pValue >= 0.01
where
pValue = erfc (sObs / sqrt 2)
sObs = abs sn / sqrt n
n = fromIntegral $ elemSize * length ls
sn = sum $ map numConv ls
-- Number of bits per element
elemSize :: Int
elemSize =
-- bitSize' ignores the argument, so it's ok if the list is empty
bitSize' $ head ls
-- Substitute each digit e in the binary representation of the input value
-- by 2e – 1, and add the results.
numConv :: Bits a => a -> Float
numConv x = fromIntegral $ numOnes - numZeroes
where
numOnes = popCount x
numZeroes = elemSize - popCount x
-- Number of bits per element
elemSize = bitSize' x
-- | Complementary Error Function, compliant with the definition of erfcf in
-- ANSI C.
erfc :: Float -> Float
erfc = realToFrac . erfcf . realToFrac
-- | ANSI C function erfcf defined in math.h
foreign import ccall "erfcf" erfcf :: CFloat -> CFloat
-- | Transform SignalSampleStreams into streams of differences.
structure :: (a, [(b, a)]) -> (a, [(b, Maybe a)])
structure (x, xs) = (x, map (second Just) xs)
-- | Implementation of bitSize that uses bitSize/bitSizeMaybe depending on the
-- version of base available.
bitSize' :: Bits a => a -> Int
bitSize' =
#if __GLASGOW_HASKELL__ < 708
bitSize
#else
fromMaybe 0 . bitSizeMaybe
#endif