packages feed

lazyppl-1.1.0.1: src/LazyPPL/Internal/ProbGen.hs

{-# LANGUAGE MultiParamTypeClasses #-}

{- | The stock 'StatefulGen' view of 'LazyPPL.Prob', used by 'LazyPPL.wis' to
resample through the bounded samplers of @mwc-random@. This module is internal
and not exposed, so the 1.1.0 interface of the exposed modules is unchanged:
the instance below mentions only the internal 'ProbGen' and the exported
'Prob', and haddock lists it under this hidden module, not under 'LazyPPL'. -}
module LazyPPL.Internal.ProbGen (ProbGen(..)) where

import Control.Monad.ST (runST)
import Data.Bits ((.|.), shiftL, shiftR)
import Data.STRef (newSTRef, readSTRef, writeSTRef)
import Data.Word (Word64)
import {-# SOURCE #-} LazyPPL (Prob(..), Tree(..))
import System.Random.Stateful (StatefulGen(..), fillByteArrayST)

{- | A stateful generator view of a probability computation, for the stock
sampling algorithms of @random@ and @mwc-random@ that take a 'StatefulGen',
such as 'System.Random.MWC.Distributions.logCategorical'.

Precondition: the tree must provide a child for every requested word; the
library's trees always do (a hand-built tree that runs out of children raises
\"LazyPPL: random tree exhausted\").

'LazyPPL.Internal.Sampler.randomLabel' puts every label on the midpoint grid
(k + 1/2) / 2^52 with k a uniform 52-bit integer, and 'labelIndex' recovers k
exactly, so each label supplies 52 uniform bits. 'uniformWord64' joins the
label at its tree node with the label of the node's first child — a subtree
that 'Prob' sequencing reserves to this primitive — into 64 uniform bits; the
smaller word methods use one label. Every request a stock algorithm makes,
including the repeated draws of its rejection loops, reads a distinct tree
coordinate under the monad structure of 'Prob', and running the same
computation on the same tree gives the same words. -}
data ProbGen = ProbGen

-- The 52-bit grid index of a label, the k of the midpoint grid
-- u = (k + 1/2) / 2^52 of 'LazyPPL.Internal.Sampler.randomLabel'. It is exact
-- for every label 'randomLabel' draws: u * 2^52 = k + 1/2 is exactly
-- representable in binary64, and floor (k + 1/2) is k. Labels of hand-built
-- trees keep the index of the grid point at or below them.
labelIndex :: Double -> Word64
labelIndex u = fromInteger (floor (u * 4503599627370496))

instance StatefulGen ProbGen Prob where
  uniformWord64 ProbGen = Prob $ \(Tree r ts) -> case ts of
    (Tree c _ : _) -> (labelIndex r `shiftL` 12) .|. (labelIndex c `shiftR` 40)
    [] -> treeExhausted
  {-# INLINE uniformWord64 #-}
  uniformWord32 ProbGen = Prob $ \(Tree r _) -> fromIntegral (labelIndex r `shiftR` 20)
  {-# INLINE uniformWord32 #-}
  -- The class default delegates to a frozen generator (System.Random.Internal
  -- 1.3.1, uniformByteArrayM's default signature), which a tree coordinate has
  -- no notion of. The stock fillByteArrayST (System.Random.Stateful, under
  -- "Helper functions for creating instances") fills the array little-endian
  -- from successive words, drawn here from successive children through an
  -- STRef; the empty list is matched explicitly, so no byte stays
  -- uninitialised.
  uniformByteArrayM isPinned n ProbGen = Prob $ \(Tree _ gs) -> runST $ do
    ref <- newSTRef gs
    fillByteArrayST isPinned n $ do
      ts <- readSTRef ref
      case ts of
        (t : ts') -> do
          writeSTRef ref ts'
          pure (treeWord t)
        [] -> treeExhausted
  {-# INLINE uniformByteArrayM #-}

-- One clear error when a hand-built tree runs out of children.
treeExhausted :: a
treeExhausted = error "LazyPPL: random tree exhausted"
{-# NOINLINE treeExhausted #-}

-- One tree word per node, as uniformWord64 makes it.
treeWord :: Tree -> Word64
treeWord (Tree r ts) = case ts of
  (Tree c _ : _) -> (labelIndex r `shiftL` 12) .|. (labelIndex c `shiftR` 40)
  [] -> treeExhausted
{-# INLINE treeWord #-}