imp-ppl-0.1.0.0: test/Test/DSL.hs
-- | Tests the graded monad primitives and the grade algebra. The type
-- signatures below only compile if 'Merge', 'Union' and 'TagAll' agree.
{-# LANGUAGE QualifiedDo #-}
module Test.DSL (tests) where
import Prelude hiding (return, (>>=), (>>), flip)
import Test.Tasty
import Test.Tasty.HUnit
import Test.Util (assertApprox, assertBounds, assertDist, assertMap, knightNames)
import qualified Imp.DSL as Imp
import Imp.DSL (Imp, flip, ifThenElse, interval, knight, observe, tag)
import Imp.Inference
-- | 'Merge' sorts, so binding out of alphabetical order still gives a sorted grade.
sorted :: Imp '["a", "b", "c"] Bool
sorted = Imp.do
c <- knight @"c"
a <- knight @"a"
b <- knight @"b"
Imp.return (a && b && c)
-- | The grade of a branch is the 'Union' of its arms. Explicit because
-- this module does not enable @RebindableSyntax@.
branched :: Imp '["x", "y"] Bool
branched = Imp.do
c <- flip 0.5
ifThenElse c
(Imp.do { a <- knight @"x"; Imp.return a })
(Imp.do { b <- knight @"y"; Imp.return (not b) })
-- | 'TagAll' prefixes every name in the subprogram.
tagged :: Imp '["t.k"] Bool
tagged = tag @"t" (knight @"k")
-- | Nested tags compose left to right.
nested :: Imp '["a.b.k"] Bool
nested = tag @"a" (tag @"b" (knight @"k"))
-- | An 'interval' keeps its bounds under a tag.
taggedInterval :: Imp '["m.i"] Bool
taggedInterval = tag @"m" (interval @"i" 0.25 0.75)
-- | The README's conditioning example.
conditioned :: Imp '["bias"] Bool
conditioned = Imp.do
biased <- interval @"bias" 0.3 0.7
observe biased
Imp.return biased
tests :: TestTree
tests = testGroup "DSL"
[ testGroup "Primitives"
[ testCase "flip p is a Bernoulli" $
assertDist "flip 0.7" [(False, 0.3), (True, 0.7)] (preciseMarginal (flip 0.7))
, testCase "knight is completely unconstrained" $
assertMap "knight" [(False, (0, 1)), (True, (0, 1))] (marginal (knight @"k"))
, testCase "interval lo hi bounds both outcomes" $ do
assertMap "interval" [(False, (0.25, 0.75)), (True, (0.25, 0.75))]
(marginal (interval @"i" 0.25 0.75))
length (credalVertices (interval @"i" 0.25 0.75)) @?= 2
, testCase "observe renormalises away the rejected world" $ do
let prog = Imp.do { h <- flip 0.5; observe h; Imp.return h }
assertDist "observed" [(True, 1.0)] (preciseMarginal prog)
, testCase "(>>) discards the first result but keeps its evidence" $
assertDist "sequenced" [(False, 0.75), (True, 0.25)]
(preciseMarginal (observe True Imp.>> flip 0.25))
, testCase "graded fmap maps the value, not the weight" $ do
assertDist "fmap" [(False, 0.7), (True, 0.3)]
(preciseMarginal (Imp.fmap not (flip 0.7)))
assertDist "<$>" [(False, 0.7), (True, 0.3)]
(preciseMarginal (not Imp.<$> flip 0.7))
]
, testGroup "Grades"
[ testCase "Merge sorts names regardless of bind order" $
knightNames sorted @?= ["a", "b", "c"]
, testCase "Union: a probabilistic condition reaches both branches" $ do
knightNames branched @?= ["x", "y"]
length (credalVertices branched) @?= 4
assertMap "branched" [(False, (0.0, 1.0)), (True, (0.0, 1.0))] (marginal branched)
, testCase "ifThenElse on non-Imp branches is the ordinary conditional" $ do
ifThenElse True "yes" "no" @?= "yes"
ifThenElse False "yes" "no" @?= "no"
, testCase "tag prefixes the Knightian name" $
knightNames tagged @?= ["t.k"]
, testCase "nested tags join with dots, outermost first" $
knightNames nested @?= ["a.b.k"]
, testCase "the empty tag leaves the name alone" $
knightNames (tag @"t" (tag @"" (knight @"k")) :: Imp '["t.k"] Bool) @?= ["t.k"]
, testCase "tag preserves the interval bounds it wraps" $ do
knightNames taggedInterval @?= ["m.i"]
assertMap "tagged interval" [(False, (0.25, 0.75)), (True, (0.25, 0.75))]
(marginal taggedInterval)
]
, testGroup "Conditioning"
[ testCase "conditioned: every backend gives P(True) = 1" $ do
assertMap "marginal" [(False, (0, 0)), (True, (1, 1))] (marginal conditioned)
assertMap "marginalSymbolic" [(False, (0, 0)), (True, (1, 1))]
(marginalSymbolic conditioned)
assertMap "marginalApprox" [(False, (0, 0)), (True, (1, 1))]
(marginalApprox conditioned)
assertBounds "intervalProbability" (1, 1) (intervalProbability conditioned id)
assertBounds "intervalProbabilityApprox" (1, 1)
(intervalProbabilityApprox conditioned id)
assertBounds "intervalProbabilitySymbolic" (1, 1)
(intervalProbabilitySymbolic conditioned id)
, testCase "conditioned: optimization cannot leave the credal set" $ do
assertApprox "ascent" 1.0 (snd (optimizeProbability conditioned id 200 0.1))
assertApprox "descent" 1.0 (snd (optimizeProbability conditioned id 200 (-0.1)))
]
]