packages feed

prob-fx-0.1.0.1: src/PrimDist.hs

{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE GADTs, TypeOperators #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE EmptyCase #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE ImplicitParams #-}
{-# OPTIONS_GHC -Wno-incomplete-patterns #-}

{- | A GADT encoding of (a selection of) primitive distributions 
    along with their corresponding sampling and density functions.
-}

module PrimDist (
  -- * Primitive distribution
    PrimDist(..)
  , PrimVal
  , IsPrimVal(..)
  , pattern PrimDistPrf
  , ErasedPrimDist(..)
  -- * Sampling
  , sample
  -- * Density
  , prob
  , logProb) where

import Data.Kind ( Constraint )
import Data.Map (Map)
import Numeric.Log ( Log(Exp) )
import OpenSum (OpenSum)
import qualified Data.Map as Map
import qualified Data.Vector as V
import qualified Data.Vector as Vec
import qualified Data.Vector.Unboxed as UV
import qualified OpenSum
import qualified System.Random.MWC.Distributions as MWC
import Statistics.Distribution ( ContDistr(density), DiscreteDistr(probability) )
import Statistics.Distribution.Beta ( betaDistr )
import Statistics.Distribution.Binomial ( binomial )
import Statistics.Distribution.CauchyLorentz ( cauchyDistribution )
import Statistics.Distribution.Dirichlet ( dirichletDensity, dirichletDistribution )
import Statistics.Distribution.DiscreteUniform ( discreteUniformAB )
import Statistics.Distribution.Gamma ( gammaDistr )
import Statistics.Distribution.Normal ( normalDistr )
import Statistics.Distribution.Poisson ( poisson )
import Statistics.Distribution.Uniform ( uniformDistr )
import Sampler
import Util ( boolToInt )

-- | Primitive distribution
data PrimDist a where
  BernoulliDist     
    :: Double           -- ^ Probability of @True@
    -> PrimDist Bool  
  BetaDist          
    :: Double           -- ^ Shape α
    -> Double           -- ^ Shape β
    -> PrimDist Double
  BinomialDist      
    :: Int              -- ^ Number of trials
    -> Double           -- ^ Probability of successful trial
    -> PrimDist Int     
  CategoricalDist   
    :: (Eq a, Show a, OpenSum.Member a PrimVal) 
    => [(a, Double)]    -- ^ Values and associated probabilities
    -> PrimDist a       
  CauchyDist        
    :: Double           -- ^ Location
    -> Double           -- ^ Scale
    -> PrimDist Double
  HalfCauchyDist      
    :: Double           -- ^ Scale
    -> PrimDist Double
  DeterministicDist 
    :: (Eq a, Show a, OpenSum.Member a PrimVal) 
    => a                -- ^ Value of probability @1@
    -> PrimDist a
  DirichletDist     
    :: [Double]         -- ^ Concentrations
    -> PrimDist [Double]
  DiscreteDist      
    :: [Double]         -- ^ List of @n@ probabilities
    -> PrimDist Int     -- ^ An index from @0@ to @n - 1@
  DiscrUniformDist      
    :: Int              -- ^ Lower-bound @a@
    -> Int              -- ^ Upper-bound @b@
    -> PrimDist Int     
  GammaDist         
    :: Double           -- ^ Shape k
    -> Double           -- ^ Scale θ
    -> PrimDist Double
  NormalDist      
    :: Double           -- ^ Mean
    -> Double           -- ^ Standard deviation
    -> PrimDist Double
  HalfNormalDist    
    :: Double           -- ^ Standard deviation
    -> PrimDist Double
  PoissonDist       
    :: Double           -- ^ Rate λ
    -> PrimDist Int
  UniformDist       
    :: Double           -- ^ Lower-bound @a@
    -> Double           -- ^ Upper-bound @b@
    -> PrimDist Double  

instance Eq (PrimDist a) where
  (==) (NormalDist m s) (NormalDist m' s') = m == m' && s == s'
  (==) (CauchyDist m s) (CauchyDist m' s') = m == m' && s == s'
  (==) (HalfCauchyDist s) (HalfCauchyDist s') = s == s'
  (==) (HalfNormalDist s) (HalfNormalDist s') = s == s'
  (==) (BernoulliDist p) (BernoulliDist p') = p == p'
  (==) (BinomialDist n p) (BinomialDist n' p') = n == n' && p == p'
  (==) (DiscreteDist ps) (DiscreteDist ps') = ps == ps'
  (==) (BetaDist a b) (BetaDist a' b') = a == a' && b == b'
  (==) (GammaDist a b) (GammaDist a' b') = a == a' && b == b'
  (==) (UniformDist a b) (UniformDist a' b') = a == a' && b == b'
  (==) (DiscrUniformDist min max) (DiscrUniformDist min' max') = min == min' && max == max'
  (==) (PoissonDist l) (PoissonDist l') = l == l'
  (==) (CategoricalDist xs) (CategoricalDist xs') = xs == xs'
  (==) (DirichletDist xs) (DirichletDist xs')  = xs == xs'
  (==) (DeterministicDist x) (DeterministicDist x') = x == x'
  (==) _ _ = False

instance Show a => Show (PrimDist a) where
  show (CauchyDist mu sigma) =
   "CauchyDist(" ++ show mu ++ ", " ++ show sigma ++ ", " ++ ")"
  show (HalfCauchyDist sigma) =
   "HalfCauchyDist(" ++ show sigma ++ ", " ++ ")"
  show (NormalDist mu sigma) =
   "NormalDist(" ++ show mu ++ ", " ++ show sigma ++ ", " ++ ")"
  show (HalfNormalDist sigma) =
   "HalfNormalDist(" ++ show sigma ++ ", " ++ ")"
  show (BernoulliDist p) =
   "BernoulliDist(" ++ show p ++ ", " ++ ")"
  show (BinomialDist n p) =
   "BinomialDist(" ++ show n ++ ", " ++ show p ++ ", " ++  ")"
  show (DiscreteDist ps) =
   "DiscreteDist(" ++ show ps ++ ", " ++ ")"
  show (BetaDist a b) =
   "BetaDist(" ++ show a ++ ", " ++ show b ++ "," ++ ")"
  show (GammaDist a b) =
   "GammaDist(" ++ show a ++ ", " ++ show b ++ "," ++ ")"
  show (UniformDist a b) =
   "UniformDist(" ++ show a ++ ", " ++ show b ++ "," ++ ")"
  show (DiscrUniformDist min max) =
   "DiscrUniformDist(" ++ show min ++ ", " ++ show max ++ ", " ++ ")"
  show (PoissonDist l) =
   "PoissonDist(" ++ show l ++ ", " ++ ")"
  show (CategoricalDist xs) =
   "CategoricalDist(" ++ show xs ++ ", " ++ ")"
  show (DirichletDist xs) =
   "DirichletDist(" ++ show xs ++ ", " ++ ")"
  show (DeterministicDist x) =
   "DeterministicDist(" ++ show x ++ ", " ++ ")"
  
-- | An ad-hoc specification of primitive value types, for constraining the outputs of distributions
type PrimVal = '[Int, Double, [Double], Bool, String]

-- | Proof that @x@ is a primitive value
data IsPrimVal x where
  IsPrimVal :: (Show x, OpenSum.Member x PrimVal) => IsPrimVal x

-- | For pattern-matching on an arbitrary @PrimDist@ with proof that it generates a primitive value 
pattern PrimDistPrf :: () => (Show x, OpenSum.Member x PrimVal) => PrimDist x -> PrimDist x
pattern PrimDistPrf d <- d@(primDistPrf -> IsPrimVal)

-- | Proof that all primitive distributions generate a primitive value
primDistPrf :: PrimDist x -> IsPrimVal x 
primDistPrf d = case d of
  HalfCauchyDist {} -> IsPrimVal
  CauchyDist {} -> IsPrimVal
  NormalDist {} -> IsPrimVal
  HalfNormalDist  {} -> IsPrimVal
  UniformDist  {} -> IsPrimVal
  DiscrUniformDist {} -> IsPrimVal
  GammaDist {} -> IsPrimVal
  BetaDist {} -> IsPrimVal
  BinomialDist {} -> IsPrimVal
  BernoulliDist {} -> IsPrimVal
  CategoricalDist {} -> IsPrimVal
  DiscreteDist {} -> IsPrimVal
  PoissonDist {} -> IsPrimVal
  DirichletDist {} -> IsPrimVal
  DeterministicDist {} -> IsPrimVal

-- | For erasing the types of primitive distributions
data ErasedPrimDist where
  ErasedPrimDist :: forall a. Show a => PrimDist a -> ErasedPrimDist

instance Show ErasedPrimDist where
  show (ErasedPrimDist d) = show d

-- | Draw a value from a primitive distribution in the @Sampler@ monad
sample :: 
     PrimDist a 
  -> Sampler a
sample (HalfCauchyDist σ )  =
  createSampler (sampleCauchy 0 σ) >>= pure . abs
sample (CauchyDist μ σ )  =
  createSampler (sampleCauchy μ σ)
sample (HalfNormalDist σ )  =
  createSampler (sampleNormal 0 σ) >>= pure . abs
sample (NormalDist μ σ )  =
  createSampler (sampleNormal μ σ)
sample (UniformDist min max )  =
  createSampler (sampleUniform min max)
sample (DiscrUniformDist min max )  =
  createSampler (sampleDiscreteUniform min max)
sample (GammaDist k θ )        =
  createSampler (sampleGamma k θ)
sample (BetaDist α β  )         =
  createSampler (sampleBeta α β)
sample (BinomialDist n p  )     =
  createSampler (sampleBinomial n p) >>=  pure .  length . filter (== True)
sample (BernoulliDist p )      =
  createSampler (sampleBernoulli p)
sample (CategoricalDist ps )   =
  createSampler (sampleCategorical (V.fromList $ fmap snd ps)) >>= \i -> pure $ fst $ ps !! i
sample (DiscreteDist ps )      =
  createSampler (sampleDiscrete ps)
sample (PoissonDist λ ) =
  createSampler (samplePoisson λ)
sample (DirichletDist xs ) =
  createSampler (sampleDirichlet xs)
sample (DeterministicDist x) = pure x

-- | Compute the density of a primitive distribution generating an observed value
prob :: 
  -- | Distribution
     PrimDist a 
  -- | Observed value
  -> a 
  -- | Density
  -> Double
prob (DirichletDist xs) ys =
  let xs' = map (/(Prelude.sum xs)) xs
  in  if Prelude.sum xs' /= 1 then error "dirichlet can't normalize" else
      case dirichletDistribution (UV.fromList xs')
      of Left e -> error "dirichlet error"
         Right d -> let Exp p = dirichletDensity d (UV.fromList ys)
                        in  exp p
prob (HalfCauchyDist σ) y
  = if y < 0 then 0 else
            2 * density (cauchyDistribution 0 σ) y
prob (CauchyDist μ σ) y
  = density (cauchyDistribution μ σ) y
prob (HalfNormalDist σ) y
  = if y < 0 then 0 else
            2 * density (normalDistr 0 σ) y
prob (NormalDist μ σ) y
  = density (normalDistr μ σ) y
prob (UniformDist min max) y
  = density (uniformDistr min max) y
prob (GammaDist k θ) y
  = density (gammaDistr k θ) y
prob  (BetaDist α β) y
  = density (betaDistr α β) y
prob (DiscrUniformDist min max) y
  = probability (discreteUniformAB min max) y
prob (BinomialDist n p) y
  = probability (binomial n p) y
prob (BernoulliDist p) i
  = probability (binomial 1 p) (boolToInt i)
prob d@(CategoricalDist ps) y
  = case lookup y ps of
      Nothing -> error $ "Couldn't find " ++ show y ++ " in categorical dist"
      Just p  -> p
prob (DiscreteDist ps) y
  = ps !! y
prob (PoissonDist λ) y
  = probability (poisson λ) y
prob (DeterministicDist x) y
  = 1

-- | Compute the log density of a primitive distribution generating an observed value
logProb :: 
  -- | Distribution
     PrimDist a 
  -- | Observed value
  -> a 
  -- | Log density
  -> Double
logProb d = log . prob d