copilot-libraries-4.8.2: src/Copilot/Library/Random.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
-- |
-- Basic pseudo-random number generation.
--
-- Information on performance of a selection of pseudo-random number generation
-- algorithms, including those implemented below, can be found at:
-- https://prng.di.unimi.it/
--
-- Important: these pseudo-random number generators are NOT cryptographically
-- secure generators.
module Copilot.Library.Random
( randomW32
, randomW64
, boundedRandomIntegral
, proportionalRandomBool
, weightedRandomSelection
)
where
-- External imports
import Copilot.Language (Array, Stream, Typed, Word32, Word64, arrayElems,
constant, mod, mux, (!), (++), (.<<.), (.>>.), (.^.),
(<), (<=))
import GHC.TypeLits (KnownNat, type (<=))
import Prelude hiding (mod, (++), (<), (<=))
-- | Given two Word32 seeds, produce a pseudo-random stream of Word32 numbers,
-- which has a period of 2^32 - 1. Neither of the seeds should be zero, since
-- the underlying randomization algorithm maps zero to itself.
--
-- This function uses an Xorshift+ algorithm.
randomW32 :: Word32 -> Word32 -> Stream Word32
randomW32 0 _ = error "randomW32: seed cannot be zero."
randomW32 _ 0 = error "randomW32: seed cannot be zero."
randomW32 seed1 seed2 = s1 + s2
where
s1 :: Stream Word32
s1 = [seed1] ++ (xorshift32 s1)
s2 :: Stream Word32
s2 = [seed2] ++ (xorshift32 s2)
-- This is a simple implementation of an Xorshift algorithm
xorshift32 :: Stream Word32 -> Stream Word32
xorshift32 x1 = x4 -- Note: 0 maps to itself with this
where
x4 = x3 .^. (x3 .<<. (5 :: Stream Word32))
x3 = x2 .^. (x2 .>>. (17 :: Stream Word32))
x2 = x1 .^. (x1 .<<. (13 :: Stream Word32))
-- | Given two Word64 seeds, produce a pseudo-random stream of Word64 numbers,
-- which has a period of 2^128 - 1. Neither of the seeds should be zero, as
-- doing so will negatively impact the quality of the randomness.
--
-- This function uses an Xorshiftrplus algorithm.
randomW64 :: Word64 -> Word64 -> Stream Word64
randomW64 0 _ = error "randomW64: seed cannot be zero."
randomW64 _ 0 = error "randomW64: seed cannot be zero."
randomW64 seed1 seed2 = x
where
s0 :: Stream Word64
s0 = [seed1] ++ s1
s1 :: Stream Word64
s1 = [seed2] ++ (x + s1)
x :: Stream Word64
x = xorshiftr128plus s0 s1
xorshiftr128plus :: Stream Word64 -> Stream Word64 -> Stream Word64
xorshiftr128plus x1 y = x4
where
x4 = x3 .^. y
x3 = x2 .^. (x2 .>>. (17 :: Stream Word64))
x2 = x1 .^. (x1 .<<. (23 :: Stream Word64))
-- | Given a stream of random integral numbers, a minimum value, and a maximum
-- value, produce a stream of random values greater than or equal to the
-- minimum and less than the maximum value, i.e., [@mN@, @mX@).
boundedRandomIntegral :: (Typed a, Integral a)
=> Stream a -> a -> a -> Stream a
boundedRandomIntegral gen mN mX = (gen `mod` constant (mX - mN)) + constant mN
-- | Given a stream of random integral numbers and two integers defining the
-- numerator and denominator of a rational probability, produce a boolean
-- stream that is randomly @True@ with the given probability. That is to say,
-- given a numerator @n@ and denominator @d@, then, over a sufficiently large
-- sample size, the resulting stream @s@ is @True@ @n@ out of @d@ times.
proportionalRandomBool :: (Typed a, Integral a)
=> Stream a -> a -> a -> Stream Bool
proportionalRandomBool gen n d = (gen `mod` constant d) < constant n
-- | Given a stream of random integral numbers, an array of values, and an
-- equal length array of weights, produce a stream of random values from the
-- given array with probabilities proportional to the given weights.
weightedRandomSelection :: forall a b s
. (KnownNat s, s <= MaxWord32, Typed a, Typed b,
Integral a, Bounded a)
=> Stream a -> Array s b -> Array s a -> Stream b
weightedRandomSelection gen values weights
| overflow
= error "weightedRandomSelection: sum of weights overflows type"
| signedType
= error "weightedRandomSelection: type of weights array must be unsigned"
| otherwise
= constant values ! idx
where
-- Check if cumulativeSum decreases at any point. For unsigned integral
-- types, a decreasing element in this cumulative sequence corresponds to
-- an overflow.
overflow = or (zipWith (>) cumulativeSum (drop 1 cumulativeSum))
signedType = 0 > (minBound :: a)
idx :: Stream Word32
idx = countLeq cumulativeSum (gen `mod` constant totalSum)
countLeq :: [a] -> Stream a -> Stream Word32
countLeq xs v = sum [ mux (constant x <= v) 1 0 | x <- xs ]
-- ^ get number of elements in @xs@ less than or equal to @v@.
-- For non-decreasing @xs@, this is equal to the index of the first
-- element greater than @v@.
cumulativeSum :: [a]
cumulativeSum = scanl1 (+) (arrayElems weights)
totalSum :: a
totalSum = last cumulativeSum
-- | Type-level representation of the upper bound on an unsigned 32-bit
-- integer.
type MaxWord32 = 4294967295 -- 2^32 - 1