packages feed

imp-ppl-0.1.0.0: src/Imp/Inference/Approx.hs

-- | Approximate credal inference via one-pass interval WMC.  Bounds are
--   sound outer approximations, but are not tight in general.
--
--   Throws an error if the evidence is unsatisfiable, i.e. the credal set is empty.
module Imp.Inference.Approx
  ( marginalApprox
  , intervalProbabilityApprox
  , intervalExpectationApprox
  ) where

import Control.Monad.State.Strict (runState)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map

import Imp.BDD.Builder (bddAny)
import Imp.BDD.Compile (compile)
import Imp.BDD.WMC (Weight(..), wmc, wmcBatch)
import Imp.DSL (Imp)
import Imp.Semiring

-- | Flips get a point interval, Knights the full unit interval.
intervalWeights :: Weight -> (IntervalS, IntervalS)
intervalWeights w = case w of
  Prob p   -> (IntervalS (1 - p) (1 - p), IntervalS p p)
  Knight _ -> (IntervalS 0 1, IntervalS 0 1)

-- | Group the return values by @key@, returning the total-mass interval
--   and one interval per group
compileApprox :: (Ord a, Ord k) => Imp g a -> (a -> k) -> (IntervalS, Map k IntervalS)
compileApprox prog key =
  let (mgr, weights, _, worlds) = compile prog
      blocks = Map.fromListWith (++) [ (key v, [g]) | (v, g) <- Map.toList worlds ]
      (bdds, mgr')      = runState (traverse bddAny blocks) mgr
      (evidence, mgr'') = runState (bddAny (Map.elems bdds)) mgr'
  in ( wmc intervalWeights mgr'' weights evidence
     , wmcBatch intervalWeights mgr'' weights bdds )

-- | Interval division where the quotient stays in @[0, 1]@.
intervalDiv :: IntervalS -> IntervalS -> IntervalS
intervalDiv (IntervalS nLo nHi) (IntervalS dLo dHi) =
  let div' a b = if b > 0 then a / b else 0
  in  IntervalS (div' nLo dHi) (div' nHi (max nHi dLo))

-- | Divide a count box by the total box, tightened by the complementary count.
condition :: IntervalS -> IntervalS -> IntervalS -> (Double, Double)
condition total@(IntervalS _ tHi) count remainder =
  let IntervalS lo hi   = intervalDiv count total
      IntervalS rLo rHi = intervalDiv remainder total
      lo' = max lo (1 - rHi)
      hi' = min hi (1 - rLo)
  in if tHi <= 0 then error "No feasible probabilities"
                 -- clamp to min in case of rounding errors
                 else (min lo' hi', hi')

-- | Approximate per-value marginal bounds via interval WMC.
marginalApprox :: Ord a => Imp g a -> Map a (Double, Double)
marginalApprox prog =
  let (total, counts) = compileApprox prog id
      remainder v = sumS (Map.elems (Map.delete v counts))
  in Map.mapWithKey (\v c -> condition total c (remainder v)) counts

-- | Approximate interval probability via interval WMC.
intervalProbabilityApprox :: Ord a => Imp g a -> (a -> Bool) -> (Double, Double)
intervalProbabilityApprox prog predicate =
  let (total, counts) = compileApprox prog predicate
      getInterval b = Map.findWithDefault zero b counts
  in condition total (getInterval True) (getInterval False)

-- | Approximate lower and upper expectations via interval WMC, clamped to
--   the range of scores the posterior can still reach.
intervalExpectationApprox :: Ord a => Imp g a -> (a -> Double) -> (Double, Double)
intervalExpectationApprox prog score =
  let reachable = [ (score v, b) | (v, b) <- Map.toList (marginalApprox prog), snd b > 0 ]
      scores    = map fst reachable
      (los, his) = unzip [ if s >= 0 then (s * lo, s * hi) else (s * hi, s * lo)
                         | (s, (lo, hi)) <- reachable ]
  in (max (minimum scores) (sum los), min (maximum scores) (sum his))