mcmc-0.6.0.0: src/Mcmc/Algorithm/MHG.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module : Mcmc.Algorithm.MHG
-- Description : Metropolis-Hastings-Green algorithm
-- Copyright : (c) Dominik Schrempf 2021
-- License : GPL-3.0-or-later
--
-- Maintainer : dominik.schrempf@gmail.com
-- Stability : unstable
-- Portability : portable
--
-- Creation date: Tue May 5 20:11:30 2020.
--
-- The Metropolis-Hastings-Green ('MHG') algorithm.
--
-- For example, see Geyer, C. J., Introduction to Markov chain Monte Carlo, In
-- Handbook of Markov Chain Monte Carlo (pp. 45) (2011). CRC press.
module Mcmc.Algorithm.MHG
( MHG (..),
mhg,
mhgSave,
mhgLoad,
MHGRatio,
mhgAccept,
)
where
import Codec.Compression.GZip
import Control.Monad
import Control.Monad.IO.Class
import Data.Aeson
import qualified Data.ByteString.Lazy.Char8 as BL
import Data.Time
import qualified Data.Vector as VB
import Mcmc.Acceptance
import Mcmc.Algorithm
import Mcmc.Chain.Chain
import Mcmc.Chain.Link
import Mcmc.Chain.Save
import Mcmc.Chain.Trace
import Mcmc.Cycle
import Mcmc.Likelihood
import Mcmc.Monitor
import Mcmc.Posterior
import Mcmc.Prior hiding (uniform)
import Mcmc.Proposal
import Mcmc.Settings
import Numeric.Log
import System.Random.MWC
import Text.Printf
import Prelude hiding (cycle)
-- | The MHG algorithm.
newtype MHG a = MHG {fromMHG :: Chain a}
instance ToJSON a => Algorithm (MHG a) where
aName = const "Metropolis-Hastings-Green (MHG)"
aIteration = iteration . fromMHG
aIsInValidState = mhgIsInValidState
aIterate = mhgIterate
aAutoTune = mhgAutoTune
aResetAcceptance = mhgResetAcceptance
aSummarizeCycle = mhgSummarizeCycle
aOpenMonitors = mhgOpenMonitors
aExecuteMonitors = mhgExecuteMonitors
aStdMonitorHeader = mhgStdMonitorHeader
aCloseMonitors = mhgCloseMonitors
aSave = mhgSave
-- | Initialize an MHG algorithm.
--
-- NOTE: Computation in the 'IO' Monad is necessary because the trace is
-- mutable.
mhg ::
Settings ->
PriorFunction a ->
LikelihoodFunction a ->
Cycle a ->
Monitor a ->
InitialState a ->
GenIO ->
IO (MHG a)
mhg s pr lh cc mn i0 g = do
-- The trace is a mutable vector and the mutable state needs to be handled by
-- a monad.
tr <- replicateT traceLength l0
return $ MHG $ Chain Nothing l0 0 tr ac g 0 pr lh cc mn
where
l0 = Link i0 (pr i0) (lh i0)
ac = emptyA $ ccProposals cc
batchMonitorSizes = map getMonitorBatchSize $ mBatches mn
minimumTraceLength = case sTraceLength s of
TraceAuto -> 1
TraceMinimum n -> n
bi = case sBurnIn s of
BurnInWithAutoTuning _ n -> n
BurnInWithCustomAutoTuning ns -> maximum ns
_ -> 0
traceLength = maximum $ minimumTraceLength : bi : batchMonitorSizes
mhgFn :: AnalysisName -> FilePath
mhgFn (AnalysisName nm) = nm ++ ".mcmc.mhg"
-- | Save an MHG algorithm.
mhgSave ::
ToJSON a =>
AnalysisName ->
MHG a ->
IO ()
mhgSave nm (MHG c) = do
savedChain <- toSavedChain c
BL.writeFile (mhgFn nm) $ compress $ encode savedChain
-- | Load an MHG algorithm.
--
-- See 'Mcmc.Mcmc.mcmcContinue'.
mhgLoad ::
FromJSON a =>
PriorFunction a ->
LikelihoodFunction a ->
Cycle a ->
Monitor a ->
AnalysisName ->
IO (MHG a)
mhgLoad pr lh cc mn nm = do
savedChain <- eitherDecode . decompress <$> BL.readFile (mhgFn nm)
chain <- either error (fromSavedChain pr lh cc mn) savedChain
return $ MHG chain
-- | MHG ratios are stored in log domain.
type MHGRatio = Log Double
-- The MHG ratio. This implementation has the following properties:
--
-- - The ratio is 'Infinity' if fX is zero. In this case, the proposal is always
-- accepted.
--
-- - The ratio 'NaN' if (fY or q or j) and fX are zero. In this case, the
-- proposal is always rejected.
--
-- This means that a chain in a state with posterior probability zero (fX=0) can
-- only move if a state with non-zero posterior probability is proposed.
-- Otherwise it is stuck. Therefore, I print a warning when the posterior
-- probability is zero in the beginning of the MCMC run. This is probably not
-- the best behavior, but see below.
--
-- There is a discrepancy between authors saying that one should (a) always
-- accept the new state when the current posterior is zero (Chapter 4 of [1],
-- [2]), or (b) almost surely reject the proposal when either fY or q are zero
-- (Chapter 1 of [1]).
--
-- Since I trust the author of Chapter 1 (Charles Geyer) I choose to follow
-- option (b). However, Option (a) is more user-friendly.
--
-- [1] Handbook of markov chain monte carlo (2011), CRC press.
--
-- [2] Dellaportas, P., & Roberts, G. O., An introduction to mcmc, Lecture Notes
-- in Statistics, (), 1–41 (2003).
-- http://dx.doi.org/10.1007/978-0-387-21811-3_1.
mhgRatio :: Posterior -> Posterior -> KernelRatio -> Jacobian -> MHGRatio
-- q = qYX / qXY * jXY; see 'ProposalSimple'.
-- j = Jacobian.
mhgRatio fX fY q j = fY / fX * q * j
{-# INLINE mhgRatio #-}
-- | Accept or reject a proposal with given MHG ratio?
mhgAccept :: MHGRatio -> GenIO -> IO Bool
mhgAccept r g
| ln r >= 0.0 = return True
| otherwise = do
b <- uniform g
return $ b < exp (ln r)
mhgPropose :: MHG a -> Proposal a -> IO (MHG a)
mhgPropose (MHG c) p = do
-- 1. Sample new state.
(!y, !q, !j) <- liftIO $ s x g
-- 2. Calculate Metropolis-Hastings-Green ratio.
let !pY = pF y
!lY = lF y
!r = mhgRatio (pX * lX) (pY * lY) q j
-- 3. Accept or reject.
-- if ln r >= 0.0
-- then do
-- let !ac' = pushA p True ac
-- return $ MHG $ c {link = Link y pY lY, acceptance = ac'}
-- else do
-- b <- uniform g
-- if b < exp (ln r)
-- then do
-- let !ac' = pushA p True ac
-- return $ MHG $ c {link = Link y pY lY, acceptance = ac'}
-- else do
-- let !ac' = pushA p False ac
-- return $ MHG $ c {acceptance = pushA p False ac'}
accept <- mhgAccept r g
if accept
then do
let !ac' = pushA p True ac
return $ MHG $ c {link = Link y pY lY, acceptance = ac'}
else do
let !ac' = pushA p False ac
return $ MHG $ c {acceptance = pushA p False ac'}
where
s = prSimple p
(Link x pX lX) = link c
pF = priorFunction c
lF = likelihoodFunction c
ac = acceptance c
g = generator c
mhgPush :: MHG a -> IO (MHG a)
mhgPush (MHG c) = do
t' <- pushT i t
return $ MHG c {trace = t', iteration = succ n}
where
i = link c
t = trace c
n = iteration c
-- Check if the current state is invalid.
--
-- At the moment this just checks whether the prior, likelihood, or posterior
-- are NaN or infinite.
mhgIsInValidState :: MHG a -> Bool
mhgIsInValidState a = check p || check l || check (p * l)
where
x = link $ fromMHG a
p = prior x
l = likelihood x
check v = let v' = ln v in isNaN v' || isInfinite v' || v' == 0
-- Ignore the number of capabilities. I have tried a lot of stuff, but the MHG
-- algorithm is just inherently sequential. Parallelization can be achieved by
-- having parallel prior and/or likelihood functions, or by using algorithms
-- running parallel chains such as 'MC3'.
mhgIterate :: ParallelizationMode -> MHG a -> IO (MHG a)
mhgIterate _ a = do
ps <- prepareProposals cc g
a' <- foldM mhgPropose a ps
mhgPush a'
where
c = fromMHG a
cc = cycle c
g = generator c
mhgAutoTune :: Int -> MHG a -> IO (MHG a)
mhgAutoTune n (MHG c) = do
tr <- VB.map state <$> takeT n (trace c)
return $ MHG $ c {cycle = autoTuneCycle ac tr cc}
where
ac = acceptance c
cc = cycle c
mhgResetAcceptance :: MHG a -> MHG a
mhgResetAcceptance (MHG c) = MHG $ c {acceptance = resetA ac}
where
ac = acceptance c
mhgSummarizeCycle :: MHG a -> BL.ByteString
mhgSummarizeCycle (MHG c) = summarizeCycle ac cc
where
cc = cycle c
ac = acceptance c
mhgOpenMonitors :: AnalysisName -> ExecutionMode -> MHG a -> IO (MHG a)
mhgOpenMonitors nm em (MHG c) = do
m' <- mOpen pre suf em m
return $ MHG c {monitor = m'}
where
m = monitor c
pre = fromAnalysisName nm
suf = maybe "" (printf "%02d") $ chainId c
mhgExecuteMonitors ::
Verbosity ->
-- Starting time.
UTCTime ->
-- Total number of iterations.
Int ->
MHG a ->
IO (Maybe BL.ByteString)
mhgExecuteMonitors vb t0 iTotal (MHG c) = mExec vb i i0 t0 tr iTotal m
where
i = iteration c
i0 = start c
tr = trace c
m = monitor c
mhgStdMonitorHeader :: MHG a -> BL.ByteString
mhgStdMonitorHeader (MHG c) = msHeader (mStdOut $ monitor c)
mhgCloseMonitors :: MHG a -> IO (MHG a)
mhgCloseMonitors (MHG c) = do
m' <- mClose m
return $ MHG $ c {monitor = m'}
where
m = monitor c