{-
Copyright (C) 2011 Dr. Alistair Ward
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
-}
{- |
[@AUTHOR@] Dr. Alistair Ward
[@DESCRIPTION@] Miscellaneous functions for probability-distributions.
-}
module Factory.Math.Probability(
-- * Types
-- ** Data-types
ContinuousDistribution(..),
DiscreteDistribution(..),
-- * Functions
boxMullerTransform,
-- minPositiveFloat,
-- reProfile,
generateStandardizedNormalDistribution,
generateContinuousPopulation,
generatePoissonDistribution,
generateDiscretePopulation
) where
import qualified Control.Arrow
import Control.Arrow((***), (&&&))
import qualified Factory.Data.Interval as Data.Interval
import qualified System.Random
import qualified ToolShed.Data.List
import qualified ToolShed.Data.Pair
import qualified ToolShed.SelfValidate
-- | Describes a /continuous probability-distribution/; .
data ContinuousDistribution f
= UniformDistribution (Data.Interval.Interval f) -- ^ Defines a /Uniform/-distribution within a closed /interval/; .
| NormalDistribution f f -- ^ Defines a /Normal/-distribution with a particular /mean/ and /variance/; .
deriving (Eq, Read, Show)
instance (Num a, Ord a, Show a) => ToolShed.SelfValidate.SelfValidator (ContinuousDistribution a) where
getErrors distribution = ToolShed.SelfValidate.extractErrors $ case distribution of
UniformDistribution interval -> [(Data.Interval.isReversed interval, "Reversed interval='" ++ show interval ++ "'.")]
NormalDistribution _ v -> [(v < 0, "Negative variance=" ++ show v ++ ".")]
-- | Describes a /discrete probability-distribution/; .
data DiscreteDistribution f = PoissonDistribution f deriving (Eq, Read, Show)
instance (Num f, Ord f, Show f) => ToolShed.SelfValidate.SelfValidator (DiscreteDistribution f) where
getErrors (PoissonDistribution lambda) = ToolShed.SelfValidate.extractErrors [(lambda < 0, "Negative lambda=" ++ show lambda ++ ".")]
{- |
* Converts a pair of independent /uniformly distributed/ random numbers, within the /semi-closed/ /unit interval/ /(0 .. 1]/,
to a pair of independent /normally distributed/ random numbers, of standardized /mean/=0, and /variance/=1.
* .
-}
boxMullerTransform :: (Floating f, Ord f, Show f)
=> (f, f) -- ^ Independent, /uniformly distributed/ random numbers, which must be within the /semi-closed unit interval/, /(0, 1]/.
-> (f, f) -- ^ Independent, /normally distributed/ random numbers, with standardized /mean/=0 and /variance/=1.
boxMullerTransform cartesian
| not . uncurry (&&) $ ToolShed.Data.Pair.mirror inSemiClosedUnitInterval cartesian = error $ "Factory.Math.Probability.boxMullerTransform:\tspecified Cartesian coordinates, must be within semi-closed unit-interval (0, 1]; " ++ show cartesian
| otherwise = polarToCartesianTransform $ (sqrt . negate . (* 2) . log *** (* 2) . (* pi)) cartesian
where
inSemiClosedUnitInterval :: (Num n, Ord n) => n -> Bool
inSemiClosedUnitInterval = uncurry (&&) . ((> 0) &&& (<= 1))
polarToCartesianTransform :: Floating f => (f, f) -> (f, f)
polarToCartesianTransform = uncurry (*) . Control.Arrow.second cos &&& uncurry (*) . Control.Arrow.second sin
{- |
* Determines the minimum positive floating-point number, which can be represented by using the parameter's type.
* Only the type of the parameter is relevant, not its value.
-}
minPositiveFloat :: RealFloat a => a -> a
minPositiveFloat = encodeFloat 1 . uncurry (-) . (fst . floatRange &&& floatDigits)
{- |
* Uses the supplied random-number generator,
to generate a conceptually infinite list, of /normally distributed/ random numbers, with standardized /mean/=0, and /variance/=1.
* , .
-}
generateStandardizedNormalDistribution :: (
RealFloat f,
Show f,
System.Random.Random f,
System.Random.RandomGen randomGen
) => randomGen -> [f]
generateStandardizedNormalDistribution = ToolShed.Data.List.linearise . uncurry (zipWith $ curry boxMullerTransform) . ToolShed.Data.Pair.mirror (
System.Random.randomRs (minPositiveFloat undefined, 1)
) . System.Random.split
-- | Stretches and shifts a /standardized normal distribution/ to achieve the required /mean/ and /standard-deviation/.
reProfile :: Num n => n -> n -> [n] -> [n]
reProfile mean standardDeviation = map ((+ mean) . (* standardDeviation))
{- |
* Generates a random sample-population, with the specified continuous probability-distribution.
* When a /Normal distribution/ is requested,
the generated population will only tend towards the requested /mean/ and /variance/ of, as the sample-size tends towards infinity.
Whilst one could arrange for these criteria to be precisely met for any sample-size, the sample would lose a degree of randomness as a result.
-}
generateContinuousPopulation :: (
RealFloat f,
Show f,
System.Random.Random f,
System.Random.RandomGen randomGen
)
=> Int -- ^ number of items.
-> ContinuousDistribution f
-> randomGen -- ^ A generator of /uniformly distributed/ random numbers.
-> [f]
generateContinuousPopulation 0 _ _ = []
generateContinuousPopulation populationSize probabilityDistribution randomGen
| populationSize < 0 = error $ "Factory.Math.Probability.generateDiscretePopulation:\tinvalid population-size=" ++ show populationSize
| not $ ToolShed.SelfValidate.isValid probabilityDistribution = error $ "Factory.Math.Probability.generateContinuousPopulation:\t" ++ ToolShed.SelfValidate.getFirstError probabilityDistribution
| otherwise = take populationSize $ (
case probabilityDistribution of
UniformDistribution interval -> System.Random.randomRs interval
NormalDistribution requiredMean requiredVariance -> reProfile requiredMean (sqrt requiredVariance) . generateStandardizedNormalDistribution
) randomGen
{- |
* Uses the supplied random-number generator,
to generate a conceptually infinite list, of random integers conforming to the /Poisson distribution/ (/mean/=lambda, /variance/=lambda).
* .
* CAVEAT:
uses an algorithm by Knuth, which having a /linear time-complexity/ in /lambda/, can be intolerably slow;
also, the term @exp $ negate lambda@, underflows for large /lambda/;
so for large /lambda/, this implementation returns the appropriate 'NormalDistribution', which is similar for large /lambda/.
-}
generatePoissonDistribution :: (
Integral events,
RealFloat lambda,
Show lambda,
System.Random.Random lambda,
System.Random.RandomGen randomGen
)
=> lambda -- ^ Defines the required approximate value of both /mean/ and /variance/.
-> randomGen
-> [events]
generatePoissonDistribution lambda
| lambda < 0 = error $ "Factory.Math.Probability.generatePoissonDistribution:\tinvalid lambda=" ++ show lambda
| lambda > (
negate . log $ minPositiveFloat lambda --Guard against underflow, in the user-defined type for lambda.
) = filter (>= 0) . map round . reProfile lambda (sqrt lambda) . generateStandardizedNormalDistribution
| otherwise = generator
where
generator = uncurry (:) . (
fst . head . dropWhile (
(> exp (negate lambda)) . snd --CAVEAT: underflows if lambda > (103 :: Float, 745 :: Double).
) . scanl (
\accumulator random -> succ *** (* random) $ accumulator
) (negate 1, 1) . System.Random.randomRs (0, 1) *** generator {-recurse-}
) . System.Random.split
-- | Generates a random sample-population, with the specified discrete probability-distribution.
generateDiscretePopulation :: (
Ord f,
RealFloat f,
Show f,
System.Random.Random f,
System.Random.RandomGen randomGen,
Integral events
)
=> Int -- ^ number of items.
-> DiscreteDistribution f
-> randomGen -- ^ A generator of /uniformly distributed/ random numbers.
-> [events]
generateDiscretePopulation 0 _ _ = []
generateDiscretePopulation populationSize probabilityDistribution randomGen
| populationSize < 0 = error $ "Factory.Math.Probability.generateDiscretePopulation:\tinvalid populationSize=" ++ show populationSize
| not $ ToolShed.SelfValidate.isValid probabilityDistribution = error $ "Factory.Math.Probability.generateDiscretePopulation:\t" ++ ToolShed.SelfValidate.getFirstError probabilityDistribution
| otherwise = take populationSize $ (
case probabilityDistribution of
PoissonDistribution lambda -> generatePoissonDistribution lambda
) randomGen