(aux hakaru10 random): the StdGen of Hakaru10’s recorded runs
Random numbers
Hakaru10 Embedded probabilistic domain-specific language Hakaru10 draws its random numbers from the StdGen of the Haskell package random-1.0.1.1, seeded with mkStdGen 17 (Metropolis.hs:858) and threaded through the chain state (Metropolis.hs:166,
204-210). Porting that generator, bit for bit, lets the tests of (aux hakaru10)
replay upstream's chains exactly: the samples recorded in the Haskell sources are
regression oracles, not merely statistical targets.
The generator is Lennart Augustsson's transliteration of L'Ecuyer's portable
combined generator for 32-bit computers: two multiplicative congruential
generators with moduli
next answers
Int in a range of size
and a Double is an Int64 drawn this way over
the whole 64-bit range,
Integer arithmetic makes
matters, and the port keeps every intermediate below
stdgen-double/reference.
The engine never threads a generator: it draws from an rng source, a
record holding a mutable state and the procedures rng-double! (Haskell's randomR (lo,hi) :: Double, a closed interval) and rng-integer! (randomR (lo,hi) :: Int, inclusive). Upstream never rolls the generator back when a proposal is
rejected (Metropolis.hs:904-906), so a source that only moves forward is
faithful; rng-source-copy replays one.
Two optional sources replay nothing upstream: L'Ecuyer's MRG32k3a Good parameters and implementations for combined multiple recursive
random number generators, Operations Research 47(1), 1999, period
(chicken random). The latter has caveats: its WELL512 state is
global to the process, so the adapter cannot be copied; an all-zero seed is a
fixed point of the generator and pseudo-random-integer then never returns (the retry loop of random_uniform in runtime.c, chicken-core main rev 55615603), so the adapter rejects it; a
seed is read in whole 8-byte words only (C_set_random_seed, same file and revision), so the adapter pads it; pseudo-random-real can answer 1.0, which the adapter redraws; and the startup seed comes from the clock.
(module
(aux hakaru10 random)
*
(import
scheme
(only (scheme base) exact-integer?)
(chicken base)
(chicken fixnum)
(chicken flonum)
(chicken bitwise)
(only (chicken format) fprintf)
(only (chicken condition)
make-property-condition
make-composite-condition
signal
condition-predicate
condition-property-accessor)
(only (chicken bytevector)
bytevector?
bytevector-length
make-bytevector
bytevector-copy!)
(only (chicken random)
set-pseudo-random-seed!
pseudo-random-integer
pseudo-random-real)
(only srfi-1 every split-at append-map)
srfi-4
(aux base))
(declare (local))
(define (hakaru10-error kind loc msg . args)
(signal
(make-composite-condition
(make-property-condition
'exn
'location
loc
'message
msg
'arguments
args)
(make-property-condition 'hakaru10 'kind kind))))
(define hakaru10-error? (condition-predicate 'hakaru10))
(define hakaru10-error-kind (condition-property-accessor 'hakaru10 'kind))
(define (check-integer-bounds who lo hi)
(unless
(and (exact-integer? lo) (exact-integer? hi))
(hakaru10-error 'parameter who "bounds must be exact integers" lo hi)))
(define-syntax-rule
(define-binary-syntax (name op) ...)
(begin (define-syntax-rule (name a b) (op a b)) ...))
(cond-expand
(|64bit| (define-binary-syntax
(stdgen+ fx+)
(stdgen- fx-)
(stdgen* fx*)
(stdgen-quot fx/)
(stdgen-mod fxmod)
(stdgen< fx<)))
(else (define-binary-syntax
(stdgen+ +)
(stdgen- -)
(stdgen* *)
(stdgen-quot quotient)
(stdgen-mod modulo)
(stdgen< <))))
(define-record stdgen s1 s2)
(set-record-printer!
stdgen
(λ (g port) (fprintf port "#<stdgen ~a ~a>" (stdgen-s1 g) (stdgen-s2 g))))
(define (stdgen-copy g) (make-stdgen (stdgen-s1 g) (stdgen-s2 g)))
(define-syntax-rule
(define-stdgen/pure (name arg ...) name!)
(define (name arg ... g)
(let1 (g* (stdgen-copy g)) (let1 (x (name! arg ... g*)) (values x g*)))))
(define stdgen-b 2147483561)
(define (seed->stdgen n)
(unless
(exact-integer? n)
(hakaru10-error
'parameter
'seed->stdgen
"seed must be an exact integer"
n))
(let* ((s (bitwise-and n 2147483647))
(q (quotient s 2147483562))
(s1 (remainder s 2147483562))
(s2 (remainder q 2147483398)))
(make-stdgen (+ s1 1) (+ s2 1))))
(define-syntax-rule
(let-stdnext ((z next1 next2) (seed1 seed2)) body ...)
(let* ((s1 seed1)
(s2 seed2)
(k (stdgen-quot s1 53668))
(s1* (stdgen-
(stdgen* 40014 (stdgen- s1 (stdgen* k 53668)))
(stdgen* k 12211)))
(next1 (if (stdgen< s1* 0) (stdgen+ s1* 2147483563) s1*))
(k* (stdgen-quot s2 52774))
(s2* (stdgen-
(stdgen* 40692 (stdgen- s2 (stdgen* k* 52774)))
(stdgen* k* 3791)))
(next2 (if (stdgen< s2* 0) (stdgen+ s2* 2147483399) s2*))
(z* (stdgen- next1 next2))
(z (if (stdgen< z* 1) (stdgen+ z* 2147483562) z*)))
body
...))
(define-syntax-rule
(stdgen-fold! g n (z acc) step init)
(let1 (gen g)
(let loop ((i n)
(acc init)
(s1 (stdgen-s1 gen))
(s2 (stdgen-s2 gen)))
(if (eq? i 0)
(begin (stdgen-s1-set! gen s1) (stdgen-s2-set! gen s2) acc)
(let-stdnext
((z next1 next2) (s1 s2))
(loop (sub1 i) step next1 next2))))))
(define-syntax-rule (stdnext! g) (stdgen-fold! g 1 (z _) z #f))
(define (stdgen-next! g) (stdnext! g))
(define-stdgen/pure (stdgen-next) stdgen-next!)
(define (ilog-base b i) (if (< i b) 1 (add1 (ilog-base b (quotient i b)))))
(define (stdgen-ival-integer! l h g)
(check-integer-bounds 'stdgen-ival-integer l h)
(if (> l h)
(stdgen-ival-integer! h l g)
(let1 (k (add1 (- h l)))
(+ l
(if (< k stdgen-b)
(stdgen-mod (stdgen+ (stdnext! g) stdgen-b) k)
(modulo
(stdgen-fold!
g
(ilog-base stdgen-b k)
(z acc)
(+ z (* acc stdgen-b))
1)
k))))))
(define-stdgen/pure (stdgen-ival-integer l h) stdgen-ival-integer!)
(cond-expand
(|64bit| (define-inline
(stdgen-acc53 x acc)
(let* ((hi (fxshr acc 27))
(lo (fxand acc 134217727))
(hib (fxshl (fxand (fx* hi stdgen-b) 67108863) 27)))
(fxand (fx+ x (fx+ hib (fx* lo stdgen-b)))
9007199254740991))))
(else (define-inline
(stdgen-acc53 x acc)
(bitwise-and (+ x (* acc stdgen-b)) 9007199254740991))))
(define (stdgen-double! g)
(fp/ (exact->inexact (stdgen-fold! g 3 (z acc) (stdgen-acc53 z acc) 1))
9007199254740992.0))
(define-stdgen/pure (stdgen-double) stdgen-double!)
(define (stdgen-double/reference g)
(let*-values
(((twoto53) (expt 2 53))
((x g*) (stdgen-ival-integer (- (expt 2 63)) (sub1 (expt 2 63)) g)))
(values
(/ (exact->inexact (bitwise-and x (sub1 twoto53)))
(exact->inexact twoto53))
g*)))
(define (stdgen-range-double! lo hi g)
(if (fp> lo hi)
(stdgen-range-double! hi lo g)
(let1 (coef (stdgen-double! g))
(fp* 2.0
(fp+ (fp* 0.5 lo)
(fp* coef (fp- (fp* 0.5 hi) (fp* 0.5 lo))))))))
(define-stdgen/pure (stdgen-range-double lo hi) stdgen-range-double!)
(define-record rng-source name state draw-double draw-integer copier)
(set-record-printer!
rng-source
(λ (src port)
(fprintf
port
"#<rng-source ~a ~s>"
(rng-source-name src)
(rng-source-state src))))
(define (rng-double! src lo hi)
((rng-source-draw-double src)
(rng-source-state src)
(exact->inexact lo)
(exact->inexact hi)))
(define (rng-integer! src lo hi)
((rng-source-draw-integer src) (rng-source-state src) lo hi))
(define (rng-source-copy src)
(make-rng-source
(rng-source-name src)
((rng-source-copier src) (rng-source-state src))
(rng-source-draw-double src)
(rng-source-draw-integer src)
(rng-source-copier src)))
(define (make-stdgen-source #!optional (seed 17))
(make-rng-source
'stdgen
(if (stdgen? seed) (stdgen-copy seed) (seed->stdgen seed))
(λ (g lo hi) (stdgen-range-double! lo hi g))
(λ (g lo hi) (stdgen-ival-integer! lo hi g))
stdgen-copy))
(define (unit->range u lo hi)
(if (fp> lo hi) (unit->range u hi lo) (fp+ lo (fp* u (fp- hi lo)))))
(define (unit->integer u lo hi)
(if (> lo hi)
(unit->integer u hi lo)
(min hi
(+ lo
(inexact->exact
(fpfloor (fp* u (exact->inexact (add1 (- hi lo))))))))))
(define mrg32k3a-m1 4294967087.0)
(define mrg32k3a-m2 4294944443.0)
(define mrg32k3a-norm 2.32830654929573e-10)
(define-inline
(mrg32k3a-component a x b y m)
(let* ((p (fp- (fp* a x) (fp* b y)))
(p (fp- p (fp* (fptruncate (fp/ p m)) m))))
(if (fp< p 0.0) (fp+ p m) p)))
(define (mrg32k3a-next! s)
(let ((p1 (mrg32k3a-component
1403580.0
(f64vector-ref s 1)
810728.0
(f64vector-ref s 0)
mrg32k3a-m1))
(p2 (mrg32k3a-component
527612.0
(f64vector-ref s 5)
1370589.0
(f64vector-ref s 3)
mrg32k3a-m2)))
(f64vector-set! s 0 (f64vector-ref s 1))
(f64vector-set! s 1 (f64vector-ref s 2))
(f64vector-set! s 2 p1)
(f64vector-set! s 3 (f64vector-ref s 4))
(f64vector-set! s 4 (f64vector-ref s 5))
(f64vector-set! s 5 p2)
(fp* (if (fp<= p1 p2) (fp+ (fp- p1 p2) mrg32k3a-m1) (fp- p1 p2))
mrg32k3a-norm)))
(define (mrg32k3a-seed->state seed)
(define moduli
'(4294967087 4294967087 4294967087 4294944443 4294944443 4294944443))
(define (all-zero? triple) (every zero? triple))
(define (component? x m) (and (real? x) (integer? x) (<= 0 x) (< x m)))
(define (spread seed)
(let loop ((moduli moduli) (x (modulo seed 4294967296)))
(if (null? moduli)
'()
(let1 (x* (modulo (add1 (* 69069 x)) 4294967296))
(cons (modulo x* (car moduli)) (loop (cdr moduli) x*))))))
(define (->state words) (list->f64vector (map exact->inexact words)))
(define seed-words
(cond ((list? seed) seed)
((vector? seed) (vector->list seed))
((f64vector? seed) (f64vector->list seed))
(else #f)))
(cond ((exact-integer? seed)
(let-values
(((t1 t2) (split-at (spread seed) 3)))
(->state
(append-map
(λ (t) (if (all-zero? t) '(12345 12345 12345) t))
(list t1 t2)))))
((and seed-words
(= (length seed-words) 6)
(every component? seed-words moduli))
(let-values
(((t1 t2) (split-at seed-words 3)))
(when (or (all-zero? t1) (all-zero? t2))
(hakaru10-error
'parameter
'make-mrg32k3a-source
"a seed triple is all zero"
seed))
(->state seed-words)))
(else
(hakaru10-error
'parameter
'make-mrg32k3a-source
"seed must be an exact integer or six integers in [0,m1)^3 x [0,m2)^3"
seed))))
(define (make-mrg32k3a-source #!optional (seed 17))
(make-rng-source
'mrg32k3a
(mrg32k3a-seed->state seed)
(λ (s lo hi) (unit->range (mrg32k3a-next! s) lo hi))
(λ (s lo hi)
(check-integer-bounds 'rng-integer! lo hi)
(unit->integer (mrg32k3a-next! s) lo hi))
(λ (s) (list->f64vector (f64vector->list s)))))
(define (chicken-random-seed seed)
(unless
(bytevector? seed)
(hakaru10-error
'parameter
'make-chicken-random-source
"seed must be a bytevector"
seed))
(when (every zero? (u8vector->list seed))
(hakaru10-error
'parameter
'make-chicken-random-source
"an all-zero seed would hang (chicken random)"
seed))
(let* ((n (bytevector-length seed))
(padded (make-bytevector (* 8 (max 1 (quotient (+ n 7) 8))) 0)))
(bytevector-copy! padded 0 seed)
padded))
(define (chicken-random-unit)
(let1 (u (pseudo-random-real))
(if (and (fp< 0.0 u) (fp< u 1.0)) u (chicken-random-unit))))
(define (make-chicken-random-source #!optional seed)
(when seed (set-pseudo-random-seed! (chicken-random-seed seed)))
(make-rng-source
'chicken-random
'global
(λ (_ lo hi) (unit->range (chicken-random-unit) lo hi))
(λ (_ lo hi)
(check-integer-bounds 'rng-integer! lo hi)
(let1 (l (min lo hi))
(+ l (pseudo-random-integer (add1 (- (max lo hi) l))))))
(λ (_)
(hakaru10-error
'parameter
'rng-source-copy
"the global state of (chicken random) cannot be copied")))))
test/seed->stdgen: pass
R1. (seed->stdgen 17), upstream's mkStdGen 17, is the generator 18 1. The seed is cut to 32 bits and masked to 31, as the conversion to Int32 does; so
mkStdGen :: Int -> StdGen -- why not Integer ?
mkStdGen s = mkStdGen32 $ fromIntegral s
mkStdGen32 :: Int32 -> StdGen
mkStdGen32 sMaybeNegative = StdGen (s1+1) (s2+1)
where
-- We want a non-negative number, but we can't just take the abs
-- of sMaybeNegative as -minBound == minBound.
s = sMaybeNegative .&. maxBound
(q, s1) = s `divMod` 2147483562
s2 = q `mod` 2147483398cf. R:235-245.
(define (test/seed->stdgen _)
(let1 (g (seed->stdgen 17))
(⊦ equal? '(18 1) (list (stdgen-s1 g) (stdgen-s2 g))))
(⊦ equal?
'((1 1) (2 1) (86 2) (82 2) (18 1))
(map (λ (n)
(let1 (g (seed->stdgen n)) (list (stdgen-s1 g) (stdgen-s2 g))))
(list 0 1 2147483647 -5 (+ (expt 2 40) 17))))
(⊦ eq? 'parameter (hakaru10-kind (τ (seed->stdgen 1.5)))))
((eta 0.0) (memory #(12582912 2241544 1048576)) (stdout "") (stderr ""))
test/stdNext: pass
R2. The first three outputs of mkStdGen 17 and the generator
they leave, 612660577 2103410263; stdgen-next is pure and leaves its argument untouched, while its in-place twin stdgen-next!, the one the sources use, steps it:
stdNext :: StdGen -> (Int, StdGen)
-- Returns values in the range stdRange
stdNext (StdGen s1 s2) = (fromIntegral z', StdGen s1'' s2'')
where z' = if z < 1 then z + 2147483562 else z
z = s1'' - s2''
k = s1 `quot` 53668
s1' = 40014 * (s1 - k * 53668) - k * 12211
s1'' = if s1' < 0 then s1' + 2147483563 else s1'
k' = s2 `quot` 52774
s2' = 40692 * (s2 - k' * 52774) - k' * 3791
s2'' = if s2' < 0 then s2' + 2147483399 else s2'cf. R:474-486.
(define (test/stdNext _)
(let*-values
(((a g) (stdgen-next (seed->stdgen 17)))
((b g) (stdgen-next g))
((c g) (stdgen-next g)))
(⊦ equal? '(679560 1394521907 656733876) (list a b c))
(⊦ equal? '(612660577 2103410263) (list (stdgen-s1 g) (stdgen-s2 g))))
(let1 (g (seed->stdgen 17))
(stdgen-next g)
(⊦ equal? '(18 1) (list (stdgen-s1 g) (stdgen-s2 g)))
(⊦ equal?
'(679560 1394521907 656733876)
(let* ((a (stdgen-next! g))
(b (stdgen-next! g))
(c (stdgen-next! g)))
(list a b c)))
(⊦ equal? '(612660577 2103410263) (list (stdgen-s1 g) (stdgen-s2 g)))))
((eta 0.0) (memory #(12582912 2240736 1048576)) (stdout "") (stderr ""))
test/randomR-double: pass
R3. The first randomR (0,1) :: Double of seed 17 is 0.2938995850243806, after three nexts; the scaling keeps Haskell's association, -1.8100773861098114 for the range
randomRFloating (l,h) g
| l>h = randomRFloating (h,l) g
| otherwise = let (coef,g') = random g in
(2.0 * (0.5*l + coef * (0.5*h - 0.5*l)), g') -- avoid overflow
instance Random Double where
randomR = randomRFloating
random rng =
case random rng of
(x,rng') ->
-- We use 53 bits of randomness corresponding to the 53 bit significand:
((fromIntegral (mask53 .&. (x::Int64)) :: Double)
/ fromIntegral twoto53, rng')
where
twoto53 = (2::Int64) ^ (53::Int64)
mask53 = twoto53 - 1cf. R:361-378; the values are those of the Python transcription that replayed upstream's records.
(define (test/randomR-double _)
(⊦ = 0.293899585024381 (rng-double! (make-stdgen-source 17) 0.0 1.0))
(⊦ = 0.293899585024381 (rng-double! (make-stdgen-source) 0 1))
(⊦ = -1.81007738610981 (rng-double! (make-stdgen-source 17) -3.5 2.25))
(let*-values
(((d g) (stdgen-double (seed->stdgen 17))))
(⊦ = 0.293899585024381 d)
(⊦ equal? '(612660577 2103410263) (list (stdgen-s1 g) (stdgen-s2 g)))))
((eta 0.0) (memory #(12582912 2242736 1048576)) (stdout "") (stderr ""))
test/randomR-int: pass
R4. The first randomR (0,9) :: Int of seed 17 is 1, that is
randomIvalInteger :: (RandomGen g, Num a) => (Integer, Integer) -> g -> (a, g)
randomIvalInteger (l,h) rng
| l > h = randomIvalInteger (h,l) rng
| otherwise = case (f n 1 rng) of (v, rng') -> (fromInteger (l + v `mod` k), rng')
where
k = h - l + 1
-- ERROR: b here (2^31-87) represents a baked-in assumption about genRange:
b = 2147483561
n = iLogBase b k
-- Here we loop until we've generated enough randomness to cover the range:
f 0 acc g = (acc, g)
f n' acc g =
let
(x,g') = next g
in
-- We shift over the random bits generated thusfar (* b) and add in the new ones.
f (n' - 1) (fromIntegral x + acc * b) g'cf. R:424-441, used by uniformly (D:82) and by the choice of the node to resample (M:962).
(define (test/randomR-int _)
(⊦ = 1 (rng-integer! (make-stdgen-source 17) 0 9))
(⊦ = 1 (rng-integer! (make-stdgen-source 17) 9 0))
(let*-values
(((v g)
(stdgen-ival-integer (- (expt 10 30)) (expt 10 30) (seed->stdgen 17))))
(⊦ = -471988287518935644257797793060 v)
(⊦ equal? '(1475456433 1872071452) (list (stdgen-s1 g) (stdgen-s2 g))))
(let1 (src (make-stdgen-source 17))
(⊦ = 5 (rng-integer! src 5 5))
(let1 (g (rng-source-state src))
(⊦ equal? '(720252 40692) (list (stdgen-s1 g) (stdgen-s2 g)))))
(⊦ eq?
'parameter
(hakaru10-kind (τ (rng-integer! (make-stdgen-source) 0.0 9)))))
((eta 0.0) (memory #(12582912 2247624 1048576)) (stdout "") (stderr ""))
test/fixnum-double-vs-reference: pass
R5. The fixnum stdgen-double, which keeps
(define (test/fixnum-double-vs-reference _)
(⊦ equal?
'(0 0 0 0 0)
(map (λ (seed) (stdgen-double-mismatches seed 100000))
'(17 0 1 2147483647 -5))))
((eta 2.393) (memory #(12582912 2242632 1048576)) (stdout "") (stderr ""))
test/copy-replays: pass
R6. A copy of a source replays its next 1000 draws (500 doubles and 500 integers, interleaved), and the copy is independent of the original.
(define (test/copy-replays _)
(let* ((src (make-stdgen-source 17))
(_ (draws src 10))
(copy (rng-source-copy src))
(a (draws src 500))
(b (draws copy 500)))
(⊦ equal? a b)
(⊦ equal?
(draws (make-stdgen-source 17) 10)
(draws (make-stdgen-source (seed->stdgen 17)) 10))))
((eta 0.001) (memory #(12582912 2309016 1048576)) (stdout "") (stderr ""))
test/bounds: pass
R7.
randomRFloating and randomIvalInteger does.
(define (test/bounds _)
(let1 (src (make-stdgen-source 17))
(⊨ (all-draws?
100000
(τ (rng-double! src -2.5 4.0))
(λ (x) (and (<= -2.5 x) (<= x 4.0)))))
(⊨ (all-draws?
1000
(τ (rng-double! src 4.0 -2.5))
(λ (x) (and (<= -2.5 x) (<= x 4.0)))))
(⊨ (all-draws?
1000
(τ (rng-integer! src 9 -3))
(λ (k) (and (<= -3 k) (<= k 9)))))
(⊦ = 1.0 (rng-double! src 1.0 1.0)))
(let*-values
(((a _) (stdgen-range-double -2.5 4.0 (seed->stdgen 17)))
((b _) (stdgen-range-double 4.0 -2.5 (seed->stdgen 17))))
(⊦ = a b)))
((eta 0.06) (memory #(12582912 2245328 1048576)) (stdout "") (stderr ""))
test/ilog-base: pass
R8. The number of outputs a range needs: one for
Int64; the comment upstream notes the
off-by-one at
-- Perform an expensive logarithm on arbitrary-size integers by repeated division.
--
-- (NOTE: This actually returns ceiling(log(i) base b) except with an
-- incorrect result at iLogBase b b = 2.)
iLogBase :: Integer -> Integer -> Integer
iLogBase b i = if i < b then 1 else 1 + iLogBase b (i `div` b)cf. R:464-469.
(define (test/ilog-base _)
(⊦ equal?
'(1 2 3 3)
(map (λ (k) (ilog-base 2147483561 k))
(list 10 2147483561 (expt 2147483561 2) (expt 2 64))))
(let*-values
(((x g) (stdgen-ival-integer 0 2147483560 (seed->stdgen 17)))
((z1 g1) (stdgen-next (seed->stdgen 17)))
((z2 g2) (stdgen-next g1)))
(⊦ = (modulo (+ z2 (* (+ z1 (* 1 2147483561)) 2147483561)) 2147483561) x)
(⊦ equal?
(list (stdgen-s1 g2) (stdgen-s2 g2))
(list (stdgen-s1 g) (stdgen-s2 g)))))
((eta 0.0) (memory #(12582912 2247576 1048576)) (stdout "") (stderr ""))
test/mrg32k3a: pass
R9 (optional source). MRG32k3a from L'Ecuyer's default state, all components 12345: the first three outputs and the state after 1000 of them
are those of the C code of the paper (Fig. 1), compiled here, bit for bit; a copy
replays, and integers and swapped bounds stay in range. A seed with an all-zero
triple, a component out of range, or of the wrong type is a parameter error.
(define (test/mrg32k3a _)
(let1 (src (make-mrg32k3a-source '(12345 12345 12345 12345 12345 12345)))
(⊦ equal?
'(0.127011122046577 0.318527565396794 0.30918601558327)
(let* ((a (rng-double! src 0.0 1.0))
(b (rng-double! src 0.0 1.0))
(c (rng-double! src 0.0 1.0)))
(list a b c)))
(⊨ (all-draws?
997
(τ (rng-double! src 0.0 1.0))
(λ (u) (and (< 0.0 u) (< u 1.0)))))
(⊦ equal?
(f64vector
4239718941.0
899640195.0
1411745448.0
2768972929.0
343921931.0
1471537888.0)
(rng-source-state src)))
(let* ((src (make-mrg32k3a-source 17)) (copy (rng-source-copy src)))
(⊦ equal? (draws src 100) (draws copy 100))
(⊨ (all-draws?
10000
(τ (rng-integer! src 9 0))
(λ (k) (and (<= 0 k) (<= k 9)))))
(⊨ (all-draws?
10000
(τ (rng-double! src 3.0 -1.0))
(λ (x) (and (<= -1.0 x) (<= x 3.0))))))
(⊦ equal?
'(parameter parameter parameter)
(map (λ (seed) (hakaru10-kind (τ (make-mrg32k3a-source seed))))
(list '(0 0 0 1 1 1) '(1 1 4294967087 1 1 1) 'seventeen))))
((eta 0.017) (memory #(12582912 2250112 1048576)) (stdout "") (stderr ""))
test/chicken-random: pass
R10 (optional source). The adapter over (chicken random) rejects an all-zero seed, which would make pseudo-random-integer loop forever, whatever its length; it pads a short seed to whole words, so #u8(17) and #u8(17 0 0 0 0 0 0 0) give the same stream; and it cannot be copied, its state being global.
(define (test/chicken-random _)
(⊦ equal?
'(parameter parameter parameter)
(map (λ (seed) (hakaru10-kind (τ (make-chicken-random-source seed))))
(list (make-bytevector 16 0) (bytevector 0) "17")))
(let* ((a (draws (make-chicken-random-source (bytevector 17)) 5))
(b (draws (make-chicken-random-source (bytevector 17 0 0 0 0 0 0 0))
5))
(src (make-chicken-random-source (bytevector 1 2 3 4 5 6 7 8 9))))
(⊦ equal? a b)
(⊨ (all-draws?
1000
(τ (rng-double! src 2.0 0.0))
(λ (x) (and (< 0.0 x) (< x 2.0)))))
(⊨ (all-draws?
1000
(τ (rng-integer! src (expt 2 70) (+ (expt 2 70) 3)))
(λ (k) (and (<= (expt 2 70) k) (<= k (+ (expt 2 70) 3))))))
(⊦ eq? 'parameter (hakaru10-kind (τ (rng-source-copy src))))))
((eta 0.002) (memory #(12582912 2251976 1048576)) (stdout "") (stderr ""))
(aux hakaru10 distribution): distributions and conditioning
Distributions
The distributions of Hakaru10 Distribution.hs are values of two type classes: a Distribution has a log-density and a sampler, and a resampleable one (an RDistribution) has a proposal kernel too, by default an
independence proposal that samples again. The existential SomeRDistr hides the instance (Distribution.hs:14-41):
class Distribution d a | d -> a where
logDensity :: d -> a -> LogLikelihood
sample :: RandomGen g => d -> g -> (a, LogLikelihood, g)
-- re-sampleable distribution. This is the place to specify
-- custom proposals
class Distribution d a => RDistribution d a | d -> a where
resample :: RandomGen g =>
d ->
a -> LogLikelihood -> -- old value and its LL
g ->
((a, -- new sample
LogLikelihood, -- its likelihood
g), -- new seed
LogLikelihood) -- (fwd - bwd) for the proposal step.
-- By default, resample is really sample again
resample d _ llold g =
let s@(_,ll,_) = sample d g in
(s,ll-llold)In Scheme SomeRDistr is the record rdistr of closures (log-density, sample, resample, plus a finite support for the exact enumerator), and the generator is an rng source that the sampler draws from in place. The engine wraps a
distribution in a distk (Metropolis.hs:46-60), whose kind is resampleable, observed (a conditioned distribution: its value is fixed, its
log-likelihood follows its parameters) or dirac (a deterministic node). The user-level distributions are the smart
constructors bern, normal and so on, upstream's bernS, normalS, ... (Metropolis.hs:70-95), which (aux hakaru10) re-exports as named.
Log-densities are the true ones, normalization included, as upstream insists (Distribution.hs:93-96):
-- Make sure we report the real LL rather than up to the constant
-- (which may depend on the argument to |categorical|).
-- When logDensity is called during the scoring of a proposal,
-- the parameters of the distribution have likely changedbecause the parameters of a node's distribution change when its parents are resampled, and the Metropolis-Hastings ratio compares log-likelihoods computed under different parameters.
The samplers are the classical ones: Marsaglia's polar method for the normal distribution (Marsaglia and Bray, 1964); G. Marsaglia and W. W. Tsang, A Simple Method for Generating Gamma Variables, ACM TOMS 26(3), 2000 for the gamma distribution; Joehnk's method (1964) for a beta distribution with both parameters at most 1, and the ratio of two gamma variates otherwise; Knuth's multiplication method (The Art of Computer Programming, vol. 2, §3.4.1) for the Poisson distribution. The log-gamma function is Macleod's Algorithm AS 245 (Applied Statistics 38(2), 1989), transcribed rather than taken from libm, accurate to 10-12 significant digits.
Every Double expression is transcribed in Haskell's association, and every sampler draws exactly where upstream draws: with the random-1.0.1.1 StdGen of (aux hakaru10 random), the samples are upstream's, bit for bit (D16). Four upstream defects are fixed, each tagged in the source with its deviation number: the gamma density (deviation 2), the gamma sampler for a shape below 1 (deviation 3), the log-likelihood of a Poisson sample (deviation 4) and the categorical walk exhausted by rounding (deviation 5).
(module
(aux hakaru10 distribution)
*
(import
scheme
(only (scheme base) exact-integer?)
(chicken base)
(chicken flonum)
(chicken foreign)
(only (chicken format) fprintf)
(only (chicken module) reexport)
(only srfi-1 every fold alist-copy make-list)
(aux base)
(aux hakaru10 random))
(declare (local))
(reexport
(only (aux hakaru10 random)
hakaru10-error
hakaru10-error?
hakaru10-error-kind))
(define (->flonum who x)
(cond ((flonum? x) x)
((real? x) (exact->inexact x))
(else
(hakaru10-error 'parameter who "a real number is expected" x))))
(define-syntax-rule
(let-flonums who (var ...) body ...)
(let ((var (->flonum who var)) ...) body ...))
(define-record
rdistr
name
params
log-density
sample
resample
support
independence?)
(set-record-printer!
rdistr
(λ (d port)
(fprintf port "#<rdistr ~a ~s>" (rdistr-name d) (rdistr-params d))))
(define-record distk kind rd value)
(set-record-printer!
distk
(λ (dk port)
(let1 (rd (distk-rd dk))
(fprintf port "#<distk ~a" (distk-kind dk))
(when rd
(fprintf port " ~a ~s" (rdistr-name rd) (rdistr-params rd)))
(unless
(eq? (distk-kind dk) 'resampleable)
(fprintf port " ~s" (distk-value dk)))
(display ">" port))))
(define ((independence-resample sample) src x llold)
(receive (x* ll) (sample src) (values x* ll (fp- ll llold))))
(define (make-resampleable
name
params
log-density
sample
#!optional
(resample #f)
(support #f))
(make-distk
'resampleable
(make-rdistr
name
params
log-density
sample
(or resample (independence-resample sample))
support
(not resample))
(void)))
(define ((sample/log-density draw log-density) src)
(let1 (x (draw src)) (values x (log-density x))))
(define-syntax
fp-horner
(syntax-rules
()
((_ x a) a)
((_ x a b c ...) (fp-horner x (fp+ (fp* a x) b) c ...))))
(define-syntax-rule
(fp-horner/monic x b c ...)
(fp-horner x (fp+ x b) c ...))
(define (log-gamma x)
(let-flonums
'log-gamma
(x)
(cond ((fp<= x 0.0) +inf.0)
((fp> x 1e+308) +inf.0)
((fp< x 1.5)
(receive
(a b c)
(if (fp< x 0.5)
(values (fpneg (fplog x)) (fp+ x 1.0) x)
(values 0.0 x (fp- x 1.0)))
(fp+ a
(fp/ (fp* c
(fp-horner
b
3.13060547623
11.1667541262
-21.9698958928
-24.4387534237
-2.66685511495))
(fp-horner/monic
b
15.234687407
31.4690115749
11.9400905721
0.607771387771)))))
((fp< x 4.0)
(fp/ (fp* (fp- x 2.0)
(fp-horner
x
4.16438922228
78.6994924154
137.519416416
-142.046296688
-78.3359299449))
(fp-horner/monic
x
43.3400022514
263.505074721
313.399215894
47.066876606)))
((fp< x 12.0)
(fp/ (fp-horner
x
-2296.6072978
-40262.1119975
27464.7644705
230661.510616
-212159.572323)
(fp-horner/monic
x
-570.691009324
-24235.7409629
-146025.937511
-116328.495004)))
(else
(let* ((y (fplog x))
(k (fp+ (fp- (fp* x (fp- y 1.0)) (fp* 0.5 y))
0.918938533204673)))
(if (fp> x 3000000.0)
k
(let* ((x1 (fp/ 1.0 x)) (x2 (fp* x1 x1)))
(fp+ k
(fp/ (fp* x1
(fp-horner
x2
0.0692910599291889
0.491731761050597
0.279195317918525))
(fp-horner/monic
x2
6.0124592597641
3.3503438150223))))))))))
(define libm-lgamma (foreign-lambda double "lgamma" double))
(define (marsaglia src)
(let* ((x (rng-double! src -1.0 1.0))
(y (rng-double! src -1.0 1.0))
(s (fp+ (fp* x x) (fp* y y))))
(if (and (fp>= 1.0 s) (fp> s 0.0))
(fp* x (fpsqrt (fp/ (fp* -2.0 (fplog s)) s)))
(marsaglia src))))
(define (normal-rng mu sd src)
(if (fp> sd 0.0)
(fp+ mu (fp* sd (marsaglia src)))
(hakaru10-error 'parameter 'normal "normal: invalid parameters" mu sd)))
(define (marsaglia-tsang shape scl src)
(let* ((d (fp- shape (fp/ 1.0 3.0))) (c (fp/ 1.0 (fpsqrt (fp* 9.0 d)))))
(let retry ()
(let* ((v (let until ((v (normal-rng 1.0 c src)))
(if (fp> v 0.0) v (until (normal-rng 1.0 c src)))))
(x (fp/ (fp- v 1.0) c))
(sqr (fp* x x))
(v3 (fp* (fp* v v) v))
(u (rng-double! src 0.0 1.0)))
(if (or (fp< u (fp- 1.0 (fp* 0.0331 (fp* sqr sqr))))
(fp< (fplog u)
(fp+ (fp* 0.5 sqr)
(fp* d (fp+ (fp- 1.0 v3) (fplog v3))))))
(fp* (fp* scl d) v3)
(retry))))))
(define (gamma-rng shape scl src)
(cond ((fp<= shape 0.0)
(hakaru10-error
'parameter
'gamma
"gamma: got a negative shape paramater"
shape
scl))
((fp<= scl 0.0)
(hakaru10-error
'parameter
'gamma
"gamma: got a negative scale paramater"
shape
scl))
((fp< shape 1.0)
(let* ((g (gamma-rng (fp+ shape 1.0) 1.0 src))
(w (rng-double! src 0.0 1.0)))
(fp* (fp* scl g) (fpexpt w (fp/ 1.0 shape)))))
(else (marsaglia-tsang shape scl src))))
(define (beta-rng a b src)
(cond ((or (fp<= a 0.0) (fp<= b 0.0))
(hakaru10-error
'parameter
'beta
"beta: parameters must be positve"
a
b))
((and (fp<= a 1.0) (fp<= b 1.0))
(let retry ()
(let* ((u (rng-double! src 0.0 1.0))
(v (rng-double! src 0.0 1.0))
(x (fpexpt u (fp/ 1.0 a)))
(y (fpexpt v (fp/ 1.0 b))))
(if (fp<= (fp+ x y) 1.0) (fp/ x (fp+ x y)) (retry)))))
(else
(let* ((ga (gamma-rng a 1.0 src)) (gb (gamma-rng b 1.0 src)))
(fp/ ga (fp+ ga gb))))))
(define (bern-with name p kernel)
(let-flonums
name
(p)
(let* ((lT (fplog p))
(lF (fplog (fp- 1.0 p)))
(log-density (λ (x) (if x lT lF))))
(make-resampleable
name
(list p)
log-density
(λ (src)
(if (fp<= (rng-double! src 0.0 1.0) p)
(values #t lT)
(values #f lF)))
(kernel log-density)
(τ `((#t unquote p) (#f unquote (fp- 1.0 p))))))))
(define (bern p) (bern-with 'bern p (K #f)))
(define ((flip-kernel log-density) src x ll)
(let1 (x* (not x)) (values x* (log-density x*) 0.0)))
(define (bern/flip p) (bern-with 'bern/flip p flip-kernel))
(define (uniformly lst)
(cond ((null? lst)
(hakaru10-error 'parameter 'uniformly "uniformly: empty list"))
((not (list? lst))
(hakaru10-error 'parameter 'uniformly "a list is expected" lst))
(else
(let* ((len (exact->inexact (length lst)))
(arr (list->vector lst))
(hi (sub1 (vector-length arr)))
(ll (fpneg (fplog len)))
(p (fp/ 1.0 len)))
(make-resampleable
'uniformly
(list lst)
(λ (x) (if (member x lst) ll -inf.0))
(λ (src) (values (vector-ref arr (rng-integer! src 0 hi)) ll))
#f
(τ (map (λ (x) (cons x p)) lst)))))))
(define (categorical-walk p alist)
(let* ((entry (car alist)) (p0 (cdr entry)))
(if (or (fp< p p0) (null? (cdr alist)))
(values (car entry) (fplog p0))
(categorical-walk (fp- p p0) (cdr alist)))))
(define (categorical alist)
(unless
(and (pair? alist)
(list? alist)
(every (λ (e) (and (pair? e) (real? (cdr e)))) alist))
(hakaru10-error
'parameter
'categorical
"a non-empty alist ((value . weight) ...) is expected"
alist))
(let* ((weighted
(map (λ (e) (cons (car e) (exact->inexact (cdr e)))) alist))
(ptotal (fold (λ (e acc) (fp+ acc (cdr e))) 0.0 weighted))
(normalized
(map (λ (e) (cons (car e) (fp/ (cdr e) ptotal))) weighted)))
(make-resampleable
'categorical
(list weighted)
(λ (x) (cond ((assoc x normalized) => (o fplog cdr)) (else -inf.0)))
(λ (src) (categorical-walk (rng-double! src 0.0 1.0) normalized))
#f
(τ (alist-copy normalized)))))
(define (uniform lo hi)
(let-flonums
'uniform
(lo hi)
(let1 (l (fplog (fp/ 1.0 (fp- hi lo))))
(make-resampleable
'uniform
(list lo hi)
(λ (x)
(let-flonums
'uniform
(x)
(if (and (fp<= lo x) (fp<= x hi)) l -inf.0)))
(λ (src) (values (rng-double! src lo hi) l))))))
(define (normal mu sd)
(let-flonums
'normal
(mu sd)
(let* ((tau (fp/ 1.0 (fp* sd sd)))
(lf (fplog (fp/ (fp/ tau 3.14159265358979) 2.0)))
(log-density
(λ (x)
(let-flonums
'normal
(x)
(let1 (y (fp- x mu))
(fp/ (fp+ (fpneg (fp* tau (fp* y y))) lf) 2.0))))))
(make-resampleable
'normal
(list mu sd)
log-density
(sample/log-density (λ (src) (normal-rng mu sd src)) log-density)))))
(define (gamma-log-density shape scl)
(if (and (fp> shape 0.0) (fp> scl 0.0))
(let ((shape-log-scl (fp* shape (fplog scl)))
(log-g-shape (log-gamma shape))
(shape-1 (fp- shape 1.0)))
(λ (x)
(let-flonums
'gamma
(x)
(if (fp< x 0.0)
-inf.0
(fp- (fp+ (fp- (fpneg shape-log-scl) (fp/ x scl))
(fp* shape-1 (fplog x)))
log-g-shape)))))
(K -inf.0)))
(define (gamma shape scl)
(let-flonums
'gamma
(shape scl)
(let1 (log-density (gamma-log-density shape scl))
(make-resampleable
'gamma
(list shape scl)
log-density
(sample/log-density
(λ (src) (gamma-rng shape scl src))
log-density)))))
(define (beta-log-density a b)
(if (or (fp<= a 0.0) (fp<= b 0.0))
(λ (x)
(hakaru10-error
'parameter
'beta
"beta: parameters must be positve"
a
b))
(let ((factor
(fp- (fp- (log-gamma (fp+ a b)) (log-gamma a)) (log-gamma b)))
(a-1 (fp- a 1.0))
(b-1 (fp- b 1.0)))
(λ (x)
(let-flonums
'beta
(x)
(if (or (fp< x 0.0) (fp> x 1.0))
(hakaru10-error
'support
'beta
"beta: value must be between 0 and 1"
x)
(fp+ (fp+ factor (fp* a-1 (fplog x)))
(fp* b-1 (fplog (fp- 1.0 x))))))))))
(define (beta a b)
(let-flonums
'beta
(a b)
(let1 (log-density (beta-log-density a b))
(make-resampleable
'beta
(list a b)
log-density
(sample/log-density (λ (src) (beta-rng a b src)) log-density)))))
(define (poisson-log-density lam)
(let1 (log-lam (fplog lam))
(λ (x)
(if (not (and (real? x) (integer? x)))
-inf.0
(let1 (x (inexact->exact x))
(cond ((= x 0) (fpneg lam))
((fp= lam 0.0) -inf.0)
((= x 1) (fp- log-lam lam))
((= x 2) (fp- (fplog (fp/ (fp* lam lam) 2.0)) lam))
(else
(let1 (xf (exact->inexact x))
(fp- (fp- (fp* xf log-lam) lam)
(log-gamma (fp+ xf 1.0)))))))))))
(define (knuth-poisson expl src)
(let loop ((p (rng-double! src 0.0 1.0)) (k 1))
(if (fp> p expl)
(loop (fp* p (rng-double! src 0.0 1.0)) (add1 k))
(sub1 k))))
(define (poisson lam)
(let-flonums
'poisson
(lam)
(unless
(fp>= lam 0.0)
(hakaru10-error
'parameter
'poisson
"In Poisson distribution, lambda cannot be negative"
lam))
(let ((expl (fpexp (fpneg lam))) (log-density (poisson-log-density lam)))
(make-resampleable
'poisson
(list lam)
log-density
(if (fp= lam 0.0)
(λ (src) (values 0 0.0))
(sample/log-density
(λ (src) (knuth-poisson expl src))
log-density))))))
(define (laplace mu sd)
(let-flonums
'laplace
(mu sd)
(let* ((log-2sd (fplog (fp* 2.0 sd)))
(log-density
(λ (x)
(let-flonums
'laplace
(x)
(fp- (fpneg log-2sd) (fp/ (fpabs (fp- x mu)) sd)))))
(draw (λ (src)
(unless
(fp> sd 0.0)
(hakaru10-error
'parameter
'laplace
"laplace: invalid parameters"
mu
sd))
(let1 (u (rng-double! src 0.0 1.0))
(if (fp< u 0.5)
(fp+ mu (fp* sd (fplog (fp+ u u))))
(fp- mu (fp* sd (fplog (fp- (fp- 2.0 u) u)))))))))
(make-resampleable
'laplace
(list mu sd)
log-density
(sample/log-density draw log-density)))))
(define (dirichlet-gammas n a src)
(if (zero? n)
(values '() 0.0)
(receive
(xs total)
(dirichlet-gammas (sub1 n) a src)
(let1 (x (gamma-rng a 1.0 src)) (values (cons x xs) (fp+ x total))))))
(define (dirichlet/symmetric n a)
(unless
(and (exact-integer? n) (positive? n))
(hakaru10-error
'parameter
'dirichlet/symmetric
"a positive exact integer is expected"
n))
(let-flonums
'dirichlet/symmetric
(a)
(let* ((log-g-a (log-gamma a))
(a-1 (fp- a 1.0))
(log-g-sum (log-gamma (fold fp+ 0.0 (make-list n a))))
(log-term
(λ (y acc)
(fp+ acc
(fp- (fp* a-1 (fplog (exact->inexact y))) log-g-a))))
(log-density
(λ (xs)
(cond ((not (and (list? xs)
(= (length xs) n)
(every real? xs)))
-inf.0)
((not (every positive? xs))
(hakaru10-error
'support
'dirichlet/symmetric
"dirichlet: all values must be between 0 and 1"
xs))
(else (fp+ (fold log-term 0.0 xs) log-g-sum))))))
(make-resampleable
'dirichlet/symmetric
(list n a)
log-density
(sample/log-density
(λ (src)
(receive
(xs total)
(dirichlet-gammas n a src)
(map (λ (x) (fp/ x total)) xs)))
log-density)))))
(define (dirac x) (make-distk 'dirac #f x))
(define (distk-sample dk src)
(case (distk-kind dk)
((resampleable) ((rdistr-sample (distk-rd dk)) src))
((dirac) (values (distk-value dk) 0.0))
((observed)
(let1 (x (distk-value dk))
(values x ((rdistr-log-density (distk-rd dk)) x))))
(else (hakaru10-error 'internal 'distk-sample "not a distk" dk))))
(define (conditioned obs dk)
(unless
(distk? dk)
(hakaru10-error
'conditioning
'condition
"only distributions can be conditioned"
dk))
(case (distk-kind dk)
((resampleable) (make-distk 'observed (distk-rd dk) obs))
((observed)
(hakaru10-error
'conditioning
'condition
"an observed distribution cannot be conditioned again"
obs
dk))
(else
(hakaru10-error
'conditioning
'condition
"a Dirac distribution cannot be conditioned"
obs
dk))))
(define (condition obs d #!optional (internal? #f))
(when (and internal? (internal? obs))
(hakaru10-error
'conditioning
'condition
"observation must be external to the model"
obs))
(if (procedure? d)
(λ args (condition obs (apply d args)))
(conditioned obs d)))
(define (make-distribution
name
#!key
log-density
sample
(resample #f)
(support #f)
(params '()))
(define (check ok? what)
(unless ok? (hakaru10-error 'construction 'make-distribution what name)))
(check (and (procedure? log-density) (procedure? sample))
"log-density: and sample: procedures are required")
(check (or (not resample) (procedure? resample))
"resample: must be a procedure or #f")
(check (or (not support) (procedure? support))
"support: must be a thunk or #f")
(make-resampleable name params log-density sample resample support))
(define (distribution-log-density dk x)
(unless
(distk? dk)
(hakaru10-error 'parameter 'distribution-log-density "not a distk" dk))
(if (eq? (distk-kind dk) 'dirac)
(if (equal? x (distk-value dk)) 0.0 -inf.0)
((rdistr-log-density (distk-rd dk)) x)))
(define (distribution-sample dk src)
(unless
(distk? dk)
(hakaru10-error 'parameter 'distribution-sample "not a distk" dk))
(distk-sample dk src))
(define (distribution-support dk)
(unless
(distk? dk)
(hakaru10-error 'parameter 'distribution-support "not a distk" dk))
(if (eq? (distk-kind dk) 'dirac)
(list (cons (distk-value dk) 1.0))
(let1 (support (rdistr-support (distk-rd dk)))
(and support (support))))))
test/normal-density: pass
D1. The log-density of the standard normal distribution at 0.1 is
data Normal = Normal !Double !Double !Double !Double
normal :: Double -> Double -> Normal
normal mu sd = Normal mu sd tau (log (tau / pi / 2))
where tau = 1 / (sd*sd)
instance Distribution Normal Double where
logDensity (Normal mu sd tau lf) x =
(-tau * square (x - mu) + lf) / 2 where square y = y * ycf. Distribution.hs:155-162.
(define (test/normal-density _)
(⊦ (equal-approx? 1e-15) -0.923938533204673 (ld (normal 0 1) 0.1))
(⊦ (equal-approx? 1e-15) -0.923938533204673 (ld (normal 0 1) 1/10))
(⊦ (equal-approx? 1e-15)
(- (+ (log 2.0) (* 0.5 (log (* 2.0 3.14159265358979)))))
(ld (normal 1 2) 1.0)))
((eta 0.0) (memory #(8402704 2732752 1048576)) (stdout "") (stderr ""))
test/bern-density: pass
D2. bern 0.4 has the log-likelihoods
log1p, so that replays stay exact. Exact parameters become flonums, as Haskell's Double literals are:
data Bern = Bern !Double !Double !Double
bern :: Double -> Bern
bern p = Bern p (log p) (log (1-p))
instance Distribution Bern Bool where
logDensity (Bern _ lT lF) x = if x then lT else lF
sample (Bern p lT lF) g =
let (t, g') = randomR (0, 1) g in
if t <= p then (True, lT, g') else (False, lF, g')cf. Distribution.hs:53-62.
(define (test/bern-density _)
(let1 (d (bern 0.4))
(⊦ (equal-approx? 1e-15) -0.916290731874155 (ld d #t))
(⊦ (equal-approx? 1e-15) -0.510825623765991 (ld d #f))
(⊦ equal?
'(bern (0.4))
(list (rdistr-name (distk-rd d)) (rdistr-params (distk-rd d)))))
(⊦ equal? '(0.5) (rdistr-params (distk-rd (bern 1/2)))))
((eta 0.0) (memory #(8402704 2734752 1048576)) (stdout "") (stderr ""))
test/uniform-density: pass
D3. uniform 10 20 has the log-density
uniform lo hi = Uniform lo hi (log (recip (hi - lo)))
instance Distribution Uniform Double where
logDensity (Uniform lo hi l) x =
if lo <= x && x <= hi then l else minusInfcf. Distribution.hs:107-117.
(define (test/uniform-density _)
(let1 (d (uniform 10 20))
(⊦ equal?
(list -2.30258509299405 -2.30258509299405 -2.30258509299405 -inf.0)
(map (λ (x) (ld d x)) '(15 10 20 9.99)))))
((eta 0.0) (memory #(8402704 2736880 1048576)) (stdout "") (stderr ""))
test/log-gamma: pass
D4. log-gamma is AS 245: at 0.3, 5, 7.5 and 20 it answers 1.0957979947868837, 3.1780538330716963, 7.534364243376615 and 39.33988418719949
(the prototype's values, through all four branches); it agrees with libm's lgamma within
logGamma :: Double -> Double
logGamma x
| x <= 0 = m_pos_inf
-- Handle positive infinity. logGamma overflows before 1e308 so
-- it's safe
| x > 1e308 = m_pos_inf
-- Normal cases
| x < 1.5 = a + c *
((((r1_4 * b + r1_3) * b + r1_2) * b + r1_1) * b + r1_0) /
((((b + r1_8) * b + r1_7) * b + r1_6) * b + r1_5)
| x < 4 = (x - 2) *
((((r2_4 * x + r2_3) * x + r2_2) * x + r2_1) * x + r2_0) /
((((x + r2_8) * x + r2_7) * x + r2_6) * x + r2_5)
| x < 12 = ((((r3_4 * x + r3_3) * x + r3_2) * x + r3_1) * x + r3_0) /
((((x + r3_8) * x + r3_7) * x + r3_6) * x + r3_5)
| x > 3e6 = k
| otherwise = k + x1 *
((r4_2 * x2 + r4_1) * x2 + r4_0) /
((x2 + r4_4) * x2 + r4_3)cf. Distribution.hs:212-272 (D4, D4b, D4c).
(define (test/log-gamma _)
(⊦ (equal-approx? 1e-14)
'(1.09579799478688 3.1780538330717 7.53436424337661 39.3398841871995)
(map log-gamma '(0.3 5.0 7.5 20.0)))
(⊦ (equal-approx? 1e-08)
(map libm-lgamma '(0.3 1.5 3.9 5.0 7.5 11.9 20.0))
(map log-gamma '(0.3 1.5 3.9 5 7.5 11.9 20)))
(⊦ (lambda (a b) (< (abs (- a b)) (* 1e-12 (abs a))))
(libm-lgamma 10000000.0)
(log-gamma 10000000.0))
(⊦ equal? '(+inf.0 +inf.0 +inf.0) (map log-gamma '(0 -1.0 1.5e+308))))
((eta 0.0) (memory #(8402704 2738456 1048576)) (stdout "") (stderr ""))
test/gamma-density: pass
D5, deviation 2. Upstream's gamma log-density is
gammaLogDensity :: Double -> Double -> Double -> Double
gammaLogDensity shape scl | shape > 0 && scl > 0 =
let logShape = log shape
logGShape = logGamma shape in
\x -> if x < 0 then minusInf else
scl * logShape - scl * x + (shape - 1) * log x - logGShape
gammaLogDensity _ _ = \_ -> minusInfThe port uses the scale form, the one gamma_rng samples:
so gamma 3 (1/3) at 1 is
gamma 3 (1/3) at 2 is
gamma 1 2 at 3 is
(define (test/gamma-density _)
(let1 (d (gamma 3 1/3))
(⊦ (equal-approx? 1e-12) -0.397310314667487 (ld d 1))
(⊦ (equal-approx? 1e-09)
(- (+ (* 3.0 (log 3.0)) (* 2.0 (log 1.0))) 3.0 (log 2.0))
(ld d 1.0))
(⊦ = -inf.0 (ld d -0.5)))
(⊦ = -inf.0 (ld (gamma 0 1) 1.0))
(⊦ = -inf.0 (ld (gamma 2 -1) 1.0))
(⊦ (equal-approx? 1e-12) (- (log 0.5) 0.5) (ld (gamma 1 2) 1.0))
(⊦ (equal-approx? 1e-09)
(- (+ (* 3.0 (log 3.0)) (* 2.0 (log 2.0))) 6.0 (log 2.0))
(ld (gamma 3 1/3) 2.0))
(⊦ (equal-approx? 1e-12) (- (- (log 2.0)) 1.5) (ld (gamma 1 2) 3.0)))
((eta 0.0) (memory #(8402704 2733560 1048576)) (stdout "") (stderr ""))
test/poisson-density: pass
D6. The Poisson log-density at
Int type, deviation 9):
poisson :: Double -> Poisson
poisson lambda | lambda >= 0 = Poisson lambda (exp(-lambda))
poisson lambda = error "In Poisson distribution, lambda cannot be negative"
-- if lambda = 0 then poisson(0) = dirac(0)
instance Distribution Poisson Int where
-- density is (lambda ^ x) * exp(-lambda) / x!
logDensity (Poisson lambda expl) x
| x == 0 = -lambda
| lambda == 0 = minusInf
| x == 1 = log lambda - lambda
| x == 2 = log (lambda*lambda/2) - lambda
| otherwise = fromIntegral x * log lambda - lambda
- logGamma (fromIntegral x + 1)cf. Distribution.hs:352-367.
(define (test/poisson-density _)
(⊦ (equal-approx? 1e-12)
'(-1.0 -1.0 -1.69314718055995 -2.79175946754953)
(map (λ (k) (ld (poisson 1) k)) '(0 1 2 3)))
(⊦ eqv? -0.0 (ld (poisson 0) 0))
(⊦ = -inf.0 (ld (poisson 0) 1))
(⊦ equal? '(-inf.0 -inf.0) (map (λ (k) (ld (poisson 1) k)) '(1.5 -1)))
(⊦ = -1.69314718055995 (ld (poisson 1) 2.0))
(⊦ eq? 'parameter (hakaru10-kind (τ (poisson -1))))
(⊦ equal?
"In Poisson distribution, lambda cannot be negative"
(hakaru10-message (τ (poisson -1)))))
((eta 0.0) (memory #(8402704 2738944 1048576)) (stdout "") (stderr ""))
test/categorical-density: pass
D7. categorical [(a,1),(b,3)] normalizes its weights:
equal?, Haskell's Eq, so a freshly consed (a) or string "b" finds its entry. With duplicate keys upstream is asymmetric, and so is the
port (plan C10): the sampler gives a key the sum of its weights, the log-density
only its first (lookup); the support lists every entry, in
the sampler's order:
data Categorical a = Categorical ![(a,Double)]
categorical :: Eq a => [(a,Double)] -> Categorical a
categorical lst =
let ptotal = sum $ map snd lst in
Categorical $ ptotal `seq` map (\ (i,p) -> (i,p/ptotal)) lst
instance Eq a => Distribution (Categorical a) a where
logDensity (Categorical lst) x = maybe minusInf log $ (lookup x lst)
sample (Categorical lst) g = loop p lst
where
(p, g1) = randomR (0, 1) g
loop p ((x,p0):t) | p < p0 = (x,log p0,g1)
| otherwise = loop (p-p0) tcf. Distribution.hs:87-91, 97-103. The walk has no [] case: when rounding exhausts it, the port answers the last entry (deviation 5).
(define (test/categorical-density _)
(let1 (d (categorical '((a . 1) (b . 3))))
(⊦ (equal-approx? 1e-15)
(list (log 0.25) (log 0.75) -inf.0)
(map (λ (x) (ld d x)) '(a b c))))
(let1 (d (categorical '((a . 1) (b . 2) (a . 1))))
(⊦ = (log 0.25) (ld d 'a))
(⊦ equal? '((a . 0.25) (b . 0.5) (a . 0.25)) (distribution-support d)))
(⊦ equal?
'((x . 1.0) (y . 3.0))
(car (rdistr-params (distk-rd (categorical '((x . 1) (y . 3)))))))
(⊦ eq? 'parameter (hakaru10-kind (τ (categorical '()))))
(let1 (d (categorical '(((a) . 1) ("b" . 3))))
(⊦ (equal-approx? 1e-15)
(list (log 0.25) (log 0.75) -inf.0)
(map (λ (x) (ld d x)) (list (list 'a) (string #\b) (list 'b))))))
((eta 0.0) (memory #(8402704 2739304 1048576)) (stdout "") (stderr ""))
test/uniformly: pass
D8. uniformly on the empty list is an error; on [a,b,c] every member has log-density
equal?, Haskell's elem, so on [(a),"b"] a freshly consed (a) or string "b" has
uniformly :: Eq a => [a] -> Uniformly a
uniformly [] = error "uniformly: empty list"
uniformly lst = Uniformly (A.listArray (0,len-1) lst) (- log (fromIntegral len))
where len = length lst
instance Eq a => Distribution (Uniformly a) a where
logDensity (Uniformly a ll) x = if x `elem` a then ll else minusInf
sample (Uniformly a ll) g = let (i,g1) = randomR (A.bounds a) g in
(unsafeAt a i, ll, g1)cf. Distribution.hs:73-85.
(define (test/uniformly _)
(⊦ eq? 'parameter (hakaru10-kind (τ (uniformly '()))))
(⊦ equal? "uniformly: empty list" (hakaru10-message (τ (uniformly '()))))
(let1 (d (uniformly '(a b c)))
(⊦ = (- (log 3.0)) (ld d 'b))
(⊦ = -inf.0 (ld d 'z))
(⊦ equal? '(a b c) (map car (distribution-support d))))
(let1 (d (uniformly '((a) "b")))
(⊦ equal?
(list (- (log 2.0)) (- (log 2.0)) -inf.0)
(map (λ (x) (ld d x)) (list (list 'a) (string #\b) (list 'b))))))
((eta 0.0) (memory #(8402704 2741680 1048576)) (stdout "") (stderr ""))
test/beta-density: pass
D9. beta 2 2 at
betaLogDensity :: Double -> Double -> Double -> Double
betaLogDensity a b | a <= 0 || b <= 0 =
error "beta: parameters must be positve"
betaLogDensity a b =
let factor = logGamma (a + b)
- logGamma a
- logGamma b
in \x -> if x < 0 || x > 1 then error "beta: value must be between 0 and 1"
else factor + (a - 1) * log x
+ (b - 1) * log (1 - x)cf. Distribution.hs:330-339.
(define (test/beta-density _)
(⊦ (equal-approx? 1e-08) (log 1.5) (ld (beta 2 2) 0.5))
(⊦ =
(+ (+ (- (- (log-gamma 4.0) (log-gamma 2.0)) (log-gamma 2.0)) (log 0.5))
(log 0.5))
(ld (beta 2 2) 0.5))
(⊦ eq? 'support (hakaru10-kind (τ (ld (beta 2 2) 1.5))))
(⊦ equal?
"beta: value must be between 0 and 1"
(hakaru10-message (τ (ld (beta 2 2) 1.5))))
(⊦ eq? 'parameter (hakaru10-kind (τ (ld (beta 0 2) 0.5))))
(⊦ equal?
"beta: parameters must be positve"
(hakaru10-message (τ (ld (beta 0 2) 0.5)))))
((eta 0.0) (memory #(8402704 2746448 1048576)) (stdout "") (stderr ""))
test/normalization: pass
D10. The log-densities are normalized: the probabilities of bern, uniformly and categorical sum to 1, as do their supports; the Poisson ones for
normal 1 2, the corrected gamma 3 (1/3) and gamma 2 1, and beta 2 5 to 1 within
(define (test/normalization _)
(⊦ (equal-approx? 1e-12)
1.0
(+ (exp (ld (bern 0.3) #t)) (exp (ld (bern 0.3) #f))))
(⊦ (equal-approx? 1e-12)
1.0
(fold +
0.0
(map (λ (x) (exp (ld (uniformly '(a b c d e f g)) x)))
'(a b c d e f g))))
(⊦ (equal-approx? 1e-12)
1.0
(fold +
0.0
(map (λ (x) (exp (ld (categorical '((a . 1) (b . 3) (c . 2.5))) x)))
'(a b c))))
(⊦ (equal-approx? 1e-12)
'(1.0 1.0 1.0)
(map support-mass
(list (bern 0.3)
(uniformly '(a b c d e f g))
(categorical '((a . 1) (b . 3) (c . 2.5))))))
(⊦ (equal-approx? 1e-09)
1.0
(fold + 0.0 (map (λ (k) (exp (ld (poisson 3.5) k))) (iota 61))))
(⊦ (equal-approx? 0.0001)
'(1.0 1.0 1.0 1.0)
(list (trapezoid (normal 1 2) -19.0 21.0 20000)
(trapezoid (gamma 3 1/3) 0.0 12.0 12000)
(trapezoid (gamma 2 1) 0.0 40.0 40000)
(trapezoid (beta 2 5) 0.0 1.0 10000))))
((eta 0.05) (memory #(8402704 2746624 1048576)) (stdout "") (stderr ""))
test/sampler-moments: pass
D11. Moments of
normal 10 0.5, gamma 3 (1/3) (mean 1, variance
beta 2 5 (mean
beta 0.5 0.5 (Joehnk: mean
poisson 3.5, uniform 10 20 and bern 0.3.
Deviation 3: gamma 0.5 2 has mean 1 and variance 2. For a
shape below 1 upstream boosts the shape by 1 but keeps the scale, then multiplies
by the scale again, so its samples have mean 2 and variance 8:
gamma_rng shape scl g | shape < 1.0 = (gvar2, g2)
where (gvar1, g1) = gamma_rng (shape + 1) scl g
(w, g2) = randomR (0,1) g1
gvar2 = scl * gvar1 * (w ** recip shape) The port draws the boosted variate with scale 1: the same draws, a correct sample (Distribution.hs:279-282).
(define (test/sampler-moments _)
(let* ((within (λ (tol expected got) (⊦ (equal-approx? tol) expected got)))
(n 20000))
(let1 (mv (mean+var (samples (normal 10 0.5) n)))
(within 0.03 10.0 (car mv))
(within 0.02 0.25 (cdr mv)))
(let1 (mv (mean+var (samples (gamma 3 1/3) n)))
(within 0.03 1.0 (car mv))
(within 0.03 (/ 1.0 3.0) (cdr mv)))
(let1 (mv (mean+var (samples (gamma 0.5 2) n)))
(within 0.05 1.0 (car mv))
(within 0.25 2.0 (cdr mv)))
(within 0.01 (/ 2.0 7.0) (car (mean+var (samples (beta 2 5) n))))
(let1 (mv (mean+var (samples (beta 0.5 0.5) n)))
(within 0.02 0.5 (car mv))
(within 0.01 0.125 (cdr mv)))
(let1 (mv (mean+var (samples (poisson 3.5) n)))
(within 0.06 3.5 (car mv))
(within 0.15 3.5 (cdr mv)))
(within 0.1 15.0 (car (mean+var (samples (uniform 10 20) n))))
(within
0.02
0.3
(/ (count identity (samples (bern 0.3) n)) (exact->inexact n)))))
((eta 0.509) (memory #(16805408 3229064 1048576)) (stdout "") (stderr ""))
test/poisson-sample-ll: pass
D12, deviation 4. The log-likelihood of a Poisson sample is the log-density of
the sampled
acc*expl itself in the LL position:
-- Knuth algorithm
sample (Poisson lambda expl) g
| lambda == 0 = (0,0,g)
| otherwise = let (u, g1) = randomR (0, 1) g
in loop u 1 1 g1
where
-- acc is lamda^(k-1)/(k-1)!
loop p k acc g | p > expl =
let (u, g1) = randomR (0, 1) g
in loop (p*u) (k+1) (acc*lambda/(fromIntegral k)) g1
| otherwise = (k-1, acc*expl, g)cf. Distribution.hs:369-379.
(define (test/poisson-sample-ll _)
(let1 (src (make-stdgen-source 17))
(⊨ (every (λ (i)
(receive
(k ll)
(distribution-sample (poisson 3.5) src)
(= ll (ld (poisson 3.5) k))))
(iota 1000))))
(⊦ equal?
'(0 0.0)
(receive
(k ll)
(distribution-sample (poisson 0) (make-stdgen-source 17))
(list k ll))))
((eta 0.004) (memory #(16805408 2751736 1048576)) (stdout "") (stderr ""))
test/sampler-errors: pass
D13. Building normal 0 0 is fine, as upstream (only normal_rng checks the deviation); sampling from it is an error, as is sampling
from a gamma distribution with a non-positive shape or scale; the messages are
upstream's, typos included. A parameter that is not a real number is an error
when the distribution is built (Haskell's Double type, deviation 9). Sampling a beta distribution with a non-positive
parameter raises upstream's density error at once, without drawing; upstream's
Joehnk loop never ends for a negative parameter (deviation 10):
normal_rng mu sd g | sd > 0 = case marsaglia g of
((x, _), g1) -> (mu + sd * x, g1)
normal_rng _ _ _ = error "normal: invalid parameters"
gamma_rng shape _ _ | shape <= 0.0 = error "gamma: got a negative shape paramater"
gamma_rng _ scl _ | scl <= 0.0 = error "gamma: got a negative scale paramater"
beta_rng a b g | a <= 1.0 && b <= 1.0 =
let (u, g1) = randomR (0.0, 1.0) g
(v, g2) = randomR (0.0, 1.0) g1
x = u ** (recip a)
y = v ** (recip b)
in case (x+y) <= 1.0 of
True -> (x / (x + y), g2)
False -> beta_rng a b g2cf. Distribution.hs:148-153, 276-278, 317-325.
(define (test/sampler-errors _)
(⊦ eq?
'parameter
(hakaru10-kind
(τ (distribution-sample (normal 0 0) (make-stdgen-source)))))
(⊦ equal?
"normal: invalid parameters"
(hakaru10-message
(τ (distribution-sample (normal 0 0) (make-stdgen-source)))))
(⊦ eq?
'parameter
(hakaru10-kind
(τ (distribution-sample (gamma -1 1) (make-stdgen-source)))))
(⊦ equal?
"gamma: got a negative shape paramater"
(hakaru10-message
(τ (distribution-sample (gamma -1 1) (make-stdgen-source)))))
(⊦ equal?
"gamma: got a negative scale paramater"
(hakaru10-message
(τ (distribution-sample (gamma 2 0) (make-stdgen-source)))))
(⊦ eq? 'parameter (hakaru10-kind (τ (normal 'zero 1))))
(let1 (src (make-stdgen-source 17))
(⊦ equal?
'(parameter parameter parameter)
(map (λ (a b)
(hakaru10-kind (τ (distribution-sample (beta a b) src))))
'(-0.5 0.5 0)
'(1 -2 1)))
(⊦ equal? '(18 1) (seeds src))))
((eta 0.0) (memory #(16805408 2756912 1048576)) (stdout "") (stderr ""))
test/bern-flip: pass
D14. bern/flip has upstream's commented-out kernel, which
always flips the value, draws nothing and has
bern keeps the default kernel, which samples again (here
#t) and answers
-- resampling always flips, so fwd and bwd steps are 0
instance RDistribution Bern Bool where
{-
resample d x _ g =
let x' = not x in
((x', logDensity d x', g),0)
-}cf. Distribution.hs:64-70 and 29-32.
(define (test/bern-flip _)
(let* ((d (bern/flip 0.3))
(rd (distk-rd d))
(src (make-stdgen-source 17))
(before (seeds src)))
(⊦ equal?
(list #f (log 0.7) 0.0)
(receive
(x ll fb)
((rdistr-resample rd) src #t (log 0.3))
(list x ll fb)))
(⊦ equal? before (seeds src)))
(let* ((rd (distk-rd (bern 0.3))) (src (make-stdgen-source 17)))
(⊦ equal?
(list #t (log 0.3) (- (log 0.3) (log 0.7)))
(receive
(x ll fb)
((rdistr-resample rd) src #f (log 0.7))
(list x ll fb)))))
((eta 0.0) (memory #(16805408 2756072 1048576)) (stdout "") (stderr ""))
test/condition: pass
D15. Conditioning a resampleable distk makes an observed one that keeps the
distribution and stores the observation; sampling it answers the observation and
its log-likelihood and draws nothing. On a constructor, condition is lifted (curried constructors included) and checked when applied.
Conditioning twice, or a Dirac distribution, is a type error upstream (design.txt
l.397-402) and a conditioning error here; so is an
observation that the predicate passed by (aux hakaru10)'s condition, the engine's internal-part, recognizes as part of the model (an SExp or a Model, at any depth of the
observation):
conditioned :: a -> DistK KResampleable a -> DistK KObserved a
conditioned obs (Resampleable d) = Observed d obs
instance Condition (DistK KResampleable a) where
type CondR (DistK KResampleable a) = DistK KObserved a
type CondV (DistK KResampleable a) = a
condition = conditioned
instance Condition b => Condition (a -> b) where
type CondR (a -> b) = a -> CondR b
type CondV (a -> b) = CondV b
condition v f = \x -> condition v (f x)cf. Metropolis.hs:434-435, Syntax.hs:180-193, and distk-sample, upstream's sampleK (Metropolis.hs:97-100):
sampleK :: DistK k a -> StdGen -> (a, LL, StdGen)
sampleK (Resampleable d) g = sample d g
sampleK (Dirac x) g = (x,0,g)
sampleK (Observed d x) g = (x,logDensity d x,g)(define (test/condition _)
(let* ((d (normal 0 1)) (o (condition 1.5 d)))
(⊦ eq? 'observed (distk-kind o))
(⊦ = 1.5 (distk-value o))
(⊦ eq? (distk-rd d) (distk-rd o))
(let1 (src (make-stdgen-source 17))
(⊦ equal?
(list 1.5 (ld d 1.5))
(receive (x ll) (distk-sample o src) (list x ll)))
(⊦ equal? '(18 1) (seeds src))))
(let1 (o ((condition 1.0 normal) 0 1))
(⊦ eq? 'observed (distk-kind o))
(⊦ equal?
'(normal (0.0 1.0))
(list (rdistr-name (distk-rd o)) (rdistr-params (distk-rd o)))))
(⊦ eq?
'observed
(distk-kind (((condition 2 (λ (a) (λ (b) (uniform a b)))) 0) 3)))
(⊦ eq?
'conditioning
(hakaru10-kind (τ (condition #t (condition #f (bern 0.5))))))
(⊦ eq? 'conditioning (hakaru10-kind (τ (condition 1 (dirac 1)))))
(⊦ eq? 'conditioning (hakaru10-kind (τ ((condition 1 dirac) 1))))
(⊦ eq? 'conditioning (hakaru10-kind (τ (condition 1 42))))
(⊦ eq? 'conditioning (hakaru10-kind (τ (condition 'x (bern 0.5) symbol?))))
(⊦ equal?
"observation must be external to the model"
(hakaru10-message (τ (condition 'x (bern 0.5) symbol?))))
(let1 (d (dirac 3))
(⊦ equal? '(dirac #f) (list (distk-kind d) (distk-rd d))))
(⊦ equal?
'(3 0.0)
(receive
(x ll)
(distk-sample (dirac 3) (make-stdgen-source))
(list x ll)))
(⊦ equal? '(0.0 -inf.0) (list (ld (dirac 3) 3) (ld (dirac 3) 4))))
((eta 0.0) (memory #(16805408 2756552 1048576)) (stdout "") (stderr ""))
test/replay: pass
D16. The samplers replay upstream's draws: from mkStdGen 17, the first three samples of each distribution and the generator after 200
samples are those of a Python transcription of Distribution.hs over the
random-1.0.1.1 StdGen (with deviation 3 for gamma 0.5 2), bit for bit. That transcription reproduced the sample records of
Examples1.hs, so these values are regression oracles: a change in the draw
order (Marsaglia's retries, the gamma until loop, Joehnk's
retries, the categorical walk) or in the floating-point association shows here
first.
(define (test/replay _)
(⊦ equal? '((#t #f #t) 2040924989 1624004889) (replay (bern 0.3)))
(⊦ equal? '((b d c) 33947404 1403159235) (replay (uniformly '(a b c d e))))
(⊦ equal?
'((b b b) 2040924989 1624004889)
(replay (categorical '((a . 1) (b . 3) (c . 2)))))
(⊦ equal?
'((12.9389958502438 13.273788866478 11.967275951981)
2040924989
1624004889)
(replay (uniform 10 20)))
(⊦ equal?
'((9.39612042942084 9.41232243145482 9.44440922646125)
238574758
1847970949)
(replay (normal 10 0.5)))
(⊦ equal?
'((0.380224876466386 1.17402250871286 0.695965406931075)
1634784357
1431524588)
(replay (gamma 3 1/3)))
(⊦ equal?
'((1.66158483367182 5.89134661162112 3.29908537903117)
1634784357
1431524588)
(replay (gamma 2.5 2)))
(⊦ equal?
'((0.230839286683393 0.364622341494618 5.99387735984082)
507113220
1216787979)
(replay (gamma 0.5 2)))
(⊦ equal?
'((0.0859997609215855 0.152619827493028 0.323475141715806)
470875069
467317093)
(replay (beta 2 5)))
(⊦ equal?
'((0.446268276733181 0.0880533825953212 0.0762135858315621)
368601130
810754135)
(replay (beta 0.5 0.5)))
(⊦ equal?
'((0.160356098218376 0.29562975594083 0.260198785673769)
609399710
1534428535)
(replay (beta 0.5 2)))
(⊦ equal? '((2 3 8) 473751772 1482242513) (replay (poisson 3.5))))
((eta 0.007) (memory #(16805408 2759704 1048576)) (stdout "") (stderr ""))
test/laplace: pass
D17 (optional extra). The Laplace distribution, commented out upstream, by
inversion of one uniform draw: log-density
laplace_rng :: (RandomGen g) => Double -> Double -> g -> (Double, g)
laplace_rng mu sd g = sample (randomR (0.0, 1.0) g)
where sample (u, g1) = case u < 0.5 of
True -> (mu + sd * log (u + u), g1)
False -> (mu - sd * log (2.0 - u - u), g1)
laplaceLogDensity :: Floating a => a -> a -> a -> a
laplaceLogDensity mu sd x = - log (2 * sd) - abs (x - mu) / sdcf. Distribution.hs:384-395. Sampling with
(define (test/laplace _)
(let1 (d (laplace 1 2))
(⊦ (equal-approx? 1e-15) (- (- (log 4.0)) 1.5) (ld d 4.0))
(⊦ (equal-approx? 0.0001) 1.0 (trapezoid d -39.0 41.0 40000))
(⊦ equal?
'((-0.0627398739205491 0.153020152986878 -0.865576184156956)
2040924989
1624004889)
(replay d))
(let1 (mv (mean+var (samples d 20000)))
(⊦ (equal-approx? 0.1) 1.0 (car mv))
(⊦ (equal-approx? 0.6) 8.0 (cdr mv))))
(⊦ eq?
'parameter
(hakaru10-kind
(τ (distribution-sample (laplace 0 0) (make-stdgen-source))))))
((eta 0.067) (memory #(16805408 3343656 1048576)) (stdout "") (stderr ""))
test/dirichlet: pass
D18 (optional extra). The symmetric Dirichlet distribution, commented out
upstream, over lists of
dirichlet_rng :: (RandomGen g) => Int -> Double -> g -> ([Double], g)
dirichlet_rng n' a g' = normalize (gammas g' n')
where gammas g 0 = ([], 0, g)
gammas g n = let (xs, total, g1) = gammas g (n-1)
( x, g2) = gamma_rng a 1 g1
in ((x : xs), x+total, g2)
normalize (b, total, h) = (map (/ total) b, h)
dirichletLogDensity :: [Double] -> [Double] -> Double
dirichletLogDensity a x | all (> 0) x = sum' (zipWith logTerm a x) + logGamma (sum a)
where sum' = foldl' (+) 0
logTerm b y = (b-1) * log y - logGamma b
dirichletLogDensity _ _ = error "dirichlet: all values must be between 0 and 1"cf. Distribution.hs:397-411. The first gamma variate drawn ends last in the
list. A value that is not a list of
(define (test/dirichlet _)
(let1 (d (dirichlet/symmetric 3 2))
(⊦ (equal-approx? 1e-08) (log 3.6) (ld d (list 0.2 0.3 0.5)))
(⊦ = -inf.0 (ld d (list 0.5 0.5)))
(⊦ eq? 'support (hakaru10-kind (τ (ld d (list 0.5 0.5 0.0)))))
(⊦ equal?
'(((0.29568912068716 0.572538668183498 0.131772211129342))
1901800541
817484612)
(let1 (r (replay d)) (cons (list (car (car r))) (cdr r))))
(let* ((xs (samples d 20000))
(mv1 (mean+var (map first xs)))
(mv3 (mean+var (map third xs))))
(⊨ (every (λ (v) (< (abs (- (fold + 0.0 v) 1.0)) 1e-12)) xs))
(⊦ (equal-approx? 0.01) (/ 1.0 3.0) (car mv1))
(⊦ (equal-approx? 0.01) (/ 1.0 3.0) (car mv3))
(⊦ (equal-approx? 0.003) (/ 8.0 252.0) (cdr mv1))))
(⊦ (equal-approx? 1e-08)
(log 2.0)
(ld (dirichlet/symmetric 3 1) (list 0.1 0.2 0.7))))
((eta 0.153) (memory #(16805408 5644072 1048576)) (stdout "") (stderr ""))
test/make-distribution: pass
make-distribution builds a user distribution, the Scheme form of a new RDistribution instance (Distribution.hs:14-41): a log-density, a
sampler that answers a value and its log-likelihood, and optionally a proposal
kernel ("This is the place to specify custom proposals", Distribution.hs:18-19),
which defaults to sampling again with
(define (test/make-distribution _)
(let* ((d (make-distribution
'exponential
#:log-density
(λ (x) (if (< x 0.0) -inf.0 (- x)))
#:sample
(λ (src)
(let1 (x (- (log (- 1.0 (rng-double! src 0.0 0.9)))))
(values x (- x))))
#:params
'(1.0)))
(rd (distk-rd d))
(src (make-stdgen-source 17)))
(⊦ eq? 'resampleable (distk-kind d))
(⊦ equal? '(exponential (1.0)) (list (rdistr-name rd) (rdistr-params rd)))
(⊦ = -2.0 (ld d 2.0))
(⊨ (not (distribution-support d)))
(receive
(x ll fb)
((rdistr-resample rd) src 3.0 -3.0)
(⊦ = fb (+ ll 3.0))))
(let* ((d (make-distribution
'coin
#:log-density
(λ (x) (log 0.5))
#:sample
(λ (src) (values (= 0 (rng-integer! src 0 1)) (log 0.5)))
#:resample
(λ (src x ll) (values (not x) ll 0.0))
#:support
(τ '((#t . 0.5) (#f . 0.5)))))
(rd (distk-rd d)))
(⊦ equal?
'(#f 0.0)
(receive
(x ll fb)
((rdistr-resample rd) (make-stdgen-source) #t (log 0.5))
(list x fb)))
(⊦ = 1.0 (support-mass d)))
(⊦ eq?
'construction
(hakaru10-kind (τ (make-distribution 'broken #:sample (λ (src) 0))))))
((eta 0.001) (memory #(16805408 5648272 1048576)) (stdout "") (stderr ""))
test/support-and-printing: pass
distribution-support answers the finite support of a distk, an
alist ((x . p) ...) in the order its sampler walks the values,
or #f for a distribution whose support is infinite or
continuous; an observed distk keeps the support of the distribution it conditions,
and a Dirac distk is certain of its value (Distribution.hs:49-51). It is what the
exact interpretation of a model branches over: where the trace-graph builder draws
one value with distk-sample, the exact enumerator branches over
the support of each active random choice of the static graph (plan §6.10), each
value weighted by its probability.
Distks and their distributions print with their kind, name and parameters, as
Metropolis.hs:62-65 shows a DistK.
(define (test/support-and-printing _)
(⊦ equal? '((#t . 0.25) (#f . 0.75)) (distribution-support (bern 1/4)))
(⊦ equal?
'((#t . 0.25) (#f . 0.75))
(distribution-support (condition #f (bern/flip 0.25))))
(⊦ equal? '((7 . 1.0)) (distribution-support (dirac 7)))
(⊦ equal?
'(#f #f #f)
(map distribution-support (list (normal 0 1) (poisson 2) (beta 1 1))))
(let1 (d (uniformly '(x y z)))
(⊨ (every (λ (x) (pair? (assq x (distribution-support d))))
(samples d 200))))
(⊦ eq? 'parameter (hakaru10-kind (τ (distribution-support 'normal))))
(⊦ equal?
'("#<distk resampleable normal (0.0 1.0)>"
"#<distk observed normal (0.0 1.0) 1.5>"
"#<distk dirac (a b)>"
"#<rdistr categorical (((\"x\" . 1.0)))>")
(map (λ (x) (format "~a" x))
(list (normal 0 1)
(condition 1.5 (normal 0 1))
(dirac '(a b))
(distk-rd (categorical '(("x" . 1))))))))
((eta 0.0) (memory #(16805408 2772576 1048576)) (stdout "") (stderr ""))
(aux hakaru10 util): histograms, moments, tabulation and KL divergence
Utilities
Helpers to summarize the samples of a chain, after Util.hs of Embedded probabilistic domain-specific language Hakaru10, November 2016. A chain of mcmC is a list of samples;
these procedures reduce it to a histogram, to its first two moments, or to the
Kullback-Leibler divergence from another histogram, which is how the paper
compares a model with its exact distribution (P §2.1 p.6-7, l.282-327).
A histogram is an alist ((key . mass) ...) sorted
ascending, standing for Haskell's type Histogram = M.Map Double Double (U:25), whose M.toList is ascending too. Where Haskell
orders keys by the Ord class, the port uses one total
order over Scheme values, generic<?: booleans, then numbers,
characters, strings, symbols, lists and vectors, each ordered in the obvious way
(#f before #t, lists lexicographically,
vectors as the lists of their elements).
The floating-point folds are those of the Haskell code, in the same direction,
so that the KL divergences and moments recorded upstream are reproduced bit for
bit: sum is a left fold, the variance and the insertions of tabulate are right folds, kl folds the keys
of
and the divergence of
which is undefined, and raises an (exn hakaru10) condition of
kind support, where
(module
(aux hakaru10 util)
*
(import
scheme
(chicken base)
(chicken sort)
(chicken flonum)
(only (chicken string) ->string)
(only srfi-1 count)
srfi-69
(aux base)
(only (aux hakaru10 distribution) hakaru10-error))
(define generic-orders
(list (cons boolean? (λ (a b) (and (not a) b)))
(cons number?
(λ (a b)
(cond ((not (and (real? a) (real? b)))
(generic<?
(list (real-part a) (imag-part a))
(list (real-part b) (imag-part b))))
((= a b) (and (exact? a) (inexact? b)))
(else (< a b)))))
(cons char? char<?)
(cons string? string<?)
(cons symbol?
(λ (a b) (string<? (symbol->string a) (symbol->string b))))
(cons (λ (v) (or (null? v) (pair? v)))
(λ (a b)
(cond ((null? a) (pair? b))
((null? b) #f)
((generic<? (car a) (car b)) #t)
((generic<? (car b) (car a)) #f)
(else (generic<? (cdr a) (cdr b))))))
(cons vector?
(λ (a b) (generic<? (vector->list a) (vector->list b))))))
(define (generic-order v)
(let loop ((rank 0) (orders generic-orders))
(cond ((null? orders) (values rank (λ (a b) #f)))
(((caar orders) v) (values rank (cdar orders)))
(else (loop (add1 rank) (cdr orders))))))
(define (generic<? a b)
(let-values
(((ra less?) (generic-order a)) ((rb _) (generic-order b)))
(or (< ra rb) (and (= ra rb) (less? a b)))))
(define (histogram-sort alist)
(sort alist (λ (p q) (generic<? (car p) (car q)))))
(define (hash-table->histogram table)
(histogram-sort (hash-table->alist table)))
(define (hist xs)
(let1 (table (make-hash-table equal?))
(for-each (λ (x) (hash-table-update!/default table x add1 0)) xs)
(hash-table->histogram table)))
(define (frequency pred xs)
(fp/ (exact->inexact (count pred xs)) (exact->inexact (length xs))))
(define (momenta xs)
(let* ((xs (map exact->inexact xs))
(n (exact->inexact (length xs)))
(av (fp/ (foldl fp+ 0.0 xs) n))
(sqr (λ (x) (fp* x x))))
(cons av
(fp/ (foldr (λ (x acc) (fp+ (sqr (fp- x av)) acc)) 0.0 xs)
(fp- n 1.0)))))
(define (to-bin step x)
(let1 (b (fpfloor (fp/ x step))) (fp* (if (fp= b 0.0) 0.0 b) step)))
(define (tabulate step xs)
(let* ((step (exact->inexact step))
(quantum (fp/ 1.0 (exact->inexact (length xs))))
(insert!
(λ (x table)
(hash-table-update!/default
table
(to-bin step (exact->inexact x))
(λ (old) (fp+ quantum old))
0.0)
table)))
(hash-table->histogram (foldr insert! (make-hash-table equal?) xs))))
(define (tabulate/bins nbins xs)
(let* ((ys (map exact->inexact xs))
(mn (foldl fpmin (car ys) (cdr ys)))
(mx (foldl fpmax (car ys) (cdr ys)))
(step-raw
(if (fp= mn mx) 1.0 (fp/ (fp- mx mn) (exact->inexact nbins)))))
(tabulate (fpexpt 10.0 (round (fp/ (fplog step-raw) (fplog 10.0)))) ys)))
(define (kl p q)
(let1 (q (alist->hash-table q equal?))
(foldl (Λ (,sum (,i unquote pi))
(let1 (qi (hash-table-ref
q
i
(τ (hakaru10-error
'support
'kl
(string-append
"Undefined KL divergence - q is 0 when p is not at point "
(->string i))
i))))
(+ sum (* pi (log (/ pi qi))))))
0.0
(histogram-sort p))))
(define (plot h #!optional (path "/tmp/plot.dat"))
(with-output-to-file
path
(τ (print "x y")
(for-each (Λ ((,k unquote v)) (print k " " v)) (histogram-sort h))))
path))
test/hist: pass
U1. hist counts the occurrences of each value (with equal?) and sorts them with generic<?:
-- Very simple histogram, for a discrete distribution
hist :: Ord t => [t] -> M.Map t Int
hist = foldr (\e -> M.insertWith' (+) e 1) M.emptycf. U:11-12; the second check exercises the order across types.
(define (test/hist _)
(⊦ equal? '((#f . 1) (#t . 2) (1 . 1) (2 . 2)) (hist '(#t #f #t 2 1 2)))
(⊦ equal? '() (hist '()))
(⊦ equal?
'((#\a . 1)
("a" . 2)
(a . 1)
(b . 1)
(() . 1)
((1) . 1)
((1 2) . 1)
((2) . 1)
(#(1) . 1))
(hist '(b (2) #(1) "a" (1 2) a () "a" (1) #\a))))
((eta 0.0) (memory #(6291456 2020976 1048576)) (stdout "") (stderr ""))
test/generic<?: pass
The order standing for Haskell's Ord (U:11): an exact number
precedes an equal inexact one, a proper prefix precedes the longer list or vector.
(define (test/generic<? _)
(⊨ (generic<? #f #t))
(⊭ (generic<? #t #f))
(⊨ (generic<? #t -1))
(⊨ (generic<? 1 1.5))
(⊨ (generic<? 1 1.0))
(⊭ (generic<? 1.0 1))
(⊭ (generic<? 2 2))
(⊨ (generic<? 'z '()))
(⊨ (generic<? '(1 2) '(1 2 0)))
(⊨ (generic<? '(1 2) '(1 3)))
(⊨ (generic<? '(9) #(0)))
(⊨ (generic<? #(1 2) #(1 2 0)))
(⊭ (generic<? #(1 3) #(1 2 0))))
((eta 0.0) (memory #(6291456 2017120 1048576)) (stdout "") (stderr ""))
test/frequency: pass
frequency is the fraction of the samples satisfying a predicate, as
a flonum: the probability of an event estimated from a chain.
(define (test/frequency _)
(⊦ equal? 0.75 (frequency even? '(2 4 5 6)))
(⊦ equal? 0.0 (frequency symbol? '(1 2))))
((eta 0.0) (memory #(6291456 2018688 1048576)) (stdout "") (stderr ""))
test/momenta: pass
U2. momenta returns the pair (mean . variance) with the unbiased variance, dividing by
-- compute the first two momenta
momenta :: [Double] -> (Double,Double)
momenta lst = (av,sigma2)
where
n = fromIntegral $ length lst
av = sum lst / n
sigma2 = (foldr (\x acc -> sqr (x - av) + acc) 0 lst) / (n - 1)
sqr x = x * xcf. U:16-22; exact numbers are taken as flonums. The last check pins the
direction of the fold of the squares: folded from the left they would sum to 0.0728702 rather than 0.0728702.
(define (test/momenta _)
(⊦ equal? '(2.5 . 1.66666666666667) (momenta '(1.0 2.0 3.0 4.0)))
(⊦ equal? '(2.5 . 1.66666666666667) (momenta '(1 2 3 4)))
(⊦ equal? '(0.2602 . 0.0728702) (momenta '(0.1 0.2 0.3 0.7 0.001))))
((eta 0.0) (memory #(6291456 2020768 1048576)) (stdout "") (stderr ""))
test/tabulate: pass
U3. tabulate puts each sample in the bin
-- Tabulate with the given bin size
tabulate :: Double -> [Double] -> Histogram
tabulate step lst =
foldr (\x -> M.insertWith' (+) (to_bin x) quantum) M.empty lst
where
quantum = 1 / fromIntegral (length lst)
to_bin x = fromInteger (floor (x / step)) * stepcf. U:40-45. Since floor goes through an Integer, the bin of -0. is 0., as the second check
shows.
(define (test/tabulate _)
(⊦ equal?
'((-0.5 . 0.25) (0.0 . 0.5) (0.5 . 0.25))
(tabulate 0.5 '(0.1 0.2 0.7 -0.1)))
(⊦ equal? '((0.0 . 0.5) (1.0 . 0.5)) (tabulate 1 '(-0.0 0.0 1.5 1.0))))
((eta 0.0) (memory #(6291456 2022976 1048576)) (stdout "") (stderr ""))
test/tabulate/bins: pass
U4. tabulate/bins, that is tabulate', takes
only a hint of the number of bins: the bin size is rounded to a power of ten.
-- The argument is the _hint_ to the number of bins
tabulate' :: Int -> [Double] -> Histogram
tabulate' nbins lst =
foldr (\x -> M.insertWith' (+) (to_bin x) quantum) M.empty lst
where
mn = minimum lst
mx = maximum lst
quantum = 1 / fromIntegral (length lst)
step_raw = if mn == mx then 1 else (mx - mn) / fromIntegral nbins
step = 10.0 ** (fromInteger . round $ log step_raw / log 10)
to_bin x = fromInteger (floor (x / step)) * stepcf. U:28-37. The first check has step 0.1 (the ratio of logarithms
is -1.0), and the floating-point bins show it: .3 falls in the bin .2. The last check pins
Haskell's round, which rounds half to even.
(define (test/tabulate/bins _)
(⊦ equal?
'((0.0 . 0.0909090909090909)
(0.1 . 0.0909090909090909)
(0.2 . 0.181818181818182)
(0.4 . 0.0909090909090909)
(0.5 . 0.181818181818182)
(0.6 . 0.0909090909090909)
(0.8 . 0.0909090909090909)
(0.9 . 0.0909090909090909)
(1.0 . 0.0909090909090909))
(tabulate/bins 10 '(0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0)))
(⊦ equal?
'((0.0 . 0.909090909090909) (10.0 . 0.0909090909090909))
(tabulate/bins 3 '(0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0)))
(⊦ equal?
'((0.0 . 0.333333333333333)
(200.0 . 0.333333333333333)
(1000.0 . 0.333333333333333))
(tabulate/bins 4 '(0.0 250.0 1000.0)))
(⊦ equal? '((3.0 . 1.0)) (tabulate/bins 5 '(3.0 3.0)))
(⊦ equal?
'((0.0 . 0.5) (300.0 . 0.5))
(tabulate/bins 1 '(0.0 316.227766016838))))
((eta 0.0) (memory #(6291456 2025232 1048576)) (stdout "") (stderr ""))
test/kl: pass
U5. kl is the Kullback-Leibler divergence
assoc:
kl :: Histogram -> Histogram -> Double
kl p q = M.foldlWithKey' f 0 p
where f sum i pi | Just qi <- M.lookup i q = sum + pi * log (pi / qi)
| otherwise =
error $ "Undefined KL divergence - q is 0 when p is not at point " ++
show icf. U:51-56; the expected value 0.14384103622589 is
(define (test/kl _)
(define p (tabulate 0.5 '(0.1 0.2 0.7 -0.1)))
(⊦ equal? 0.0 (kl p p))
(⊦ equal?
0.14384103622589
(kl '((0.0 . 0.5) (1.0 . 0.5)) '((1.0 . 0.75) (0.0 . 0.25) (2.0 . 0.0))))
(⊦ equal? 0.693147180559945 (kl '((0.0 . 1.0)) '((0.0 . 0.5) (0.0 . 0.25))))
(⊦raises (exn hakaru10) (kl p '((0.0 . 0.5) (0.5 . 0.5))))
(⊦ equal?
'(support
.
"Undefined KL divergence - q is 0 when p is not at point -0.5")
(condition-case
(kl p '((0.0 . 0.5) (0.5 . 0.5)))
(c (hakaru10)
(cons (get-condition-property c 'hakaru10 'kind)
(get-condition-property c 'exn 'message))))))
((eta 0.0) (memory #(6291456 2031560 1048576)) (stdout "") (stderr ""))
test/plot: pass
U6. plot writes a histogram as a two-column file for gnuplot,
by default /tmp/plot.dat, sorting it first:
plot :: Histogram -> IO ()
plot dat = do
withFile fname WriteMode write_data
where
fname = "/tmp/plot.dat"
write_data h = do
hPutStrLn h "x y"
mapM_ (\ (k,v) -> hPutStrLn h $ unwords [show k,show v]) $ M.toList datcf. U:58-65; upstream plots the histogram of a mixture's components at E:166-167:
_ =
let n = 10000 in plot (tabulate 0.2 $ mcmC (n `div` 2) (dist normal 10 2) ++ mcmC (n `div` 2) (dist gamma 3 (1/3)))(define (test/plot _)
(define path (create-temporary-file "dat"))
(⊦ equal? path (plot '((0.5 . 0.25) (-0.5 . 0.25) (0.0 . 0.5)) path))
(⊦ equal?
'("x y" "-0.5 0.25" "0.0 0.5" "0.5 0.25")
(with-input-from-file path read-lines))
(delete-file path))
((eta 0.001) (memory #(6291456 2033072 1048576)) (stdout "") (stderr ""))
(aux hakaru10): the language
Introduction
Quoting from Embedded probabilistic domain-specific language Hakaru10:
❝
This system description paper introduces the probabilistic programming language Hakaru10, for expressing, and performing inference on (general) graphical models. The language supports discrete and continuous distributions, mixture distributions and conditioning. Hakaru10 is a DSL embedded in Haskell and supports Monte-Carlo Markov Chain (MCMC) inference. Hakaru10 is designed to address two main challenges of probabilistic programming: performance and correctness. It implements the incremental Metropolis-Hastings method, avoiding all redundant computations. In the presence of conditional branches, efficiently maintaining dependencies and correctly computing the acceptance ratio are non-trivial problems, solved in Hakaru10. The implementation is unique in being explicitly designed to satisfy the common equational laws of probabilistic programs. Hakaru10 is typed; specifically, its type system statically prevents meaningless conditioning, enforcing that the values to condition upon must indeed come from outside the model.
The paper is Probabilistic Programming Language and its Incremental Evaluation, APLAS 2016, LNCS 10017 (design.pdf, abstract at design.txt l.8-23). This is a port of the November 2016 code
(index.html:52-53; the page was last updated on July 14, 2018) to CHICKEN Scheme 6:
the modules (aux hakaru10 random) (the random-1.0.1.1 StdGen of GHC, so that upstream's recorded runs replay bit for bit), (aux hakaru10 distribution) (Distribution.hs), (aux hakaru10 util) (Util.hs), (aux hakaru10 engine) (Metropolis.hs and Memory.hs, and the
internals of Syntax.hs), (aux hakaru10 exact) (new: exact enumeration, a forward sampler and
a reference evaluator, the oracles of the tests) and the public (aux hakaru10) (Syntax.hs). Every definition carries a cf. comment naming the upstream lines it transcribes, every
departure a numbered deviation comment (the register is in the
section on findings below). Citations read M:404-419 for Metropolis.hs lines 404-419, and likewise S Syntax.hs, Mem Memory.hs, D
Distribution.hs, U Util.hs, E Examples1.hs, CB ColoredBalls.hs, B1 Birds1.hs, BE
Bench.hs, W index.html, BMP BirdMigrationProblem.txt and P the paper's text
(design.txt), all from okmij.org/ftp/kakuritu/Hakaru10, and R the System/Random.hs of random-1.0.1.1 (hackage, 567 lines), whose StdGen GHC used for upstream's recorded runs.
In Scheme a model is a State computation (S:82, Model a = MCMCM (SExp
a), MCMCM a = State MCState a, M:200) written with do/monad, as in Syntax.hs: its state is the chain, and running it
builds the trace graph, dist, diracN and if_ each adding their nodes. The same grass model below is sampled
by incremental Metropolis-Hastings (mcmC), enumerated exactly over
its trace graph into a Hansei search tree (exact-posterior) and sampled forward over the same graph with likelihood weights (likelihood-weighting).
(define (nnot p b) (if b p 1.0))
(define ((nor strength-x strength-y noise) x y)
(bern (- 1.0
(* (* (nnot (- 1.0 strength-x) x) (nnot (- 1.0 strength-y) y))
(- 1.0 noise)))))
(define grass
(do/monad
(← rain (dist bern 0.3))
(← sprinkler (dist bern 0.5))
(← grass-is-wet (dist (condition #t (nor 0.9 0.8 0.1)) rain sprinkler))
(return rain)))
(((mcmC 10 grass) (#t #t #t #f #f #f #f #f #f #f))
((exact-posterior grass) ((#f . 0.53152855727963) (#t . 0.46847144272037)))
((likelihood-weighting 2000 grass)
((#f . 0.53654028664459) (#t . 0.46345971335541))))
Hakaru10 by example
The tutorial of P §2.1 (l.202-327), with each Haskell model next to its Scheme
transcription in the cases of the replay and laws suites. Haskell's do is do/monad, x <- m is (← x m) and return e is (return e); a model is a value, defined with define as in Haskell, and a model with arguments a procedure
answering one. Quoting P l.204-219:
❝
The first, elementary program is pbern = dist bern 0.4 whose inferred type is Model Bool. The program represents the model consisting of a single Boolean ('Bernoulli') random variable, whose distribution is True with the probability 40% and False with the probability 60%. [...] The function mcmC, the interpreter of probabilistic programs, [...] performs the MH inference on a model and returns the list of samples from the model's distribution. In particular, mcmC 10 pbern produces a list of 10 booleans, which indeed contains 4 True and 6 False values.
(define (pbern* bern) (dist bern 0.4))
(define pnorm (dist normal 10.0 0.5))
(define pdep1 (do/monad (← x pnorm) (diracN (sexp+ x 1.0))))
The Dirac distribution is the unit of model composition (P l.255-261, §4.3
l.803-808), for every model
and independent variables marginalize (P l.273-281): pmarg has the distribution of pnorm. The hierarchical model of Hur
et al. [10, Fig. 3] (P l.283-300), on which STAN infers a wrong distribution, is the
10th iterate of a model transformer; in Scheme, where models are values, it is a fold:
(define (deeper m) (do/monad (← x m) (dist normal x 3.0)))
(define (phierN n) (fold (λ (_ m) (deeper m)) (dist normal 0.0 1.0) (iota n)))
Branching models
Quoting P §2.2 l.330-347:
❝
Hakaru10 can express not only 'straight-line' but also branching models. An example is a simple mixture model [mixng] whose distribution is the mixture of normal and gamma distributions, with the random variable x determining the proportion of the mixture. [...] The mixng program is the first betraying Model being not quite a monad. The variable x is not actually of the type Double, as one might have thought. [...] As should be apparent from the use of Applicative operator <$>, which is just fmap, Model a is something like M (A a) where M is a monad but A is an applicative.
In Scheme the applicative of SExp is lift (with pair, collect and the sexp+ family for the Num instance, S:99-135), while <$> and <*> are those of the Model monad
(B1:52 abs <$> dist normal 0 10), and the branch is if_, whose test must be a boolean stochastic expression:
(define mixng
(do/monad
(← x (dist normal 0.0 1.0))
(if_ (lift (λ (xv) (> xv 0.0)) x)
(dist normal 10.0 2.0)
(dist gamma 3.0 (/ 1.0 3.0)))))
(define (mixng′* bern)
(do/monad
(← x (dist bern 0.5))
(if_ x (dist normal 10.0 2.0) (dist gamma 3.0 (/ 1.0 3.0)))))
Both should give the same distribution, as should the interleaving of 2500 samples of each component (P l.361-366; replay case X12).
Conditioning
Quoting P §2.3 l.393-398 and l.418-421:
❝
As biased coin code demonstrates, in Hakaru10 conditioning may only be applied to distributions. One may think that (c1 `condition` bern) creates a new distribution out of bern, with the singular value c1. Conditioning on arbitrary boolean formulas is fraught with peril, both theoretical and practical, degenerating MCMC algorithm into the inefficient rejection sampling. [...] Here, we attempted to condition bern on the random choice within the model. This is not allowed: observations must be external to the model. In [20], this semantic condition was a merely a coding convention, whose violation manifested as a run-time exception. In Hakaru10, the violation is a type error.
(define (biased-coin c1 c2)
(do/monad
(← b (dist beta 1.0 1.0))
(dist (condition c1 bern) b)
(dist (condition c2 bern) b)
(return b)))
Why distributions: the temperature model of P l.653-673 observes 25 from normal tempr 0.1, an event of probability zero; what can be asked
is how likely 25 is under that distribution, its density, which the observed node
contributes to the trace's log-likelihood. Scheme has no static types, so the
static discipline of Syntax.hs becomes run-time checks (deviation 9, in the style of
Ścibior et al. [20]), each an (exn hakaru10) condition whose kind property says what was violated (case test/language/errors):
- observations are external (S:38-42, S:174-193):
conditionof a stochastic expression, of a model, or of a Scheme value holding either raisesconditioning, and so does an observation that a distribution constructor computes from its argument, at the update that changes it; - only resampleable distributions are conditioned, never twice (S:185-193,
E:180-186):
conditioning; - SExp is not a monad (S:118, P l.739-747): a model answered by a lifted function,
at the build or at an update, raises
construction; - a model is a type of its own (S:82, plan §15 C32): a model in an SExp position
(an argument of
dist, the result, inside a Scheme value) or a value where a model is expected (mcmC's argument, a branch ofif_) raisesconstruction; procedures are plain values (S:107Val f); - the test of
if_isSExp Bool(S:275): a non-boolean raisesbranch; - lexical scoping, implicit upstream (deviation 10): a node of another chain raises
construction, a node escaping its branch, as a parent or as the result of another branch or of the model,branch; - the static kind of a node (M:46-60; deviation 10): a constructor that changes
kind raises
construction.
The conditioned grass model of P §1 (l.73-89) and its posterior P(rain | wet) = 0.4684714427203697 (E:322) close the tutorial; see the first figure above.
Implementation
Quoting P §4.1 l.723-747:
❝
Hakaru10 represents the trace -- random variables of the model and their dependencies -- as a directed acyclic graph (DAG). Each node (vertex) in the graph stands for one random variable (whose value can be sampled and resampled) or an observed variable, whose value cannot be resampled. There are also computational nodes, representing 'samples' from the dirac distribution. [...] A Hakaru10 model has the typetype Model a = MCMCM (SExp a)It is a computation that constructs the trace graph. MCMCM is a monad, but SExp is not. It is an applicative [13]: it lets us construct new graph nodes, without looking at their values. [...] The fact that SExp is not a monad is significant: the current node values cannot influence the graph construction. Therefore, after the graph is built, its structure does not change. All dependencies among nodes can be computed once and for all.
In (aux hakaru10 engine) the trace is a chain, a vector of node records indexed by creation order (the address,
replacing Memory.hs); a node's dynamic state is an immutable nstate record (value, log-likelihood, distribution, timestamp)
replaced as a whole, as Mem.modify_ref does. Model a = MCMCM (SExp a) is a State monad threading the chain (plan
§15 C34), run once per build by build-chain (M:872-880
initial_step'), whose construction functions mirror Syntax.hs and
Metropolis.hs by name (create-node, with-node-ctor, to-node, ifnode, ...);
an SExp is a node, an nt (leaves and a function: S:85-116's NT) or any other value (Val). For the grass
model the graph has the three nodes of P's Fig. 1 (case test/language/graph/grass).
Incremental recomputation (P §4.2 l.765-800): resampling a node enqueues its
dependents; propagate! (M:293-317) pops the queue in increasing
address order, a binary heap, because
❝Creation order is therefore load-bearing (plan C18): a node is created when the model that makes it runs, in bind order,
The fact that Hakaru10 models are acyclic and Hakaru10 programs are declarative (with no mutations) lets us avoid the topological sort and rely on the 'creation times' of trace nodes. A node can only depend on those constructed before it.
do/monad top to bottom
and the traversals head first (plan §15 C35; see the embedding notes).The Dirac distribution (P §4.3 l.801-843): a Dirac node is a pure computation,
recomputed whenever a parent changes, its log-likelihood staying 0, so a proposal is
extended to all dependent Dirac nodes and the unit laws hold by construction
(laws suite). diracN of a node answers the node itself, shared
(S:164-168).
Branching (P §4.4 l.844-869):
❝
Hakaru10 avoids any modifications to the graph structure during the trace update. It compiles both if-branches when constructing the initial graph. [...] Updating the nodes in the inactive branch, whose values will be ultimately ignored, is not good for performance. Therefore, we mark the nodes in the non-current branch as inactive, and delay their updates until they are activated. Thus each node, along with its current value, LL and the distribution also keeps the inactivation count.
if_ (S:275-279) builds the entry (the test, always a new Dirac
node); ifnode (M:641-686) then builds the inactive branch first,
deactivates its nodes (deactivate, M:696-717, which takes the observed nodes' LL back), the active branch, and the
exit node, whose value is the active branch's result (update-exit, M:778-794). When the entry's value changes, update-entry (M:760-775) moves the counts and the observed LL
between the branches and replays the updates delayed while a node was inactive.One MH step, transit! (M:908-945): pick an active resampleable
node uniformly (M:956-977), resample it from its own distribution, propagate, and
accept with the log ratio of M:929-934, where
A rejected step restores the whole state from a journal of undo thunks, except the random generator (M:904-906). The engine's source:
(module
(aux hakaru10 engine)
*
(import
scheme
(only (scheme base)
vector-map
vector-for-each
make-parameter
exact-integer?)
(chicken base)
(chicken fixnum)
(chicken flonum)
(only (chicken format) fprintf)
(only (chicken port) with-output-to-port)
(only (chicken sort) sort!)
(only (chicken condition)
make-property-condition
signal
condition-case
with-exception-handler
condition-predicate
condition-property-accessor)
(only srfi-1 any every find delete-duplicates append-map list-index iota)
(only srfi-69 make-hash-table hash-table-exists? hash-table-set!)
(aux base)
(only (aux category state)
category-state?
state
run-state
eval-state
exec-state
get
gets
modify
return
>>=)
(only (aux category monad state) do/monad ▷ when/monad)
(aux hakaru10 random)
(aux hakaru10 distribution))
(declare (local))
(define-record nstate val ll dst tstamp)
(set-record-printer!
nstate
(λ (s port)
(fprintf
port
"#<nstate ~s ll ~s t ~s>"
(nstate-val s)
(nstate-ll s)
(nstate-tstamp s))))
(define-record
node
chain
addr
kind
role
parents
fn
update
scope
deps
branch
state
inactive
delayed
pending
queued)
(set-record-printer!
node
(λ (n port) (fprintf port "#<node ~a ~a>" (node-addr n) (node-kind n))))
(define (node-value n) (nstate-val (node-state n)))
(define (node-inactive? n) (fx> (node-inactive n) 0))
(define-record
chain
rng
semantics
now
nodes
count
llcum
sampleable
samp
card-change
queue
pending
journal
scope
step-hook)
(set-record-printer!
chain
(λ (ch port)
(fprintf
port
"#<chain ~a nodes, now ~a>"
(chain-count ch)
(chain-now ch))))
(define (new-chain src semantics)
(unless
(memq semantics '(upstream extended))
(hakaru10-error
'construction
'mcmC
"semantics must be 'upstream or 'extended"
semantics))
(make-chain
src
semantics
1
(make-vector 64 #f)
0
0.0
'()
(vector)
0
#f
'()
#f
'()
#f))
(define (chain-node ch a) (vector-ref (chain-nodes ch) a))
(define (chain-store! ch n)
(let ((a (node-addr n)) (v (chain-nodes ch)))
(when (fx>= a (vector-length v))
(chain-nodes-set!
ch
(vector-resize v (fx* 2 (vector-length v)) #f)))
(vector-set! (chain-nodes ch) a n)
(chain-count-set! ch a)))
(define (chain-fold kons knil ch #!optional (lo 0) (hi (chain-count ch)))
(let loop ((a (fx+ lo 1)) (acc knil))
(if (fx> a hi) acc (loop (fx+ a 1) (kons (chain-node ch a) acc)))))
(define (chain-freeze! ch)
(chain-samp-set! ch (list->vector (chain-sampleable ch)))
(chain-queue-set!
ch
(make-update-queue (make-vector (fx+ 1 (chain-count ch)) #f) 0))
(chain-pending-set! ch '())
(chain-scope-set! ch '()))
(define (extended? ch) (eq? (chain-semantics ch) 'extended))
(define (journal-begin! ch)
(when (chain-journal ch) (journal-rollback! ch))
(let ((now (chain-now ch)) (card (chain-card-change ch)))
(chain-journal-set!
ch
(list (τ (chain-now-set! ch now) (chain-card-change-set! ch card))))))
(define (journal-commit! ch) (chain-journal-set! ch #f))
(define (journal-rollback! ch)
(for-each (λ (undo) (undo)) (chain-journal ch))
(chain-journal-set! ch #f)
(chain-pending-set! ch '())
(update-queue-clear! (chain-queue ch)))
(define-syntax-rule
(journaled-set! (getter setter) n v)
(let* ((n* n)
(ch (node-chain n*))
(old (getter n*))
(journal (chain-journal ch)))
(when journal (chain-journal-set! ch (cons (τ (setter n* old)) journal)))
(setter n* v)))
(define (set-node-state! n s)
(journaled-set! (node-state node-state-set!) n s))
(define (set-inactive! n k)
(journaled-set! (node-inactive node-inactive-set!) n k))
(define (set-delayed! n flag)
(unless
(eq? flag (node-delayed n))
(journaled-set! (node-delayed node-delayed-set!) n flag)))
(define (set-pending! n flag)
(unless
(eq? flag (node-pending n))
(journaled-set! (node-pending node-pending-set!) n flag)
(when flag
(let1 (ch (node-chain n))
(chain-pending-set! ch (cons n (chain-pending ch)))))))
(define-record update-queue heap size)
(define-syntax-rule (node<? a b) (fx< (node-addr a) (node-addr b)))
(define (enqueue! q n)
(unless
(node-queued n)
(node-queued-set! n #t)
(let ((heap (update-queue-heap q)) (i (update-queue-size q)))
(update-queue-size-set! q (fx+ i 1))
(let up ((i i))
(if (fx= i 0)
(vector-set! heap 0 n)
(let* ((p (fxshr (fx- i 1) 1)) (parent (vector-ref heap p)))
(if (node<? n parent)
(begin (vector-set! heap i parent) (up p))
(vector-set! heap i n))))))))
(define (dequeue! q)
(let* ((heap (update-queue-heap q))
(size (fx- (update-queue-size q) 1))
(top (vector-ref heap 0))
(last (vector-ref heap size)))
(update-queue-size-set! q size)
(node-queued-set! top #f)
(let down ((i 0))
(let* ((l (fx+ 1 (fx* 2 i))) (r (fx+ l 1)))
(if (fx>= l size)
(vector-set! heap i last)
(let* ((c (if (and (fx< r size)
(node<? (vector-ref heap r) (vector-ref heap l)))
r
l))
(child (vector-ref heap c)))
(if (node<? child last)
(begin (vector-set! heap i child) (down c))
(vector-set! heap i last))))))
top))
(define (update-queue-empty? q) (fx= 0 (update-queue-size q)))
(define (update-queue-clear! q)
(let ((heap (update-queue-heap q)))
(do ((i 0 (fx+ i 1)))
((fx= i (update-queue-size q)))
(node-queued-set! (vector-ref heap i) #f))
(update-queue-size-set! q 0)))
(define (enqueue-all! ch nodes)
(let1 (q (chain-queue ch)) (for-each (λ (n) (enqueue! q n)) nodes)))
(define-record nt leaves fn)
(set-record-printer!
nt
(λ (e port)
(fprintf port "#<nt ~s>" (map node-addr (vector->list (nt-leaves e))))))
(define (sexp? x) (or (node? x) (nt? x)))
(define (sexp-chain e)
(node-chain (if (node? e) e (vector-ref (nt-leaves e) 0))))
(define (sexp-leaves e)
(cond ((node? e) (vector e)) ((nt? e) (nt-leaves e)) (else (vector))))
(define (leaf-values leaves) (vector-map node-value leaves))
(define-record deferred fn args)
(set-record-printer! deferred (λ (d port) (fprintf port "#<deferred>")))
(define (deferrable? e) (or (not (hakaru10-error? e)) (parameter-error? e)))
(define (force-value x)
(if (deferred? x) ((deferred-fn x) (deferred-args x)) x))
(define (first-deferred vs)
(let loop ((i 0))
(cond ((fx= i (vector-length vs)) #f)
((deferred? (vector-ref vs i)) (vector-ref vs i))
(else (loop (fx+ i 1))))))
(define (forced-values vs)
(if (first-deferred vs) (vector-map force-value vs) vs))
(define (forced-list vs) (if (any deferred? vs) (map force-value vs) vs))
(define (call/deferring g args)
(condition-case
(g args)
(e (exn) (if (deferrable? e) (make-deferred g args) (signal e)))))
(define (lazy-value g vs inactive?)
(if inactive?
(or (first-deferred vs) (call/deferring g vs))
(g (forced-values vs))))
(define lazy-lifting (make-parameter #f))
(define (sexp-value e)
(cond ((node? e) (force-value (node-value e)))
((nt? e) ((nt-fn e) (forced-values (leaf-values (nt-leaves e)))))
(else e)))
(define (check-not-model who x)
(when (model? x)
(hakaru10-error
'construction
who
"a model where an SExp is expected; bind it with ← first"
x)))
(define (internal-part x)
(let1 (r (internal-part/bounded x 100000))
(cond ((not (fixnum? r)) r)
((fx>= r 0) #f)
(else (internal-part/cyclic x)))))
(define (internal-part/bounded x budget)
(cond ((fx< budget 0) budget)
((or (number? x) (null? x) (boolean? x) (symbol? x)) budget)
((pair? x)
(let1 (r (internal-part/bounded (car x) (fx- budget 1)))
(if (fixnum? r) (internal-part/bounded (cdr x) r) r)))
((vector? x)
(let loop ((i 0) (b (fx- budget 1)))
(if (fx= i (vector-length x))
b
(let1 (r (internal-part/bounded (vector-ref x i) b))
(if (fixnum? r) (loop (fx+ i 1) r) r)))))
((or (sexp? x) (model? x)) x)
(else budget)))
(define (internal-part/cyclic x)
(let1 (seen (make-hash-table eq?))
(let walk ((x x))
(cond ((or (pair? x) (vector? x))
(and (not (hash-table-exists? seen x))
(begin
(hash-table-set! seen x #t)
(if (pair? x)
(or (walk (car x)) (walk (cdr x)))
(any walk (vector->list x))))))
((or (sexp? x) (model? x)) x)
(else #f)))))
(define (check-no-sexp who x)
(let1 (y (internal-part x))
(cond ((sexp? y)
(hakaru10-error
'construction
who
"an SExp inside a Scheme value; combine with collect, pair or lift"
y))
((model? y)
(hakaru10-error
'construction
who
"a model inside a Scheme value"
y)))))
(define (check-lifted-value who v #!optional deep?)
(cond ((or (number? v) (boolean? v)) v)
((sexp? v)
(hakaru10-error
'construction
who
"a lifted function answered an SExp"
v))
((model? v)
(hakaru10-error
'construction
who
"a lifted function answered a model"
v))
((and deep? (or (pair? v) (vector? v))) (check-no-sexp who v) v)
(else v)))
(define (lift* f es #!optional (checked? #t))
(for-each (λ (e) (check-not-model 'lift e)) es)
(if (any sexp? es)
(lift/fuse f es checked?)
(let1 (v (lift/apply f es))
(if checked? (check-lifted-value 'lift v #t) v))))
(define (lift/apply f vs)
(cond ((not (lazy-lifting)) (apply f (forced-list vs)))
((find deferred? vs))
(else (call/deferring (λ (args) (apply f args)) vs))))
(define (lift/fuse f es checked?)
(let* ((leaves
(delete-duplicates
(append-map (λ (e) (vector->list (sexp-leaves e))) es)
eq?))
(owner (node-chain (car leaves)))
(position (λ (n) (list-index (λ (m) (eq? n m)) leaves)))
(extractors (map (λ (e) (lift/extractor position e)) es)))
(unless
(every (λ (n) (eq? (node-chain n) owner)) leaves)
(hakaru10-error
'construction
'lift
"nodes of different model runs"
es))
(make-nt
(list->vector leaves)
(if checked?
(let1 (first? #t)
(λ (vs)
(let1 (v (check-lifted-value
'lift
(apply f (map (λ (x) (x vs)) extractors))
first?))
(set! first? #f)
v)))
(λ (vs) (apply f (map (λ (x) (x vs)) extractors)))))))
(define (lift/extractor position e)
(cond ((node? e) (let1 (i (position e)) (λ (vs) (vector-ref vs i))))
((nt? e)
(let* ((g (nt-fn e))
(ps (map position (vector->list (nt-leaves e)))))
(if (equal? ps (iota (length ps)))
g
(let1 (ps (list->vector ps))
(λ (vs) (g (vector-map (λ (p) (vector-ref vs p)) ps)))))))
((deferred? e) (λ (vs) (force-value e)))
(else (K e))))
(define model? category-state?)
(define (check-model m who)
(unless
(model? m)
(hakaru10-error
'construction
who
"a model (a State computation: do/monad, dist, diracN, if_ or return) is expected"
m))
m)
(define-syntax-rule (modify! (ch) body ...) (modify (λ (ch) body ... ch)))
(define (hakaru10-fail kind who msg . irritants)
(state (λ (_) (apply hakaru10-error kind who msg irritants))))
(define (lazy-model who thunk)
(state (λ (ch) (run-state (check-model (thunk) who) ch))))
(define (condition/external obs d) (condition obs d internal-part))
(define (check-owner ch e who)
(when (and (sexp? e) (not (eq? (sexp-chain e) ch)))
(hakaru10-error
'construction
who
"a node of another model run (a stale node) is used"
e)))
(define (check-distk who dk)
(unless
(distk? dk)
(hakaru10-error
'construction
who
"a distribution constructor must return a distribution"
dk))
dk)
(define (check-observation n obs)
(unless
(equal? obs (node-value n))
(hakaru10-error
'conditioning
'update
"observation must be external to the model"
n
(node-value n)
obs)))
(define (inactive-scope? ch)
(any (λ (p) (not (eq? (node-value (car p)) (cdr p)))) (chain-scope ch)))
(define (parameter-error? e) (eq? (hakaru10-error-kind e) 'parameter))
(define (build-guard ch thunk)
(if (and (pair? (chain-scope ch)) (extended? ch) (inactive-scope? ch))
(condition-case
(thunk)
(e (exn)
(signal
(if (deferrable? e)
(make-property-condition 'hakaru10-rejected-build)
e))))
(thunk)))
(define (scope-tail? s s*)
(let tail? ((cs s*)) (or (eq? cs s) (and (pair? cs) (tail? (cdr cs))))))
(define (check-branch-escape parent child)
(unless
(or (eq? (node-role child) 'exit)
(scope-tail? (node-scope parent) (node-scope child)))
(hakaru10-error 'branch 'if_ "node escapes its branch" parent child)))
(define (add-dependency! parent child)
(check-branch-escape parent child)
(unless
(memq child (node-deps parent))
(node-deps-set! parent (cons child (node-deps parent)))))
(define (check-active-update n leaves)
(vector-for-each
(λ (p)
(when (node-inactive? p)
(hakaru10-error
'dependency
'update
"the parent or the node itself are inactive"
p
n)))
leaves)
(when (node-inactive? n)
(hakaru10-error
'dependency
'update
"the parent or the node itself are inactive"
n)))
(define (leaves-tstamp leaves)
(let loop ((i 0) (ts 0))
(if (fx= i (vector-length leaves))
ts
(loop (fx+ i 1)
(fxmax ts (nstate-tstamp (node-state (vector-ref leaves i))))))))
(define (doupdate n)
(let ((ch (node-chain n)) (self (node-state n)) (leaves (node-parents n)))
(unless (extended? ch) (check-active-update n leaves))
(unless
(fx> (leaves-tstamp leaves) (nstate-tstamp self))
(hakaru10-error
'dependency
'update
"dependency violation!"
n
(leaves-tstamp leaves)))
(let* ((dk (check-distk 'update ((node-fn n) (leaf-values leaves))))
(kind (distk-kind dk)))
(unless
(eq? kind (node-kind n))
(hakaru10-error
'construction
'update
"a distribution constructor changed the kind of its node"
n
(node-kind n)
kind))
(values
(if (eq? kind 'dirac)
(make-nstate
(check-lifted-value 'update (distk-value dk))
0.0
#f
(chain-now ch))
(let ((rd (distk-rd dk)) (v (nstate-val self)))
(when (eq? kind 'observed)
(check-observation n (distk-value dk)))
(make-nstate
v
((rdistr-log-density rd) v)
rd
(nstate-tstamp self))))
'()))))
(define (new-nref n)
(state (λ (ch)
(chain-store! ch n)
(when (eq? (node-kind n) 'resampleable)
(chain-sampleable-set! ch (cons n (chain-sampleable ch))))
(chain-llcum-set!
ch
(fp+ (chain-llcum ch) (nstate-ll (node-state n))))
(cons n ch))))
(define (new-dependency parent child)
(modify! (ch) (add-dependency! parent child)))
(define (register-dependencies leaves child)
(modify! (ch) (vector-for-each (λ (p) (add-dependency! p child)) leaves)))
(define (create-node
update
dk
#!optional
(role 'root)
(parents (vector))
(fn dk))
(state (λ (ch)
(receive
(v ll)
(build-guard ch (τ (distk-sample dk (chain-rng ch))))
(run-state
(new-nref
(make-node
ch
(fx+ 1 (chain-count ch))
(distk-kind dk)
role
parents
fn
update
(chain-scope ch)
'()
#f
(make-nstate v ll (distk-rd dk) (chain-now ch))
0
#f
#f
#f))
ch)))))
(define (with-node-none dk) (create-node #f dk))
(define (with-node-tree leaves fn #!optional (who 'dist))
(state (λ (ch)
(let1 (dk (check-distk
who
(build-guard ch (τ (fn (leaf-values leaves))))))
(when (eq? (distk-kind dk) 'dirac)
(check-no-sexp who (distk-value dk)))
(let* ((r (run-state
(create-node doupdate dk 'plain leaves fn)
ch))
(n (car r)))
(cons n
(exec-state
(register-dependencies leaves n)
(cdr r))))))))
(define (with-node ndr f #!optional (who 'dist))
(with-node-tree
(vector ndr)
(λ (vs) (f (force-value (vector-ref vs 0))))
who))
(define (lazy-tree leaves g df who)
(do/monad
(← ch get)
(let self #f)
(← n
(with-node-tree
leaves
(λ (vs)
(df (lazy-value
g
vs
(if self (node-inactive? self) (inactive-scope? ch)))))
who))
(let _ (set! self n))
(return n)))
(define (with-node-ctor e df #!optional (who 'dist) lazy)
(check-not-model who e)
(do/monad
(gets (λ (ch) (check-owner ch e who)))
(cond ((node? e)
(if lazy
(lazy-tree (vector e) (λ (vs) (vector-ref vs 0)) df who)
(with-node e df who)))
((nt? e)
(if lazy
(lazy-tree (nt-leaves e) (nt-fn e) df who)
(let1 (g (nt-fn e))
(with-node-tree
(nt-leaves e)
(λ (vs) (df (g (forced-values vs))))
who))))
(else
(check-no-sexp who e)
(>>= (gets (λ (ch)
(check-distk
who
(build-guard ch (τ (df (force-value e)))))))
with-node-none)))))
(define (to-node e #!optional (who 'to_node))
(check-not-model who e)
(cond ((node? e)
(do/monad
(gets (λ (ch)
(check-owner ch e who)
(unless
(scope-tail? (node-scope e) (chain-scope ch))
(hakaru10-error
'branch
who
"node escapes its branch"
e))))
(return e)))
((nt? e) (with-node-ctor e dirac who #t))
(else (check-no-sexp who e) (with-node-none (dirac e)))))
(define (pair a b) (lift* cons (list a b) #f))
(define (collect es) (lift* list es #f))
(define-syntax-rule
(ctor-apply applied? d args call)
(check-distk
'dist
(if applied?
call
(let1 (dk (call/arity-check d (λ () call) args))
(set! applied? #t)
dk))))
(define (call/arity-check d thunk args)
(with-exception-handler
(λ (e)
(if (and (arity-condition? e) (memq d (exn-arguments e)))
(hakaru10-error
'construction
'dist
"wrong number of arguments for the distribution constructor"
d
args)
(signal e)))
thunk))
(define arity-condition? (condition-predicate 'arity))
(define exn-arguments (condition-property-accessor 'exn 'arguments '()))
(define (dist d . args)
(cond ((distk? d)
(unless
(null? args)
(hakaru10-error
'construction
'dist
"a distribution takes no arguments"
d
args)))
((procedure? d)
(for-each
(λ (a)
(check-not-model 'dist a)
(unless (sexp? a) (check-no-sexp 'dist a)))
args))
(else
(hakaru10-error
'construction
'dist
"a distribution or a distribution constructor is expected"
d)))
(let1 (applied? #f)
(match/first
args
(()
(if (distk? d)
(with-node-none d)
(with-node-ctor '() (λ (_) (ctor-apply applied? d '() (d))))))
((,x)
(with-node-ctor x (λ (v) (ctor-apply applied? d (list v) (d v)))))
((,x ,y)
(with-node-ctor
(pair x y)
(Λ ((,a unquote b))
(ctor-apply applied? d (list a b) (d a b)))))
(else (with-node-ctor
(collect args)
(λ (vs) (ctor-apply applied? d vs (apply d vs))))))))
(define (diracN e) (to-node e 'diracN))
(define (if_* test th el)
(check-not-model 'if_ test)
(do/monad
(← entry (with-node-ctor test (λ (v) (dirac (check-test 'if_ v))) 'if_))
(modify! (ch) (node-role-set! entry 'entry))
(ifnode
entry
(▷ (check-model th 'if_) to-node)
(▷ (check-model el 'if_) to-node))))
(define (model->trace m who) (▷ (check-model m who) to-node))
(define-record branch entry then-result else-result then-range else-range)
(set-record-printer!
branch
(λ (b port)
(fprintf
port
"#<branch ~a ~a ~a>"
(node-addr (branch-entry b))
(node-addr (branch-then-result b))
(node-addr (branch-else-result b)))))
(define (branch-active-result b)
(if (node-value (branch-entry b))
(branch-then-result b)
(branch-else-result b)))
(define (check-test who v . irritants)
(unless
(boolean? v)
(apply hakaru10-error
'branch
who
"the test of if_ must be a boolean"
v
irritants))
v)
(define (move-activity!
ch
lo
hi
step
switching
observed-weight
dirac-switched)
(let loop ((a (fx+ lo 1)) (llc 0.0) (k 0))
(if (fx> a hi)
(values llc k)
(let* ((n (chain-node ch a))
(count (node-inactive n))
(kind (node-kind n)))
(when (fx< (fx+ count step) 0)
(hakaru10-error
'internal
'activate
"activating an active node"
n))
(set-inactive! n (fx+ count step))
(if (fx= count switching)
(loop (fx+ a 1)
(fp+ llc
(case kind
((observed) (observed-weight n))
((dirac) (dirac-switched n) 0.0)
(else 0.0)))
(if (eq? kind 'resampleable) (fx+ k 1) k))
(loop (fx+ a 1) llc k))))))
(define (deactivate! ch lo hi)
(receive
(llc k)
(move-activity!
ch
lo
hi
1
0
(λ (n)
(if (node-pending n)
(begin (set-pending! n #f) 0.0)
(nstate-ll (node-state n))))
void)
(chain-llcum-set! ch (fp- (chain-llcum ch) llc))
(chain-card-change-set! ch (fx- (chain-card-change ch) k))))
(define (activate! ch lo hi)
(receive
(llc k)
(move-activity!
ch
lo
hi
-1
1
(if (extended? ch)
(λ (n) (set-pending! n #t) 0.0)
(λ (n) (nstate-ll (node-state n))))
(λ (n)
(when (deferred? (node-value n))
(chain-pending-set! ch (cons n (chain-pending ch))))))
(chain-llcum-set! ch (fp+ (chain-llcum ch) llc))
(chain-card-change-set! ch (fx+ (chain-card-change ch) k))))
(define (settle-pending! ch)
(let1 (pending (chain-pending ch))
(when (pair? pending)
(for-each
(λ (n)
(cond ((node-pending n)
(set-pending! n #f)
(chain-llcum-set!
ch
(fp+ (chain-llcum ch) (nstate-ll (node-state n)))))
((and (eq? (node-kind n) 'dirac)
(not (node-inactive? n)))
(let1 (s (node-state n))
(when (deferred? (nstate-val s))
(set-node-state!
n
(make-nstate
(force-value (nstate-val s))
(nstate-ll s)
(nstate-dst s)
(nstate-tstamp s))))))))
(sort! pending (λ (a b) (node<? a b))))
(chain-pending-set! ch '()))))
(define (switch-branches! ch s* extra newactive newinactive)
(deactivate! ch (car newinactive) (cdr newinactive))
(activate! ch (car newactive) (cdr newactive))
(values
s*
(if (extended? ch)
extra
(chain-fold
(λ (n deps)
(if (node-delayed n)
(begin (set-delayed! n #f) (cons n deps))
deps))
extra
ch
(car newactive)
(cdr newactive)))))
(define ((update-entry prev b) n)
(let1 (old (node-value n))
(receive
(s* extra)
(prev n)
(let ((tv (nstate-val s*))
(ths (branch-then-range b))
(els (branch-else-range b)))
(check-test 'if_ tv n)
(cond ((eq? tv old) (values s* extra))
(tv (switch-branches! (node-chain n) s* extra ths els))
(else
(switch-branches! (node-chain n) s* extra els ths)))))))
(define ((update-exit b) n)
(let* ((self (node-state n))
(ts (nstate-tstamp self))
(e (node-state (branch-entry b)))
(result (node-state (branch-active-result b))))
(values
(if (or (fx> (nstate-tstamp e) ts) (fx> (nstate-tstamp result) ts))
(make-nstate
(let1 (v (nstate-val result))
(if (node-inactive? n) v (force-value v)))
(nstate-ll self)
(nstate-dst self)
(chain-now (node-chain n)))
self)
'())))
(define (deactivate lo hi) (modify! (ch) (deactivate! ch lo hi)))
(define (in-branch entry polarity m)
(state (λ (ch)
(let1 (scope (chain-scope ch))
(chain-scope-set! ch (cons (cons entry polarity) scope))
(let1 (r (if (inactive-scope? ch)
(parameterize
((lazy-lifting #t))
(run-state m ch))
(run-state m ch)))
(chain-scope-set! (cdr r) scope)
r)))))
(define (ifnode entry th el)
(do/monad
(when/monad
(eq? (node-kind entry) 'resampleable)
(hakaru10-fail
'internal
'if_
"Entry node for if should not be sampleable"
entry))
(let testv (check-test 'if_ (node-value entry) entry))
(let (,active ,inactive) (if testv (list th el) (list el th)))
(← added-old (gets chain-count))
(← inactive-nr (in-branch entry (not testv) inactive))
(← added-inactive (gets chain-count))
(deactivate added-old added-inactive)
(← active-nr (in-branch entry testv active))
(← added-active (gets chain-count))
(let b (if
testv
(make-branch
entry
active-nr
inactive-nr
(cons added-inactive added-active)
(cons added-old added-inactive))
(make-branch
entry
inactive-nr
active-nr
(cons added-old added-inactive)
(cons added-inactive added-active))))
(← exit
(create-node
(update-exit b)
(dirac (node-value active-nr))
'exit
(list->vector
(delete-duplicates (list entry active-nr inactive-nr) eq?))
#f))
(modify! (ch) (node-branch-set! exit b))
(new-dependency entry exit)
(new-dependency active-nr exit)
(new-dependency inactive-nr exit)
(modify!
(ch)
(let1 (prev (node-update entry))
(when prev (node-update-set! entry (update-entry prev b))))
(node-branch-set! entry b))
(return exit)))
(define (modify-node! n s*)
(let ((ch (node-chain n)) (old (node-state n)))
(set-node-state! n s*)
(unless
(and (extended? ch)
(eq? (node-kind n) 'observed)
(or (node-inactive? n) (node-pending n)))
(chain-llcum-set!
ch
(fp- (fp+ (chain-llcum ch) (nstate-ll s*)) (nstate-ll old))))
(if (fx= (nstate-tstamp old) (nstate-tstamp s*)) '() (node-deps n))))
(define (guarded-update upd n)
(condition-case
(upd n)
(e (exn) (if (deferrable? e) (values #f '()) (signal e)))))
(define (abort-propagation! ch)
(chain-llcum-set! ch -inf.0)
(chain-pending-set! ch '())
(update-queue-clear! (chain-queue ch)))
(define (propagate! ch)
(let ((q (chain-queue ch)) (extended (extended? ch)) (now (chain-now ch)))
(let loop ()
(if (update-queue-empty? q)
(settle-pending! ch)
(let1 (n (dequeue! q))
(cond ((and (not extended) (node-inactive? n))
(set-delayed! n #t)
(loop))
((fx>= (nstate-tstamp (node-state n)) now)
(hakaru10-error
'dependency
'propagate
"dependency violation: updating the already updated"
n))
((node-update n)
=>
(λ (upd)
(receive
(s* extra)
(if (and extended (node-inactive? n))
(guarded-update upd n)
(upd n))
(if s*
(begin
(enqueue-all! ch extra)
(enqueue-all! ch (modify-node! n s*))
(loop))
(abort-propagation! ch)))))
(else (loop))))))))
(define hakaru10-debug (make-parameter #f))
(define-syntax-rule
(debug-trace arg ...)
(when (hakaru10-debug)
(with-output-to-port (current-error-port) (τ (print arg ...)))))
(define (apply-proposal! n v ll fb)
(let ((ch (node-chain n)) (s (node-state n)))
(enqueue-all!
ch
(modify-node! n (make-nstate v ll (nstate-dst s) (chain-now ch))))
fb))
(define (resample-trace! ch)
(let* ((samp (chain-samp ch))
(hi (fx- (vector-length samp) 1))
(src (chain-rng ch)))
(let retry ()
(let1 (n (vector-ref samp (rng-integer! src 0 hi)))
(if (node-inactive? n)
(retry)
(let1 (s (node-state n))
(receive
(v ll fb)
((rdistr-resample (nstate-dst s))
src
(nstate-val s)
(nstate-ll s))
(debug-trace
"resample "
(node-addr n)
": "
(nstate-val s)
" -> "
v
" fb "
fb)
(apply-proposal! n v ll fb))))))))
(define (log-acceptance-ratio ch sampleables card-old fb)
(let1 (card-new (chain-card-change ch))
(fp+ (fp- (chain-llcum ch) fb)
(if (fx= card-new card-old)
0.0
(fplog (fp/ (exact->inexact (fx+ sampleables card-old))
(exact->inexact (fx+ sampleables card-new))))))))
(define (rejected-ll? x) (and (not (nan? x)) (infinite? x)))
(define (run-step-hook ch accepted? delta)
(let1 (hook (chain-step-hook ch)) (when hook (hook ch accepted? delta))))
(define (transit! ch)
(journal-begin! ch)
(chain-now-set! ch (fx+ (chain-now ch) 1))
(chain-llcum-set! ch 0.0)
(let ((card-old (chain-card-change ch))
(sampleables (vector-length (chain-samp ch))))
(if (fx= 0 (fx+ sampleables card-old))
(begin (journal-commit! ch) (run-step-hook ch #t 0.0) #t)
(let* ((fb (resample-trace! ch))
(delta (begin (propagate! ch) (chain-llcum ch)))
(ratio (and (not (rejected-ll? delta))
(log-acceptance-ratio ch sampleables card-old fb)))
(accept?
(and ratio
(or (fp>= ratio 0.0)
(fp< (rng-double! (chain-rng ch) 0.0 1.0)
(fpexp ratio))))))
(debug-trace
"step "
(chain-now ch)
": delta "
delta
" ratio "
ratio
(if accept? " accepted" " rejected"))
(if accept? (journal-commit! ch) (journal-rollback! ch))
(run-step-hook ch accept? delta)
accept?))))
(define (chain-extended-ll ch)
(chain-fold
(λ (n acc)
(case (node-kind n)
((resampleable) (fp+ acc (nstate-ll (node-state n))))
((observed)
(if (node-inactive? n) acc (fp+ acc (nstate-ll (node-state n)))))
(else acc)))
0.0
ch))
(define (build-chain prog src semantics)
(check-model prog 'mcmC)
(let* ((ch (new-chain src semantics))
(result
(condition-case
(parameterize ((lazy-lifting #f)) (eval-state prog ch))
(e (hakaru10-rejected-build) #f))))
(chain-freeze! ch)
(cond ((not result) (chain-llcum-set! ch -inf.0))
((extended? ch) (chain-llcum-set! ch (chain-extended-ll ch))))
(values ch result)))
(define (initial-step limit prog src semantics)
(let retry ((limit limit))
(if (<= limit 0)
(values #f #f 0)
(receive
(ch result)
(build-chain prog src semantics)
(if (rejected-ll? (chain-llcum ch))
(retry (- limit 1))
(values ch result limit))))))
(define (mcmc-generator
limit
prog
#!key
(seed 17)
(rng #f)
(semantics 'upstream)
(step-hook #f)
(build-hook #f))
(unless
(and (exact-integer? limit) (>= limit 0))
(hakaru10-error
'construction
'mcmC
"limit: must be a non-negative integer"
limit))
(receive
(ch result limit*)
(initial-step limit prog (or rng (make-stdgen-source seed)) semantics)
(and ch
(let1 (started #f)
(chain-step-hook-set! ch step-hook)
(when build-hook (build-hook ch))
(τ (if started (transit! ch) (set! started #t))
(force-value (node-value result)))))))
(define (mcmc-fold kons knil limit prog . keys)
(let1 (next (apply mcmc-generator limit prog keys))
(if next
(let loop ((i 0) (acc knil))
(if (>= i limit) acc (loop (+ i 1) (kons (next) acc))))
knil))))
Embedding notes
- Models are State computations (plan §15 C32, C33). A model is a value
of the State monad of
(aux category state),Model a = MCMCM (SExp a)(S:82), whose state is the chain:dist,diracNandif_are models,lift,pair,collectandconditionare pure, andreturnmakes no node (S:197, S:211-214). Running a model again makes fresh nodes, so there is no accidental sharing (P l.175-179) and sharing comes only from binding a name (S:53-73). Models compose by binding one another ((phierN′ 9 x 1.)incondhier) and are values (phierNfolds a model transformer). The engine's construction functions keep upstream's names, each a State action (create-node, M:404-419;with-node-ctor, S:218-228;ifnode, M:641-686). - Bind order is creation order (plan §15 C35). Node creation order is the
topological order of propagation and fixes the random draws of a replay. A node
is created when the model that makes it runs, in bind order:
do/monadtop to bottom, andfold/monad,traverse/monad(mapM),for-each/monad(mapM_) andreplicate/monadhead first. Building a model creates no node, so a model may be built anywhere, in an argument position or insidemap: the order is the binds', asordered-pairshows (casetest/language/applicative). if_delays its branches (plan §15 C36).if_is syntax over the engine'sif_*(S:275-279): its branch expressions are evaluated whenifnodebuilds each branch, inside the branch's scope and, for the inactive one, under lazy lifting, the stand-in for Haskell's non-strict arguments on which deviation 11 relies. A branch must be a model,(return v)for a value:(if_ c 1 2)raisesconstructionwhen the branch is built. Being syntax,if_is used inside aλwhere a procedure is needed, as in thefoldrs of the colored balls.- Running a model several times (plan §15 C35). A model may run several
times: the retries of the initial trace (M:882-891), the rebuild of
#:check, the trace graph of the oracles. A Scheme effect that must happen once per run belongs inside a bind, after it: the first clause of a top-leveldo/monadis evaluated once, when the definition is. That is harmless fordist,diracN,if_andreturn, which only build models; an SExp computed there, such as(lift car '()), is computed at once (deviation 11). The counters of case B4′ run their effect per run with(>>= (return #f) (λ (_) (add1! k) (return k))), the models of case L5 after the bind of the node they capture, with(let _ (set! captured x)). - A clause that is not a model. A bare clause of
do/monadmust be a model: one that is not, such as(add1! k)between two binds, fails with CHICKEN's record-type error when the model runs, a Scheme error, not aconstructionone (case L5). Bind it with(let _ ...)instead. - The static discipline is the build (plan §15 C37). SExp is applicative,
not a monad (S:118), and
if_builds both branches (M:650-661), so one build is the whole trace graph, and it checks the static discipline (deviations 9, 10) as it goes, but for what only a later value can show, which the updates and the oracles' recomputation check: a node's kind, the boolean test ofif_, a lifted function's answer, and an observation that a distribution constructor computes from its argument (case L5).exact-posterior,model->search-tree,weighted-samplesandlikelihood-weightingwalk the graph of one'extendedbuild (plan §6.10), reusing none of the incremental machinery. That assumes a model whose Scheme code does not branch on build-time values, which nothing catches: the rebuild of#:checkdraws the same values from a copy of the same source, so it takes the same branch, and catches only a construction that depends on state outliving a run (the counters andswitchingof case B4′). condition.(chicken condition)exports a procedure of the same name: import it as(except (chicken condition) condition), as this file does.- Stochastic memoization (P §3 l.531-544, footnote 5 l.566-569, CB:41-44)
becomes pre-allocation, as upstream's own
balls_prior(CB:43,sequence . replicate):(replicate/monad n m)runsmn times, in order, before theif_s that use its nodes, which so belong to no branch;balls-priorkeys them by ball in an alist. - Sampling.
mcmC,mcmC-fold(constant memory) andmcmC§(an(aux stream)stream:take§,§->list,car§,cdr§) take the keys#:seed(17, as M:858),#:rng(an rng source, used as is),#:semantics('upstreamor'extended, see the semantics suite) and#:check(the reference evaluator on every step, or on the first n). There is no burn-in nor thinning:dropthe samples, as upstream does.
Findings and deviations
The deviations, each tagged in the source with its number:
- the exit's update returns the node unchanged when neither the entry nor the
active result is newer, where upstream fails (M:788-792; case S20
if/shared-inactive); - the gamma density uses the scale form; upstream's formula matches no parameterization (D:298-304);
- the gamma sampler with a shape below 1 applies the scale once, upstream twice (D:279-282);
- a Poisson sample's log-likelihood is its log-density, upstream returns the probability (D:379);
- a categorical walk exhausted by rounding returns the last entry; upstream has no case for the empty list (D:99-102);
- with no active resampleable node a step is a no-op; upstream loops or crashes (M:962-966; case L7);
- a model with no random node is a constant chain, a consequence of 6 (case L6);
- the opt-in
'extendedsemantics, which removes the reactivation bias of upstream's kernel (see the semantics suite); - the static checks of Haskell's types become run-time errors (P l.153-155,
l.419-421), among them the boolean test of
if_(S:275); a Model is a value of its own type (plan §15 C32), so a model where an SExp is expected, a model answered by a lifted function, and a model inside a Scheme value (a Val, a Dirac value, or the first answer of a lifted function) are run-timeconstructionerrors; a bare non-model clause is a Scheme error; procedures are plain values (S:107); a Val argument ofdistholding an SExp is rejected, and so is a constructor applied to the wrong number of arguments (S:137-151; case L5); a sample count must be an exact integer (S:196); and an observation must be external to the model, so one that a distribution constructor computes from its argument raisesconditioningat the update that changes it, where upstream's types accept it and its update keeps the first value (S:251-252, M:487-488; case L5); - extra checks with no upstream counterpart: branch escape, as a parent or as the result of a branch or of the model, owner chain, kind stability (M:46-60), the parameter checks of beta, laplace and dirichlet (D:318-328, D:384-391, D:407-411), and a negative sample count, where upstream retries the build forever or answers an infinite list (M:885-891, M:897-898; case L5);
- strict propagation of a deferred value: the value of an inactive Dirac node
whose computation raises is deferred, as upstream's lazy values are (M:99,
M:120-129), but a function applied to it in an inactive branch is not called, so
a strict reader of the result raises the deferred error where upstream computes
a value that ignores it (S:100-116; case B6). Scheme is strict, so
if_delays its branch models untilifnodebuilds them (plan §15 C36), where a partialliftof an inactive branch is deferred; an SExp computed outside any branch, e.g. by a top-level(define m (diracN (lift car '()))), raises at once where upstream raises when the value is forced. Likewise an active diracN or exit, or a lift of Vals in an active branch, computes its value at once, where upstream forces it only when it is read: when it is made or updated (case B6,strict-activeandstrict-active/val), and when a step activates it holding a deferred value that no update of the step recomputes (case H5 of hakaru10-exact.scm).
Kept as upstream, with a comment in the source: the reactivation bias in 'upstream mode (M:1159-1165), the categorical duplicate-key
asymmetry (the sampler sums, the density takes the first match, D:98), beta raising
an error outside [0, 1] (D:331-337), NaN on deactivating an observed node of LL
-inf, a NaN ratio drawing u and rejecting, and the empty chain when the initial
retries run out (M:868).
Upstream's own records that this port does not reproduce, because they come from
older revisions of the engine: tac1-3's 620/523/532 (S:346-368; the three chains are
identical here: tac2 and tac3 as S:370-371 says they should be, and tac1 too, since a
constant Dirac node draws nothing, X16), phier_kl's
0.7863/0.2362/0.03442/0.002157 (E:76-85; the transcription gives 0.78688 and 0.23811
for the first two), Bench's 1e4 line (BE:28-29, -3.151396208304257 and 54.17557680794696;
-3.151835923727823 and 54.168535367816084 here and in the transcription), and the older mixng KLs 2.276e-2
and 1.356e-2 (E:153-162). Likewise, presumably from older revisions of the engine:
the sums of 1000 samples of nice_prog_mult_conditions, 499.799988743311 and
257.2928205920028 (S:297-301), are 502.706370138924 and 252.485601387305 here, although the chain of (biased-coin #t #f), identical to the first one's, replays E:208-210
bit for bit (X11); niceex1's 7 samples are "All True" (S:288-289), here (#t #t #t #t #f #f #f); pcond3's first record (1,5011), (20,2507),
(21,2482) (E:135), next to the "new" one that X6 replays; the "incremental"
moments of test_multiple_conditions13, (0.2445212550806874, 3.651432172401049e-2)
(E:219-220, commented out upstream, of a prog_multiple_conditions defined nowhere),
here (0.251523121569678, 0.0379368207086475) for (biased-coin #f #f); and exgrass/exgrass' of the
low-level node API, not ported, whose record is [0.9, 0.9, 0.9, 0.9, 0.8, 0.1, 0.1,
0.1, 0.1, 0.8] (M:808-809, M:821-822), where their transcription into models (rain'
and sprinkler' as diracN) gives (0.9 0.9 0.9 0.8 0.8 0.1 0.1 0.8
0.8 0.1) with the default kernel. The "original Metropolis" moments of E:207-208
and E:217-218 are those of the old code that M:1174-1415 keeps commented out "for
reference", which is not ported.
The records E:94 (5809), E:102 (2026), P l.217-218 (4 True, for E:10-12) and E:157
(1.1301558326703463e-2) were made with the always-flip kernel of D:64-70, commented
out upstream and kept here as bern/flip.
Paper errata: P l.695 gives the LL of x = 0.1 under N(0, 1) as -0.616, it is
-0.9239385332046727; P l.363 writes dist gamma (1/3) for gamma 3 (1/3) (E:144); P l.655-658 lacks a dist; P l.261 says diracN "has the alias return", but return creates no node (S:197, S:211-214): the two agree only through the unit laws.
Related work
From P §5 (l.870-902): Figaro [18], an embedded DSL in Scala that also builds a trace
graph updated by MH, apparently without incremental evaluation; Infer.NET [15],
which builds a graph but relies on expectation propagation; Ścibior et al. [20],
Haskell and MCMC but a monad, so the construction is too dynamic for static
conditioning checks (and it can express cyclic models); Anglican [22] and more
advanced MCMC algorithms; Yang et al. [23], incremental MH by generating code from
the initial trace; Hur et al. [10], a provably correct MH for an imperative
language, whose examples (Fig. 2 and 3) are mixng and phier here; their Fig. 1,
start_example (E:324-336, commented out upstream), conditions on the boolean formula observe (x || y), which Hakaru10 forbids by design (P l.393-398,
S:43-49), and is not ported. Hakaru10 is a rewrite of the original Hakaru [24],
fixing the problems of the lightweight implementation of Wingate et al. [21] pointed
out in [11] (P §1 l.137-143, 175-183): the unit law of Dirac, accidental sharing of
random primitives, and conditioning inside branches.
The exact oracle of these tests reuses Hansei, the embedded probabilistic DSL of Kiselyov and Shan (aux.hansei.scm): its lazy search trees and their exploration. The public module:
(module
(aux hakaru10)
(dist diracN
condition
if_
lift
pair
collect
sexp-map
sexp+
sexp-
sexp*
sexp/
return
>>=
>>
▷
do/monad
<$>
<*>
fold/monad
traverse/monad
sequence/monad
for-each/monad
replicate/monad
bern
bern/flip
categorical
uniformly
uniform
normal
gamma
beta
poisson
laplace
dirichlet/symmetric
make-distribution
distribution-log-density
distribution-sample
mcmC
mcmC-fold
mcmC§
model-graph
exact-posterior
model->search-tree
likelihood-weighting
weighted-samples
make-stdgen-source
make-mrg32k3a-source
make-chicken-random-source
hist
frequency
momenta
tabulate
tabulate/bins
kl
plot
hakaru10-error?
hakaru10-error-kind)
(import
scheme
(only (scheme base) exact-integer?)
(chicken base)
(only srfi-1 every)
(aux base)
(only (aux stream) τ->§)
(only (aux category state) return >>=)
(only (aux category monad state)
▷
do/monad
>>
<$>
<*>
fold/monad
traverse/monad
sequence/monad
for-each/monad
replicate/monad)
(only (aux hakaru10 random)
make-stdgen-source
make-mrg32k3a-source
make-chicken-random-source
rng-source-copy)
(only (aux hakaru10 distribution)
hakaru10-error
hakaru10-error?
hakaru10-error-kind
bern
bern/flip
categorical
uniformly
uniform
normal
gamma
beta
poisson
laplace
dirichlet/symmetric
make-distribution
distribution-log-density
distribution-sample)
(only (aux hakaru10 util)
hist
frequency
momenta
tabulate
tabulate/bins
kl
plot)
(only (aux hakaru10 engine)
dist
diracN
if_*
lazy-model
model->trace
pair
collect
lift*
condition/external
build-chain
mcmc-generator
mcmc-fold
extended?
chain-count
chain-fold
chain-now
chain-card-change
chain-extended-ll
chain-step-hook-set!
node-kind
node-role
node-state
node-inactive
node-inactive?
node-delayed
node-pending)
(only (aux hakaru10 exact)
exact-posterior
model->search-tree
likelihood-weighting
weighted-samples
construction-check
reference-check))
(define-syntax-rule
(if_ test then else)
(if_* test (lazy-model 'if_ (τ then)) (lazy-model 'if_ (τ else))))
(define condition condition/external)
(define (lift f . es) (lift* f es))
(define sexp-map lift)
(define (sexp+ . es) (lift* + es))
(define (sexp- . es) (lift* - es))
(define (sexp* . es) (lift* * es))
(define (sexp/ . es) (lift* / es))
(define-record step-snapshot now card-change ll nodes)
(define (take-step-snapshot ch)
(make-step-snapshot
(chain-now ch)
(chain-card-change ch)
(chain-extended-ll ch)
(chain-fold
(λ (n acc)
(cons (list (node-state n)
(node-inactive n)
(node-delayed n)
(node-pending n))
acc))
'()
ch)))
(define (same-state? a b)
(and (= (step-snapshot-now a) (step-snapshot-now b))
(= (step-snapshot-card-change a) (step-snapshot-card-change b))
(every (λ (m n) (every eq? m n))
(step-snapshot-nodes a)
(step-snapshot-nodes b))))
(define (check-step before ch accepted? delta)
(let1 (after (take-step-snapshot ch))
(cond ((not accepted?)
(unless
(same-state? before after)
(hakaru10-error
'internal
'check
"a rejected step did not restore the state"
(step-snapshot-now before))))
((and (extended? ch)
(finite? (step-snapshot-ll before))
(finite? (step-snapshot-ll after)))
(let* ((ll0 (step-snapshot-ll before))
(expected (- (step-snapshot-ll after) ll0)))
(unless
(<= (abs (- delta expected))
(* 1e-09 (max 1.0 (abs ll0))))
(hakaru10-error
'internal
'check
"the step's delta is not the extended LL change"
(chain-now ch)
delta
expected)))))))
(define (check-hooks check)
(let ((before #f) (steps 0))
(define (more?) (or (eq? check #t) (< steps check)))
(define (verify! ch)
(reference-check ch)
(if (more?)
(set! before (take-step-snapshot ch))
(chain-step-hook-set! ch #f)))
(values
verify!
(λ (ch accepted? delta)
(check-step before ch accepted? delta)
(add1! steps)
(verify! ch)))))
(define (with-chain-keys driver model seed rng semantics check)
(unless
(or (boolean? check) (and (exact-integer? check) (>= check 0)))
(hakaru10-error
'construction
'mcmC
"check: must be a boolean or a step count"
check))
(let1 (src (or rng (make-stdgen-source seed)))
(if check
(begin
(construction-check
model
#:rng
(rng-source-copy src)
#:semantics
semantics)
(receive
(build-hook step-hook)
(check-hooks check)
(driver
#:rng
src
#:semantics
semantics
#:build-hook
build-hook
#:step-hook
step-hook)))
(driver #:rng src #:semantics semantics))))
(define (mcmC-fold
kons
knil
limit
model
#!key
(seed 17)
(rng #f)
(semantics 'upstream)
(check #f))
(let1 (prog (model->trace model 'mcmC))
(with-chain-keys
(λ keys (apply mcmc-fold kons knil limit prog keys))
model
seed
rng
semantics
check)))
(define (mcmC limit model . keys)
(reverse (apply mcmC-fold cons '() limit model keys)))
(define (mcmC§
model
#!key
(seed 17)
(rng #f)
(semantics 'upstream)
(check #f)
(retries 1000))
(let* ((prog (model->trace model 'mcmC))
(next (with-chain-keys
(λ keys (apply mcmc-generator retries prog keys))
model
seed
rng
semantics
check)))
(if next (τ->§ next) '())))
(define (model-graph model #!key (seed 17) (rng #f) (semantics 'upstream))
(receive
(ch result)
(build-chain
(model->trace model 'model-graph)
(or rng (make-stdgen-source seed))
semantics)
(define (count-nodes node?)
(chain-fold (λ (n k) (if (node? n) (add1 k) k)) 0 ch))
(define ((kind? kind) n) (eq? (node-kind n) kind))
`((nodes unquote (chain-count ch))
(resampleable unquote (count-nodes (kind? 'resampleable)))
(observed unquote (count-nodes (kind? 'observed)))
(dirac unquote (count-nodes (kind? 'dirac)))
(inactive unquote (count-nodes node-inactive?))
(entries unquote (count-nodes (λ (n) (eq? (node-role n) 'entry))))))))
test/language/graph/grass: pass
L1: the trace graph of grass is P's Fig. 1 (P §4.1 l.734-736): two
resampleable nodes, rain and sprinkler, and the observed wet grass; model-graph counts what the debug print of M:860-867 shows.
(define (test/language/graph/grass _)
(⊦ equal?
'((nodes . 3)
(resampleable . 2)
(observed . 1)
(dirac . 0)
(inactive . 0)
(entries . 0))
(model-graph grass)))
((eta 0.0) (memory #(12582912 6198944 1048576)) (stdout "") (stderr ""))
test/language/graph/regression: pass
L2: the Bayesian linear regression of P §3 (l.461-504) with five parameters and
500 points: "its graph has 511 vertices (there are five hundred observations and
five parameters)" (P l.493-495), here 512 with the Dirac node of the result, collect w. The noise's (1/) <$> is a lifted
expression, not a node.
(define (normals means std) (traverse/monad (λ (m) (dist normal m std)) means))
(define (dot xs ays)
(reduce-right sexp+ #f (map (λ (x ay) (lift (λ (a) (* a x)) ay)) xs ays)))
(define (regression xsy)
(do/monad
(let mu (make-list (length (caar xsy)) 0.0))
(← w-mean (normals mu 2.0))
(← w (normals w-mean 1.0))
(← noise-sd (<$> (cut lift (λ (g) (/ 1.0 g)) <>) (dist gamma 0.5 0.5)))
(traverse/monad
(Λ ((,xs unquote y)) (dist (condition y normal) (dot xs w) noise-sd))
xsy)
(return (collect w))))
-- normals means std
-- returns several independently distributed normal random variables
-- whose means are given by the list means, with the same std
normals :: [SExp Double] -> SExp Double -> MCMCM [SExp Double]
normals means std = mapM (\m -> dist normal m std) means
dot :: [Double] -> [SExp Double] -> SExp Double
dot xs ays = foldr1 (+) $ zipWith (\x ay -> (*x) <$> ay) xs ays
collect :: [SExp a] -> SExp [a]
collect = sequenceA(Birds1.hs:103-114.) The model, as printed in P l.471-486 (re-indented, and with the underscores the PDF's text extraction drops restored):
type RegrDatum = ([Double],Double) -- Xs and Y
model :: [RegrDatum] -> Model [Double]
model xsy = do
let mu = replicate dimK 0
w_mean <- normals mu 2
w <- normals w_mean 1
noise_sd <- (1/) <$> dist gamma 0.5 0.5
let make_cond (xs,y) = dist (y `condition` normal) (dot xs w) noise_sd
mapM make_cond xsy
return $ collect w(define (test/language/graph/regression _)
(⊦ equal?
'((nodes . 512)
(resampleable . 11)
(observed . 500)
(dirac . 1)
(inactive . 0)
(entries . 0))
(model-graph (regression (regression-data 5 500)))))
((eta 0.02) (memory #(12582912 7805176 1048576)) (stdout "") (stderr ""))
test/language/graph/colored-balls: pass
L3: the colored balls of BLOG (CB:1-13, P §3 l.505-616) with 8 possible balls and 10 blue draws: 2114 nodes, 89 resampleable (8 colors, the number of balls and one drawn ball per draw and per number of balls, 8 + 1 + 10 × 8), and 1994 of them inactive in the initial trace at seed 17 (prototype regression value).
(define (opposite-color color) (if (eq? color 'blue) 'green 'blue))
(define (observed-color color)
(categorical `((,color . 0.8) (,(opposite-color color) . 0.2))))
(define max-balls 8)
(define (balls-prior n)
(do/monad
(← balls (replicate/monad n (dist uniformly '(blue green))))
(return (map cons (iota n 1) balls))))
(define (balls-observe balls obs nballs)
(>> (for-each/monad
(λ (color)
(do/monad
(← b (dist uniformly (iota nballs 1)))
(fold-right
(λ (i rest)
(if_ (lift (λ (bv) (= bv i)) b)
(dist (condition color observed-color)
(alist-ref i balls))
rest))
(return color)
(iota max-balls 1))))
obs)
(return '())))
(define (cballs-model obs)
(do/monad
(← balls (balls-prior max-balls))
(← nballs (dist uniformly (iota max-balls 1)))
(fold-right
(λ (i rest)
(if_ (lift (λ (n) (= n i)) nballs) (balls-observe balls obs i) rest))
(return '())
(iota max-balls 1))
(return nballs)))
-- Submodel: Given balls color and the observations, perform conditioning
balls_observe :: M.IntMap (SExp Color) -> [Color] -> Int -> Model ()
balls_observe balls obs nballs = mapM_ observe_ball obs >> return (pure ())
where
-- The same ball can be drawn twice as we return the drawn balls
-- into the urn. A ball keeps its true color, no matter how many
-- times it is drawn.
observe_ball color = do
b <- dist uniformly (pure [1..nballs])
-- It is more efficient to create if statements, rather than
-- create an SExp(IntMap Color).
let observe i =
let true_color = balls ! i in
if_ ((== i) <$> b) $
dist (color `condition` observed_color) true_color
foldr observe (return (pure color)) [1..maxBalls]
-- The model takes a list of observations as an argument
cballs_model :: [Color] -> Model Int
cballs_model obs = do
balls <- balls_prior maxBalls
nballs <- dist uniformly (pure [1..maxBalls])
let obs_number i =
if_ ((== i) <$> nballs) $ balls_observe balls obs i
foldr obs_number (return (pure())) [1..maxBalls]
return nballs(ColoredBalls.hs:74-99.) if_ test $ m is if_ short of its else branch, which the foldr supplies: a chain
of nested if_s, built by fold-right as upstream's foldr (if_ being syntax, inside a λ).
(define (test/language/graph/colored-balls _)
(let1 (graph (model-graph (cballs-model (make-list 10 'blue))))
(⊦ equal?
'(2114 89 1994)
(map (λ (key) (alist-ref key graph))
'(nodes resampleable inactive)))))
((eta 0.01) (memory #(12582912 7894904 1048576)) (stdout "") (stderr ""))
test/language/applicative: pass
The rest of the applicative surface (S:99-135). The Model applicative, pair <$> dist normal 0 1 <*> dist normal 5 1, runs its
arguments left to right, so ordered-pair creates its nodes in the order of ordered-pair* and replays it sample for sample, while swapped-pair, the same distribution with the nodes made in the other order, draws other
samples: node creation order is observable (plan C18, §15 C35).
(define ordered-pair
(<*> (<$> (λ (a) (cut pair a <>)) (dist normal 0.0 1.0))
(dist normal 5.0 1.0)))
(define ordered-pair*
(do/monad
(← a (dist normal 0.0 1.0))
(← b (dist normal 5.0 1.0))
(return (pair a b))))
(define swapped-pair
(do/monad
(← b (dist normal 5.0 1.0))
(← a (dist normal 0.0 1.0))
(return (pair a b))))
sexp- and sexp/ of one argument are negate and recip (S:125, S:131), of two - and / (S:123, S:130); sexp-map is fmap (S:99-102), evaluated now
over plain values, a procedure among them (S:107 Val f); and the engine's sexp? holds of nodes and lifted
expressions only (S:85-89).
(define (test/language/applicative _)
(⊦ equal? (mcmC 50 ordered-pair*) (mcmC 50 ordered-pair))
(⊦≠ (mcmC 50 swapped-pair) (mcmC 50 ordered-pair))
(⊦ equal?
'(-3.0 0.25 2 0.5 4 #f)
(list (sexp- 3.0)
(sexp/ 4.0)
(sexp- 3 1)
(sexp/ 1.0 2.0)
(sexp-map add1 3)
(sexp? 3)))
(⊦ equal?
'((-3.0) (0.25))
(list (mcmC 1 (return (sexp- 3.0))) (mcmC 1 (return (sexp/ 4.0)))))
(⊦ = 4 (sexp-map (λ (f v) (f v)) add1 3))
(for-each
(Λ ((,x ,x? ,nx? ,neg ,inv ,diff ,quot ,mapped ,applied))
(⊦ equal?
(list #t #t (- x) (/ x) (- 10.0 x) (/ x 2.0) (- x) (- x))
(list x? nx? neg inv diff quot mapped applied)))
(mcmC 20
(do/monad
(← x (dist normal 0.0 1.0))
(return
(lift list
x
(sexp? x)
(sexp? (sexp- x))
(sexp- x)
(sexp/ x)
(sexp- 10.0 x)
(sexp/ x 2.0)
(sexp-map - x)
(sexp-map (λ (f v) (f v)) - x)))))))
((eta 0.001) (memory #(12582912 6201232 1048576)) (stdout "") (stderr ""))
test/language/sharing: pass
L4: diracN of a node answers the node itself, shared rather
than copied (S:161-169):
-- NB: if x is just a NoderRef, (diracN x) returns this reference (shares)
-- rather than creating a copy of it!
diracN :: Show a => SExp a -> Model a
diracN (Val x) = fmap NRef $ create_node Nothing (diracS x)
diracN x@NRef{} = return x
diracN x = fmap NRef $ with_node_ctor x diracSwhile diracN of a value makes a constant Dirac node and of a
lifted expression a Dirac node depending on its leaves, deduplicated: (lift + x x x) has the single parent x (S:99-116; the engine's nt keeps the leaves as a vector of distinct nodes).
(define (test/language/sharing _)
(⊦ equal?
'(#t)
(mcmC 1
(do/monad
(← x (dist normal 0.0 1.0))
(← y (diracN x))
(return (eq? x y)))))
(⊦ equal?
'((nodes . 1)
(resampleable . 0)
(observed . 0)
(dirac . 1)
(inactive . 0)
(entries . 0))
(model-graph (diracN 3.0)))
(⊦ =
2
(alist-ref
'nodes
(model-graph
(do/monad (← x (dist normal 0.0 1.0)) (diracN (sexp+ x x))))))
(receive
(ch result)
(build-chain
(model->trace
(do/monad (← x (dist normal 0.0 1.0)) (diracN (lift + x x x)))
'test)
(make-stdgen-source 17)
'upstream)
(⊦ = 1 (vector-length (node-parents result)))))
((eta 0.0) (memory #(12582912 6205384 1048576)) (stdout "") (stderr ""))
test/language/errors: pass
L5: the static discipline of Syntax.hs as run-time errors (deviation 9; see the section on conditioning), each case naming the kind it raises. Upstream's ill-typed examples are E:180-201:
{- Type error
biased_coinX c1 c2 = do
b <- dist beta 1 1
dist (c1 `condition` (c2 `condition` bern)) b
dist (c2 `condition` bern) b
return b
-}
{-
biased_coin_ill_typed c1 c2 = do
b <- dist bern 0.5
dist (c1 `condition` bern) b
dist (c2 `condition` bern) b
return b
-}
{-
biased_coin_ill_typed_too c1 c2 = do
b <- dist bern 0.5
dist (b `condition` bern) 0.4
return b
-}Without a counterpart in these examples: a model where an SExp is expected (an
argument of dist or of lift, the result, a model inside a Scheme value)
and a value where a model is expected (a number, a thunk, a branch of if_), for mcmC and exact-posterior alike (check-model), which Haskell's types reject since a Model is a type of its own (plan §15
C32); a model or an SExp that a lifted function answers, whatever the node it
feeds, at the build or at an update, or a Scheme value it answers that holds
one, walked when the function first answers, at the build (check-lifted-value); an observation that is a model or a
Scheme value holding an SExp (CondV a = a, S:180-193), or that a distribution constructor computes from its argument,
raised by the update that changes it, where upstream keeps the observation the
node was built with (S:251-252, M:487-488) and so silently observes the first
value; a Scheme container of SExps as the result, which the engine would
otherwise take for a constant (deviation 9), at any depth of pairs and vectors,
and likewise inside the value of a diracN or of an argument
of dist, also next to an SExp argument, where the pair or the
list of the arguments hides it from the node's construction (the Show of the sample type, S:144-151); a distribution
constructor that answers something other than a distribution (plan §7), a
distribution given arguments, and a value given to dist that is neither a distribution nor a constructor; a sample count that is not
an exact integer (mcmC :: Integer -> Model a -> [a], S:196) or is negative, for each of mcmC, mcmC-fold and mcmC§ (deviation 10 for a
negative one: upstream retries the build forever or answers an infinite list,
M:885-891, M:897-898); a #:check that is neither a boolean nor a step count; and a node of an earlier run, which
a model captured with set!, used alone (check-owner) or lifted together with a node of the current run,
whose NT would mix two chains. A node captured in a branch escapes it as a
parent of a node outside (branch-escape), and also as the result of a sibling if_'s branch, which that if_'s exit would read, or as the model's result: each
raises branch (deviation 10), for mcmC and exact-posterior alike, whose walks rely on it to skip
the inactive nodes. The errors of an if_'s test, a node of an earlier run or a Val that holds a model or an SExp, name if_. An observation that a constructor recomputes as a fresh
value equal to the first, a list or a flonum, is unchanged: both chains sample
with #:check, and enumeration gives P(x) = .6. dist takes any number of arguments (deviation 9): three, combined with collect, give the chain of the equivalent two-argument
constructor; but as many as its constructor takes, which upstream's Args family makes part of the type (S:137-151): (dist normal 0.), a nullary dist of a one-argument constructor and three arguments to a two-argument one raise construction, located at dist, also in an inactive branch in 'extended mode, which would otherwise take CHICKEN's arity error for a rejection of the
build (C22) and silently empty the chain, and under enumeration; an arity error
raised inside the constructor's own body is a Scheme error. A model built and
discarded,
such as (dist bern .5) evaluated outside any bind, makes no
node, as in Haskell, and raises nothing.
The case sampleable-entry runs the engine's ifnode directly, on the chain eval-state threads: "Entry node for if should not be sampleable" (M:643-647). The set!s run after the bind of the node they capture, so once per
run (plan §15 C35, embedding notes). Not a kind: a bare clause that is not a
model, (add1! k) between two binds, raises CHICKEN's own
error when the model runs, a Scheme exn.
(define (test/language/errors _)
(define captured #f)
(define leaked/sibling
(do/monad
(← c (dist bern 0.5))
(let leak #f)
(if_ c
(do/monad (← h (dist bern 0.9)) (let _ (set! leak h)) (return h))
(return #f))
(← y (if_ #t (return leak) (return #f)))
(return (pair c y))))
(define leaked/result
(do/monad
(← c (dist bern 0.5))
(let leak #f)
(if_ c
(do/monad
(← h (dist normal 10.0 1.0))
(let _ (set! leak h))
(return h))
(return 0.0))
(return leak)))
(define cases
`((condition-on-a-random-choice
conditioning
,(τ (mcmC 3
(do/monad
(← b (dist bern 0.5))
(dist (condition b bern) 0.4)
(return b)))))
(condition-on-a-model
conditioning
,(τ (mcmC 3
(do/monad
(← b (dist bern 0.5))
(dist (condition (dist bern 0.5) bern) 0.5)
(return b)))))
(condition-on-an-sexp-inside-a-value
conditioning
,(τ (mcmC 3
(do/monad
(← c (dist bern 0.5))
(dist (condition (list c) uniformly) '((#t) (#f)))
(return c)))))
(condition-twice
conditioning
,(τ (condition #t (condition #t (bern 0.5)))))
(condition-twice-applied
conditioning
,(τ (mcmC 3
(do/monad
(← b (dist beta 1.0 1.0))
(dist (condition #t (condition #f bern)) b)
(return b)))))
(condition-on-a-constructor-argument
conditioning
,(τ (mcmC 50
(do/monad
(← x (dist bern 0.5))
(dist (λ (xv) (condition xv (bern 0.8))) x)
(return x)))))
(dist-in-a-lifted-function
construction
,(τ (mcmC 3
(do/monad
(← x (dist normal 0.0 1.0))
(return (lift (λ (xv) (dist normal xv 1.0)) x))))))
(model-from-a-lifted-function-at-update
construction
,(τ (mcmC 200
(do/monad
(← x (dist bern 0.5))
(diracN (lift (λ (xv) (if xv 0.5 (dist bern 0.5))) x))))))
(sexp-from-a-lifted-function-at-update
construction
,(τ (mcmC 200
(do/monad
(← x (dist bern 0.5))
(diracN (lift (λ (xv) (if xv 0.5 (sexp+ x 1))) x))))))
(model-from-a-lifted-function-as-a-parameter
construction
,(τ (mcmC 3
(do/monad
(← c (dist bern 0.5))
(dist normal (lift (λ (cv) pnorm) c) 1.0)))))
(model-inside-a-lifted-value
construction
,(τ (mcmC 3
(do/monad
(← c (dist bern 0.5))
(dist uniformly (lift (λ (cv) (list pnorm 1)) c))))))
(a-model-as-a-lift-argument
construction
,(τ (mcmC 3
(do/monad
(← x (dist bern 0.5))
(return (lift (λ (xv m) xv) x pnorm))))))
(a-model-as-the-only-lift-argument
construction
,(τ (lift (λ (m) 1) pnorm)))
(a-model-as-a-dist-argument
construction
,(τ (mcmC 3 (dist normal pnorm 1.0))))
(a-model-as-the-result construction ,(τ (mcmC 3 (return pnorm))))
(a-model-inside-a-value construction ,(τ (mcmC 3 (diracN (list pnorm)))))
(not-a-model construction ,(τ (mcmC 3 42)))
(not-a-model/exact construction ,(τ (exact-posterior 42)))
(a-thunk-is-not-a-model construction ,(τ (mcmC 3 (λ () 1))))
(a-branch-that-is-not-a-model construction ,(τ (mcmC 3 (if_ #t 1 2))))
(a-container-of-sexps-as-the-result
construction
,(τ (mcmC 3 (do/monad (← x (dist bern 0.5)) (return (list x))))))
(sexps-nested-in-the-result
construction
,(τ (mcmC 3
(do/monad
(← x (dist bern 0.5))
(← y (dist bern 0.5))
(return (cons 1 (vector x y)))))))
(an-sexp-inside-a-diracN-value
construction
,(τ (mcmC 3
(do/monad
(← x (dist bern 0.5))
(diracN (vector 1 (list x)))))))
(an-sexp-inside-a-dist-argument
construction
,(τ (mcmC 3
(do/monad (← x (dist bern 0.5)) (dist uniformly (list x))))))
(an-sexp-inside-a-dist-argument-next-to-an-sexp
construction
,(τ (mcmC 3
(do/monad
(← x (dist bern 0.5))
(← s (dist bern 0.5))
(dist (λ (xs sv) (uniformly xs)) (list x) s)))))
(a-model-inside-a-dist-argument-next-to-sexps
construction
,(τ (mcmC 3
(do/monad
(← s (dist bern 0.5))
(dist (λ (ms sv mu) (normal mu 1.0))
(list pnorm)
s
0.0)))))
(ctor-not-a-distribution
construction
,(τ (mcmC 3
(do/monad (← x (dist normal 0.0 1.0)) (dist (λ (m) 42) x)))))
(a-distribution-with-arguments construction ,(τ (dist (bern 0.5) 1)))
(not-a-distribution construction ,(τ (dist 42)))
(negative-limit construction ,(τ (mcmC -1 (dist bern 0.5))))
(negative-limit/fold
construction
,(τ (mcmC-fold cons '() -1 (dist bern 0.5))))
(negative-limit/stream
construction
,(τ (mcmC§ (dist bern 0.5) #:retries -1)))
(fractional-limit construction ,(τ (mcmC 2.5 (dist bern 0.4))))
(rational-limit construction ,(τ (mcmC 1/2 (dist bern 0.4))))
(inexact-limit/fold
construction
,(τ (mcmC-fold cons '() 3.0 (dist bern 0.5))))
(infinite-limit/stream
construction
,(τ (mcmC§ (dist bern 0.5) #:retries +inf.0)))
(check-not-a-boolean-or-a-count
construction
,(τ (mcmC 3 (dist bern 0.5) #:check 'yes)))
(check-a-negative-count
construction
,(τ (mcmC 3 (dist bern 0.5) #:check -1)))
(non-boolean-test branch ,(τ (mcmC 3 (if_ 1 (return 1) (return 2)))))
(non-boolean-test-at-update
branch
,(τ (mcmC 200
(do/monad
(← x
(dist categorical '((#t . 0.5) (#f . 0.25) (0 . 0.25))))
(if_ (lift identity x) (return 1) (return 2))))))
(a-node-of-a-previous-chain
construction
,(τ (mcmC 3
(do/monad
(← x (dist normal 0.0 1.0))
(let _ (set! captured x))
(return x)))
(mcmC 3 (dist normal captured 1.0))))
(a-node-of-a-previous-chain-lifted-with-a-fresh-one
construction
,(τ (mcmC 3
(do/monad
(← x (dist normal 0.0 1.0))
(let _ (set! captured x))
(return x)))
(mcmC 3
(do/monad
(← x (dist normal 0.0 1.0))
(dist normal (sexp+ x captured) 1.0)))))
(branch-escape
branch
,(τ (mcmC 3
(do/monad
(← c (dist bern 0.5))
(let leak #f)
(if_ c
(do/monad
(← l (dist normal 0.0 1.0))
(let _ (set! leak l))
(return l))
(return 0.0))
(dist normal leak 1.0)))))
(branch-escape/as-the-result-of-a-sibling-if_
branch
,(τ (mcmC 3 leaked/sibling)))
(branch-escape/as-the-result-of-a-sibling-if_/exact
branch
,(τ (exact-posterior leaked/sibling)))
(branch-escape/as-the-model-result branch ,(τ (mcmC 3 leaked/result)))
(kind-change
construction
,(τ (mcmC 200
(do/monad
(← x (dist bern 0.5))
(dist (λ (xv) (if xv (bern 0.3) (condition #t (bern 0.3))))
x)))))
(sampleable-entry
internal
,(τ (eval-state
(do/monad
(← c (dist bern 0.5))
(ifnode c (to-node 1) (to-node 2)))
(new-chain (make-stdgen-source 17) 'upstream))))))
(for-each
(Λ ((,name ,kind ,thunk))
(⊦ equal? (list name kind) (list name (hakaru10-kind thunk))))
cases)
(mcmC 3
(do/monad (← x (dist bern 0.5)) (let _ (set! captured x)) (return x)))
(⊦ equal?
'((construction if_) (construction if_) (construction if_))
(list (raised/location (τ (mcmC 3 (if_ captured (return 1) (return 2)))))
(raised/location
(τ (mcmC 3 (if_ (list (return 1)) (return 1) (return 2)))))
(raised/location
(τ (mcmC 3
(do/monad
(← c (dist bern 0.5))
(if_ (list c) (return 1) (return 2))))))))
(let1 (inactive-typo
(do/monad
(← c (dist bern 0.5))
(if_ #f (dist normal 0.0) (return 1))
(return c)))
(⊦ equal?
(make-list 6 '(construction dist))
(list (raised/location (τ (mcmC 3 (dist normal 0.0))))
(raised/location (τ (mcmC 3 (dist (λ (m) (normal m 1.0))))))
(raised/location
(τ (mcmC 3 (dist (λ (m s) (normal m s)) 0.0 1.0 2.0))))
(raised/location (τ (mcmC 3 inactive-typo)))
(raised/location
(τ (mcmC 3 inactive-typo #:semantics 'extended)))
(raised/location (τ (exact-posterior inactive-typo))))))
(⊦ eq? 'exn (raised (τ (mcmC 3 (dist (λ (m) (normal m)) 0.0)))))
(let1 (fresh-list
(do/monad
(← x (dist bern 0.5))
(dist (λ (xv)
(condition
(list 1)
(uniformly (if xv '((1) (2)) '((1) (3) (4))))))
x)
(return x)))
(⊦ (equal-approx? 1e-12)
'((#f . 0.4) (#t . 0.6))
(exact-posterior fresh-list))
(⊦ equal? (mcmC 500 fresh-list) (mcmC 500 fresh-list #:check #t)))
(let1 (fresh-flonum
(do/monad
(← x (dist normal 0.0 1.0))
(dist (λ (xv) (condition (exact->inexact 1) (normal xv 1.0))) x)
(return x)))
(⊦ equal? (mcmC 500 fresh-flonum) (mcmC 500 fresh-flonum #:check #t)))
(let1 (k 0)
(⊦ eq?
'exn
(raised
(τ (mcmC 3
(do/monad
(← x (dist bern 0.5))
(add1! k)
(return x)))))))
(⊦ equal?
(mcmC 50
(do/monad
(← a (dist normal 0.0 1.0))
(dist (λ (m s) (normal (- m 10.0) s)) a 1.0)))
(mcmC 50
(do/monad
(← a (dist normal 0.0 1.0))
(dist (λ (m s k) (normal (- m k) s)) a 1.0 10.0)))))
((eta 0.012) (memory #(12582912 6222856 1048576)) (stdout "") (stderr ""))
test/language/constant-chain: pass
L6, deviation 7: a model with no random node is a constant chain; upstream's step would pick from an empty list of resampleable nodes (M:962).
(define (test/language/constant-chain _)
(⊦ equal? '(3 3 3 3 3) (mcmC 5 (return 3))))
((eta 0.0) (memory #(12582912 6224432 1048576)) (stdout "") (stderr ""))
test/language/no-active-resampleable: pass
L7, deviation 6: the only resampleable node is in the inactive branch, so no
node can be proposed and every step is a no-op; upstream's resample_trace retries forever (M:966).
(define (test/language/no-active-resampleable _)
(⊦ equal?
(make-list 10 3)
(mcmC 10 (if_ #f (dist normal 0.0 1.0) (return 3)))))
((eta 0.0) (memory #(12582912 6226008 1048576)) (stdout "") (stderr ""))
test/language/stream: pass
L8: mcmC§ is Haskell's lazy list of samples (M:897-906), an (aux stream) stream made by τ->§ from the engine's generator: its first 100 elements are (mcmC 100 pcond4). A model with no trace of finite
log-likelihood gives the empty stream after #:retries builds (1000 by default; upstream's initial_step returns [], M:868, M:882-891). The budget is exact, as upstream's
"repeat up to limit times" (M:882-891): at seed 17 the model rare, which observes #t from bern of 1 or 0 as its c is true or false, draws c false in its
first six builds and true in the seventh, so six builds (mcmC's limit, or #:retries) find no trace and seven do.
(define (test/language/stream _)
(let1 (s (mcmC§ pcond4))
(⊦ equal? (mcmC 100 pcond4) (§->list (take§ 100 s)))
(⊦ equal? (cadr (mcmC 2 pcond4)) (car§ (cdr§ s))))
(⊦ equal? '() (mcmC§ (dist (condition 5.0 uniform) 0.0 1.0)))
(let1 (rare (do/monad
(← c (dist bern 0.1))
(dist (condition #t bern) (lift (λ (cv) (if cv 1.0 0.0)) c))
(return c)))
(⊦ equal? '() (mcmC 6 rare))
(⊦ equal? (make-list 7 #t) (mcmC 7 rare))
(⊦ equal? '() (mcmC§ rare #:retries 6))
(⊦ equal?
(make-list 3 #t)
(§->list (take§ 3 (mcmC§ rare #:retries 7))))))
((eta 0.004) (memory #(12582912 6219544 1048576)) (stdout "") (stderr ""))
test/language/fold: pass
L9: mcmC-fold folds (kons sample acc) over the samples in chain order, in constant memory, and agrees with a fold
over mcmC; #:rng takes an rng source
as is, #:seed seeds a StdGen (17 by default, M:858).
(define (test/language/fold _)
(⊦ =
(count-true (mcmC 2000 grass))
(mcmC-fold (λ (rain n) (if rain (add1 n) n)) 0 2000 grass))
(⊦ equal? (mcmC 100 pcond1 #:rng (make-stdgen-source 17)) (mcmC 100 pcond1))
(⊦≠ (mcmC 100 pcond1 #:seed 18) (mcmC 100 pcond1)))
((eta 0.015) (memory #(12582912 6224328 1048576)) (stdout "") (stderr ""))
test/language/three-interpretations: pass
One model, three interpretations (plan §15 C37): grass under exact enumeration
(new: a Hansei search tree over the trace graph of one build, explored and
normalized by probcc-explore and probcc-normalize), under likelihood weighting (new: forward
sampling over the same graph) and under the incremental MH of the engine. The
exact posterior is upstream's "theoretical" 0.4684714427203697 (E:322);
likelihood weighting agrees within 0.02, the Markov chain, autocorrelated, within
0.04. The oracles suite of hakaru10-exact.scm runs this check on eleven models.
(define (test/language/three-interpretations _)
(let1 (exact (exact-posterior grass))
(⊦ (equal-approx? 1e-12)
'((#f . 0.53152855727963) (#t . 0.46847144272037))
exact)
(⊦ (equal-approx? 0.02) exact (likelihood-weighting 20000 grass))
(⊦ (equal-approx? 0.04) exact (proportions (mcmC 20000 grass)))))
((eta 0.174) (memory #(12582912 6700624 1048576)) (stdout "") (stderr ""))
test/language/search-tree-paths: pass
A fourth reading of a model, written in this file over the search tree of model->search-tree, in the format of Hansei's probcc-reify/0 (aux.hansei.scm): the number of paths through a
model whose choices have finite supports. The tree has a subtree (C thunk) per support point of each active choice of the trace
graph and per active observation of positive weight and finite LL (an LL of
NaN or +inf prunes its path, as one of weight 0 does), and a value (V v) at each leaf; forcing every subtree and counting the
leaves gives grass the four paths of rain × sprinkler, pcond1 the four of x × the
branch's choice, pcond4 one for x = 1 and two for x = 2 (the observation has one
subtree), ph2 four.
(define (search-tree-paths m)
(let paths ((tree (model->search-tree m)))
(fold (λ (entry n)
(+ n
(match/first
(car entry)
((V ,_) 1)
((C ,thunk) (paths (thunk))))))
0
tree)))
(define (test/language/search-tree-paths _)
(⊦ equal? '(4 4 3 4) (map search-tree-paths (list grass pcond1 pcond4 ph2))))
((eta 0.0) (memory #(12582912 6222888 1048576)) (stdout "") (stderr ""))
test/language/procedure-values: pass
L10: procedures as values. A random choice may draw procedures, and (diracN car) makes a Dirac node of one. A procedure is a
plain Val (S:107 Val f), which a model passes on to lift: it is never taken for a model, a State computation of a
type of its own (plan §15 C32). Exact enumeration, the chain (with and without #:check) and likelihood weighting agree; the counts at seed 17
are regression values.
(define procedure-choice
(do/monad
(← f (dist categorical (list (cons car 0.5) (cons cadr 0.5))))
(return (lift (λ (g) (g '(1 2))) f))))
(define procedure-constant
(do/monad
(← c (dist bern 0.5))
(← f (diracN car))
(return (lift (λ (g cv) (if cv (g '(1 2)) 0)) f c))))
A lifted function may also answer a closure it builds, which procedure-built keeps in a Dirac node: #:check recomputes it as a fresh closure and takes two
procedures for the same value (their consumers, which apply them, are checked
themselves), as it does for a model that answers such closures. A value may be
circular: in circular-value the Dirac node holds a circular
list, which the build's check for SExps inside a value (deviation 9) and the
comparisons of #:check walk to an end. Both chains sample
with #:check what they sample without it, in both
semantics, and agree with exact enumeration.
(define procedure-built
(do/monad
(← k (dist uniformly '(1 2 3)))
(← scale (diracN (lift (λ (kv) (λ (x) (* kv x))) k)))
(return (lift (λ (f) (f 10)) scale))))
(define circular-value
(do/monad
(← b (dist bern 0.5))
(← cyc (diracN (lift (λ (bv) (circular-list bv)) b)))
(return (lift car cyc))))
The closure may also sit inside a record: in distribution-value a Dirac node holds a distribution (a distk, whose rdistr holds its sampler's and
its density's closures), from which (dist identity d) draws x, and in record-value a record of the model's own and a promise. #:check walks a record's fields as a vector's and takes two
promises for the same value (a forced one holds its value, a fresh one a closure),
so their recomputations match, and the chains sample with it what they sample
without it.
(define distribution-value
(do/monad
(← c (dist bern 0.5))
(← d
(diracN
(lift (λ (cv) (if cv (uniformly '(1 2)) (uniformly '(1 2 3 4)))) c)))
(← x (dist identity d))
(dist (condition #t bern) (lift (λ (xv) (if (odd? xv) 0.8 0.3)) x))
(return (pair c x))))
(define record-value
(do/monad
(← k (dist uniformly '(1 2 3)))
(← s (diracN (lift (λ (kv) (make-scaler (λ (x) (* kv x)))) k)))
(← p (diracN (lift (λ (kv) (delay (* kv 10))) k)))
(return (lift (λ (sv pv) (list ((scaler-f sv) 10) (force pv))) s p))))
(define (test/language/procedure-values _)
(⊦ (equal-approx? 1e-12)
'((1 . 0.5) (2 . 0.5))
(exact-posterior procedure-choice))
(⊦ (equal-approx? 1e-12)
'((0 . 0.5) (1 . 0.5))
(exact-posterior procedure-constant))
(⊦ equal? '((1 . 974) (2 . 1026)) (hist (mcmC 2000 procedure-choice)))
(⊦ equal? '((0 . 1026) (1 . 974)) (hist (mcmC 2000 procedure-constant)))
(⊦ equal?
'(50 50)
(list (length (mcmC 50 procedure-choice #:check #t))
(length (mcmC 50 procedure-constant #:check #t))))
(⊦ equal?
'((1 2) (0 1))
(list (map car (likelihood-weighting 100 procedure-choice))
(map car (likelihood-weighting 100 procedure-constant))))
(⊦ (equal-approx? 1e-12)
(map (λ (v) (cons v (/ 1.0 3.0))) '(10 20 30))
(exact-posterior procedure-built))
(⊦ (equal-approx? 1e-12)
'((#f . 0.5) (#t . 0.5))
(exact-posterior circular-value))
(⊦ (equal-approx? 1e-12)
(map (Λ ((,v unquote w)) (cons v (/ w 11.0)))
'(((#f . 1) . 2.0)
((#f . 2) . 0.75)
((#f . 3) . 2.0)
((#f . 4) . 0.75)
((#t . 1) . 4.0)
((#t . 2) . 1.5)))
(exact-posterior distribution-value))
(⊦ (equal-approx? 1e-12)
(map (λ (v) (cons (list v v) (/ 1.0 3.0))) '(10 20 30))
(exact-posterior record-value))
(let1 (scales
(do/monad
(← k (dist uniformly '(1 2 3)))
(diracN (lift (λ (kv) (λ (x) (* kv x))) k))))
(⊦ equal?
(map (λ (f) (f 10)) (mcmC 50 scales))
(map (λ (f) (f 10)) (mcmC 50 scales #:check #t))))
(for-each
(λ (m)
(⊦ equal? (mcmC 50 m) (mcmC 50 m #:check #t))
(⊦ equal?
(mcmC 50 m #:semantics 'extended)
(mcmC 50 m #:semantics 'extended #:check #t)))
(list procedure-built circular-value distribution-value record-value)))
((eta 0.451) (memory #(12582912 6233600 1048576)) (stdout "") (stderr ""))
(aux hakaru10): upstream records, bit for bit
Replays
Chains replayed bit-exactly against upstream's recorded runs (the comments of
Examples1.hs, Syntax.hs and ColoredBalls.hs) and against the Python transcription of
the engine (prototype regression values), with the random-1.0.1.1 StdGen of mkStdGen 17 (M:858). A replay
holds only if the port draws exactly where upstream draws, in the same order,
computes every Double expression in Haskell's association, and creates the nodes
in the same order (plan §6.1, C18). The triage order for a failing replay: the
generator, the samplers' draw order, the order of the resampleable nodes (M:239,
M:962), floating-point order, node creation order, (de)activation sums.
test/replay/pdep: pass
X1, pdep_run: literally the same samples (E:16-22; P
l.248-249).
(define pnorm (dist normal 10.0 0.5))
(define pdep1 (do/monad (← x pnorm) (diracN (sexp+ x 1.0))))
pnorm = dist normal 10 0.5
pdep1 = do
x <- pnorm
diracN (x+1)
-- Literally the same samples
pdep_run = mcmC 100 (dist normal 11 0.5) == mcmC 100 pdep1
-- True(define (test/replay/pdep _)
(⊦ equal? (mcmC 100 (dist normal 11.0 0.5)) (mcmC 100 pdep1)))
((eta 0.001) (memory #(12582912 6205976 1048576)) (stdout "") (stderr ""))
test/replay/pdepR: pass
X2: the right unit law, sample for sample (E:24-32, P l.254-258): "Even the sequences of samples for the two programs are identical."
(define pdepR (do/monad (← x pnorm) (diracN x)))
(define pdepR′ (do/monad (← x pnorm) (return x)))
pdepR = do
x <- pnorm
diracN x
pdepR_run = mcmC 100 pdepR
pdepR' = do
x <- pnorm
return x(define (test/replay/pdepR _)
(let1 (reference (mcmC 100 pnorm))
(⊦ equal? reference (mcmC 100 pdepR))
(⊦ equal? reference (mcmC 100 pdepR′))
(⊦ < 10 (length (delete-duplicates reference)))))
((eta 0.001) (memory #(12582912 6207360 1048576)) (stdout "") (stderr ""))
test/replay/grass: pass
X3: 9363 rainy samples of 20000, 0.46815, upstream's "0.46815 for 20000 -- with old resampling" (E:321).
(define (nnot p b) (if b p 1.0))
(define ((nor strength-x strength-y noise) x y)
(bern (- 1.0
(* (* (nnot (- 1.0 strength-x) x) (nnot (- 1.0 strength-y) y))
(- 1.0 noise)))))
(define grass
(do/monad
(← rain (dist bern 0.3))
(← sprinkler (dist bern 0.5))
(← grass-is-wet (dist (condition #t (nor 0.9 0.8 0.1)) rain sprinkler))
(return rain)))
grass = do
rain <- dist bern 0.3
sprinkler <- dist bern 0.5
-- dist (nor 0.9 0.8 0.1) rain sprinkler
grass_is_wet <- dist (True `condition` nor 0.9 0.8 0.1) rain sprinkler
return rain
-- noisy-or function
nor :: Double -> Double -> Double -> Bool -> Bool -> Distribution Bool
nor strengthX strengthY noise x y =
bern $ 1 - nnot (1-strengthX) x * nnot (1-strengthY) y * (1-noise)
-- noisy not function
nnot :: Num a => a -> Bool -> a
nnot p True = p
nnot p False = 1
grass_run = let ns = 20000
r = mcmC ns grass
in
((fromIntegral . length . filter id $ r) /
(fromIntegral ns :: Double))
-- 0.46815 for 20000 -- with old resampling
-- 0.4684714427203697 -- theoretical(define (test/replay/grass _) (⊦ = 9363 (count-true (mcmC 20000 grass))))
((eta 0.069) (memory #(12582912 7162792 1048576)) (stdout "") (stderr ""))
test/replay/pcond1: pass
X4: both of upstream's records (E:112-114), of 1000 and 10000 samples (the
first sums to 1000, though the code reads mcmC 10000).
(define (is1 x) (lift (λ (xv) (= xv 1)) x))
(define pcond1
(do/monad
(← x (dist categorical '((1 . 0.5) (2 . 0.5))))
(if_ (is1 x)
(dist categorical '((10 . 0.5) (11 . 0.5)))
(dist categorical '((20 . 0.5) (21 . 0.5))))))
pcond1 = do
x <- dist categorical (pure [(1,0.5), (2,0.5)])
if_ ((==1) <$> x)
(dist categorical (pure [(10,0.5), (11,0.5)]))
(dist categorical (pure [(20,0.5), (21,0.5)]))
pcond1r = hist $ mcmC 10000 pcond1
-- fromList [(10,246),(11,222),(20,240),(21,292)]
-- fromList [(10,2481),(11,2394),(20,2565),(21,2560)](define (test/replay/pcond1 _)
(⊦ equal?
'((10 . 246) (11 . 222) (20 . 240) (21 . 292))
(hist (mcmC 1000 pcond1)))
(⊦ equal?
'((10 . 2481) (11 . 2394) (20 . 2565) (21 . 2560))
(hist (mcmC 10000 pcond1))))
((eta 0.031) (memory #(12582912 6216672 1048576)) (stdout "") (stderr ""))
test/replay/pcond2: pass
X5: both of upstream's records (E:122-124), of 1000 and 10000 samples (the
first sums to 1000, though the code reads mcmC 10000).
(define pcond2
(do/monad
(← x (dist categorical '((1 . 0.5) (2 . 0.5))))
(if_ (is1 x) (return x) (dist categorical '((20 . 0.5) (21 . 0.5))))))
pcond2 = do
x <- dist categorical (pure [(1,0.5), (2,0.5)])
if_ ((==1) <$> x)
(return x)
(dist categorical (pure [(20,0.5), (21,0.5)]))
pcond2r = hist $ mcmC 10000 pcond2
-- fromList [(1,500),(20,214),(21,286)]
-- fromList [(1,5000),(20,2321),(21,2679)](define (test/replay/pcond2 _)
(⊦ equal? '((1 . 500) (20 . 214) (21 . 286)) (hist (mcmC 1000 pcond2)))
(⊦ equal? '((1 . 5000) (20 . 2321) (21 . 2679)) (hist (mcmC 10000 pcond2))))
((eta 0.037) (memory #(12582912 6213776 1048576)) (stdout "") (stderr ""))
test/replay/pcond3: pass
X6: upstream's "new" record (E:134-136).
(define pcond3
(do/monad
(← x (dist categorical '((1 . 0.5) (2 . 0.5))))
(if_ (is1 x)
(return x)
(>> (dist normal 0.0 1.0)
(dist categorical '((20 . 0.5) (21 . 0.5)))))))
pcond3 = do
x <- dist categorical (pure [(1,0.5), (2,0.5)])
if_ ((==1) <$> x)
(return x)
(do
dist normal 0 1
dist categorical (pure [(20,0.5), (21,0.5)]))
pcond3r = hist $ mcmC 10000 pcond3
-- fromList [(1,5011),(20,2507),(21,2482)]
-- fromList [(1,5040),(20,2509),(21,2451)] -- new(define (test/replay/pcond3 _)
(⊦ equal? '((1 . 5040) (20 . 2509) (21 . 2451)) (hist (mcmC 10000 pcond3))))
((eta 0.034) (memory #(12582912 6220784 1048576)) (stdout "") (stderr ""))
test/replay/pcond4: pass
X7: both of upstream's records (E:273-279), conditioning inside a branch.
(define pcond4
(do/monad
(← x (dist categorical '((1 . 0.5) (2 . 0.5))))
(if_ (is1 x)
(return x)
(>> (dist (condition #t bern) 0.5)
(dist categorical '((20 . 0.5) (21 . 0.5)))))))
pcond4 = do
x <- dist categorical (pure [(1,0.5), (2,0.5)])
if_ ((==1) <$> x)
(return x)
(do
dist (True `condition` bern) 0.5
dist categorical (pure [(20,0.5), (21,0.5)]))
_ = hist $ mcmC 1000 pcond4
{-
*Examples1> hist $ mcmC 1000 pcond4
fromList [(1,648),(20,174),(21,178)]
*Examples1> hist $ mcmC 10000 pcond4
fromList [(1,6697),(20,1641),(21,1662)]
-}
-- Exact:
-- [(0.166666666666666657, V 21); (0.166666666666666657, V 20);
-- (0.66666666666666663, V 1)])(define (test/replay/pcond4 _)
(⊦ equal? '((1 . 648) (20 . 174) (21 . 178)) (hist (mcmC 1000 pcond4)))
(⊦ equal? '((1 . 6697) (20 . 1641) (21 . 1662)) (hist (mcmC 10000 pcond4))))
((eta 0.04) (memory #(12582912 6218096 1048576)) (stdout "") (stderr ""))
test/replay/pcorr: pass
X8: Yufei Cai's example (E:223-238; Figaro issue 548), a custom distribution conditioned on two random choices.
(define (pcorr* pfuzz result)
(define ((fuzzy2 pfuzz op) x y)
(let1 (v (op x y))
(categorical
`((,v unquote pfuzz) (,(not v) unquote (- 1.0 pfuzz))))))
(define (xor y x) (not (eq? x y)))
(do/monad
(← x (dist bern 0.5))
(← y (dist bern 0.5))
(dist (condition #t (fuzzy2 pfuzz xor)) x y)
(return (result x y))))
pcorr pfuzz = do
x <- dist bern 0.5
y <- dist bern 0.5
dist (True `condition` fuzzy2 pfuzz xor) x y
return y
where
-- Creating a custom distribution
fuzzy2 pfuzz op x y = let v = op x y in
categorical [(v,pfuzz),(not v,1-pfuzz)]
xor y x = not (x == y)
_ = hist $ mcmC 1000 (pcorr 0.1)
-- fromList [(False,419),(True,581)](define (test/replay/pcorr _)
(⊦ equal? '((#f . 419) (#t . 581)) (hist (mcmC 1000 (pcorr 0.1)))))
((eta 0.004) (memory #(12582912 6214952 1048576)) (stdout "") (stderr ""))
test/replay/girl: pass
X9: the eldest daughter puzzle, "77, without the bias" (E:354-355).
(define girl
(let ()
(define ((fuzzy2 pfuzz op) x y)
(let1 (v (op x y))
(categorical
`((,v unquote (- 1.0 pfuzz)) (,(not v) unquote pfuzz)))))
(do/monad
(← girl1 (dist bern 0.5))
(← girl2 (dist bern 0.5))
(dist (condition #t (fuzzy2 0.01 (λ (a b) (or a b)))) girl1 girl2)
(return girl1))))
girl = do
girl1 <- dist bern 0.5
girl2 <- dist bern 0.5
dist (True `condition` fuzzy2 0.01 (||)) girl1 girl2
return girl1
where
-- Creating a custom distribution
fuzzy2 pfuzz op x y = let v = op x y in
categorical [(v,1-pfuzz),(not v,pfuzz)]
_ = length . filter id $ mcmC 100 girl
-- 77, without the bias(define (test/replay/girl _) (⊦ = 77 (count-true (mcmC 100 girl))))
((eta 0.001) (memory #(12582912 6217896 1048576)) (stdout "") (stderr ""))
test/replay/post-bias: pass
X10: "All true" of mcmC 10 (E:262-263) and "all True
samples" of mcmC 100 (P l.435-437), both replayed: the
observation in the branch not returned still weighs the coin.
(define (post-bias c)
(do/monad
(← coin (dist bern 0.5))
(if_ coin
(dist (condition c normal) 0.0 1.0)
(dist (condition c normal) 100.0 1.0))
(return coin)))
post_bias c = do
coin <- dist bern 0.5
if_ coin (dist (c `condition` normal) 0 1)
(dist (c `condition` normal) 100 1)
return coin
_ = mcmC 10 (post_bias 1)
-- All true(define (test/replay/post-bias _)
(⊦ equal? (make-list 10 #t) (mcmC 10 (post-bias 1.0)))
(⊦ equal? (make-list 100 #t) (mcmC 100 (post-bias 1.0))))
((eta 0.0) (memory #(12582912 6218880 1048576)) (stdout "") (stderr ""))
test/replay/biased-coin: pass
X11: upstream's "incremental" record (E:203-210; P l.389-392: "we estimate the average as 0.499"), the posterior being beta 2 2 of mean 0.5 and variance 0.05.
(define (biased-coin c1 c2)
(do/monad
(← b (dist beta 1.0 1.0))
(dist (condition c1 bern) b)
(dist (condition c2 bern) b)
(return b)))
biased_coin c1 c2 = do
b <- dist beta 1 1
dist (c1 `condition` bern) b
dist (c2 `condition` bern) b
return bbiased_coin22 =
momenta . drop 1000 $
mcmC 10000 (biased_coin True False)
-- beta 2 2, av = 0.5, sigma2 = 5e-2
-- original Metropolis
-- (0.49433040918945736,5.013851472980666e-2)
-- incremental
-- (0.49910027237934973,4.97358737659099e-2)(define (test/replay/biased-coin _)
(⊦ equal?
'(0.49910027237935 . 0.0497358737659099)
(momenta (drop (mcmC 10000 (biased-coin #t #f)) 1000))))
((eta 0.052) (memory #(12582912 6357056 1048576)) (stdout "") (stderr ""))
test/replay/mixng-interleaved: pass
X12: the KL divergence of mixng against the two components' samples, upstream's "now" record (E:159-164; P l.361-364).
(define mixng
(do/monad
(← x (dist normal 0.0 1.0))
(if_ (lift (λ (xv) (> xv 0.0)) x)
(dist normal 10.0 2.0)
(dist gamma 3.0 (/ 1.0 3.0)))))
mixng = do
x <- dist normal 0 1
if_ ((>0) <$> x)
(dist normal 10 2)
(dist gamma 3 (1/3))_ =
let n = 5000 in kl (tabulate 0.5 (mcmC n mixng)) (tabulate 0.5 $ mcmC (n `div` 2) (dist normal 10 2) ++ mcmC (n `div` 2) (dist gamma 3 (1/3)))
{-
5000 samples 1.3563495645465825e-2
1.3896442111895212e-2 now
-}(define (test/replay/mixng-interleaved _)
(⊦ =
0.0138964421118952
(kl (tabulate 0.5 (mcmC 5000 mixng))
(tabulate
0.5
(append
(mcmC 2500 (dist normal 10.0 2.0))
(mcmC 2500 (dist gamma 3.0 (/ 1.0 3.0))))))))
((eta 0.031) (memory #(12582912 6222736 1048576)) (stdout "") (stderr ""))
test/replay/phier: pass
X13: Bench.hs's record for n = 10 and 1000 samples (BE:23-25).
(define (deeper m) (do/monad (← x m) (dist normal x 3.0)))
(define (phierN n) (fold (λ (_ m) (deeper m)) (dist normal 0.0 1.0) (iota n)))
phier n =
(iterate (\m -> do {x<-m; dist normal x 3}) $ dist normal 0 1) !! n
main = do
[n',nchain'] <- getArgs
let n = read n' :: Int
let nchain = read nchain' :: Integer
print . momenta $ mcmC nchain (phier n)
{-
1000
(3.4117556376053577,32.27027981263249)(define (test/replay/phier _)
(⊦ equal?
'(3.41175563760536 . 32.2702798126325)
(momenta (mcmC 1000 (phierN 10)))))
((eta 0.004) (memory #(12582912 6284280 1048576)) (stdout "") (stderr ""))
test/replay/colored-balls: pass
X14: cbr, upstream's record "Good agreement" (CB:101-104),
10000 samples of a model of 2114 nodes, most of them in branches. Its
listing is with case test/language/graph/colored-balls.
cbr = hist $ mcmC 10000 (cballs_model (replicate 10 Blue))
-- fromList [(1,4265),(2,2203),(3,1244),(4,805),(5,486),(6,291),(7,565),(8,141)]
-- Good agreement(define (test/replay/colored-balls _)
(⊦ equal?
'((1 . 4265)
(2 . 2203)
(3 . 1244)
(4 . 805)
(5 . 486)
(6 . 291)
(7 . 565)
(8 . 141))
(hist (mcmC 10000 (cballs-model (make-list 10 'blue))))))
((eta 0.696) (memory #(25165824 6220976 1048576)) (stdout "") (stderr ""))
test/replay/always-flip: pass
X15: records made with the always-flip kernel of the Bernoulli distribution
(D:64-70, commented out upstream and kept as bern/flip): pbern's 4 True of 10 (P l.217-218), ph2's 5809 and ph21's 2026 (E:94, E:102)
and the KL of mixng against mixng' (E:153-157). The general forms of the models
take the coin; (let1 (bern bern/flip) ...) runs them as
upstream did.
(define (pbern* bern) (dist bern 0.4))
(define (ph2* bern)
(do/monad
(← x (dist bern 0.2))
(dist bern (lift (λ (xv) (if xv 0.1 0.7)) x))))
(define (ph21* bern)
(do/monad
(← x (dist bern 0.2))
(← y (dist bern (lift (λ (xv) (if xv 0.1 0.7)) x)))
(return x)))
(define (mixng′* bern)
(do/monad
(← x (dist bern 0.5))
(if_ x (dist normal 10.0 2.0) (dist gamma 3.0 (/ 1.0 3.0)))))
-- Simple models
pbern = dist bern 0.4
pbern_run = mcmC 10 pbernph2 = do
x <- dist bern 0.2
dist bern ((\xv -> if xv then 0.1 else 0.7) <$> x)
-- 0.2 * 0.1 + 0.8*0.7 = 0.58
ph2r = length . filter id $ mcmC 10000 ph2
-- 5809
ph21 = do
x <- dist bern 0.2
y <- dist bern ((\xv -> if xv then 0.1 else 0.7) <$> x)
return x
ph21r = length . filter id $ mcmC 10000 ph21
-- 2026mixng' = do
x <- dist bern 0.5
if_ x
(dist normal 10 2)
(dist gamma 3 (1/3))
_ =
let n = 5000 in kl (tabulate 0.5 (mcmC n mixng)) (tabulate 0.5 (mcmC n mixng'))
{-
5000 samples 2.275866271810348e-2
5000 samples 1.1301558326703463e-2 now-- resampling always flips, so fwd and bwd steps are 0
instance RDistribution Bern Bool where
{-
resample d x _ g =
let x' = not x in
((x', logDensity d x', g),0)
-}(Distribution.hs:64-70.)
(define (test/replay/always-flip _)
(let1 (bern bern/flip)
(⊦ = 4 (count-true (mcmC 10 (pbern* bern))))
(⊦ = 5809 (count-true (mcmC 10000 (ph2* bern))))
(⊦ = 2026 (count-true (mcmC 10000 (ph21* bern))))
(⊦ =
0.0113015583267035
(kl (tabulate 0.5 (mcmC 5000 mixng))
(tabulate 0.5 (mcmC 5000 (mixng′* bern)))))))
((eta 0.071) (memory #(25165824 6227456 1048576)) (stdout "") (stderr ""))
test/replay/tac: pass
X16: "tac2 and tac3 produce identical results, as expected: since diracN is unit, e >>= diracN is indeed the same as diracN" (S:370-371); here tac1 too, since a constant Dirac node in a branch draws nothing. Upstream's 620, 523 and 532 (S:346-368) come from an older revision of the engine; 505 is the prototype's value.
(define tac1
(do/monad
(← c (dist bern 0.5))
(if_ c (dist normal 0.0 1.0) (dist uniform 10.0 20.0))
(return c)))
(define tac2
(do/monad
(← c (dist bern 0.5))
(if_ c (>> (diracN #t) (dist normal 0.0 1.0)) (dist uniform 10.0 20.0))
(return c)))
(define tac3
(do/monad
(← c (dist bern 0.5))
(if_ c
(>> (diracN #t) (dist normal 0.0 1.0))
(>>= (dist uniform 10.0 20.0) diracN))
(return c)))
-- Programs from my messages about the problem with conditionals
tac1 = do
c <- dist bern 0.5
if_ c
(dist normal 0 1)
(dist uniform 10 20)
return c
tac1_run = length . filter id $ mcmC 1000 tac1
-- 620 True
tac2 = do
c <- dist bern 0.5
if_ c
(diracN (Val True) >> dist normal 0 1)
(dist uniform 10 20)
return c
tac2_run = length . filter id $ mcmC 1000 tac2
-- 523 True
tac3 = do
c <- dist bern 0.5
if_ c
(diracN (Val True) >> dist normal 0 1)
(dist uniform 10 20 >>= diracN)
return c
tac3_run = length . filter id $ mcmC 1000 tac3
-- 532 True
-- tac2 and tac3 produce identical results, as expected:
-- since diracN is unit, e >>= diracN is indeed the same as diracN(define (test/replay/tac _)
(let ((t1 (mcmC 1000 tac1)) (t2 (mcmC 1000 tac2)) (t3 (mcmC 1000 tac3)))
(⊦ equal? t1 t2)
(⊦ equal? t2 t3)
(⊦ = 505 (count-true t1))))
((eta 0.008) (memory #(25165824 6320328 1048576)) (stdout "") (stderr ""))
(aux hakaru10): the laws of model composition
Laws
The equational laws of model composition (P §2.1 l.250-281, §4.3 l.803-843): Dirac is the left and the right unit of composition, and an unused variable is marginalized. Under the MCMC engine the right unit law holds sample for sample (X2, X16); the left unit law and marginalization hold in distribution. Under exact enumeration, which walks the built graph and reuses no incremental machinery (plan §6.10), they hold exactly.
test/laws/left-unit: pass
A1: do {x <- diracN 5; dist normal x 1} is dist normal 5 1 sample for sample: the constant Dirac node
draws nothing and is never proposed (P l.259-261).
(define (test/laws/left-unit _)
(⊦ equal?
(mcmC 200 (dist normal 5.0 1.0))
(mcmC 200 (do/monad (← x (diracN 5.0)) (dist normal x 1.0)))))
((eta 0.001) (memory #(25165824 6212720 1048576)) (stdout "") (stderr ""))
test/laws/diractest: pass
A2: the port of diractest (M:1029-1037): "The result
should show both True and False, in the same proportion". The Dirac node is
recomputed with its parent, so the proposals to change x are accepted.
{-
diractest = mcmC 5 $ do
a <- bernN (Val 0.5)
diracN a
-}
diractest = mcmc 10 $
with_node_none (bernS 0.5) `next` (\a ->
with_node a (\a -> diracS a))
-- The result should show both True and False, in the same proportion(define (test/laws/diractest _)
(⊦ (equal-approx? 0.03)
0.5
(frequency
identity
(mcmC 10000
(do/monad (← x (dist bern 0.5)) (diracN (lift identity x)))))))
((eta 0.022) (memory #(25165824 6438560 1048576)) (stdout "") (stderr ""))
test/laws/p2: pass
A3: P's p2 = do {x <- dist uniform 0 1; diracN x} (§4.3 l.812-825), the example on which an MH that treats Dirac as an ordinary
distribution never moves: here the independence proposal is always accepted (the
uniform density is constant), so nearly every sample is new.
(define (test/laws/p2 _)
(⊦ <=
9990
(length
(hist (mcmC 10000
(do/monad
(← x (dist uniform 0.0 1.0))
(diracN (lift identity x))))))))
((eta 0.053) (memory #(25165824 6842280 1048576)) (stdout "") (stderr ""))
test/laws/marginal: pass
A4: pmarg has pnorm's distribution, N(10, 0.5²), though not its samples (P l.278-281; E:43-48).
(define pmarg (do/monad (← x pbern) (← y pnorm) (return y)))
pmarg = do
x <- pbern
y <- pnorm
return y
pmarg_run = mcmC 100 pmarg(define (test/laws/marginal _)
(let1 (m (momenta (mcmC 10000 pmarg)))
(⊦ (equal-approx? 0.05) '(10.0 . 0.25) m)))
((eta 0.017) (memory #(25165824 6315008 1048576)) (stdout "") (stderr ""))
test/laws/joint: pass
A5: pjoin samples the product of pbern and pnorm (P l.264-272; E:36-42): P(x) = 0.4 and x, y uncorrelated.
(define pjoin (do/monad (← x pbern) (← y pnorm) (return (pair x y))))
pjoin = do
x <- pbern
y <- pnorm
return (pair x y)
_ = mcmC 100 pjoin(define (test/laws/joint _)
(let* ((samples (mcmC 10000 pjoin))
(xs (map (λ (s) (if (car s) 1.0 0.0)) samples))
(ys (map cdr samples))
(mx (mean xs))
(my (mean ys))
(cov (mean (map (λ (x y) (* (- x mx) (- y my))) xs ys)))
(corr (/ cov (sqrt (* (cdr (momenta xs)) (cdr (momenta ys)))))))
(⊦ (equal-approx? 0.03) 0.4 mx)
(⊦ < (abs corr) 0.05)))
((eta 0.038) (memory #(25165824 7391856 1048576)) (stdout "") (stderr ""))
test/laws/branch-unit: pass
A6: e >>= diracN inside a branch is e (S:370-371), sample for sample; see X16.
(define (test/laws/branch-unit _) (⊦ equal? (mcmC 1000 tac2) (mcmC 1000 tac3)))
((eta 0.005) (memory #(25165824 6258408 1048576)) (stdout "") (stderr ""))
test/laws/exact: pass
The laws under exact enumeration, where a model is its distribution: the right and left units, marginalization, and a Dirac node introduced for a lifted parameter (ph2, whose posterior is 0.58, E:91). Enumeration walks the built graph; it reuses no incremental machinery (plan §6.10): a Dirac node's value is recomputed from its parents' on each path.
(define (test/laws/exact _)
(let ((coin (dist bern 0.4))
(pmarg/coin
(do/monad (← x (dist bern 0.4)) (← y (dist bern 0.3)) (return y))))
(⊦ equal?
(exact-posterior coin)
(exact-posterior (do/monad (← x (dist bern 0.4)) (diracN x))))
(⊦ equal?
(exact-posterior (dist bern 0.3))
(exact-posterior (do/monad (← p (diracN 0.3)) (dist bern p))))
(⊦ (equal-approx? 1e-15)
(exact-posterior (dist bern 0.3))
(exact-posterior pmarg/coin))
(⊦ (equal-approx? 1e-15)
(exact-posterior ph2)
(exact-posterior
(do/monad
(← x (dist bern 0.2))
(← q (diracN (lift (λ (xv) (if xv 0.1 0.7)) x)))
(dist bern q))))))
((eta 0.0) (memory #(25165824 6263624 1048576)) (stdout "") (stderr ""))
(aux hakaru10): the semantics of branch reactivation
Semantics of branches
In upstream's kernel an inactive node's updates are delayed (M:298-303) and replayed
when its branch is activated again (M:767-775). For an observed node this is exact:
activation adds back the LL that deactivation took. For a resampleable node the
replayed update adds
So, when it comes to computing the acceptance ratio for a program
with branches, we take
p4 = do
c <- bernN (Val 0.5)
if_ c (do e1 e2 ...)
(do e3 e4 e5 ...)
to be
p4' = do
c <- bernN (Val 0.5)
if_ c e_then e_else
That is, we merge (at least for the purpose of presentation) e1, e2 ...
into one node e_then, whose ll is the sum of ll of constituents. Ditto
for e_else.
When we switch the branches, we remove the ll of one branch from
llcum and add ll of the other branch. In our terms, we remove the ll
of the deactivated nodes and add ll of the activated nodes.
This algorithm is resilient with respect to adding replacing a node e
with e >>= diracN -- the desired behavior.The argument assumes the reactivated nodes are fresh draws from their prior, while
the kernel reuses their stale values. The smallest counterexample, pswitch (plan §6.8), has P(c) = 0.5; the exact stationary
distribution of upstream's kernel gives P(c) = 0.377286. The upstream examples are
unaffected: their branch-local resampleable nodes have constant parameters.
(define pswitch
(do/monad
(← c (dist bern 0.5))
(← m (dist uniformly '(0 1)))
(if_ c
(dist categorical (lift (λ (mv) `((,mv . 0.9) (,(- 1 mv) . 0.1))) m))
(diracN (lift identity m)))
(return c)))
The opt-in #:semantics 'extended (deviation 8) runs MH on the
space extended with the inactive resampleable nodes as auxiliary variables drawn
from their prior, the merged-node argument made exact: updates are never delayed,
an inactive resampleable node's LL change counts in Δ and an inactive observed
node's does not, (de)activation still moves only observed weights, and the
proposal, the retry and the cardinality term are upstream's. An inactive node whose
update raises a parameter error, or any other error that an inactive Dirac node would
defer, rejects the step (plan C22). The kernel residual of
hakaru10-exact.scm shows that the extended kernel leaves the posterior invariant,
exactly, on small models.
test/semantics/pswitch/upstream: pass
B1: upstream's bias, documented: P(c) near 0.3773 instead of 0.5.
(define (test/semantics/pswitch/upstream _)
(⊦ (equal-approx? 0.03) 0.3773 (frequency identity (mcmC 40000 pswitch))))
((eta 0.137) (memory #(25165824 7160488 1048576)) (stdout "") (stderr ""))
test/semantics/pswitch/extended: pass
B2: the extended semantics samples the right posterior, P(c) = 0.5.
(define (test/semantics/pswitch/extended _)
(⊦ (equal-approx? 0.02)
0.5
(frequency identity (mcmC 40000 pswitch #:semantics 'extended))))
((eta 0.154) (memory #(25165824 7160976 1048576)) (stdout "") (stderr ""))
test/semantics/extended/examples: pass
B3: the extended semantics on upstream's branching examples gives the same posteriors as S7 and S8, within S7's tolerances.
The colored balls line is a regression value at seed 17, not a statistical check: P(1) = 0.4265, with P(1) > P(2) > P(3), against the closed form 0.4119636809525704 of hakaru10-exact.scm. At 10000 extended samples the standard deviation of P(1) across seeds is about 0.05 (seeds 1 to 40: mean 0.4085, sd 0.0515), so a tolerance of 4 to 5 of them could not tell a biased sampler from a correct one. That the extended kernel leaves the colored balls' posterior invariant is shown exactly, by the kernel residual of hakaru10-exact.scm (E10), on the colored balls cut to 3 balls and 2 draws.
(define (test/semantics/extended/examples _)
(let1 (run (λ (m) (proportions (mcmC 10000 m #:semantics 'extended))))
(⊦ (equal-approx? 0.03)
'((10 . 0.25) (11 . 0.25) (20 . 0.25) (21 . 0.25))
(run pcond1))
(⊦ (equal-approx? 0.04)
'((1 . 0.5) (20 . 0.25) (21 . 0.25))
(run pcond2))
(⊦ (equal-approx? 0.05)
'((1 . 0.5) (20 . 0.25) (21 . 0.25))
(run pcond3))
(⊦ (equal-approx? 0.05)
`((1 unquote (/ 2.0 3.0))
(20 unquote (/ 1.0 6.0))
(21 unquote (/ 1.0 6.0)))
(run pcond4)))
(let1 (xs (mcmC 10000 mixng #:semantics 'extended))
(⊦ (equal-approx? 0.4) 5.5 (mean xs))
(⊦ (equal-approx? 0.05) 0.5 (frequency (λ (x) (> x 5.0)) xs)))
(let1 (balls (proportions
(mcmC 10000
(cballs-model (make-list 10 'blue))
#:semantics
'extended
#:seed
17)))
(⊦ = 0.4265 (alist-ref 1 balls))
(⊦ > (alist-ref 1 balls) (alist-ref 2 balls) (alist-ref 3 balls))))
((eta 1.36) (memory #(25165824 6442552 1048576)) (stdout "") (stderr ""))
test/semantics/check: pass
B4: #:check #t on 300 samples of seven models, in both
semantics, raises no condition: a rebuild of the model reproduces the initial
trace node by node (graph, values, and LLs within 1e-9), an address-order redraw
of its random choices reproduces their values and the trace's LL (plan §6.9, §15
C37; within 1e-9 of the sum of the LLs' magnitudes, so that far-inactive, whose build at seed 17 deactivates an observation
of LL about -5·10⁷, passes in both semantics: the 'upstream trace's LL adds and
then subtracts it, M:659, and the 'extended one never counts it), and after every
step the reference evaluator recomputes every node from scratch (activity
counts, Dirac values, distributions and LLs, exits) and finds the incremental
state, a rejected step restores the state exactly, and in 'extended mode an accepted step's Δ is the change of the
extended target's LL; the next case shows that each of these checks fails when it
should.
The rebuild builds the branch its test does not select as the first build
does, under lazy lifting, so a model whose inactive branch applies car to the '() its test rules out (a
deferred Dirac value, M:99, M:120-129) passes the check, with a constant test and
with a lifted one, in both semantics, and samples what it samples without #:check:
(define guarded-car/constant
(do/monad
(let xs '())
(← b (dist bern 0.5))
(if_ (lift null? xs) (return b) (diracN (lift car xs)))))
(define guarded-car/node
(do/monad
(← b (dist bern 0.5))
(let xs (lift (λ (bv) (if bv '() '(1))) b))
(if_ (lift null? xs) (return 'none) (diracN (lift car xs)))))
Three models reach the pending weight of the 'extended semantics (deviation 8), each checked on 2000 samples. In act-update and act-update/finite a step that moves x from 2 activates the observation and updates its parameters
at once: its weight is added once, with its new LL, when propagation ends, which
the step check verifies against the change of the extended LL (the stale LL is
-inf in the first, log .5 in the second). In pending-abort the step that activates the observation is then rejected mid-propagation by the
inactive uniformly '() of the second branch (plan C22): the
journal restores the pending flag, which the reference evaluator finds clear and
the step check finds restored. Its chain, which the extended target keeps off x
= 0 (as in invalid-inactive, case B5), gives P(x = 1) = .3/1.3
within .02 over 20000 samples. The 'extended chain of act-update gives its exact posterior, (.9 .3 1)/2.2, within .02
over 20000 samples, and the kernel residual (case E10 of hakaru10-exact.scm) is below
act-update models. Counting the
weight at activation, or again at the update, makes the residual of act-update about .18, and its chain never leaves x = 2.
(define act-update
(do/monad
(← x (dist uniformly '(0 1 2)))
(if_ (lift (λ (v) (< v 2)) x)
(dist (condition 0 categorical)
(lift (λ (v)
(case v
((0) '((0 . 0.9) (1 . 0.1)))
((1) '((0 . 0.3) (1 . 0.7)))
(else '((1 . 1.0)))))
x))
(return 'none))
(return x)))
(define act-update/finite
(do/monad
(← x (dist uniformly '(0 1 2)))
(if_ (lift (λ (v) (< v 2)) x)
(dist (condition 0 categorical)
(lift (λ (v)
(case v
((0) '((0 . 0.9) (1 . 0.1)))
((1) '((0 . 0.3) (1 . 0.7)))
(else '((0 . 0.5) (1 . 0.5)))))
x))
(return 'none))
(return x)))
(define pending-abort
(do/monad
(← x (dist uniformly '(0 1 2)))
(if_ (lift (λ (v) (< v 2)) x)
(dist (condition 0 categorical)
(lift (λ (v)
`((0 unquote (if (= v 0) 0.9 0.3))
(1 unquote (if (= v 0) 0.1 0.7))))
x))
(return 'none))
(if_ (lift (λ (v) (not (= v 0))) x)
(dist uniformly (lift (λ (v) (if (= v 0) '() '(a b))) x))
(return 'z))
(return x)))
The chain keeps an initial trace of LL NaN, which only an infinite LL would make
it rebuild (Metropolis.hs:853-854, 882-891): at seed 17 nan-initial's build draws b = 3, whose observation has LL NaN.
A step that moves b has a NaN ratio, which fails both acceptance tests
(Metropolis.hs:939-944), so b stays 3, while c, whose steps leave the NaN term
alone, takes both values. The step check of #:check cannot compare a Δ with a change of NaN extended LLs and skips it there, so
both chains sample with it what they sample without it.
(define nan-initial
(do/monad
(← b (dist uniformly '(1 2 3)))
(dist (condition 0.0 normal)
0.0
(lift (λ (bv) (case bv ((3) 0.0) ((2) 1.0) (else 2.0))) b))
(← c (dist bern 0.5))
(return (pair b c))))
A value of NaN is equal? to no flonum, not even to itself, and a recomputation
builds a fresh one, so the checks match two NaNs, in a value and in a
distribution's parameters (same-value?): the 300 samples of nan-mean are the same with and without #:check, in both semantics, and the rebuild check passes at seed 107336, where k = 0
and the mean is NaN.
(define nan-mean
(do/monad
(let mean-of (λ (kv) (/ (exact->inexact (* kv 3)) (exact->inexact kv))))
(← k (dist uniformly '(0 1 2)))
(← mean (diracN (lift mean-of k)))
(← z
(dist categorical
(lift (λ (kv) `((,(mean-of kv) . 0) (a . 1) (b . 1))) k)))
(return (pair k z))))
(define (test/semantics/check _)
(for-each
(λ (semantics)
(for-each
(Λ ((,name ,m))
(⊦ equal?
(list name semantics 300)
(list name
semantics
(length (mcmC 300 m #:semantics semantics #:check #t)))))
`((phier3 ,(phierN 3))
(mhex ,mhex)
(exif2 ,exif2)
(pcond4 ,pcond4)
(colored-balls ,(cballs-model '(blue green blue)))
(pswitch ,pswitch)
(regression ,(regression (regression-data 2 10))))))
'(upstream extended))
(for-each
(λ (semantics)
(for-each
(Λ ((,name ,m))
(⊦ equal?
(list name semantics (mcmC 50 m #:semantics semantics))
(list name
semantics
(mcmC 50 m #:semantics semantics #:check #t))))
`((guarded-car/constant ,guarded-car/constant)
(guarded-car/node ,guarded-car/node))))
'(upstream extended))
(for-each
(Λ ((,name ,m))
(⊦ equal?
(list name 2000)
(list name (length (mcmC 2000 m #:semantics 'extended #:check #t)))))
`((act-update ,act-update)
(act-update/finite ,act-update/finite)
(pending-abort ,pending-abort)))
(⊦ (equal-approx? 0.02)
`((1 unquote (/ 0.3 1.3)) (2 unquote (/ 1.0 1.3)))
(proportions (mcmC 20000 pending-abort #:semantics 'extended)))
(let1 (far-inactive
(do/monad
(← c (dist bern 0.5))
(if_ (lift not c)
(do/monad (dist (condition 1000.0 normal) 0.0 0.1) (return 1))
(return 0))))
(for-each
(λ (semantics)
(⊦ equal?
(list semantics #t 100)
(list semantics
(construction-check far-inactive #:semantics semantics)
(length
(mcmC 100
far-inactive
#:semantics
semantics
#:check
#t)))))
'(upstream extended)))
(let1 (exact (exact-posterior act-update))
(⊦ (equal-approx? 1e-12)
`((0 unquote (/ 0.9 2.2))
(1 unquote (/ 0.3 2.2))
(2 unquote (/ 1.0 2.2)))
exact)
(⊦ (equal-approx? 0.02)
exact
(proportions (mcmC 20000 act-update #:semantics 'extended))))
(for-each
(λ (semantics)
(let1 (xs (mcmC 200 nan-initial #:semantics semantics))
(⊦ equal?
(list semantics #t xs)
(list semantics
(construction-check nan-initial #:semantics semantics)
(mcmC 200 nan-initial #:semantics semantics #:check #t)))
(⊦ equal?
(list semantics '(3))
(list semantics (delete-duplicates (map car xs))))
(⊦ equal?
(list semantics 2)
(list semantics (length (delete-duplicates (map cdr xs)))))))
'(upstream extended))
(for-each
(λ (semantics)
(⊦ equal?
(list semantics #t (mcmC 300 nan-mean #:semantics semantics) 0)
(list semantics
(construction-check
nan-mean
#:seed
107336
#:semantics
semantics)
(mcmC 300 nan-mean #:semantics semantics #:check #t)
(car (car (mcmC 1
nan-mean
#:seed
107336
#:semantics
semantics
#:check
#t))))))
'(upstream extended))
(⊦ equal?
'()
(filter-map
(Λ ((,name unquote m))
(let1 (residual (kernel-residual m))
(and (not (< residual 1e-12)) (cons name residual))))
`((act-update unquote act-update)
(act-update/finite unquote act-update/finite)))))
((eta 1.051) (memory #(25165824 6207944 1048576)) (stdout "") (stderr ""))
test/semantics/check/negative: pass
B4′, the negative controls of B4: each oracle of #:check reports a chain it should reject, with an internal condition. construction-check (plan §6.9, §15 C37) catches
a model whose two builds, the initial trace and a rebuild from the same seed,
differ in value (a counter answered as the result) or in LL (the same counter as
an observation's standard deviation): each run increments the counter after a
bind, so each build sees another value. An LL -inf agrees only with -inf: in counter/bound the counter bounds the uniform of an observation
in a branch never taken, which is impossible on the first build only, and 'extended mode, which keeps llcum finite, reports it. mcmC runs it once per chain under #:check, and reports both models the same way. A deferred value (deviation 11) is
compared by its arguments, since its closure is fresh per build: counter/deferred reads the counter in the inactive branch with a
function that raises, and its two builds differ only there. The rebuild also
catches a model whose graph alone changes, switching, whose Dirac node reads x on odd runs and y, of the same value, on even ones;
and the address-order redraw catches a sampler that draws once more on its third
call, the redraw's (third-skips, another value), or that then reports its LL plus 1 (third-shifts, another LL). In 'extended mode the LL that the redraw checks is the extended one, which never counts an
inactive observation, so neither does the check's tolerance: the shift is caught
next to an inactive observation that is impossible (LL -inf) or far off (LL about
-5·10²³). In 'upstream mode, whose LL adds and then
subtracts that observation's (M:659), the shift is lost: next to the impossible
observation the LL is NaN, which the redraw cannot compare, and next to the far
one it is lost in the rounding. A build of LL NaN is still checked, all but the
redraw's LL, since initial-step keeps it (it retries only an
infinite LL, M:853-854): next to the impossible observation, the redraw of third-skips reports its value.
reference-check catches each of nine corruptions of the
initial trace of shifted-normal, x and the Dirac node x + 1,
made behind the engine's back with its exported setters, each with its own
message: an LL shifted by 1 or made -inf, a value, a distribution or a tstamp
after now in a node's state, a deactivation count, a delayed update on an active
node, a pending weight left over, and the chain's card change. It also catches,
through the wiring of mcmC, a user distribution whose
density drifts with the number of its calls. In 'extended mode, where no update is delayed, it checks the inactive nodes' states too: the
LL of the inactive branch's choice of tac1, shifted by 1, is reported in an 'extended build and not in an 'upstream one, where an inactive node
may hold a stale state.
The step check catches a rejected step that did not restore the state: in behind-the-journal the sampler of the proposed node replaces
the state of the observed x by an equal copy with the raw node-state-set!, which no journal entry undoes; the reference
evaluator compares values and LLs and accepts the copy, the step check compares
the state objects themselves (C5) and does not, in both semantics. Without #:check the chain runs.
In 'extended mode it also catches an accepted step whose Δ
is not the change of the extended LL: in tampered the sampler of the only resampleable node, armed before step k, adds 1 to the
stored LL of the observed x, which the step's Δ does not count; with no
observation of the proposed node every step is accepted. The step check runs
before the reference evaluator, which would report the LL instead. #:check 5 checks the first 5 steps only: it reports a
corruption at step 5 and runs through one at step 6.
(define (test/semantics/check/negative _)
(let ((k 0)
(j 0)
(u 0)
(runs 0)
(calls 0)
(skips 0)
(shifts 0)
(victim #f)
(shifted #f)
(tamper! #f))
(define (drifting)
(let1 (d (normal 0.0 1.0))
(make-distribution
'drifting
#:log-density
(λ (x)
(add1! calls)
(- (distribution-log-density d x) (* 0.001 calls)))
#:sample
(λ (src) (distribution-sample d src)))))
(define (unjournaled)
(let1 (d (normal 0.0 1.0))
(make-distribution
'unjournaled
#:log-density
(λ (x) (distribution-log-density d x))
#:sample
(λ (src)
(when (node? victim)
(let1 (s (node-state victim))
(node-state-set!
victim
(make-nstate
(nstate-val s)
(nstate-ll s)
(nstate-dst s)
(nstate-tstamp s)))))
(distribution-sample d src)))))
(define behind-the-journal
(do/monad
(← x (dist (condition 0.5 normal) 0.0 1.0))
(let _ (set! victim x))
(← s (dist unjournaled))
(dist (condition 3.0 normal) s 0.5)
(return s)))
(define (restate-node! n f)
(let1 (s (node-state n))
(node-state-set!
n
(f (nstate-val s)
(nstate-ll s)
(nstate-dst s)
(nstate-tstamp s)))))
(define (restate! ch addr f) (restate-node! (chain-node ch addr) f))
(define shifted-normal
(do/monad (← x (dist normal 0.0 1.0)) (diracN (sexp+ x 1.0))))
(define (corrupted-message
corrupt!
#!optional
(m shifted-normal)
(semantics 'upstream))
(internal-message
(τ (receive
(ch result)
(build-chain
(model->trace m 'test)
(make-stdgen-source 17)
semantics)
(corrupt! ch)
(reference-check ch)))))
(define (shift-inactive-ll! ch)
(restate-node!
(chain-fold
(λ (n found)
(or found
(and (node-inactive? n) (eq? (node-kind n) 'resampleable) n)))
#f
ch)
(λ (v ll d t) (make-nstate v (+ 1.0 ll) d t))))
(define (tampering)
(let1 (d (normal 0.0 1.0))
(make-distribution
'tampering
#:log-density
(λ (x) (distribution-log-density d x))
#:sample
(λ (src)
(when tamper! (let1 (t! tamper!) (set! tamper! #f) (t!)))
(distribution-sample d src)))))
(define tampered
(do/monad
(← x (dist (condition 0.5 normal) 0.0 1.0))
(let _ (set! shifted x))
(dist tampering)))
(define counter/k (>>= (return #f) (λ (_) (add1! k) (return k))))
(define counter/j
(>>= (return #f)
(λ (_)
(add1! j)
(dist (condition 0.0 normal) 0.0 (exact->inexact j)))))
(define counter/bound
(>>= (return #f)
(λ (_)
(add1! u)
(if_ #f
(dist (condition 5.0 uniform) 0.0 (if (= u 1) 1.0 10.0))
(return 0.0)))))
(define counter/deferred
(do/monad
(← c (dist bern 0.5))
(>>= (return #f)
(λ (_)
(add1! k)
(if_ #t
(return c)
(diracN (lift (λ (kv) (car (list-tail '() kv))) k)))))
(return c)))
(define switching
(do/monad
(← x (diracN 1.0))
(← y (diracN 1.0))
(>>= (return #f)
(λ (_) (add1! runs) (diracN (sexp+ (if (odd? runs) x y) 0.0))))))
(define (third-skips)
(let1 (d (normal 0.0 1.0))
(make-distribution
'third-skips
#:log-density
(λ (x) (distribution-log-density d x))
#:sample
(λ (src)
(add1! skips)
(when (zero? (modulo skips 3)) (distribution-sample d src))
(distribution-sample d src)))))
(define (third-shifts)
(let1 (d (normal 0.0 1.0))
(make-distribution
'third-shifts
#:log-density
(λ (x) (distribution-log-density d x))
#:sample
(λ (src)
(add1! shifts)
(receive
(x ll)
(distribution-sample d src)
(values x (if (zero? (modulo shifts 3)) (+ ll 1.0) ll)))))))
(define (third-shifts-message obs semantics)
(set! shifts 0)
(internal-message
(τ (construction-check
(do/monad
(← x (dist third-shifts))
(if_ #f obs (return 0))
(return x))
#:semantics
semantics))))
(define (tampered-message check k)
(set! tamper! #f)
(internal-message
(τ (let loop ((s (mcmC§ tampered #:semantics 'extended #:check check))
(i 0))
(car§ s)
(when (= i (sub1 k))
(set! tamper!
(τ (restate-node!
shifted
(λ (v ll d t) (make-nstate v (+ 1.0 ll) d t))))))
(when (< i 20) (loop (cdr§ s) (add1 i)))))))
(⊦ equal?
"a rebuild and the initial trace differ in value"
(internal-message (τ (construction-check counter/k))))
(⊦ equal?
"a rebuild and the initial trace differ in LL"
(internal-message (τ (construction-check counter/j))))
(⊦ equal?
"a rebuild and the initial trace differ in value"
(internal-message (τ (mcmC 10 counter/k #:check #t))))
(⊦ equal?
"a rebuild and the initial trace differ in LL"
(internal-message (τ (mcmC 10 counter/j #:check #t))))
(⊦ equal?
"a rebuild and the initial trace differ in LL"
(internal-message
(τ (construction-check counter/bound #:semantics 'extended))))
(⊦ equal?
(make-list 3 "a rebuild and the initial trace differ in value")
(list (internal-message (τ (construction-check counter/deferred)))
(internal-message
(τ (construction-check counter/deferred #:semantics 'extended)))
(internal-message (τ (mcmC 10 counter/deferred #:check #t)))))
(⊦ equal?
"a rebuild and the initial trace differ in graph"
(internal-message (τ (construction-check switching))))
(⊦ equal?
"an address-order redraw and the initial trace differ in value"
(internal-message (τ (construction-check (dist third-skips)))))
(⊦ equal?
"an address-order redraw and the initial trace differ in LL"
(internal-message (τ (construction-check (dist third-shifts)))))
(let ((impossible (dist (condition 5.0 (uniform 0.0 1.0))))
(far (dist (condition 1000000000000.0 normal) 0.0 1.0)))
(⊦ equal?
(make-list
2
"an address-order redraw and the initial trace differ in LL")
(map (cut third-shifts-message <> 'extended) (list impossible far)))
(⊦ equal?
'(none none)
(map (cut third-shifts-message <> 'upstream) (list impossible far)))
(set! skips 0)
(⊦ equal?
"an address-order redraw and the initial trace differ in value"
(internal-message
(τ (construction-check
(do/monad
(← x (dist third-skips))
(if_ #f impossible (return 0))
(return x)))))))
(⊦ equal?
'("an LL differs from its recomputation" none)
(map (λ (semantics)
(corrupted-message shift-inactive-ll! tac1 semantics))
'(extended upstream)))
(for-each
(Λ ((,expected ,corrupt!))
(⊦ equal? expected (corrupted-message corrupt!)))
`(("an LL differs from its recomputation"
,(λ (ch)
(restate! ch 1 (λ (v ll d t) (make-nstate v (+ 1.0 ll) d t)))))
("an LL differs from its recomputation"
,(λ (ch) (restate! ch 1 (λ (v ll d t) (make-nstate v -inf.0 d t)))))
("a value differs from its recomputation"
,(λ (ch) (restate! ch 2 (λ (v ll d t) (make-nstate 99.0 ll d t)))))
("a distribution differs from its recomputation"
,(λ (ch)
(restate!
ch
1
(λ (v ll d t)
(make-nstate v ll (distk-rd (normal 0.0 2.0)) t)))))
("a tstamp is after now"
,(λ (ch) (restate! ch 1 (λ (v ll d t) (make-nstate v ll d 99)))))
("a deactivation count differs from its recomputation"
,(λ (ch) (node-inactive-set! (chain-node ch 1) 1)))
("an update is delayed on an active node or in 'extended mode"
,(λ (ch) (node-delayed-set! (chain-node ch 1) #t)))
("a node is still pending or queued after the step"
,(λ (ch) (node-pending-set! (chain-node ch 1) #t)))
("the card change differs from its recomputation"
,(λ (ch) (chain-card-change-set! ch 1)))))
(⊦ equal?
"an LL differs from its recomputation"
(internal-message (τ (mcmC 50 (dist drifting) #:check #t))))
(⊦ = 50 (length (mcmC 50 behind-the-journal)))
(for-each
(λ (semantics)
(⊦ equal?
(list semantics "a rejected step did not restore the state")
(list semantics
(internal-message
(τ (mcmC 50
behind-the-journal
#:check
#t
#:semantics
semantics))))))
'(upstream extended))
(⊦ equal?
"the step's delta is not the extended LL change"
(tampered-message #t 3))
(⊦ equal?
"the step's delta is not the extended LL change"
(tampered-message 5 5))
(⊦ eq? 'none (tampered-message 5 6))
(⊦ eq? 'none (tampered-message #f 3))))
((eta 0.002) (memory #(25165824 6207952 1048576)) (stdout "") (stderr ""))
test/semantics/check/escaped-error: pass
B4″: an error that escapes a step leaves its journal open, and the next step
first rolls it back (journal-begin!, no upstream counterpart),
so a reader that catches the error and reads on continues from the last accepted
state. In flaky the update of y raises once, the first time
x is proposed true after the build; at seed 3 that is the fifth read of the
stream, and #:check #t finds the state sound at every one of
the 11 other reads.
(define (test/semantics/check/escaped-error _)
(let ((armed #f))
(define flaky
(do/monad
(← x (dist bern 0.1))
(← y
(dist normal
(lift (λ (xv)
(if xv
(if armed
(begin (set! armed #f) (error "flaky"))
5.0)
0.0))
x)
1.0))
(return (pair x y))))
(let ((s (mcmC§ flaky #:check #t #:seed 3)))
(set! armed #t)
(let1 (reads (let loop ((i 0) (acc '()))
(if (= i 12)
(reverse acc)
(loop (add1 i)
(cons (condition-case
(let1 (v (car§ s)) (set! s (cdr§ s)) v)
(e (exn)
((condition-property-accessor 'exn 'message) e)))
acc)))))
(⊦ equal? '("flaky") (filter string? reads))
(⊦ = 11 (count pair? reads))))))
((eta 0.001) (memory #(25165824 6214696 1048576)) (stdout "") (stderr ""))
test/semantics/invalid-inactive: pass
B5, plan C22: in extended mode an inactive node is updated, so the gamma of a
negative shape in the branch not taken takes part in every step. Its sampler
raises a parameter error, which rejects the initial trace; its density answers
-inf, so an update with x ≤ 0 rejects the step through Δ = -inf. Beta's density
raises the parameter error itself (D:330-339): in invalid-inactive/beta the update that raises it rejects the
step (plan C22's guarded update) instead of stopping the chain. The extended
target is then restricted to the states where every inactive node has
valid parameters: here every state with x ≤ 0 holds the inactive gamma x 1, so the chain stays where x > 0 and the result, the
gamma sample, is always positive, where the model's posterior has P(x > 0) = 0.5.
The extended semantics is opt-in for this reason, among others. In 'upstream mode, at seed 17, the chain stops with the parameter
error, raised when the inactive branch is built or updated with x ≤ 0 (at seeds
15 to 19 too; at seed 20 the chain runs, delaying the inactive updates, and gives
P(x > 0) = 0.3995); the beta model stops too.
The other side of C22: of the (exn hakaru10) errors only parameter errors reject. Any other (exn hakaru10) error of an inactive node propagates in extended mode, as upstream, at
construction (the build guard) and at an update (the guarded update) alike. An
error that is not an (exn hakaru10) one, such as car of '() in a lifted function, is one that
an inactive Dirac node defers (M:99), and in extended mode it rejects too, raised
by the node's own parameters or by forcing a deferred value (case B6). In invalid-inactive/branch the inner test of the else branch is 0, not a boolean, whenever c holds: with
p = 0.5 the initial trace at seed 17 has c true and the build raises branch; with p = 0.05 it starts with c false, and the first
update that proposes c true raises branch instead of
rejecting the step, which would keep the chain at c = #f. In 'upstream mode the build raises too, while the update, which
leaves the inactive branch alone, runs the chain through c = #t. The oracles
(exact enumeration, likelihood weighting and weighted samples) walk the graph of
one 'extended build from seed 17, retried only when it is rejected (plan §15.3,
C22), so they share that build's fate: with p = 0.5 they raise branch; with p = 0.05 the build completes, a walk that takes c
true skips the inactive inner if_, and enumeration gives P(c) = 0.05.
And of the nodes only the inactive ones reject: an active node's parameter error
propagates in 'extended mode as in 'upstream, at construction (a normal of standard deviation 0)
and at an update. In active-invalid the observation, always active, gets a beta whose first parameter is -1 when b
is false: the initial trace at seed 17 has b true, and the first step that
proposes b false raises parameter instead of rejecting.
(define invalid-inactive
(do/monad
(← x (dist normal 0.0 1.0))
(if_ (lift positive? x) (dist gamma x 1.0) (return 0.0))))
(define invalid-inactive/beta
(do/monad
(← x (dist normal 0.0 1.0))
(if_ (lift positive? x) (dist beta x 1.0) (return 0.0))))
(define (invalid-inactive/branch p)
(do/monad
(← c (dist bern p))
(← x (dist bern 0.5))
(if_ c
(return 3)
(if_ (lift (λ (cv xv) (if cv 0 xv)) c x) (return 1) (return 2)))
(return c)))
(define active-invalid
(do/monad
(← b (dist bern 0.9))
(dist (condition 0.5 beta) (lift (λ (bv) (if bv 1.0 -1.0)) b) 1.0)
(return b)))
(define (test/semantics/invalid-inactive _)
(let1 (xs (mcmC 2000 invalid-inactive #:semantics 'extended))
(⊦ = 2000 (length xs))
(⊦ every positive? xs))
(⊦ eq? 'parameter (hakaru10-kind (τ (mcmC 2000 invalid-inactive))))
(let1 (xs (mcmC 2000 invalid-inactive/beta #:semantics 'extended))
(⊦ = 2000 (length xs))
(⊦ every positive? xs))
(⊦ eq? 'parameter (hakaru10-kind (τ (mcmC 2000 invalid-inactive/beta))))
(⊦ eq?
'branch
(hakaru10-kind
(τ (mcmC 1 (invalid-inactive/branch 0.5) #:semantics 'extended))))
(⊦ eq?
'branch
(hakaru10-kind
(τ (mcmC 2000 (invalid-inactive/branch 0.05) #:semantics 'extended))))
(⊦ eq? 'branch (hakaru10-kind (τ (mcmC 1 (invalid-inactive/branch 0.5)))))
(let1 (xs (mcmC 2000 (invalid-inactive/branch 0.05)))
(⊦ = 2000 (length xs))
(⊦ any identity xs))
(⊦ equal?
'(branch branch branch)
(map (λ (oracle)
(hakaru10-kind (τ (oracle (invalid-inactive/branch 0.5)))))
(list exact-posterior
(cut likelihood-weighting 100 <>)
(cut weighted-samples 100 <>))))
(⊦ (equal-approx? 1e-12)
'((#f . 0.95) (#t . 0.05))
(exact-posterior (invalid-inactive/branch 0.05)))
(⊦ equal?
'(none none)
(map (λ (oracle)
(hakaru10-kind (τ (oracle 100 (invalid-inactive/branch 0.05)))))
(list likelihood-weighting weighted-samples)))
(for-each
(λ (semantics)
(⊦ equal?
(list semantics 'parameter 'parameter)
(list semantics
(hakaru10-kind
(τ (mcmC 10 (dist normal 0.0 0.0) #:semantics semantics)))
(hakaru10-kind
(τ (mcmC 2000 active-invalid #:semantics semantics))))))
'(extended upstream))
(⊦ = 1 (length (mcmC 1 active-invalid #:semantics 'extended))))
((eta 0.029) (memory #(25165824 6224216 1048576)) (stdout "") (stderr ""))
test/semantics/deferred: pass
B6: deferred Dirac values (M:99, M:120-129). Upstream never forces the value of a
Dirac node that nothing reads, so in guarded-car the else branch, inactive exactly when xs is empty, applies car to '() harmlessly. Here the value of an inactive diracN node
whose computation raises is a deferred value, which only a strict reader forces.
Exact enumeration gives 1/3 to each of a, b and none; 6000 samples of the chain land within .02 of it in both semantics, and #:check finds every state sound; the first five samples at seed
17 are a regression value.
(define guarded-car
(do/monad
(← k (dist uniformly '(0 1 2)))
(← xs (diracN (lift (λ (kv) (list-tail '(a b) kv)) k)))
(← empty (diracN (lift null? xs)))
(if_ empty (return 'none) (diracN (lift car xs)))))
In nested-guarded-car the car sits under a second if_, on c: when xs is empty and c holds, the inner if_ is
inactive and its exit copies the deferred value without forcing it, which an 'extended step, updating that inactive exit, must do too;
forcing it there would reject every step to (#t . none), whose probability is
1/6. The chains of 20000 samples land within .03 of the exact posterior in both
semantics, and #:check finds every state sound. The kernel
residual of the 'extended semantics (hakaru10-exact.scm, E10) is below
guarded-car/reversed, whose initial trace has the else branch active (a) where guarded-car's has it
inactive (none). The residual enumerates and loads every assignment of the
extended space, the else branch's car of '() included, which it must compute as lazily as the chain does, whichever
assignment the node was last computed in.
(define nested-guarded-car
(do/monad
(← k (dist uniformly '(0 1 2)))
(← xs (diracN (lift (λ (kv) (list-tail '(a b) kv)) k)))
(← c (dist bern 0.5))
(← r
(if_ (lift null? xs)
(return 'none)
(if_ c (diracN (lift car xs)) (return 'other))))
(return (pair c r))))
(define guarded-car/reversed
(do/monad
(← k (dist uniformly '(2 1 0)))
(← xs (diracN (lift (λ (kv) (list-tail '(a b) kv)) k)))
(← empty (diracN (lift null? xs)))
(if_ empty (return 'none) (diracN (lift car xs)))))
A handler that a model sets up inside a lifted function, around no model, is an
ordinary Scheme handler: the error is raised and caught inside the lifted
function, on the values of two choices, and exact enumeration of guarded-inside-lift gives 1/6 to each of its six outcomes.
(define guarded-inside-lift
(do/monad
(← b (dist uniformly '(0 1 2)))
(← c (dist bern 0.5))
(return
(lift (λ (bv cv)
(handle-exceptions
e
'caught
(if (and cv (= bv 0))
(error "guarded-inside-lift")
(list bv cv))))
b
c))))
Deviation 11, strict propagation: strict-reader's mu is the cdr of a pair whose car is the deferred h, so upstream computes mu =
1.0, while here mu is the deferred value itself and the normal, a strict reader,
raises car's error, a plain Scheme one, both with a constant
test ('()) and with a lifted one; with a non-empty xs the
branch is active and nothing is deferred. In 'extended mode the error rejects instead (C22): every build of (strict-reader '()) is rejected, so the chain is empty, and strict-reader/node never takes its then branch.
(define (strict-reader xs)
(do/monad
(← c (dist bern 0.5))
(if_ (lift null? xs)
(return c)
(do/monad
(← h (diracN (lift car xs)))
(← mu (diracN (lift cdr (pair h 1.0))))
(dist normal mu 1.0)))))
(define strict-reader/node
(do/monad
(← c (dist bern 0.5))
(let xs (lift (λ (cv) (if cv '() '(1))) c))
(if_ (lift null? xs)
(return 'empty)
(do/monad
(← h (diracN (lift car xs)))
(← mu (diracN (lift cdr (pair h 1.0))))
(dist normal mu 1.0)))))
A function applied to a deferred value in an inactive branch is not called, even
one that ignores it: in ignoring-reader mu is the deferred car of '() itself, so the normal raises
in 'upstream mode and rejects every build in 'extended mode, where upstream computes mu = 1.0. And an
active diracN computes its value at once: strict-active's then branch, always active, makes a Dirac node of the car of a node whose
value is '(), which raises car's error when it is made, in both semantics and under enumeration, where
upstream, which never reads that node, never forces it. Lifting too is lazy only
in an inactive branch: in strict-active/val the lift of car over the Val '() raises as it is applied.
(define ignoring-reader
(do/monad
(← c (dist bern 0.5))
(if_ #t
(return c)
(do/monad
(let d (lift car '()))
(← mu (diracN (lift (λ (x) 1.0) d)))
(dist normal mu 1.0)))))
(define strict-active
(do/monad
(← c (dist bern 0.5))
(← xs (diracN (lift (λ (cv) '()) c)))
(if_ #t (>> (diracN (lift car xs)) (return c)) (return c))))
(define strict-active/val
(do/monad
(← c (dist bern 0.5))
(if_ #t (>> (diracN (lift car '())) (return c)) (return c))))
Lazy lifting does not leak: a model run from inside an inactive branch's lifted
function, here an exact-posterior of a model that computes (lift car xs) of an empty xs after a bind, and discards it, starts without it and raises. With lazy
lifting leaked, (lift car xs) would be deferred and never
forced, and the posterior would be ((leaked . 1.)).
(define (test/semantics/deferred _)
(let1 (thirds (map (λ (v) (cons v (/ 1.0 3.0))) '(a b none)))
(⊦ (equal-approx? 1e-12) thirds (exact-posterior guarded-car))
(for-each
(λ (semantics)
(⊦ (equal-approx? 0.02)
thirds
(proportions (mcmC 6000 guarded-car #:semantics semantics)))
(⊦ equal?
(mcmC 300 guarded-car #:semantics semantics)
(mcmC 300 guarded-car #:semantics semantics #:check #t)))
'(upstream extended)))
(⊦ equal? '(none none b none a) (mcmC 5 guarded-car))
(let1 (exact (exact-posterior nested-guarded-car))
(⊦ (equal-approx? 1e-12)
(map cons
'((#f . none) (#f . other) (#t . a) (#t . b) (#t . none))
(map (λ (w) (/ w 6.0)) '(1.0 2.0 1.0 1.0 1.0)))
exact)
(for-each
(λ (semantics)
(⊦ (equal-approx? 0.03)
(list semantics exact)
(list semantics
(proportions
(mcmC 20000
nested-guarded-car
#:semantics
semantics))))
(⊦ equal?
(mcmC 300 nested-guarded-car #:semantics semantics)
(mcmC 300
nested-guarded-car
#:semantics
semantics
#:check
#t)))
'(upstream extended)))
(⊦ equal?
'()
(filter-map
(Λ ((,name unquote m))
(let1 (residual (kernel-residual m))
(and (not (< residual 1e-12)) (cons name residual))))
`((guarded-car unquote guarded-car)
(guarded-car/reversed unquote guarded-car/reversed)
(nested-guarded-car unquote nested-guarded-car))))
(⊦ equal?
'(a none)
(list (car (mcmC 1 guarded-car/reversed)) (car (mcmC 1 guarded-car))))
(let1 (sixths
(map (λ (v) (cons v (/ 1.0 6.0)))
'(caught (0 #f) (1 #f) (1 #t) (2 #f) (2 #t))))
(⊦ (equal-approx? 1e-12) sixths (exact-posterior guarded-inside-lift)))
(⊦ equal?
'(exn none exn)
(map (λ (m) (raised (τ (mcmC 3 m))))
(list (strict-reader '()) (strict-reader '(5)) strict-reader/node)))
(⊦ equal? '() (mcmC 3 (strict-reader '()) #:semantics 'extended))
(⊦ every flonum? (mcmC 300 strict-reader/node #:semantics 'extended))
(for-each
(λ (m)
(⊦ equal?
'(exn exn exn)
(list (raised (τ (mcmC 3 m)))
(raised (τ (mcmC 3 m #:semantics 'extended)))
(raised (τ (exact-posterior m))))))
(list strict-active strict-active/val))
(⊦ equal?
'(exn ())
(list (raised (τ (mcmC 3 ignoring-reader)))
(mcmC 3 ignoring-reader #:semantics 'extended)))
(let1 (inner #f)
(mcmC 1
(do/monad
(← c (dist bern 0.5))
(if_ #t
(return c)
(diracN
(lift (λ (k)
(set! inner
(raised
(τ (exact-posterior
(do/monad
(← xs (return '()))
(let _ (lift car xs))
(return 'leaked))))))
k)
1)))))
(⊦ eq? 'exn inner)))
((eta 0.339) (memory #(25165824 6222072 1048576)) (stdout "") (stderr ""))
test/semantics/incremental: pass
B8: incremental recomputation (P §4.2 l.765-800). Every node's update is wrapped
to record, step by step, which updates ran and in which order. In pdep resampling x updates y (whose LL changes, not its value) and
the Dirac result, never z; resampling y updates z and the result; resampling z
the result alone:
❝
Suppose MH proposes to resample x (and only x). Although the y sample keeps its old value, its distribution parameters, x specifically, changed. Therefore, the LL of the old y sample in the new distribution has to be recomputed. The variable z is not affected by the resampling proposal; also, the parameters of its distribution remain the same. Therefore, no update to the z node is needed.
(define pdep
(do/monad
(← x (dist normal 0.0 1.0))
(← y (dist normal x 1.0))
(← z (dist normal y 1.0))
(return (sexp+ x y z))))
(define (update-traces n model . keys)
(let ((updated '()) (steps '()))
(define (spy! ch)
(chain-fold
(λ (node _)
(let1 (update (node-update node))
(when update
(node-update-set!
node
(λ (n) (push! (node-addr n) updated) (update n))))))
(void)
ch))
(define (collect! ch accepted? delta)
(push! (reverse updated) steps)
(set! updated '()))
(let1 (next (apply mcmc-generator
n
(model->trace model 'update-traces)
#:build-hook
spy!
#:step-hook
collect!
keys))
(do ((i 0 (add1 i))) ((= i n)) (next)))
(reverse steps)))
The propagation-order note of Metropolis.hs (M:319-397): in upp1 and upp2, d depends on a, b and c,
and resampling a updates b, c and d, each once, in this order, d last; resampling
d updates nothing. Both models also run under #:check #t, whose reference evaluator recomputes every node after each step, in both
semantics.
(define upp1
(do/monad
(← a (dist uniform 0.0 1.0))
(← b (diracN (sexp+ a 2.0)))
(← c (diracN (sexp+ a 3.0)))
(← d (dist uniform (sexp+ a b) c))
(return d)))
(define upp2
(do/monad
(← a (dist uniform 0.0 1.0))
(← b (diracN (sexp+ a 2.0)))
(← c (diracN (sexp+ b 3.0)))
(← d (dist uniform (sexp+ a b) c))
(return d)))
Upstream runs upp1 as M:336-341 writes it, on the raw trace API, where d depends on c and on an extra Dirac node ab = a + b: resampling a updates b, c, ab and d, each once, in this order.
(define upp1/raw
(do/monad
(← a (with-node-none (uniform 0.0 1.0)))
(← b (with-node a (λ (av) (dirac (+ av 2.0)))))
(← c (with-node a (λ (av) (dirac (+ av 3.0)))))
(← ab
(with-node-tree
(vector a b)
(λ (vs) (dirac (+ (vector-ref vs 0) (vector-ref vs 1))))))
(with-node-tree
(vector ab c)
(λ (vs) (uniform (vector-ref vs 0) (vector-ref vs 1))))))
(define (test/semantics/incremental _)
(for-each
(λ (semantics)
(⊦ equal?
(list semantics '((2 4) (3 4) (4)))
(list semantics
(map car
(hist (update-traces 1000 pdep #:semantics semantics)))))
(for-each
(λ (m)
(⊦ equal?
(list semantics '(() (2 3 4)))
(list semantics
(map car
(hist (update-traces 1000 m #:semantics semantics)))))
(⊦ = 300 (length (mcmC 300 m #:semantics semantics #:check #t))))
(list upp1 upp2))
(⊦ equal?
(list semantics '(() (2 3 4 5)))
(list semantics
(map car
(hist (update-traces
1000
upp1/raw
#:semantics
semantics)))))
(⊦ = 300 (length (mcmC 300 upp1/raw #:semantics semantics #:check #t))))
'(upstream extended)))
((eta 0.056) (memory #(25165824 6234936 1048576)) (stdout "") (stderr ""))
(aux hakaru10): posterior estimates within statistical tolerances
Statistics
Posterior estimates of the examples, checked against exact or analytic values.
Each tolerance is at least about 4 standard deviations of the estimate across the
seeds 1 to 40, measured on this port, and more where a looser plan value is kept (the chains are autocorrelated, so the iid
standard error underestimates the spread two- to fourfold); the averages over those
seeds agree with the exact values, with no sign of bias. The suite runs at seed 17
by default; HAKARU10_SEEDS=17,1,2,3,4 (make test-hakaru10-long) runs every case over the listed seeds,
a failure reporting its seed. Seeds this run: (17).
test/statistics/pbern: pass
S1: pbern under the default kernel, an independence proposal (D:29-32).
(define (test/statistics/pbern _)
(for-each
(λ (seed)
(⊦≈ seed 0.025 0.4 (frequency identity (mcmC 10000 pbern #:seed seed))))
seeds))
((eta 0.012) (memory #(25165824 6437536 1048576)) (stdout "") (stderr ""))
test/statistics/pnorm: pass
S2: pnorm, N(10, 0.5²) (E:14).
(define (test/statistics/pnorm _)
(for-each
(λ (seed)
(⊦≈ seed 0.03 '(10.0 . 0.25) (momenta (mcmC 10000 pnorm #:seed seed))))
seeds))
((eta 0.02) (memory #(25165824 6347008 1048576)) (stdout "") (stderr ""))
test/statistics/ph1: pass
S3: ph1, x ~ N(0, 1) and N(x, 3²): mean 0, variance 1 + 9 (E:65-67).
(define ph1 (do/monad (← x (dist normal 0.0 1.0)) (dist normal x 3.0)))
(define (test/statistics/ph1 _)
(for-each
(λ (seed)
(let1 (m (momenta (mcmC 20000 ph1 #:seed seed)))
(⊦≈ seed 0.3 0.0 (car m))
(⊦≈ seed 1.5 10.0 (cdr m))))
seeds))
((eta 0.056) (memory #(25165824 6295680 1048576)) (stdout "") (stderr ""))
test/statistics/phier3: pass
S4: phier with 3 steps: mean 0, variance 1 + 3 × 9 (BE:14-15).
(define (test/statistics/phier3 _)
(for-each
(λ (seed)
(let1 (m (momenta (mcmC 20000 (phierN 3) #:seed seed)))
(⊦≈ seed 1.5 0.0 (car m))
(⊦≈ seed 8.0 28.0 (cdr m))))
seeds))
((eta 0.065) (memory #(25165824 6297192 1048576)) (stdout "") (stderr ""))
test/statistics/phier-kl: pass
S5: phier, Hur et al.'s Fig. 3, has the distribution N(0, √91) (P l.313-318), on which STAN goes wrong. The KL divergence of 40000 samples, tabulated in bins of 0.5, from the exact bin masses is below 0.25 (over the seeds 1 to 40: mean 0.080, standard deviation 0.038, at most 0.207; at 10000 samples the mean is 0.29 but the spread reaches 0.98, too wide for a test). Upstream's estimates, sample against sample (E:76-85):
phier_kl n =
kl (tabulate 0.5 (mcmC n phier))
(tabulate 0.5 (mcmC n (dist normal 0 (sqrt <$> 91))))
_ = phier_kl 10
{-
*Examples1> phier_kl 1000
0.7862745014080637
*Examples1> phier_kl 10000
0.23618257021796316
*Examples1> phier_kl 100000
3.441636543766045e-2
*Examples1> phier_kl 1000000
2.1573977909591603e-3
-}((kl (tabulate 0.5 (mcmC 40000 (phierN 10)))
(normal-bins 0.5 (sqrt 91.0) bins))
0.0770140470263418)
(define (test/statistics/phier-kl _)
(for-each
(λ (seed)
(let1 (p (tabulate 0.5 (mcmC 40000 (phierN 10) #:seed seed)))
(⊦< seed (kl p (normal-bins 0.5 (sqrt 91.0) (map car p))) 0.25)))
seeds))
((eta 0.297) (memory #(25165824 6223368 1048576)) (stdout "") (stderr ""))
test/statistics/ph2: pass
S6: ph2 and ph21 under the default kernel: 0.2 × 0.1 + 0.8 × 0.7 = 0.58 (E:91) and the prior 0.2 of x, since y is not observed.
(define (test/statistics/ph2 _)
(for-each
(λ (seed)
(⊦≈ seed 0.05 0.58 (frequency identity (mcmC 10000 ph2 #:seed seed)))
(⊦≈ seed 0.05 0.2 (frequency identity (mcmC 10000 ph21 #:seed seed))))
seeds))
((eta 0.038) (memory #(25165824 6446432 1048576)) (stdout "") (stderr ""))
test/statistics/pcond: pass
S7: the branching models pcond1-4 (E:104-282): the exact posteriors, pcond4's given upstream as "Exact: [(0.166666666666666657, V 21); (0.166666666666666657, V 20); (0.66666666666666663, V 1)]" (E:280-282). The tolerances are per model, at 4 to 5 standard deviations of P(10), P(1), P(1) and P(1) across the seeds 1 to 40 (0.0066, 0.0079, 0.0121 and 0.0097): .03 (plan §9.4), .04, .05 and .05.
(define (test/statistics/pcond _)
(for-each
(λ (seed)
(let1 (run (λ (m) (proportions (mcmC 10000 m #:seed seed))))
(⊦≈ seed
0.03
'((10 . 0.25) (11 . 0.25) (20 . 0.25) (21 . 0.25))
(run pcond1))
(⊦≈ seed 0.04 '((1 . 0.5) (20 . 0.25) (21 . 0.25)) (run pcond2))
(⊦≈ seed 0.05 '((1 . 0.5) (20 . 0.25) (21 . 0.25)) (run pcond3))
(⊦≈ seed
0.05
`((1 unquote (/ 2.0 3.0))
(20 unquote (/ 1.0 6.0))
(21 unquote (/ 1.0 6.0)))
(run pcond4))))
seeds))
((eta 0.132) (memory #(25165824 6449016 1048576)) (stdout "") (stderr ""))
test/statistics/mixng: pass
S8: the mixture of N(10, 2²) and gamma(3, 1/3), whose means are 10 and 1, in equal parts: mean 5.5, half the samples above 5 (E:139-151; P §2.2).
(define (test/statistics/mixng _)
(for-each
(λ (seed)
(for-each
(λ (m)
(let1 (xs (mcmC 10000 m #:seed seed))
(⊦≈ seed 0.4 5.5 (mean xs))
(⊦≈ seed 0.05 0.5 (frequency (λ (x) (> x 5.0)) xs))))
(list mixng mixng′)))
seeds))
((eta 0.069) (memory #(25165824 6526144 1048576)) (stdout "") (stderr ""))
test/statistics/biased-coin: pass
S9: the posteriors beta(2, 2), of mean 0.5 and variance 0.05 (E:206), and beta(1, 3), of mean 0.25 and variance 0.0375 (E:216), after 1000 samples of burn-in. The "incremental" record of the commented-out test_multiple_conditions13 below is not reproduced: at seed 17 this chain gives (0.251523121569678 . 0.0379368207086475); see the findings of the language suite.
{-
test_multiple_conditions13 =
momenta . drop 1000 $
mcmC 10000 (prog_multiple_conditions False False)
-- beta 1 3: 0.25, sigma2 = 3.75e-2
-- original Metropolis
-- (0.25144728141545775,3.782788408489756e-2)
-- incremental
-- (0.2445212550806874,3.651432172401049e-2)
-}(define (test/statistics/biased-coin _)
(for-each
(λ (seed)
(let ((tf (momenta
(drop (mcmC 10000 (biased-coin #t #f) #:seed seed) 1000)))
(ff (momenta
(drop (mcmC 10000 (biased-coin #f #f) #:seed seed) 1000))))
(⊦≈ seed 0.02 0.5 (car tf))
(⊦≈ seed 0.008 0.05 (cdr tf))
(⊦≈ seed 0.02 0.25 (car ff))
(⊦≈ seed 0.008 0.0375 (cdr ff))))
seeds))
((eta 0.104) (memory #(25165824 6377440 1048576)) (stdout "") (stderr ""))
test/statistics/nice-prog-mult-conditions: pass
S10: the biased coin of Syntax.hs, ending in diracN b;
upstream's sums of 1000 samples, 499.8 and 257.29 (S:297-301), come from an older
revision of the engine: at seed 17 they are 502.706370138924 and
252.485601387305 here, and are not asserted (see the findings of the language
suite).
(define (nice-prog-mult-conditions c1 c2)
(do/monad
(← b (dist beta 1.0 1.0))
(dist (condition c1 bern) b)
(dist (condition c2 bern) b)
(diracN b)))
nice_prog_mult_conditions c1 c2 = do
b <- dist beta 1 1
dist (c1 `condition` bern) b
dist (c2 `condition` bern) b
diracN b
niceexmcr11 = sum $ mcmC 1000 (nice_prog_mult_conditions True False)
-- 499.799988743311
niceexmcr12 = sum $ mcmC 1000 (nice_prog_mult_conditions False False)
-- 257.2928205920028(define (test/statistics/nice-prog-mult-conditions _)
(for-each
(λ (seed)
(let1 (average
(λ (c1 c2)
(/ (fold +
0.0
(mcmC 1000
(nice-prog-mult-conditions c1 c2)
#:seed
seed))
1000.0)))
(⊦≈ seed 0.05 0.5 (average #t #f))
(⊦≈ seed 0.05 0.25 (average #f #f))))
seeds))
((eta 0.01) (memory #(25165824 6244456 1048576)) (stdout "") (stderr ""))
test/statistics/niceex1: pass
S11: P(e and d) = E[c] × 0.5 = 0.25 (S:282-289). Upstream's 7 samples are "All True", presumably from an older revision of the engine: at seed 17 they are (#t #t #t #t #f #f #f) here, and are not asserted (see the findings of the language suite).
(define niceex1
(do/monad
(← c (dist uniform 0.0 1.0))
(← d (dist bern 0.5))
(← e (dist bern c))
(diracN (lift (λ (ev dv) (and ev dv)) e d))))
niceex1 = do
c <- dist uniform 0 1
d <- dist bern 0.5
e <- dist bern c
diracN ((&&) <$> e <*> d)
niceex1r = mcmC 7 niceex1
-- All True(define (test/statistics/niceex1 _)
(for-each
(λ (seed)
(⊦≈ seed
0.05
0.25
(frequency identity (mcmC 10000 niceex1 #:seed seed))))
seeds))
((eta 0.025) (memory #(25165824 6454392 1048576)) (stdout "") (stderr ""))
test/statistics/impossible-branches: pass
S12: the else branches observe values outside the uniform's support (log
likelihood -inf): an initial trace taking them is built again (M:882-891), and no
step can switch to them (rejectedLL, M:853-854, M:927). Exact enumeration
prunes the paths of weight 0, so the three posteriors are exactly ((#t . 1.)).
(define (prog-mixture1 cv)
(do/monad
(← c (dist bern 0.5))
(if_ c
(dist (condition cv normal) 1.0 1.0)
(dist (condition cv uniform) 0.0 3.0))
(return c)))
(define tac10
(do/monad
(← c (dist bern 0.5))
(if_ c
(dist (condition 0.0 normal) 0.0 1.0)
(dist (condition 0.0 uniform) 10.0 20.0))
(return c)))
(define tac11
(do/monad
(← c (dist bern 0.5))
(if_ c
(dist (condition 0.0 normal) 0.0 1.0)
(do/monad (← d (diracN 10.0)) (dist (condition 0.0 uniform) d 20.0)))
(return c)))
prog_mixture1 cv = do
c <- dist bern 0.5
if_ c (dist (cv `condition` normal) 1 1)
(dist (cv `condition` uniform) 0 3)
return c
prog_mixture1_run = mcmC 20 (prog_mixture1 (-2))
-- all truetac10 = do
c <- dist bern 0.5
if_ c
-- then
(dist (0 `condition` normal) 0 1)
-- else
(dist (0 `condition` uniform) 10 20)
return c
tac10_run = length . filter id $ mcmC 100 tac10
-- 100 -- all True
tac11 = do
c <- dist bern 0.5
if_ c
-- then
(dist (0 `condition` normal) 0 1)
-- else
(do
d <- diracN 10
dist (0 `condition` uniform) d 20)
return c
tac11_run = length . filter id $ mcmC 100 tac11
-- 100 -- all True(define (test/statistics/impossible-branches _)
(for-each
(λ (seed)
(⊦ equal?
(list seed (make-list 100 #t))
(list seed (mcmC 100 (prog-mixture1 -2.0) #:seed seed)))
(⊦ equal?
(list seed (make-list 100 #t))
(list seed (mcmC 100 tac10 #:seed seed)))
(⊦ equal?
(list seed (make-list 100 #t))
(list seed (mcmC 100 tac11 #:seed seed))))
seeds)
(⊦ equal?
(make-list 3 '((#t . 1.0)))
(map exact-posterior (list (prog-mixture1 -2.0) tac10 tac11))))
((eta 0.002) (memory #(25165824 6218208 1048576)) (stdout "") (stderr ""))
test/statistics/tac1: pass
S13: tac1's branch does not influence c, so P(c) = 0.5 (S:340-348).
(define (test/statistics/tac1 _)
(for-each
(λ (seed)
(⊦≈ seed 0.05 0.5 (frequency identity (mcmC 20000 tac1 #:seed seed))))
seeds))
((eta 0.054) (memory #(25165824 6697184 1048576)) (stdout "") (stderr ""))
test/statistics/exif: pass
S14: exif1 is 2d with d ~ N(1, 1) when c holds, else 5 + N(20, 1): mean 13.5, half above 10; exif2 is 10 or 20 a quarter of the time each, else the sum of two uniforms (S:314-337).
(define exif1
(do/monad
(← c (dist bern 0.5))
(← d
(if_ c
(dist normal (lift (λ (cv) (if cv 1.0 0.0)) c) 1.0)
(diracN 5.0)))
(← e (if_ (lift not c) (dist normal 20.0 1.0) (diracN d)))
(diracN (sexp+ d e))))
(define exif2
(let ()
(define (cl1 x) (do/monad (← d (dist bern 0.5)) (if_ d cl3 (diracN x))))
(define cl3
(do/monad
(← a (dist uniform 0.0 1.0))
(← b (dist uniform 0.0 1.0))
(diracN (sexp+ a b))))
(do/monad (← c (dist bern 0.5)) (if_ c (cl1 10.0) (cl1 20.0)))))
exif1 = do
c <- dist bern 0.5
d <- if_ c (dist normal ((fromIntegral . fromEnum) <$> c) 1)
(diracN 5)
e <- if_ (not <$> c) (dist normal 20 1) (diracN d)
diracN (d + e)
exif1r = mcmC 7 exif1
exif2 = do
c <- dist bern 0.5
if_ c (cl1 10) (cl1 20)
where
cl1 x = do
d <- dist bern 0.5
if_ d cl3 (diracN x)
cl3 = do
a <- dist uniform 0 1
b <- dist uniform 0 1
diracN (a + b)
exif2r = mcmC 17 exif2(define (test/statistics/exif _)
(for-each
(λ (seed)
(let1 (xs (mcmC 20000 exif1 #:seed seed))
(⊦≈ seed 0.8 13.5 (mean xs))
(⊦≈ seed 0.05 0.5 (frequency (λ (x) (> x 10.0)) xs)))
(let1 (xs (mcmC 20000 exif2 #:seed seed))
(⊦≈ seed 0.04 0.25 (frequency (λ (x) (eqv? x 10.0)) xs))
(⊦≈ seed 0.04 0.25 (frequency (λ (x) (eqv? x 20.0)) xs))
(⊦≈ seed 0.04 0.5 (frequency (λ (x) (<= x 2.0)) xs))))
seeds))
((eta 0.199) (memory #(25165824 6772096 1048576)) (stdout "") (stderr ""))
test/statistics/pcorr: pass
S15: pcorr returning (pair x y): the observation makes x and
y equal with probability 0.9, and y stays fair (E:223-238).
(define (test/statistics/pcorr _)
(for-each
(λ (seed)
(let1 (xys (mcmC 20000 (pcorr* 0.1 pair) #:seed seed))
(⊦≈ seed 0.03 0.9 (frequency (Λ ((,x unquote y)) (eq? x y)) xys))
(⊦≈ seed 0.1 0.5 (frequency cdr xys))))
seeds))
((eta 0.116) (memory #(25165824 6984120 1048576)) (stdout "") (stderr ""))
test/statistics/condhier: pass
S16: the conditioned hierarchical model (E:240-251): x ~ N(0, 1) observed as 1 through nine steps of variance 9, so the posterior of x is N(1/82, 81/82).
(define (phierN′ n start obs)
(fold/monad
(λ (x k) (k x))
start
(append
(make-list (sub1 n) (λ (x) (dist normal x 3.0)))
(list (λ (x) (dist (condition obs normal) x 3.0))))))
(define condhier
(do/monad (← x (dist normal 0.0 1.0)) (phierN′ 9 x 1.0) (return x)))
phierN' :: Int -> SExp Double -> Double -> Model Double
phierN' n start obs =
foldl (>>=) (pure start) $
replicate (n-1) (\x -> dist normal x 3) ++
[(\x -> dist (obs `condition` normal) x 3)]
condhier = do
x <- dist normal 0 1
phierN' 9 x 1
return x(define (test/statistics/condhier _)
(for-each
(λ (seed)
(let1 (m (momenta (mcmC 20000 condhier #:seed seed)))
(⊦≈ seed 0.2 (/ 1.0 82.0) (car m))
(⊦≈ seed 0.25 (/ 81.0 82.0) (cdr m))))
seeds))
((eta 0.073) (memory #(25165824 6261240 1048576)) (stdout "") (stderr ""))
test/statistics/grass-girl: pass
S17: grass against "0.4684714427203697 -- theoretical" (E:322) and girl
against its exact posterior 0.6644295302013423 (hakaru10-exact.scm). The
grass of E:284-296, commented out upstream, conditions in the leaves of nested
branches; its posterior P(rain | wet) is 0.3 × 0.9 / (0.3 × 0.9 + 0.7 × (0.5 ×
0.8 + 0.5 × 0.1)) = 6/13 = 0.4615, which exact enumeration gives and the chain
approaches (standard deviation 0.0084 across the seeds 1 to 40). Without the
observation (E:302, commented out upstream), Fig. 1 gives "the probability of
the grass being wet is 60.6%" (P l.60-62): grass/wet returns the wet node, whose exact probability is 0.3 × 0.5 × 0.982 + 0.3 × 0.5
× 0.91 + 0.7 × 0.5 × 0.82 + 0.7 × 0.5 × 0.1 = 0.6058. A step redraws one of the
three nodes, so the chain of the wet node is strongly autocorrelated: 40000
samples are within 0.05 (0.5935 at seed 17, from 0.5928 to 0.6471 over the seeds
1 to 10).
(define grass/if
(do/monad
(← rain (dist bern 0.3))
(← sprinkler (dist bern 0.5))
(let obs (λ (p) (dist (condition #t bern) p)))
(if_ rain (obs 0.9) (if_ sprinkler (obs 0.8) (obs 0.1)))
(return rain)))
(define grass/wet
(do/monad
(← rain (dist bern 0.3))
(← sprinkler (dist bern 0.5))
(dist (nor 0.9 0.8 0.1) rain sprinkler)))
{-
grass = do
rain <- dist bern 0.3
sprinkler <- dist bern 0.5
let obs = dist (True `condition` bern)
if_ rain
(obs 0.9)
(if_ sprinkler
(obs 0.8)
(obs 0.1))
return rain
-}(define (test/statistics/grass-girl _)
(for-each
(λ (seed)
(⊦≈ seed
0.04
0.4684714
(frequency identity (mcmC 20000 grass #:seed seed)))
(⊦≈ seed
0.05
0.6644295
(frequency identity (mcmC 10000 girl #:seed seed)))
(⊦≈ seed
0.04
(/ 6.0 13.0)
(frequency identity (mcmC 20000 grass/if #:seed seed)))
(⊦≈ seed
0.05
0.6058
(frequency identity (mcmC 40000 grass/wet #:seed seed))))
seeds)
(⊦ (equal-approx? 1e-12)
`((#f unquote (/ 7.0 13.0)) (#t unquote (/ 6.0 13.0)))
(exact-posterior grass/if))
(⊦ (equal-approx? 1e-12)
'((#f . 0.3942) (#t . 0.6058))
(exact-posterior grass/wet)))
((eta 0.276) (memory #(25165824 6224024 1048576)) (stdout "") (stderr ""))
test/statistics/mhex: pass
S18: the example of P §4 (l.677-720) explaining MH; its mean is 0.345731 by numerical integration.
(define mhex
(do/monad
(← x (dist normal 0.0 1.0))
(← y (dist normal x 2.0))
(if_ (lift (λ (xv) (> xv 0.5)) x)
(return x)
(do/monad (← z (dist beta 1.0 1.0)) (return (sexp+ y z))))))
mhex = do
x <- dist normal 0 1
y <- dist normal x 2
if_ ((>0.5) <$> x)
(return x)
(do {z <- dist beta 1 1; return (y + z)})(define (test/statistics/mhex _)
(for-each
(λ (seed) (⊦≈ seed 0.1 0.345731 (mean (mcmC 20000 mhex #:seed seed))))
seeds))
((eta 0.093) (memory #(25165824 6917160 1048576)) (stdout "") (stderr ""))
test/statistics/post-bias: pass
S19: post_bias with 20 samples of burn-in: the else branch's observation, at 99 standard deviations, makes c = #f a trap the chain leaves at its first accepted proposal and never enters again (E:256-263).
(define (test/statistics/post-bias _)
(for-each
(λ (seed)
(⊦ equal?
(list seed (make-list 80 #t))
(list seed (drop (mcmC 100 (post-bias 1.0) #:seed seed) 20))))
seeds))
((eta 0.001) (memory #(25165824 6230600 1048576)) (stdout "") (stderr ""))
test/statistics/if/shared-inactive: pass
S20, deviation 1: d belongs to the rest of the model and is the then branch's result; when c is false, resampling d enqueues the exit through the inactive result (M:673), yet neither the entry nor the active result changed, and upstream fails:
-- the parent nodes must have been updated
when (tstamp (Mem.dref entry) <= tstamp self &&
tstamp nd <= tstamp self) $
fail $ unwords ["the parents of the exit node", show (Mem.addr ns),
"namely", show [Mem.addr entry, Mem.addr ndr],
"must have been updated"](Metropolis.hs:787-792.) Here the exit keeps its state, and the chain samples the mixture of N(0, 1) and N(5, 1), half below 2.5.
(define shared-inactive
(do/monad
(← c (dist bern 0.5))
(← d (dist normal 0.0 1.0))
(if_ c (return d) (dist normal 5.0 1.0))))
(define (test/statistics/if/shared-inactive _)
(for-each
(λ (seed)
(⊦≈ seed
0.06
0.5
(frequency
(λ (x) (< x 2.5))
(mcmC 10000 shared-inactive #:seed seed))))
seeds))
((eta 0.029) (memory #(25165824 6539568 1048576)) (stdout "") (stderr ""))
(aux hakaru10 exact): exact posteriors of finite models
Exact oracles
Hakaru10 infers by Markov chain Monte Carlo, so its answers are estimates. To
test them we need exact answers, and oracles that share as little code as
possible with the engine they check. This page is about the oracles of (aux hakaru10 exact): exact enumeration, a reference
evaluator, and the exact kernel of the 'extended semantics. None of them is in
upstream: they are new in the port.
A model is a value of the State monad that threads the chain
(Syntax.hs:82), written with do/monad: dist (an observation is the dist of a conditioned distribution), diracN and if_ are models, and running
the model creates upstream's trace graph, node by node in bind order. The
oracles do not run a model in a semantics of their own: they build it once, in
'extended mode, and walk the graph of that build (plan §6.10, C37). One build
is the whole graph, because SExp is applicative, not a monad (Syntax.hs:118),
and if_ builds both of its branches
(Metropolis.hs:650-661). Exact enumeration visits the nodes in address order,
which is their creation order, and builds a lazy search tree in the format of Embedded probabilistic domain-specific language HANSEI:
type 'a vc = V of 'a | C of (unit -> 'a pV) and 'a pV = (prob * 'a vc) listin Scheme the list (((V v) p) ...) and (((C thunk) p) ...). A choice becomes one C entry per support point (x . p) with p > 0, whose thunk continues the walk with x. An observation becomes one entry weighted by its
likelihood, or no entry at all when that likelihood is 0 or its LL is not
finite (NaN, +inf; E6). The nodes of a
branch that the values on the path leave inactive are skipped. A thunk may be
forced in any order, so it first restores the node states of its own path. probcc-explore and probcc-normalize of (aux hansei) then explore and normalize the tree. Only
Hansei's data-level tools are used: no oracle uses delimited control.
Enumeration and forward sampling share the graph and the node functions with
the engine, but none of its incremental machinery: propagation, the update
queue, the switch of branches, the delayed and pending updates, the journal
and the acceptance ratio. So an exact posterior against an MH chain tests the
kernel and the incremental update. The build checks the static discipline as
it goes, such as observations external to the model or no node escaping its
branch (H5). Continuous observations weigh by their density, which is right
for posteriors over discrete choices. A continuous choice on a path the enumeration takes is a support error, and so are more paths than #:max-paths.
A model runs several times: once per oracle call, and once more for the
rebuild of construction-check. So a Scheme effect that must happen once per run goes inside a bind (plan
§15, C35). Pre-allocation with replicate/monad (H8), folds and traversals are all safe. One build stands for every build
only if the model does not branch in plain Scheme on a node or on a value drawn
while it is built. Nothing enforces that, and nothing catches a model that
breaks it: the rebuild of construction-check (H2) draws the same values from a copy of the same source, so it takes the
same branch. The rebuild catches only a construction that depends on state
outliving a run, such as a counter (hakaru10.scm, case B4′).
The kernel residual (E10) checks the MH kernel itself, not one of
its chains. In 'extended mode the inactive resampleable nodes are auxiliary
variables with their prior. Over every assignment
So, when it comes to computing the acceptance ratio for a program
with branches, we take
p4 = do
c <- bernN (Val 0.5)
if_ c (do e1 e2 ...)
(do e3 e4 e5 ...)
to be
p4' = do
c <- bernN (Val 0.5)
if_ c e_then e_else
That is, we merge (at least for the purpose of presentation) e1, e2 ...
into one node e_then, whose ll is the sum of ll of constituents. Ditto
for e_else.
When we switch the branches, we remove the ll of one branch from
llcum and add ll of the other branch. In our terms, we remove the ll
of the deactivated nodes and add ll of the activated nodes.
This algorithm is resilient with respect to adding replacing a node e
with e >>= diracN -- the desired behavior.In 'upstream mode the argument does not hold for resampleable nodes whose
parameters change while they are inactive, as pswitch shows (E8; the main suite's hakaru10-semantics). 'extended mode is the fix,
and E10 shows that it leaves the target invariant.
(module
(aux hakaru10 exact)
*
(import
scheme
(chicken base)
(only (chicken memory representation) record-instance? record->vector)
(only srfi-1 filter-map count fold iota any)
srfi-69
(aux base)
(only (aux hansei) probcc-τ probcc-unit probcc-explore probcc-normalize)
(only (aux hakaru10 random) make-stdgen-source rng-source-copy)
(aux hakaru10 distribution)
(only (aux hakaru10 util) histogram-sort)
(aux hakaru10 engine))
(define (ll-close? a b #!optional (scale (abs a)))
(or (= a b)
(and (nan? a) (nan? b))
(and (finite? a)
(finite? b)
(<= (abs (- a b)) (* 1e-09 (max 1.0 scale))))))
(define (same-value? a b)
(let1 (budget 100000)
(define (spent?) (set! budget (sub1 budget)) (negative? budget))
(let walk ((a a) (b b))
(cond ((eq? a b) #t)
((and (flonum? a) (flonum? b))
(or (eqv? a b) (and (nan? a) (nan? b))))
((and (procedure? a) (procedure? b)) #t)
((and (promise? a) (promise? b)) #t)
((and (pair? a) (pair? b))
(or (spent?)
(and (walk (car a) (car b)) (walk (cdr a) (cdr b)))))
((and (vector? a) (vector? b))
(and (= (vector-length a) (vector-length b))
(let loop ((i 0))
(or (= i (vector-length a))
(spent?)
(and (walk (vector-ref a i) (vector-ref b i))
(loop (add1 i)))))))
((or (deferred? a) (deferred? b)) (equal? a b))
((and (record-instance? a) (record-instance? b))
(walk (record->vector a) (record->vector b)))
(else (equal? a b))))))
(define (choice-support dk who)
(or (distribution-support dk)
(hakaru10-error
'support
who
"a random choice without a finite support"
dk)))
(define (build model src semantics who)
(build-chain (model->trace model who) src semantics))
(define (static-graph model who)
(let1 (src (make-stdgen-source 17))
(let retry ((builds 1000))
(if (zero? builds)
(hakaru10-error
'construction
who
"no complete build of the model in 1000 tries"
model)
(receive
(ch result)
(build model src 'extended who)
(if result (values ch result) (retry (sub1 builds))))))))
(define (walk-active! n)
(let1 (k (scope-inactivity n)) (node-inactive-set! n k) (zero? k)))
(define (branch-starts ch)
(let1 (starts (make-vector (add1 (chain-count ch)) '()))
(chain-fold
(λ (n _)
(when (eq? (node-role n) 'exit)
(let1 (b (node-branch n))
(for-each
(Λ ((,lo unquote hi) ,polarity)
(when (< lo hi)
(vector-set!
starts
(add1 lo)
(cons (list hi (branch-entry b) polarity)
(vector-ref starts (add1 lo))))))
(list (branch-then-range b) (branch-else-range b))
'(#t #f)))))
(void)
ch)
starts))
(define (past-inactive-branches starts a)
(let1 (hi (and (< a (vector-length starts))
(any (Λ ((,hi ,entry ,polarity))
(and (not (eq? (node-value entry) polarity)) hi))
(vector-ref starts a))))
(if hi (past-inactive-branches starts (add1 hi)) a)))
(define (graph->search-tree
ch
result
#!key
(max-paths #f)
(who 'exact-posterior))
(let ((last (chain-count ch))
(now (chain-now ch))
(starts (branch-starts ch))
(paths 0))
(define (restore! frame)
(for-each
(Λ ((,n unquote s)) (node-inactive-set! n 0) (node-state-set! n s))
frame))
(let walk ((a (past-inactive-branches starts 1)) (frame '()))
(define (enter n s)
(node-state-set! n s)
(walk (past-inactive-branches starts (add1 a))
(cons (cons n s) frame)))
(if (> a last)
(begin
(add1! paths)
(when (and max-paths (> paths max-paths))
(hakaru10-error
'support
who
"more paths than max-paths"
max-paths))
(probcc-unit (force-value (node-value result))))
(let1 (n (chain-node ch a))
(cond ((not (walk-active! n))
(walk (past-inactive-branches starts (add1 a)) frame))
((resampleable? n)
(filter-map
(Λ ((,x unquote p))
(and (positive? p)
(probcc-τ
p
(restore! frame)
(enter n (recomputed-state n now x)))))
(choice-support (node-distk n) who)))
((eq? (node-kind n) 'observed)
(let* ((s (recomputed-state n now))
(w (exp (nstate-ll s))))
(if (or (not (finite? (nstate-ll s))) (zero? w))
'()
(list (probcc-τ w (restore! frame) (enter n s))))))
(else (enter n (recomputed-state n now)))))))))
(define (model->search-tree model #!key (max-paths #f))
(receive
(ch result)
(static-graph model 'model->search-tree)
(graph->search-tree
ch
result
#:max-paths
max-paths
#:who
'model->search-tree)))
(define (exact-posterior model #!key (max-paths #f))
(receive
(ch result)
(static-graph model 'exact-posterior)
(histogram-sort
(map (Λ (((V ,v) ,p)) (cons v p))
(probcc-normalize
(probcc-explore
+inf.0
(graph->search-tree ch result #:max-paths max-paths)))))))
(define (graph-forward ch result src #!optional (starts (branch-starts ch)))
(let ((last (chain-count ch)) (now (chain-now ch)))
(let walk ((a (past-inactive-branches starts 1)) (ll 0.0))
(define (next) (past-inactive-branches starts (add1 a)))
(if (> a last)
(values (force-value (node-value result)) ll)
(let1 (n (chain-node ch a))
(cond ((not (walk-active! n)) (walk (next) ll))
((memq (node-kind n) '(resampleable observed))
(receive
(x llx)
(distk-sample (node-distk n) src)
(node-state-set! n (recomputed-state n now x))
(walk (next) (if (resampleable? n) ll (+ ll llx)))))
(else
(node-state-set! n (recomputed-state n now))
(walk (next) ll))))))))
(define (weighted-samples n model #!key (seed 17) (rng #f))
(receive
(ch result)
(static-graph model 'weighted-samples)
(let ((src (or rng (make-stdgen-source seed)))
(starts (branch-starts ch)))
(let loop ((i 0) (samples '()))
(if (>= i n)
(reverse samples)
(receive
(v ll)
(graph-forward ch result src starts)
(loop (add1 i) (cons (cons v ll) samples))))))))
(define (likelihood-weighting n model #!key (seed 17) (rng #f))
(let* ((samples
(filter-map
(Λ ((,v unquote ll)) (and (finite? ll) (cons v ll)))
(weighted-samples n model #:seed seed #:rng rng)))
(top (fold (λ (s m) (max (cdr s) m)) -inf.0 samples))
(table (make-hash-table equal?)))
(for-each
(Λ ((,v unquote ll))
(hash-table-update!/default
table
v
(λ (w) (+ w (exp (- ll top))))
0.0))
samples)
(let1 (total (hash-table-fold table (λ (v w acc) (+ acc w)) 0.0))
(histogram-sort
(map (Λ ((,v unquote w)) (cons v (/ w total)))
(hash-table->alist table))))))
(define (node-signature n)
(list (node-addr n)
(node-kind n)
(node-role n)
(map node-addr (vector->list (node-parents n)))
(map (Λ ((,entry unquote polarity))
(cons (node-addr entry) polarity))
(node-scope n))))
(define (same-built-value? a b)
(cond ((and (deferred? a) (deferred? b))
(same-value? (deferred-args a) (deferred-args b)))
((or (deferred? a) (deferred? b)) #f)
(else (same-value? a b))))
(define (first-mismatch same? ch ch*)
(let loop ((a 1))
(and (<= a (chain-count ch))
(if (same? (chain-node ch a) (chain-node ch* a))
(loop (add1 a))
a))))
(define (construction-check
model
#!key
(seed 17)
(rng #f)
(semantics 'upstream))
(let1 (src (or rng (make-stdgen-source seed)))
(receive
(ch result)
(build model (rng-source-copy src) semantics 'check)
(if (or (not result) (rejected-ll? (chain-llcum ch)))
'skipped
(receive
(ch* result*)
(build model (rng-source-copy src) semantics 'check)
(rebuild-check ch result ch* result*)
(redraw-check ch (rng-source-copy src))
#t)))))
(define (rebuild-check ch result ch* result*)
(define (differ what . args)
(apply hakaru10-error
'internal
'check
(string-append "a rebuild and the initial trace differ in " what)
args))
(unless
(and result*
(= (chain-count ch) (chain-count ch*))
(= (node-addr result) (node-addr result*)))
(differ "graph" (chain-count ch) (and result* (chain-count ch*))))
(cond ((first-mismatch
(λ (n n*) (equal? (node-signature n) (node-signature n*)))
ch
ch*)
=>
(λ (a) (differ "graph" a)))
((first-mismatch
(λ (n n*) (same-built-value? (node-value n) (node-value n*)))
ch
ch*)
=>
(λ (a)
(differ
"value"
a
(node-value (chain-node ch a))
(node-value (chain-node ch* a)))))
((first-mismatch
(λ (n n*)
(ll-close?
(nstate-ll (node-state n))
(nstate-ll (node-state n*))))
ch
ch*)
=>
(λ (a) (differ "LL" a)))
((not (ll-close? (chain-llcum ch) (chain-llcum ch*)))
(differ "LL" (chain-llcum ch) (chain-llcum ch*)))))
(define (redraw-check ch src)
(define (differ what . args)
(apply hakaru10-error
'internal
'check
(string-append
"an address-order redraw and the initial trace differ in "
what)
args))
(match/first
(chain-fold
(Λ (,n (,ll unquote scale))
(cond ((resampleable? n)
(receive
(x llx)
(distk-sample (node-distk n) src)
(unless
(same-value? x (node-value n))
(differ "value" (node-addr n) x (node-value n)))
(cons (+ ll llx) (+ scale (abs llx)))))
((eq? (node-kind n) 'observed)
(let ((llo (nstate-ll (node-state n)))
(inactive? (node-inactive? n)))
(cons (if inactive? ll (+ ll llo))
(if (and inactive? (extended? ch))
scale
(+ scale (abs llo))))))
(else (cons ll scale))))
'(0.0 . 0.0)
ch)
((,ll unquote scale)
(let1 (llcum (chain-llcum ch))
(unless
(or (nan? llcum) (ll-close? ll llcum scale))
(differ "LL" ll llcum))))))
(define (resampleable? n) (eq? (node-kind n) 'resampleable))
(define (node-distk n)
(let1 (fn (node-fn n))
(if (distk? fn) fn (fn (leaf-values (node-parents n))))))
(define (recomputed-state n ts #!optional (v (node-value n)))
(if (eq? (node-role n) 'exit)
(make-nstate
(let1 (v (node-value (branch-active-result (node-branch n))))
(if (node-inactive? n) v (force-value v)))
0.0
#f
ts)
(let* ((dk (node-distk n)) (kind (distk-kind dk)) (rd (distk-rd dk)))
(unless
(eq? kind (node-kind n))
(hakaru10-error
'construction
'check
"a distribution constructor changed the kind of its node"
n
(node-kind n)
kind))
(case kind
((dirac)
(make-nstate
(let1 (x (distk-value dk))
(if (node-inactive? n) x (force-value x)))
0.0
#f
ts))
((observed)
(let1 (obs (distk-value dk))
(check-observation n obs)
(make-nstate obs ((rdistr-log-density rd) obs) rd ts)))
(else (make-nstate v ((rdistr-log-density rd) v) rd ts))))))
(define (scope-inactivity n)
(count (Λ ((,entry unquote polarity))
(not (eq? (node-value entry) polarity)))
(node-scope n)))
(define (same-rdistr? a b)
(if (and a b)
(and (eq? (rdistr-name a) (rdistr-name b))
(same-value? (rdistr-params a) (rdistr-params b)))
(eq? a b)))
(define (reference-check ch)
(define (fail msg n . args)
(apply hakaru10-error 'internal 'check msg (node-addr n) args))
(define (check-state n s)
(let1 (s* (recomputed-state n (nstate-tstamp s) (nstate-val s)))
(unless
(same-value? (nstate-val s) (nstate-val s*))
(fail "a value differs from its recomputation"
n
(nstate-val s)
(nstate-val s*)))
(unless
(same-rdistr? (nstate-dst s) (nstate-dst s*))
(fail "a distribution differs from its recomputation"
n
(nstate-dst s)
(nstate-dst s*)))
(unless
(ll-close? (nstate-ll s) (nstate-ll s*))
(fail "an LL differs from its recomputation"
n
(nstate-ll s)
(nstate-ll s*)))))
(let ((now (chain-now ch)) (every-node? (extended? ch)))
(chain-fold
(λ (n _)
(let ((s (node-state n)) (inactive (node-inactive n)))
(unless
(= inactive (scope-inactivity n))
(fail "a deactivation count differs from its recomputation"
n
inactive
(scope-inactivity n)))
(when (and (node-delayed n) (or every-node? (zero? inactive)))
(fail "an update is delayed on an active node or in 'extended mode"
n))
(when (or (node-pending n) (node-queued n))
(fail "a node is still pending or queued after the step" n))
(when (> (nstate-tstamp s) now)
(fail "a tstamp is after now" n (nstate-tstamp s) now))
(when (or every-node? (zero? inactive)) (check-state n s))))
(void)
ch)
(let1 (card (- (count node-inactive? (chain-sampleable ch))))
(unless
(= (chain-card-change ch) card)
(hakaru10-error
'internal
'check
"the card change differs from its recomputation"
(chain-card-change ch)
card)))
#t))
(define (resampleable-nodes ch) (reverse (chain-sampleable ch)))
(define (load-state! ch xs)
(let* ((now (chain-now ch))
(rest (chain-fold
(λ (n xs)
(node-inactive-set! n (scope-inactivity n))
(node-state-set!
n
(if (resampleable? n)
(if (pair? xs)
(recomputed-state n now (car xs))
(hakaru10-error
'construction
'load-state!
"too few values"
n))
(recomputed-state n now)))
(node-delayed-set! n #f)
(node-pending-set! n #f)
(if (resampleable? n) (cdr xs) xs))
xs
ch)))
(unless
(null? rest)
(hakaru10-error 'construction 'load-state! "too many values" rest))
(chain-card-change-set!
ch
(- (count node-inactive? (chain-sampleable ch))))))
(define (ext-states ch #!key (max-states 100000))
(let ((now (chain-now ch)) (last (chain-count ch)) (found 0))
(reverse
(let walk ((a 1) (prefix '()) (states '()))
(if (> a last)
(begin
(add1! found)
(when (> found max-states)
(hakaru10-error
'support
'ext-states
"more states than max-states"
max-states))
(cons (reverse prefix) states))
(let1 (n (chain-node ch a))
(node-inactive-set! n (scope-inactivity n))
(if (resampleable? n)
(fold (Λ ((,x unquote p) ,states)
(if (positive? p)
(begin
(node-state-set! n (recomputed-state n now x))
(walk (add1 a) (cons x prefix) states))
states))
states
(choice-support (node-distk n) 'ext-states))
(begin
(node-state-set! n (recomputed-state n now))
(walk (add1 a) prefix states)))))))))
(define (forced-step-log-alpha ch n x)
(when (or (not (resampleable? n)) (node-inactive? n))
(hakaru10-error
'internal
'forced-step-log-alpha
"an active resampleable node is expected"
n))
(journal-begin! ch)
(chain-now-set! ch (add1 (chain-now ch)))
(chain-llcum-set! ch 0.0)
(let* ((card-old (chain-card-change ch))
(s (node-state n))
(ll ((rdistr-log-density (nstate-dst s)) x))
(fb (apply-proposal! n x ll (- ll (nstate-ll s))))
(delta (begin (propagate! ch) (chain-llcum ch)))
(ratio (if (rejected-ll? delta)
-inf.0
(log-acceptance-ratio
ch
(vector-length (chain-samp ch))
card-old
fb))))
(journal-rollback! ch)
ratio))
(define (kernel-residual
model
#!key
(seed 17)
(max-states 100000)
(semantics 'extended))
(receive
(ch result)
(build model (make-stdgen-source seed) semantics 'kernel-residual)
(unless
result
(hakaru10-error
'construction
'kernel-residual
"the model's first build was rejected"))
(let* ((nodes (resampleable-nodes ch))
(states (ext-states ch #:max-states max-states))
(log-weights
(map (λ (xs) (load-state! ch xs) (chain-extended-ll ch))
states))
(top (let1 (top (fold (λ (l top) (if (finite? l) (max l top) top))
-inf.0
log-weights))
(if (finite? top)
top
(hakaru10-error
'construction
'kernel-residual
"no state of the model has a positive finite weight"
top))))
(weights
(map (λ (l) (if (finite? l) (exp (- l top)) 0.0)) log-weights))
(z (fold + 0.0 weights))
(π (make-hash-table equal?))
(πT (make-hash-table equal?)))
(define (flow! xs mass)
(hash-table-update!/default πT xs (λ (m) (+ m mass)) 0.0))
(for-each
(λ (xs w)
(hash-table-update!/default π xs (λ (p) (+ p (/ w z))) 0.0))
states
weights)
(hash-table-walk
π
(λ (xs p)
(when (positive? p)
(load-state! ch xs)
(let* ((active
(filter-map
(λ (n i)
(and (not (node-inactive? n)) (cons n i)))
nodes
(iota (length nodes))))
(pick (if (null? active) 0.0 (/ p (length active)))))
(when (null? active) (flow! xs p))
(for-each
(Λ ((,n unquote i))
(for-each
(Λ ((,x unquote q))
(when (positive? q)
(let* ((ratio (forced-step-log-alpha ch n x))
(accept
(if (nan? ratio) 0.0 (min 1.0 (exp ratio))))
(mass (* pick q)))
(flow! (list-replace xs i x) (* mass accept))
(flow! xs (* mass (- 1.0 accept))))))
(proposal-support n)))
active)))))
(fold (λ (xs m)
(max m
(abs (- (hash-table-ref/default πT xs 0.0)
(hash-table-ref/default π xs 0.0)))))
0.0
(append (hash-table-keys π) (hash-table-keys πT))))))
(define (proposal-support n)
(let1 (dk (node-distk n))
(unless
(rdistr-independence? (distk-rd dk))
(hakaru10-error
'support
'kernel-residual
"a random choice whose proposal is not the independence one"
dk))
(choice-support dk 'kernel-residual)))
(define (list-replace xs i x)
(if (zero? i)
(cons x (cdr xs))
(cons (car xs) (list-replace (cdr xs) (sub1 i) x)))))
test/grass: pass
E1. The grass model: rain and a sprinkler may wet the grass, and the grass is observed wet. The exact posterior of rain is upstream's 'theoretical' value.
(define (nnot p x) (if x p 1.0))
(define ((nor strength-x strength-y noise) x y)
(bern (- 1.0
(* (nnot (- 1.0 strength-x) x)
(nnot (- 1.0 strength-y) y)
(- 1.0 noise)))))
(define grass
(do/monad
(← rain (dist bern 0.3))
(← sprinkler (dist bern 0.5))
(dist (condition #t (nor 0.9 0.8 0.1)) rain sprinkler)
(return rain)))
grass = do
rain <- dist bern 0.3
sprinkler <- dist bern 0.5
-- dist (nor 0.9 0.8 0.1) rain sprinkler
grass_is_wet <- dist (True `condition` nor 0.9 0.8 0.1) rain sprinkler
return rain
-- noisy-or function
nor :: Double -> Double -> Double -> Bool -> Bool -> Distribution Bool
nor strengthX strengthY noise x y =
bern $ 1 - nnot (1-strengthX) x * nnot (1-strengthY) y * (1-noise)
-- noisy not function
nnot :: Num a => a -> Bool -> a
nnot p True = p
nnot p False = 1cf. Examples1.hs:299-314; the expected value is the comment at Examples1.hs:321-322:
-- 0.46815 for 20000 -- with old resampling
-- 0.4684714427203697 -- theoretical(define (test/grass _)
(⊦ (equal-approx? 1e-12)
'((#f . 0.53152855727963) (#t . 0.46847144272037))
(exact-posterior grass)))
((eta 0.0) (memory #(8396592 3852176 1048576)) (stdout "") (stderr ""))
test/girl: pass
E2. The eldest daughter puzzle: a family has two kids, one of them a girl
(observed through a fuzzy or that errs with probability .01); what is the chance
the older is a girl? Exactly
(define girl
(let ()
(define ((fuzzy2 pfuzz op) x y)
(let1 (v (op x y))
(categorical
`((,v unquote (- 1.0 pfuzz)) (,(not v) unquote pfuzz)))))
(do/monad
(← girl1 (dist bern 0.5))
(← girl2 (dist bern 0.5))
(dist (condition #t (fuzzy2 0.01 (λ (x y) (or x y)))) girl1 girl2)
(return girl1))))
girl = do
girl1 <- dist bern 0.5
girl2 <- dist bern 0.5
dist (True `condition` fuzzy2 0.01 (||)) girl1 girl2
return girl1
where
-- Creating a custom distribution
fuzzy2 pfuzz op x y = let v = op x y in
categorical [(v,1-pfuzz),(not v,pfuzz)]cf. Examples1.hs:340-355 (upstream records 77 True in 100 samples).
(define (test/girl _)
(⊦ (equal-approx? 1e-12)
'((#f . 0.335570469798658) (#t . 0.664429530201342))
(exact-posterior girl)))
((eta 0.0) (memory #(8396592 3857088 1048576)) (stdout "") (stderr ""))
test/pcorr: pass
E3. Yufei Cai's example of correlated booleans: x and y agree with probability .9 a posteriori, and y alone is a fair coin. The model returns the pair, where
upstream returns y.
(define (pcorr/pair pfuzz)
(define (xor y x) (not (eq? x y)))
(define ((fuzzy2 pfuzz op) x y)
(let1 (v (op x y))
(categorical
`((,v unquote pfuzz) (,(not v) unquote (- 1.0 pfuzz))))))
(do/monad
(← x (dist bern 0.5))
(← y (dist bern 0.5))
(dist (condition #t (fuzzy2 pfuzz xor)) x y)
(return (pair x y))))
pcorr pfuzz = do
x <- dist bern 0.5
y <- dist bern 0.5
dist (True `condition` fuzzy2 pfuzz xor) x y
return y
where
-- Creating a custom distribution
fuzzy2 pfuzz op x y = let v = op x y in
categorical [(v,pfuzz),(not v,1-pfuzz)]
xor y x = not (x == y)cf. Examples1.hs:223-238.
(define (test/pcorr _)
(let1 (posterior (exact-posterior (pcorr/pair 0.1)))
(⊦ (equal-approx? 1e-12)
0.9
(probability-of (Λ ((,x unquote y)) (eq? x y)) posterior))
(⊦ (equal-approx? 1e-12) 0.5 (probability-of cdr posterior))
(⊦ (equal-approx? 1e-12)
'(((#f . #f) . 0.45)
((#f . #t) . 0.05)
((#t . #f) . 0.05)
((#t . #t) . 0.45))
posterior)))
((eta 0.0) (memory #(8396592 3853560 1048576)) (stdout "") (stderr ""))
test/ph2: pass
E4. A dependent coin:
(define ph2
(do/monad
(← x (dist bern 0.2))
(dist bern (lift (λ (xv) (if xv 0.1 0.7)) x))))
(define ph21
(do/monad
(← x (dist bern 0.2))
(dist bern (lift (λ (xv) (if xv 0.1 0.7)) x))
(return x)))
ph2 = do
x <- dist bern 0.2
dist bern ((\xv -> if xv then 0.1 else 0.7) <$> x)
-- 0.2 * 0.1 + 0.8*0.7 = 0.58
ph21 = do
x <- dist bern 0.2
y <- dist bern ((\xv -> if xv then 0.1 else 0.7) <$> x)
return xcf. Examples1.hs:88-102 (upstream records 5809 and 2026 in 10000 samples).
(define (test/ph2 _)
(⊦ (equal-approx? 1e-12) 0.58 (probability #t (exact-posterior ph2)))
(⊦ (equal-approx? 1e-12) 0.2 (probability #t (exact-posterior ph21))))
((eta 0.0) (memory #(8396592 3858080 1048576)) (stdout "") (stderr ""))
test/pcond: pass
E5. Mixtures by if_: in pcond2 a branch returns the test's own node, and in pcond4 the else branch observes a fair coin True, halving its weight.
(define (one? x) (lift (cut = <> 1) x))
(define pcond1
(do/monad
(← x (dist categorical '((1 . 0.5) (2 . 0.5))))
(if_ (one? x)
(dist categorical '((10 . 0.5) (11 . 0.5)))
(dist categorical '((20 . 0.5) (21 . 0.5))))))
(define pcond2
(do/monad
(← x (dist categorical '((1 . 0.5) (2 . 0.5))))
(if_ (one? x) (return x) (dist categorical '((20 . 0.5) (21 . 0.5))))))
(define pcond4
(do/monad
(← x (dist categorical '((1 . 0.5) (2 . 0.5))))
(if_ (one? x)
(return x)
(>> (dist (condition #t bern) 0.5)
(dist categorical '((20 . 0.5) (21 . 0.5)))))))
pcond1 = do
x <- dist categorical (pure [(1,0.5), (2,0.5)])
if_ ((==1) <$> x)
(dist categorical (pure [(10,0.5), (11,0.5)]))
(dist categorical (pure [(20,0.5), (21,0.5)]))
pcond2 = do
x <- dist categorical (pure [(1,0.5), (2,0.5)])
if_ ((==1) <$> x)
(return x)
(dist categorical (pure [(20,0.5), (21,0.5)]))
pcond4 = do
x <- dist categorical (pure [(1,0.5), (2,0.5)])
if_ ((==1) <$> x)
(return x)
(do
dist (True `condition` bern) 0.5
dist categorical (pure [(20,0.5), (21,0.5)]))cf. Examples1.hs:104-124 and 265-282, where upstream records the exact answer
of pcond4:
-- Exact:
-- [(0.166666666666666657, V 21); (0.166666666666666657, V 20);
-- (0.66666666666666663, V 1)])(define (test/pcond _)
(⊦ (equal-approx? 1e-12)
'((10 . 0.25) (11 . 0.25) (20 . 0.25) (21 . 0.25))
(exact-posterior pcond1))
(⊦ (equal-approx? 1e-12)
'((1 . 0.5) (20 . 0.25) (21 . 0.25))
(exact-posterior pcond2))
(⊦ (equal-approx? 1e-12)
`((1 unquote (/ 2.0 3.0))
(20 unquote (/ 1.0 6.0))
(21 unquote (/ 1.0 6.0)))
(exact-posterior pcond4)))
((eta 0.0) (memory #(8396592 3862472 1048576)) (stdout "") (stderr ""))
test/pcond3-support: pass
E5b. pcond3 draws a normal in its else branch, so exact
enumeration raises a support error when it takes that
branch. The build of the graph completes without complaint, so the error comes
from the enumeration, not from the build.
(define pcond3
(do/monad
(← x (dist categorical '((1 . 0.5) (2 . 0.5))))
(if_ (one? x)
(return x)
(>> (dist normal 0.0 1.0)
(dist categorical '((20 . 0.5) (21 . 0.5)))))))
pcond3 = do
x <- dist categorical (pure [(1,0.5), (2,0.5)])
if_ ((==1) <$> x)
(return x)
(do
dist normal 0 1
dist categorical (pure [(20,0.5), (21,0.5)]))cf. Examples1.hs:126-136. Likelihood weighting gives its answer (H7).
(define (test/pcond3-support _)
(⊦= 'support (hakaru10-kind (τ (exact-posterior pcond3))))
(⊦= 'none (hakaru10-kind (τ (mcmC 10 pcond3)))))
((eta 0.001) (memory #(8396592 3870880 1048576)) (stdout "") (stderr ""))
test/post-bias: pass
E6. A coin selects which of two normals, centered at 0 and 100, the observation
1 comes from. The likelihood of the observation under N(100, 1) is
#t, with probability 1.: an observation of weight 0
prunes its path from the search tree, so #f is not in the histogram at all. A model whose every path has weight 0, here 5
observed from the uniform on [0, 1], has the empty posterior '().
(define (post-bias c)
(do/monad
(← coin (dist bern 0.5))
(if_ coin
(dist (condition c normal) 0.0 1.0)
(dist (condition c normal) 100.0 1.0))
(return coin)))
An observation whose LL is not finite weighs nothing either, as likelihood
weighting drops such a run: in nan-path the normal of standard deviation 0 gives 0 the LL NaN,
so k = 0 is pruned and the posterior is the 2 : 1 of the densities at k = 1 and 2,
which likelihood weighting visits alone. MH rejects a step to k = 0, whose ratio
is NaN and fails both acceptance tests (Metropolis.hs:939-944), but keeps an
initial trace of LL NaN, since only an infinite LL makes it rebuild
(Metropolis.hs:853-854, 882-891), and then never leaves it: at seed 17 the build
draws k = 2, so the chain visits 1 and 2 alone (a regression value), while at seed
110866 it draws k = 0 and the chain stays there, where the oracles and the chain
disagree, as upstream's would. In infinite-path the observation's LL is +inf when b is true and
-inf when it is false, so no path has a positive finite weight and enumeration,
likelihood weighting and the chain all answer '().
(define nan-path
(do/monad
(← k (dist uniformly '(0 1 2)))
(dist (condition 0.0 normal) 0.0 (lift exact->inexact k))
(return k)))
(define infinite-path
(do/monad
(← b (dist bern 0.5))
(dist (condition 0.0 beta) (lift (λ (bv) (if bv 0.5 2.0)) b) 1.0)
(return b)))
post_bias c = do
coin <- dist bern 0.5
if_ coin (dist (c `condition` normal) 0 1)
(dist (c `condition` normal) 100 1)
return coin
_ = mcmC 10 (post_bias 1)
-- All truecf. Examples1.hs:256-263; P l.427-437.
(define (test/post-bias _)
(⊦ equal? '((#t . 1.0)) (exact-posterior (post-bias 1.0)))
(⊦ equal? '() (exact-posterior (dist (condition 5.0 uniform) 0.0 1.0)))
(⊦ (equal-approx? 1e-12)
`((1 unquote (/ 2.0 3.0)) (2 unquote (/ 1.0 3.0)))
(exact-posterior nan-path))
(⊦ equal?
'((1 2) (1 2) ((0 . 30)))
(list (map car (likelihood-weighting 1000 nan-path))
(map car (hist (mcmC 1000 nan-path)))
(hist (mcmC 30 nan-path #:seed 110866))))
(⊦ equal?
'(() () ())
(list (exact-posterior infinite-path)
(likelihood-weighting 100 infinite-path)
(mcmC 5 infinite-path))))
((eta 0.007) (memory #(8396592 3870664 1048576)) (stdout "") (stderr ""))
test/colored-balls: pass
E7. The colored balls of BLOG (Milch et al., BLOG: Probabilistic Models with Unknown Objects, example 1.1), quoted at ColoredBalls.hs:6-11:
❝
An urn contains an unknown number of balls--say, a number chosen from a Poisson or a uniform distributions. Balls are equally likely to be blue or green. We draw some balls from the urn, observing the color of each and replacing it. We cannot tell two identically colored balls apart; furthermore, observed colors are wrong with probability 0.2. How many balls are in the urn? Was the same ball drawn twice?
With at most 3 balls and 4 draws, all seen blue, the exact posterior of the
number of balls agrees with its closed form: given
((1 . 0.41196368095257)
(2 . 0.209728624754566)
(3 . 0.120691976674053)
(4 . 0.0801847685132436)
(5 . 0.0590318166419537)
(6 . 0.046603790739908)
(7 . 0.0386298621005105)
(8 . 0.0331654796231948))
next to Hansei's importance sampling, t1 and the average t2 (ColoredBalls.hs:110-147):
let
[(0.031942788306915014, V 8); (0.0422088248548995876, V 7);
(0.0481489259210673634, V 6); (0.0611501366775565255, V 5);
(0.0843776503618006435, V 4); (0.123159075103266638, V 3);
(0.203898291338526577, V 2); (0.405114307435967658, V 1)] =
snd (Inference.normalize t1);;
let
[(0.0318386565368576138, V 8); (0.0384083436381660176, V 7);
(0.0445344709122082669, V 6); (0.055953476873295431, V 5);
(0.0777907061915841, V 4); (0.123751456856916756, V 3);
(0.211354336837841883, V 2); (0.416368552153129956, V 1)]
= t2;;The model, with the number of balls a parameter and the balls' colors
pre-allocated by replicate/monad, as upstream's sequence . replicate does:
(define (opposite-color color) (if (eq? color 'blue) 'green 'blue))
(define (observed-color color)
(categorical `((,color . 0.8) (,(opposite-color color) . 0.2))))
(define (balls-prior max-balls)
(do/monad
(← balls (replicate/monad max-balls (dist uniformly '(blue green))))
(return (map cons (iota max-balls 1) balls))))
(define (balls-observe max-balls balls obs nballs)
(>> (for-each/monad
(λ (color)
(do/monad
(← b (dist uniformly (iota nballs 1)))
(fold-right
(λ (i rest)
(if_ (lift (cut = <> i) b)
(dist (condition color observed-color)
(alist-ref i balls))
rest))
(return color)
(iota max-balls 1))))
obs)
(return (void))))
(define (cballs-model max-balls obs)
(do/monad
(← balls (balls-prior max-balls))
(← nballs (dist uniformly (iota max-balls 1)))
(fold-right
(λ (i rest)
(if_ (lift (cut = <> i) nballs)
(balls-observe max-balls balls obs i)
rest))
(return (void))
(iota max-balls 1))
(return nballs)))
(define (cballs-closed-form max-balls n)
(define (binomial m k)
(if (zero? k) 1 (/ (* m (binomial (sub1 m) (sub1 k))) k)))
(define (likelihood m)
(fold (λ (k acc)
(+ acc
(* (binomial m k)
(expt 2.0 (- m))
(expt (/ (+ (* 0.8 k) (* 0.2 (- m k))) m) n))))
0.0
(iota (add1 m))))
(let* ((ls (map likelihood (iota max-balls 1))) (z (fold + 0.0 ls)))
(map (cut / <> z) ls)))
-- Distribution for the observing of a ball, with the failure rate 20%
observed_color color =
categorical [(color,0.8),(opposite_color color,0.2)]
maxBalls :: Int
maxBalls = 8
-- Create the prior for the max amount of balls that may be in the urn
balls_prior n = do
balls <- sequence . replicate n $ dist uniformly (pure [Blue,Green])
return $ M.fromList $ zip [1..] balls
-- Submodel: Given balls color and the observations, perform conditioning
balls_observe :: M.IntMap (SExp Color) -> [Color] -> Int -> Model ()
balls_observe balls obs nballs = mapM_ observe_ball obs >> return (pure ())
where
-- The same ball can be drawn twice as we return the drawn balls
-- into the urn. A ball keeps its true color, no matter how many
-- times it is drawn.
observe_ball color = do
b <- dist uniformly (pure [1..nballs])
-- It is more efficient to create if statements, rather than
-- create an SExp(IntMap Color).
let observe i =
let true_color = balls ! i in
if_ ((== i) <$> b) $
dist (color `condition` observed_color) true_color
foldr observe (return (pure color)) [1..maxBalls]
-- The model takes a list of observations as an argument
cballs_model :: [Color] -> Model Int
cballs_model obs = do
balls <- balls_prior maxBalls
nballs <- dist uniformly (pure [1..maxBalls])
let obs_number i =
if_ ((== i) <$> nballs) $ balls_observe balls obs i
foldr obs_number (return (pure())) [1..maxBalls]
return nballscf. ColoredBalls.hs:34-44, 74-99, without the first model that lines 46-72
comment out; W #colored-balls (l.110-119).
(define (test/colored-balls _)
(let1 (expected '(0.45765164162493 0.298386199220924 0.243962159154146))
(⊦ (equal-approx? 1e-12)
(map cons '(1 2 3) expected)
(exact-posterior (cballs-model 3 (make-list 4 'blue))))
(⊦ (equal-approx? 1e-12) expected (cballs-closed-form 3 4)))
(⊦ (equal-approx? 1e-12)
'(0.41196368095257
0.209728624754566
0.120691976674053
0.0801847685132436
0.0590318166419537
0.046603790739908
0.0386298621005105
0.0331654796231948)
(cballs-closed-form 8 10)))
((eta 0.013) (memory #(8396592 3867096 1048576)) (stdout "") (stderr ""))
test/pswitch: pass
E8. pswitch is the counterexample to upstream's
reactivation of branches (plan §6.8). The branch-local y depends on m, which keeps changing while y is inactive. The answer is
(define pswitch
(do/monad
(← c (dist bern 0.5))
(← m (dist uniformly '(0 1)))
(if_ c
(dist categorical (lift (λ (mv) `((,mv . 0.9) (,(- 1 mv) . 0.1))) m))
(diracN (lift identity m)))
(return c)))
-- (new, plan §6.8; no upstream source)
c ~ bern 0.5
m ~ uniformly [0,1]
if_ c (y ~ categorical [(m,.9),(1-m,.1)])
(diracN (id <$> m))
return c(define (test/pswitch _)
(⊦ (equal-approx? 1e-12) '((#f . 0.5) (#t . 0.5)) (exact-posterior pswitch)))
((eta 0.0) (memory #(8396592 3869328 1048576)) (stdout "") (stderr ""))
test/hansei: pass
E9. The same posteriors from programs written in Hansei itself ((aux hansei), which runs on aux's own delimited control, resetcc). Hansei's grass is its canonical formulation, with the
noisy-or spelled out as coin flips (test/hansei.scm:87-100). The soft observation
of girl is a coin of the observation's likelihood, and pcond4 is a transcription.
(define (hansei-grass)
(let* ((rain (probcc-coin 0.3))
(sprinkler (probcc-coin 0.5))
(grass-is-wet
(or (and (probcc-coin 0.9) rain)
(and (probcc-coin 0.8) sprinkler)
(probcc-coin 0.1))))
(probcc-when grass-is-wet rain)))
(define (hansei-girl)
(let* ((girl1 (probcc-coin 0.5)) (girl2 (probcc-coin 0.5)))
(probcc-when (probcc-coin (if (or girl1 girl2) 0.99 0.01)) girl1)))
(define (hansei-pcond4)
(let1 (x (probcc-distribution '((1 0.5) (2 0.5))))
(if (= x 1)
x
(probcc-when
(probcc-coin 0.5)
(probcc-distribution '((20 0.5) (21 0.5)))))))
Hansei is described in Embedded probabilistic domain-specific language HANSEI and in Kiselyov and Shan, Embedded Probabilistic Programming, DSL 2009, LNCS 5658; W:111 cites Hansei's implementation of the colored balls.
(define (test/hansei _)
(⊦ (equal-approx? 1e-12)
(exact-posterior grass)
(hansei->histogram (probcc-reify/exact/a hansei-grass)))
(⊦ (equal-approx? 1e-12)
(exact-posterior girl)
(hansei->histogram (probcc-reify/exact/a hansei-girl)))
(⊦ (equal-approx? 1e-12)
(exact-posterior pcond4)
(hansei->histogram (probcc-reify/exact/a hansei-pcond4))))
((eta 0.0) (memory #(8396592 3871216 1048576)) (stdout "") (stderr ""))
test/kernel-residual: pass
E10. The 'extended semantics leaves its target invariant: the kernel residual
nested-dispatch reaches the deactivation of an observation that
the same step activated (deviation 8): subtracting its stale LL there would bias
the kernel, with a residual of about .078.
(define nested-dispatch
(do/monad
(← x (dist uniformly '(1 2 3)))
(if_ (lift (cut > <> 1) x)
(if_ (lift (cut > <> 2) x)
(dist (condition #t bern) 0.1)
(dist (condition #t bern) 0.3))
(dist (condition #t bern) 0.6))
(return x)))
hakaru10.scm runs the kernel residual on five more models, next to their other
checks: act-update and act-update/finite (case B4), whose step activates an observation and updates its parameters at
once, and guarded-car, guarded-car/reversed and nested-guarded-car (case B6), whose inactive branches hold a deferred Dirac value.
A state with no active resampleable node stays put (deviation 6): the residual
is 0 on a model whose only choice sits in a branch never taken. A support with
duplicate points gives a value the weight of all its points, as the sampler
does: the residual of uniformly '(1 1 2), whose 1 weighs 2/3,
is below
As a positive control, the residual exceeds
liar declares a support that disagrees with
its density, so that its proposal is not the one the acceptance ratio assumes.
(define (liar)
(make-distribution
'liar
#:log-density
(λ (x) (log (if x 0.9 0.1)))
#:sample
(λ (src)
(let1 (x (< (rng-double! src 0.0 1.0) 0.9))
(values x (log (if x 0.9 0.1)))))
#:support
(τ '((#t . 0.5) (#f . 0.5)))))
(define liar-or
(do/monad
(← a (dist liar))
(← b (dist bern 0.3))
(dist (condition #t bern) (lift (λ (av bv) (if (or av bv) 0.8 0.2)) a b))
(return a)))
A state whose extended LL is NaN or +inf weighs nothing, as exact enumeration
prunes it (E6) and no step enters it: the residual is that of the other states. It
is still positive on liar-or/nan, the liar next to nan-path's choice, and below
nan-path and on plus-inf-state, whose b = 1 has LL +inf and whose posterior is 2/3 and 1/3 on b = 2 and 3.
(define liar-or/nan
(do/monad
(← a (dist liar))
(← b (dist bern 0.3))
(dist (condition #t bern) (lift (λ (av bv) (if (or av bv) 0.8 0.2)) a b))
(← k (dist uniformly '(0 1 2)))
(dist (condition 0.0 normal) 0.0 (lift exact->inexact k))
(return a)))
(define plus-inf-state
(do/monad
(← b (dist uniformly '(1 2 3)))
(dist (condition 0.0 uniform)
0.0
(lift (λ (bv) (case bv ((1) 0.0) ((2) 1.0) (else 2.0))) b))
(return b)))
With #:semantics 'upstream the kernel is upstream's, whose
step delays the updates of inactive nodes (Metropolis.hs:298-303), run from the
states where no update is delayed. On pswitch its residual is .05, while the extended kernel's is below
T is the independence proposal's (Distribution.hs:29-32), the default kernel: a
choice whose distribution has another kernel, such as bern/flip's flip (Distribution.hs:64-70), is a support error, since T would not describe the engine's step. A
model where no state has a positive weight, here one that observes #t from bern 0, has no target π: a construction error.
cf. plan §6.11; Design 3 §5.9.
(define (test/kernel-residual _)
(define models
`((grass unquote grass)
(girl unquote girl)
(pcorr unquote (pcorr/pair 0.1))
(pcond1 unquote pcond1)
(pcond2 unquote pcond2)
(pcond4 unquote pcond4)
(post-bias unquote (post-bias 1.0))
(colored-balls-3x2 unquote (cballs-model 3 '(blue blue)))
(pswitch unquote pswitch)
(nested-dispatch unquote nested-dispatch)))
(⊦ equal?
'()
(filter-map
(Λ ((,name unquote model))
(let1 (residual (kernel-residual model))
(and (not (< residual 1e-12)) (cons name residual))))
models))
(⊦ = 0.0 (kernel-residual (if_ #f (dist bern 0.5) (return 0))))
(⊦ > 1e-12 (kernel-residual (dist (uniformly '(1 1 2)))))
(⊦ < 0.001 (kernel-residual liar-or))
(⊦ < 0.001 (kernel-residual liar-or/nan))
(⊦ > 1e-12 (kernel-residual nan-path))
(⊦ > 1e-12 (kernel-residual plus-inf-state))
(⊦ (equal-approx? 1e-12)
`((2 unquote (/ 2.0 3.0)) (3 unquote (/ 1.0 3.0)))
(exact-posterior plus-inf-state))
(⊦ < 0.01 (kernel-residual pswitch #:semantics 'upstream))
(⊦ > 1e-12 (kernel-residual pswitch #:semantics 'extended))
(⊦= 'support (hakaru10-kind (τ (kernel-residual (dist bern/flip 0.5)))))
(⊦= 'construction
(hakaru10-kind
(τ (kernel-residual
(do/monad
(← b (dist bern 0.5))
(dist (condition #t bern) 0.0)
(return b)))))))
((eta 0.448) (memory #(16793184 3888576 1048576)) (stdout "") (stderr ""))
(aux hakaru10 exact): one model, three interpretations
One model, three interpretations
A Hakaru10 model is a value of the State monad of (aux category state), whose state is the chain: Model a = MCMCM (SExp a) (Syntax.hs:82) and MCMCM a = State MCState a (Metropolis.hs:200). dist, diracN and if_ are models, and do/monad, ← and return combine them as Haskell's do does:
(do/monad
(← x (dist d args))
(diracN e)
(if_ test then-model else-model)
(return x))
Running a model creates its nodes in bind order, and if_ builds both branches, the inactive one first, as upstream's ifnode does (Metropolis.hs:641-686). condition and lift are not models: conditioning makes an observed distribution
(Syntax.hs:174-193), and lifting is pure (Syntax.hs:99-116).
The three interpretations are:
- the build of
mcmC: the model runs once on a fresh chain, as upstream'sinitial_step'runs it (Metropolis.hs:872-880), and incremental Metropolis-Hastings then updates the graph it built; - exact enumeration: a walk of the graph of one build into a lazy Hansei search tree (page hakaru10-exact);
- the forward sampler: walks of the same graph that draw its active choices in address order and weigh its active observations, for likelihood weighting.
The last two share the graph and the node functions with the engine, but none of its incremental machinery, so each is an oracle of the kernel and of the incremental update (plan §6.10, C37). The tests below run the same models under all three.
test/three-interpretations: pass
H1. The same models under the three interpretations: exact enumeration, 20000
runs of likelihood weighting, and an MCMC chain. Likelihood weighting draws
independent samples and lands within .016 of the exact posterior over the seeds
17 and 1-40; the tolerance is .02. The MCMC samples are autocorrelated, and the
chain tolerance is .03. Over the same seeds, 20000 samples land within .021 on
all but two models, which mix slowly. On pcorr the observation favours
pswitch runs under 'extended only, since upstream's semantics
is biased on it (plan §6.8).
(define (test/three-interpretations _)
(define models
`((grass ,grass)
(girl ,girl)
(pcorr ,(pcorr/pair 0.1) #:chain-samples 80000)
(ph2 ,ph2)
(ph21 ,ph21)
(pcond1 ,pcond1)
(pcond2 ,pcond2)
(pcond4 ,pcond4)
(post-bias ,(post-bias 1.0))
(colored-balls-3x4
,(cballs-model 3 (make-list 4 'blue))
#:chain-samples
80000)
(pswitch ,pswitch #:semantics extended)))
(define (disagreement
name
model
#!key
(semantics 'upstream)
(chain-samples 20000))
(let ((exact (exact-posterior model))
(weighted (likelihood-weighting 20000 model))
(chain (mcmc-posterior chain-samples model #:semantics semantics)))
(and (not (and ((histograms-within 0.02) exact weighted)
((histograms-within 0.03) exact chain)))
(list name exact weighted chain))))
(⊦ equal? '() (filter-map (cut apply disagreement <>) models)))
((eta 3.154) (memory #(16793184 3865240 1048576)) (stdout "") (stderr ""))
test/construction-order: pass
H2. construction-check, the first check of #:check, builds the model, rebuilds it and redraws it, each from
its own copy of one rng source. The rebuild must give the same graph (the kind,
role, parents and scope of every node), the same values and the same LLs: a model
that is not the same each time it runs fails it (plan §15, C35). The redraw draws
every resampleable node in address order, from its distribution and its stored
parents, and must give back every stored value, and the drawn LLs plus those of
the active observations must give the trace's LL within
if_ first has the branch it does not take, whose choices are drawn and whose
observations are not weighed, as ifnode builds it (Metropolis.hs:650-661). In 'upstream mode the LL is not
bit-identical, because deactivation adds and then subtracts the inactive
observations' LL; in 'extended mode the build recomputes it from the redraw's
terms, in the redraw's order. So the redraw checks the draw order against the
graph and the LL accounting. It runs the engine's construction code, so with the
replays of hakaru10.scm it stands for an oracle of the node creation order (plan
C18) that would not. #:check runs this check once per
chain.
(define nested
(do/monad
(← c (dist bern 0.5))
(← d (dist bern 0.4))
(← z (dist normal 0.0 1.0))
(if_ c
(do/monad
(← e (dist bern (lift (λ (zv) (if (> zv 0.0) 0.7 0.2)) z)))
(if_ (lift (λ (ev dv) (and ev dv)) e d)
(dist (condition 0.5 normal) z 1.0)
(>> (dist (condition #t bern) 0.3)
(diracN (lift (cut * 2.0 <>) z)))))
(if_ d (dist normal z 2.0) (dist gamma 2.0 1.0)))))
(define (phier n)
(fold (λ (_ m) (do/monad (← x m) (dist normal x 3.0)))
(dist normal 0.0 1.0)
(iota n)))
(define (normals means sd) (traverse/monad (cut dist normal <> sd) means))
(define (dot xs ws)
(reduce-right sexp+ 0.0 (map (λ (x w) (lift (cut * <> x) w)) xs ws)))
(define (regression k data)
(do/monad
(← w-mean (normals (make-list k 0.0) 2.0))
(← w (normals w-mean 1.0))
(← noise-sd (<$> (cut lift (cut / 1.0 <>) <>) (dist gamma 0.5 0.5)))
(traverse/monad
(Λ ((,xs unquote y)) (dist (condition y normal) (dot xs w) noise-sd))
data)
(return (collect w))))
phier n =
(iterate (\m -> do {x<-m; dist normal x 3}) $ dist normal 0 1) !! ncf. Bench.hs:14-15; the regression model of P §3 l.461-504 with Birds1.hs:100-111:
normals :: [SExp Double] -> SExp Double -> MCMCM [SExp Double]
normals means std = mapM (\m -> dist normal m std) means
dot :: [Double] -> [SExp Double] -> SExp Double
dot xs ays = foldr1 (+) $ zipWith (\x ay -> (*x) <$> ay) xs ays(define (test/construction-order _)
(define models
`((grass unquote grass)
(pcond3 unquote pcond3)
(pcond4 unquote pcond4)
(pswitch unquote pswitch)
(nested unquote nested)
(colored-balls-8x10 unquote (cballs-model 8 (make-list 10 'blue)))
(phier-10 unquote (phier 10))
(regression-5x500 unquote (regression 5 (regression-data 5 500)))))
(⊦ equal?
(map (λ (entry) (list (car entry) #t #t)) models)
(map (Λ ((,name unquote model))
(list name
(construction-check model #:semantics 'upstream)
(construction-check model #:semantics 'extended)))
models)))
((eta 0.132) (memory #(16793184 5489272 1048576)) (stdout "") (stderr ""))
test/hansei-format: pass
H3. model->search-tree answers a genuine Hansei search tree.
Explored to depth 0 it still has suspended C entries, and explored fully and normalized it is the exact posterior:
(((C #<procedure (a3773)>) 0.35)
((C #<procedure (a3773)>) 0.35)
((C #<procedure (a3773)>) 0.15)
((C #<procedure (a3773)>) 0.15))
Its thunks may be forced in any order: each first restores the states of its
path's nodes. Explored level by level, the thunks of each level forced in
reverse order, the trees of grass and of pcond4, whose observation is a thunk of its own, give the exact
posterior too.
(define (test/hansei-format _)
(let1 (shallow (probcc-explore 0 (model->search-tree grass)))
(⊨ (any (λ (entry) (eq? 'C (caar entry))) shallow)))
(⊦ (equal-approx? 1e-12)
(exact-posterior grass)
(hansei->histogram (probcc-explore +inf.0 (model->search-tree grass))))
(for-each
(Λ ((,name unquote m))
(⊦ (equal-approx? 1e-12)
(list name (exact-posterior m))
(list name
(hansei->histogram
(probcc-explore
0
(explore/reversed-levels (model->search-tree m)))))))
`((grass unquote grass) (pcond4 unquote pcond4))))
((eta 0.001) (memory #(16793184 3865928 1048576)) (stdout "") (stderr ""))
test/errors: pass
H4. The run-time checks that replace Haskell's types (plan §7) hold under every
interpretation. A model answered by a lifted function (the function of a node,
run when the node is built or updated) is a construction error (deviation 9). A non-boolean
test is a branch error (SExp Bool, Syntax.hs:275).
(define (test/errors _)
(⊦ equal?
'((builder . construction)
(exact . construction)
(forward . construction))
(kinds-under-interpretations
(do/monad
(← x (dist bern 0.5))
(return (lift (λ (xv) (dist bern 0.5)) x)))))
(⊦ equal?
'((builder . branch) (exact . branch) (forward . branch))
(kinds-under-interpretations
(do/monad
(← x (dist uniformly '(0 1)))
(if_ x (return 1) (return 2))))))
((eta 0.0) (memory #(16793184 3873232 1048576)) (stdout "") (stderr ""))
test/static-graph: pass
H5. Conditioning on a random choice is ill-typed upstream: observations are
external to the model (Syntax.hs:38-42, P §2.3 l.412-421). exact-posterior walks the graph of a build, and the build raises conditioning.
{-
biased_coin_ill_typed_too c1 c2 = do
b <- dist bern 0.5
dist (b `condition` bern) 0.4
return b
-}cf. Examples1.hs:196-201. Upstream's types accept a distribution constructor
that observes its argument, and its update keeps the observation the node was
built with (Syntax.hs:251-252, Metropolis.hs:487-488), so its chain silently
observes the first value (deviation 9). Here the engine's update raises conditioning when the observation changes, and so do the
oracles, which recompute every node: enumeration, likelihood weighting, the
reference evaluator of #:check and the kernel residual all
agree with the chain.
(define observes-its-argument
(do/monad
(← x (dist bern 0.5))
(dist (λ (xv) (condition xv (bern 0.8))) x)
(return x)))
The graph is that of a build, which builds both branches of an if_, so a branch the drawn values leave inactive may raise a
parameter error that enumeration, which skips the inactive branches, never meets.
It is a dynamic condition, not a breach of the discipline: the build runs in
'extended mode, where it rejects the build (plan C22), and is retried until one
completes. So the answer does not depend on what seed 17 draws first, for an
error that rejects the build; another error of the build is raised, as in
hakaru10.scm's invalid-inactive/branch (case B5). In nested-uniformly, when xs is the empty list, the inactive then
branch draws from uniformly '(), an error; with the support
of xs in either order the posterior is the same:
(define (nested-uniformly choices)
(do/monad
(← xs (dist uniformly choices))
(if_ (lift pair? xs) (dist uniformly xs) (return 'none))))
and the retry still finds a breach that follows: nested-uniformly/ill-typed observes a choice after the branch, and raises conditioning with the support in either order, also when the
first build is rejected.
(define (nested-uniformly/ill-typed choices)
(do/monad
(← xs (dist uniformly choices))
(if_ (lift pair? xs) (dist uniformly xs) (return 'none))
(← y (dist bern 0.5))
(dist (condition y bern) 0.5)
(return xs)))
A model that every build rejects has no graph, so the oracles raise construction after 1000 builds (plan §6.10). Below, the else
branch is never taken, but in 'extended mode its normal is drawn as an inactive
choice, from a mean that needs (car '()):
(define always-rejected
(do/monad
(← c (dist bern 0.5))
(if_ #t
(return c)
(do/monad
(← h (diracN (lift car '())))
(← mu (diracN (lift cdr (pair h 1.0))))
(dist normal mu 1.0)))))
Enumeration stops at #:max-paths leaves, with a support error: grass has 4 paths, more than 3, and as many as 4. A tree that model->search-tree builds with #:max-paths counts the leaves of all its explorations, so it can be explored once: the
second exploration of grass's tree of 4 paths raises.
A walk of the graph forces the value of an active exit or Dirac node, as the
engine's step does (deviation 11), so the oracles fail where mcmC does, whichever branch the graph's build took: in guarded-result the step that makes c false forces the deferred (car '()) of the else branch, which the graph's build leaves
inactive, and the walks that take that branch raise the same Scheme error; guarded-result/mirror's build takes it, and raises there.
(define guarded-result
(do/monad
(← c (dist bern 0.5))
(← r (if_ c (return 'ok) (diracN (lift car '()))))
(return c)))
(define guarded-result/mirror
(do/monad
(← c (dist bern 0.5))
(← r (if_ (lift not c) (return 'ok) (diracN (lift car '()))))
(return c)))
A walk also writes each node's deactivation count before it recomputes the
node, which reads it: in guarded-head the then branch's Dirac node, which is not the branch's result, applies car to '() whenever that branch is active,
and the walks that take it raise, though the graph's build left it inactive; guarded-head/mirror's build takes it, and raises there.
(define guarded-head
(do/monad
(← c (dist bern 0.5))
(let xs (lift (λ (cv) (if cv '(1) '())) c))
(if_ (lift not c)
(>> (diracN (lift car xs)) (return 'then))
(return 'else))))
(define guarded-head/mirror
(do/monad
(← c (dist bern 0.5))
(let xs (lift (λ (cv) (if cv '() '(1))) c))
(if_ c (>> (diracN (lift car xs)) (return 'then)) (return 'else))))
A node that a switch activates may also keep the deferred value it got while
inactive, when no update of the step recomputes it: the Dirac node of guarded-head/constant reads a constant, the inner exit of guarded-exit/constant an inner result that does not change, the
root of guarded-root/constant has no update. The step forces such a value when its propagation ends, so it
raises as the build of that branch active does, and as the walks that take the
branch do, in both semantics, with or without #:check, and under the kernel residual: the chain's first sample is 'else, and a later step raises car's error. Upstream, which never reads these nodes, samples 'then and 'else about half the time each.
(define guarded-head/constant
(do/monad
(← c (dist bern 0.5))
(← xs (diracN '()))
(if_ (lift not c)
(>> (diracN (lift car xs)) (return 'then))
(return 'else))))
(define guarded-exit/constant
(do/monad
(← c (dist bern 0.5))
(if_ (lift not c)
(do/monad
(← inner (if_ #t (diracN (lift car '())) (return 'x)))
(return 'then))
(return 'else))))
(define guarded-root/constant
(do/monad
(← c (dist bern 0.5))
(if_ (lift not c)
(>> (diracN (lift car '())) (return 'then))
(return 'else))))
(define (test/static-graph _)
(define observes-a-choice
(do/monad (← x (dist bern 0.5)) (dist (condition x bern) 0.5)))
(⊦= 'conditioning (hakaru10-kind (τ (exact-posterior observes-a-choice))))
(⊦ equal?
(make-list 4 'conditioning)
(map (λ (run) (hakaru10-kind (τ (run observes-its-argument))))
(list exact-posterior
(cut likelihood-weighting 100 <>)
(cut mcmC 50 <> #:check #t)
kernel-residual)))
(for-each
(λ (choices)
(⊦ (equal-approx? 1e-12)
(list choices '((1 . 0.25) (2 . 0.25) (none . 0.5)))
(list choices (exact-posterior (nested-uniformly choices))))
(⊦ equal?
(list choices 'conditioning)
(list choices
(hakaru10-kind
(τ (exact-posterior
(nested-uniformly/ill-typed choices)))))))
'(((1 2) ()) (() (1 2))))
(⊦= 'construction (hakaru10-kind (τ (exact-posterior always-rejected))))
(⊦= 'support (hakaru10-kind (τ (exact-posterior grass #:max-paths 3))))
(⊦ (equal-approx? 1e-12)
(exact-posterior grass)
(exact-posterior grass #:max-paths 4))
(let1 (tree (model->search-tree grass #:max-paths 4))
(⊦ (equal-approx? 1e-12)
(exact-posterior grass)
(hansei->histogram (probcc-explore +inf.0 tree)))
(⊦= 'support (hakaru10-kind (τ (probcc-explore +inf.0 tree)))))
(for-each
(Λ ((,name unquote m))
(⊦ equal?
(list name 'exn 'exn 'exn)
(list name
(error-kind (τ (exact-posterior m)))
(error-kind (τ (likelihood-weighting 100 m)))
(error-kind (τ (mcmC 100 m))))))
`((guarded-result unquote guarded-result)
(guarded-result/mirror unquote guarded-result/mirror)
(guarded-head unquote guarded-head)
(guarded-head/mirror unquote guarded-head/mirror)
(guarded-head/constant unquote guarded-head/constant)
(guarded-exit/constant unquote guarded-exit/constant)
(guarded-root/constant unquote guarded-root/constant)))
(for-each
(Λ ((,name unquote m))
(⊦ equal?
(list name 'else 'exn 'exn 'exn)
(list name
(car (mcmC 1 m))
(error-kind (τ (mcmC 100 m #:semantics 'extended)))
(error-kind (τ (mcmC 100 m #:check #t)))
(error-kind (τ (kernel-residual m))))))
`((guarded-head/constant unquote guarded-head/constant)
(guarded-exit/constant unquote guarded-exit/constant)
(guarded-root/constant unquote guarded-root/constant))))
((eta 0.022) (memory #(16793184 3873704 1048576)) (stdout "") (stderr ""))
test/nested-branches: pass
H6. Nested if_s under the oracles. A branch answers (diracN d) of a node of the enclosing scope, which the build
shares and does not copy (Syntax.hs:164-169), or the diracN of a lifted expression. The else branch observes a coin
whose parameter depends on an outer node. The MCMC chains, in both semantics,
and likelihood weighting each run 20000 samples and agree with exact enumeration
within .02:
(define two-levels
(do/monad
(← c (dist bern 0.4))
(← d (dist uniformly '(0 1 2)))
(if_ c
(do/monad
(← e (dist bern 0.7))
(if_ e (diracN d) (diracN (lift (cut + 10 <>) d))))
(do/monad
(← y (dist categorical '((5 . 0.5) (6 . 0.5))))
(dist (condition #t bern)
(lift (λ (yv dv) (if (= yv 5) 0.8 (/ (+ dv 1.0) 4.0))) y d))
(return y)))))
((0 . 0.118143459915612)
(1 . 0.118143459915612)
(2 . 0.118143459915612)
(5 . 0.30379746835443)
(6 . 0.189873417721519)
(10 . 0.0506329113924051)
(11 . 0.0506329113924051)
(12 . 0.0506329113924051))
(define (test/nested-branches _)
(let1 (exact (exact-posterior two-levels))
(⊦ (histograms-within 0.02) exact (mcmc-posterior 20000 two-levels))
(⊦ (histograms-within 0.02)
exact
(mcmc-posterior 20000 two-levels #:semantics 'extended))
(⊦ (histograms-within 0.02)
exact
(likelihood-weighting 20000 two-levels))))
((eta 0.289) (memory #(16793184 3877088 1048576)) (stdout "") (stderr ""))
test/continuous: pass
H7. Likelihood weighting needs no finite support. pcond3 (E5b) gets its exact answer within .02, and the mean of mixng is
uniform 0 1, gives the empty histogram, as exact-posterior does (E6).
(define mixng
(do/monad
(← x (dist normal 0.0 1.0))
(if_ (lift (cut > <> 0.0) x)
(dist normal 10.0 2.0)
(dist gamma 3.0 (/ 1.0 3.0)))))
mixng = do
x <- dist normal 0 1
if_ ((>0) <$> x)
(dist normal 10 2)
(dist gamma 3 (1/3))cf. Examples1.hs:139-144; P §2.2 l.331-366.
(define (test/continuous _)
(⊦ (histograms-within 0.02)
'((1 . 0.5) (20 . 0.25) (21 . 0.25))
(likelihood-weighting 20000 pcond3))
(⊦ (equal-approx? 0.4)
5.5
(fold (Λ ((,x unquote p) ,mean) (+ mean (* x p)))
0.0
(likelihood-weighting 20000 mixng)))
(⊦ equal?
'()
(likelihood-weighting 100 (dist (condition 5.0 uniform) 0.0 1.0))))
((eta 0.291) (memory #(16793184 3876320 1048576)) (stdout "") (stderr ""))
test/pre-allocation: pass
H8. Pre-allocation, upstream's way to stand for stochastic memoization
(ColoredBalls.hs:41-44): replicate/monad creates one coin per key, in order, and a table maps the keys to them. The
table is built inside a bind, so each run of the model has its own (plan §15,
C35). Enumeration gives each coin its own value on each path, so two
pre-allocated coins are independent, and so are the colors of E7's balls.
(define (test/pre-allocation _)
(⊦ (equal-approx? 1e-12)
'(((#f . #f) . 0.25)
((#f . #t) . 0.25)
((#t . #f) . 0.25)
((#t . #t) . 0.25))
(exact-posterior
(do/monad
(← coins (replicate/monad 2 (dist bern 0.5)))
(let table (map cons '(a b) coins))
(return (pair (alist-ref 'a table) (alist-ref 'b table))))))
(⊦ (equal-approx? 1e-12)
(map cons '(1 2 3) (cballs-closed-form 3 4))
(exact-posterior (cballs-model 3 (make-list 4 'blue)))))
((eta 0.014) (memory #(16793184 3992488 1048576)) (stdout "") (stderr ""))
(aux hakaru10): colored balls, bird migration and linear regression
Examples
Section 3 of the paper Probabilistic Programming Language and its Incremental Evaluation (APLAS 2016, DOI 10.1007/978-3-319-47958-3_19) evaluates Hakaru10 on two realistic models; quoting design.txt l.454-460:
❝
The Hakaru10 tutorial might have given an impression that Hakaru10 tries so hard to preclude problematic behavior, by restricting conditioning and models, that one cannot do much interesting in it. In this section we briefly evaluate the expressiveness of the language on two realistic models.
The two models are the Bayesian linear regression of the PPAML small problems
collection (problem 4.1) and the population estimation of Milch et al.'s
colored balls. To them the Hakaru10 page adds the bird migration problem of the PPAML challenge, which these tests run on synthetic data (the real
data run is hakaru10-birds.scm, opt-in). Upstream's ColoredBalls.hs and Birds1.hs are transcribed definition by definition; the regression exists only in the
paper, with its helpers in Birds1.hs.
Each model is a computation of the State monad that threads the chain, as
upstream's Model a = MCMCM (SExp a) (Syntax.hs:82), written with do/monad as the Haskell
is written with do: dist, diracN and if_ are models, and mcmC runs one under the MCMC engine. The oracles walk the
trace graph of one build of the same model: exact enumeration (exact-posterior) and the forward sampler (likelihood-weighting, weighted-samples). So the same cballs-model is checked by MCMC, by
likelihood weighting and, on a smaller urn, by exact enumeration, against a
closed form; and the generative half of the bird and the regression models, run
forward from seed 17, simulates the data that their conditioned versions then
explain. The construction check rebuilds the model, which must give the same
graph, values and log-likelihoods, and redraws its choices in address order,
which must give back the values and the log-likelihood of the initial trace.
Nodes are created when a model runs, in bind order: do/monad top to bottom, and replicate/monad, traverse/monad, for-each/monad and fold/monad head first, upstream's sequence . replicate, mapM, mapM_ and foldM_: creation order is the topological order of the trace (design.txt
l.791-794). A chain of if_ is built by fold-right over models, which create no node until the
outermost one runs, and if_ delays its branches until it
builds them, which reproduces upstream's foldr node for node: at 10000 samples the colored balls' histogram is upstream's
own record (hakaru10-replay, X14).
test/colored-balls: pass
C1. Population estimation, from Milch et al.'s BLOG: Probabilistic Models with Unknown Objects (Introduction to Statistical Relational Learning, MIT Press, 2007, pp. 373-398), example 1.1, as ColoredBalls.hs:1-13 quotes it:
❝
An urn contains an unknown number of balls--say, a number chosen from a Poisson or a uniform distributions. Balls are equally likely to be blue or green. We draw some balls from the urn, observing the color of each and replacing it. We cannot tell two identically colored balls apart; furthermore, observed colors are wrong with probability 0.2. How many balls are in the urn? Was the same ball drawn twice?
The Hakaru10 page (index.html:111) calls it an elegant example of a model with an indefinite number of objects whose real identity is unobservable, a simplified population estimation, which in Hansei makes the case for the memoization of stochastic functions; the paper (design.txt l.519-520) notes that it motivated BLOG, being hard to implement in many probabilistic languages. Hakaru10 bounds the number of balls and pre-allocates one color per ball, which the unused branches marginalize (design.txt l.531-544); its footnote 5 (l.566-569) wishes for lazily instantiated branches instead. Upstream's model, ColoredBalls.hs:28-44 and 74-103:
data Color = Blue | Green deriving (Eq, Show)
opposite_color :: Color -> Color
opposite_color Blue = Green
opposite_color Green = Blue
-- Distribution for the observing of a ball, with the failure rate 20%
observed_color color =
categorical [(color,0.8),(opposite_color color,0.2)]
maxBalls :: Int
maxBalls = 8
-- Create the prior for the max amount of balls that may be in the urn
balls_prior n = do
balls <- sequence . replicate n $ dist uniformly (pure [Blue,Green])
return $ M.fromList $ zip [1..] balls-- Submodel: Given balls color and the observations, perform conditioning
balls_observe :: M.IntMap (SExp Color) -> [Color] -> Int -> Model ()
balls_observe balls obs nballs = mapM_ observe_ball obs >> return (pure ())
where
-- The same ball can be drawn twice as we return the drawn balls
-- into the urn. A ball keeps its true color, no matter how many
-- times it is drawn.
observe_ball color = do
b <- dist uniformly (pure [1..nballs])
-- It is more efficient to create if statements, rather than
-- create an SExp(IntMap Color).
let observe i =
let true_color = balls ! i in
if_ ((== i) <$> b) $
dist (color `condition` observed_color) true_color
foldr observe (return (pure color)) [1..maxBalls]
-- The model takes a list of observations as an argument
cballs_model :: [Color] -> Model Int
cballs_model obs = do
balls <- balls_prior maxBalls
nballs <- dist uniformly (pure [1..maxBalls])
let obs_number i =
if_ ((== i) <$> nballs) $ balls_observe balls obs i
foldr obs_number (return (pure())) [1..maxBalls]
return nballs
cbr = hist $ mcmC 10000 (cballs_model (replicate 10 Blue))
-- fromList [(1,4265),(2,2203),(3,1244),(4,805),(5,486),(6,291),(7,565),(8,141)]
-- Good agreementIn Scheme, with dispatch the two foldrs of nested if_:
(begin
(define (opposite-color color) (case color ((blue) 'green) ((green) 'blue)))
(define (observed-color color)
(categorical `((,color . 0.8) (,(opposite-color color) . 0.2))))
(define (balls-prior n)
(do/monad
(← balls (replicate/monad n (dist uniformly '(blue green))))
(return (map cons (iota n 1) balls))))
(define (dispatch x keys branch otherwise)
(fold-right
(λ (key rest) (if_ (lift (cut equal? <> key) x) (branch key) rest))
otherwise
keys))
(define (balls-observe balls obs nballs max-balls)
(>> (for-each/monad
(λ (color)
(do/monad
(← b (dist uniformly (iota nballs 1)))
(dispatch
b
(iota max-balls 1)
(λ (i)
(dist (condition color observed-color)
(alist-ref i balls)))
(return color))))
obs)
(return (void))))
(define (cballs-model obs #!optional (max-balls 8))
(do/monad
(← balls (balls-prior max-balls))
(← nballs (dist uniformly (iota max-balls 1)))
(dispatch
nballs
(iota max-balls 1)
(λ (n) (balls-observe balls obs n max-balls))
(return (void)))
(return nballs))))
Given that ten balls were drawn and all looked blue, the posterior of the
number of balls has a closed form: with
(define (cballs-closed-form max-balls obs)
(define (binomial n k)
(fold (λ (i c) (/ (* c (- n i)) (add1 i))) 1 (iota k)))
(define (seen color n k)
(let1 (blue (/ (+ (* 0.8 k) (* 0.2 (- n k))) n))
(if (eq? color 'blue) blue (- 1.0 blue))))
(define (likelihood n)
(fold (λ (k acc)
(+ acc
(* (binomial n k)
(expt 2.0 (- n))
(fold (λ (color p) (* p (seen color n k))) 1.0 obs))))
0.0
(iota (add1 n))))
(let* ((ls (map likelihood (iota max-balls 1))) (z (fold + 0.0 ls)))
(map (λ (n l) (cons n (/ l z))) (iota max-balls 1) ls)))
which is
((1 0.41196368095257)
(2 0.209728624754566)
(3 0.120691976674053)
(4 0.0801847685132436)
(5 0.0590318166419537)
(6 0.046603790739908)
(7 0.0386298621005105)
(8 0.0331654796231948))
cballs-model on an urn of at most 3 balls with 4 blue
observations, (exact-posterior (cballs-model (make-list 4 'blue) 3)), within
((1 . 0.457651641624931) (2 . 0.298386199220923) (3 . 0.243962159154146))
dispatch that its tests leave inactive. With 8
balls and 10 observations enumeration is out of reach (the paths number
- MCMC, 20000 samples from seed 17: P(1) within .13 of the exact .41196 (over seeds
1-20 P(1) has mean .417 and sd .030, so .13 is about 4.3 sd), and
P(1) > P(2) > P(3):Single-site MH mixes slowly here: CB:46-49 explains why upstream dispatches on the number of balls first, since nballs and the drawn balls are strongly correlated; C1′ runs the model it set aside.
((1 0.4397) (2 0.2066) (3 0.11305) (4 0.0882) (5 0.04605) (6 0.05185) (7 0.0448) (8 0.00975)) - Likelihood weighting, 10000 forward runs of the same model weighted by their
observations: every probability within .05 of the closed form:
((1 0.418438355439409) (2 0.212643519168293) (3 0.111059542060967) (4 0.0904212532791994) (5 0.0568639053222174) (6 0.0442100525842873) (7 0.0347098277987992) (8 0.0316535443468282))
The paper reports 16 seconds for 10000 samples in GHCi bytecode (design.txt l.609-616), against Hansei's 13.4 s for 5000 importance samples and BLOG's 35 s. Hakaru10 has no counterpart of the second question, whether the same ball was drawn twice, which only Hansei answers (ColoredBalls.hs:154-185: a 25% chance that some ball was drawn only once).
(define (test/colored-balls _))
((eta 2.51) (memory #(12582912 3243232 1048576)) (stdout "") (stderr ""))
test/colored-balls/direct: pass
C1′. ColoredBalls.hs:46-72 keeps, commented out, a first version of cballs_model in which every drawn ball is uniform among the nballs balls, a support that depends on a random variable:
{-
Problem with the following model: nballs and b below are strongly correlated.
Hence if nballs is drawn as 5 and b is drawn as, say, 3, all the
proposals to resample nbalss as 1 or 2 will be rejected.
-- The model takes an array of observations as an argument
cballs_model :: [Color] -> Model Int
cballs_model obs = do
balls <- balls_prior maxBalls
nballs <- dist uniformly (pure [1..maxBalls])
-- The same ball can be drawn twice as we return the drawn balls
-- into the urn. A ball keeps its true color, no matter how many
-- times it is drawn.
let observe_ball color = do
b <- dist uniformly ((\n -> [1..n]) <$> nballs)
-- It is more efficient to create if statements, rather than
-- create an SExp(IntMap Color).
let observe i =
let true_color = balls ! i in
if_ ((== i) <$> b) $
dist (color `condition` categorical)
((\bc -> [(bc,0.8),(opposite_color bc,0.2)]) <$>
true_color)
foldr observe (return (pure color)) [1..maxBalls]
mapM_ observe_ball obs
return nballs
-}In Scheme the support is (lift (λ (n) (iota n 1)) nballs) and the inlined categorical is observed-color:
(define (cballs-model/direct obs #!optional (max-balls 8))
(do/monad
(← balls (balls-prior max-balls))
(← nballs (dist uniformly (iota max-balls 1)))
(for-each/monad
(λ (color)
(do/monad
(← b (dist uniformly (lift (λ (n) (iota n 1)) nballs)))
(dispatch
b
(iota max-balls 1)
(λ (i)
(dist (condition color observed-color) (alist-ref i balls)))
(return color))))
obs)
(return nballs)))
It is the same model: exact enumeration on the urn of at most 3 balls with 4
blue observations gives the closed form within
((1 . 0.457651641624931) (2 . 0.298386199220923) (3 . 0.243962159154146))
26
302
((2 0.41845)
(3 0.01495)
(4 0.1611)
(5 0.22275)
(6 0.0668)
(7 0.08425)
(8 0.0317))
(define (test/colored-balls/direct _))
((eta 1.719) (memory #(12582912 4205872 1048576)) (stdout "") (stderr ""))
test/birds/neighbors: pass
C2. The grid of the PPAML challenge problem: bird migration (Tom Dietterich and Shahed Sorower, version 6, August 30, 2014): cells are
numbered in Matlab order, column major (BirdMigrationProblem.txt:103-122, 140), so
that on the 4x4 grid of the 1-bird data set cell 1 is the bottom-left corner, 4 the
top-left, 13 the bottom-right and 16 the top-right; divMod (n-1) 4 answers the column and the row. A bird flies at most
neighbors :: CellNum -> [CellNum]
neighbors n = filter (\i -> dist n i <= d_max) [1..nCells]
where
dist n m = let (ix,iy) = divMod (n-1) gridDim
(jx,jy) = divMod (m-1) gridDim
in (ix-jx)^2+(iy-jy)^2
d_max = 18(begin
(define (cell->xy n #!optional (dim grid-dim))
(let1 (m (sub1 n)) (cons (quotient m dim) (remainder m dim))))
(define squared-distance
(Λ ((,ix unquote iy) (,jx unquote jy))
(+ (* (- ix jx) (- ix jx)) (* (- iy jy) (- iy jy)))))
(define (neighbors n #!optional (dim grid-dim))
(let1 (from (cell->xy n dim))
(filter
(λ (m) (<= (squared-distance from (cell->xy m dim)) 18))
(iota (* dim dim) 1)))))
The 4x4 grid's diagonal is
(1 2 3 4 5 11 12 13 14 15 21 22 23 24 31 32 33 34 41 42)
(define (test/birds/neighbors _)
(⊦ equal? '((0 . 0) (0 . 3) (3 . 0) (3 . 3)) (map cell->xy '(1 4 13 16)))
(⊦ equal?
'((0 . 0) (0 . 9) (9 . 0) (9 . 9))
(map (cut cell->xy <> 10) '(1 10 91 100)))
(⊨ (every (λ (n) (equal? (iota 16 1) (neighbors n))) (iota 16 1)))
(⊦ equal?
'(1 2 3 4 5 11 12 13 14 15 21 22 23 24 31 32 33 34 41 42)
(neighbors 1 10))
(⊦ = 20 (length (neighbors 1 10))))
((eta 0.002) (memory #(12582912 3250680 1048576)) (stdout "") (stderr ""))
test/birds/transitions: pass
C3. The migration model is log-linear (BirdMigrationProblem.txt:32-33, 60-79):
the potential of a flight from cell
Birds1.hs computes the numerators, phi (B1:82-85) and theta (B1:95-99), and leaves the normalization to categorical (Distribution.hs:87-91), which divides each weight
by their sum; birdpos (B1:76-80) finds the bird where exactly
one cell counts it:
-- where is the single bird on the day d of the year y
birdpos :: ObsData -> Year -> Day -> Maybe CellNum
birdpos obs y d = case [i | i <- [1..nCells], obs ! (y,d,i) > 0] of
[i] -> Just i
_ -> Nothingphi :: Year -> Day -> CellNum -> CellNum -> Features -> [Double] -> Double
phi y d from to features bs = sum $ zipWith (*) fs bs
where
fs = [ realToFrac $ features ! (y, d, from, to, i) | i <- [1..nFeatures]]-- multinomial (0; a,b,c) --> 0
-- multinomial (1,theta(i,1),,,theta(i,16)) -->
-- categorical [(1,theta(i,1)),(2,(theta(i,2))),... (16,theta(i,16))]
theta :: Year -> Day -> CellNum -> CellNum -> Features -> [Double] -> Double
theta y d from to features bs = exp $ phi y d from to features bs(begin
(define (phi fs bs) (fold + 0.0 (map * fs bs)))
(define (theta fs bs) (exp (phi fs bs)))
(define ((transitions features year day) n bs)
(map (λ (j) (cons j (theta (features year day n j) bs))) (neighbors n)))
(define (birdpos obs year day)
(match/first
(filter (λ (i) (positive? (obs year day i))) (iota n-cells 1))
((,i) i)
(else #f))))
The checks: the weights of the flights from cell 6 on the first night, under
categorical as probabilities summing to 1 within
birdpos finds the bird on days 1
and 2 (a count of 2 in one cell still is one cell) and not on day 3 (two cells)
or day 4 (none).
Each count is a Poisson observation whose intensity is the number of birds in
the cell, 0 or 1 with one bird (l.134-138, D:358-367): seeing no bird where it
is has LL
logDensity (Poisson lambda expl) x
| x == 0 = -lambda
| lambda == 0 = minusInf
| x == 1 = log lambda - lambda
| x == 2 = log (lambda*lambda/2) - lambdacf. Distribution.hs:361-365.
(define (test/birds/transitions _))
((eta 0.001) (memory #(12582912 3263336 1048576)) (stdout "") (stderr ""))
test/birds/synthetic: pass
C4. The bird migration problem, quoting the Hakaru10 page (index.html:122-128):
❝
Bird migration problem was one of the Challenge problems of the DARPA PPAML (Probabilistic Programming for Advanced Machine Learning) Program. The goal was to estimate the model parameters from the set of bird migration observations over the period of thirty years. The migration model is a log-linear model with four parameters that gives the probability of a bird flying from cell i to cell j under given conditions.
The problem statement (BirdMigrationProblem.txt:19-79) models the population as
a collective graphical model: every night each bird flies from cell
model :: Features -> ObsData -> Model [Double]
model features obs = do
b1 <- abs <$> dist normal 0 10 -- see Statement of Problem, p9
b2 <- abs <$> dist normal 0 10 -- see Statement of Problem, p9
b3 <- abs <$> dist normal 0 10 -- see Statement of Problem, p9
b4 <- abs <$> dist normal 0 10 -- see Statement of Problem, p9
let bs = collect [b1,b2,b3,b4]
forM_ [1..nYears] $ \year -> do -- each year
foldM_ (\n day -> do -- one bird exists in cell n on each day(1..nDays-1)
let trans n bs = [ (j,theta year day n j features bs) | j<-neighbors n]
-- n' cell where the bird is on day+1
let day' = day + 1
{-
n' <- case birdpos obs year day' of
Just nreal -> dist (nreal `condition` categorical) $
liftA2 trans n bs
Nothing -> dist categorical $ liftA2 trans n bs
-}
n' <- dist categorical $ liftA2 trans n bs
forM_ [1..nCells] $ \i ->
dist (fromIntegral (obs ! (year,day',i)) `condition` poisson)
((\nv -> if i==nv then 1 else 0) <$> n')
return n'
) (1 :: SExp Int) [1..nDays-1]
return bs(define (birds features obs #!key (years 30) (days 20) (pin #t))
(do/monad
(← betas
(replicate/monad
n-features
(<$> (cut lift abs <>) (dist normal 0.0 10.0))))
(let bs (collect betas))
(for-each/monad
(λ (year)
(fold/monad
(λ (n day)
(do/monad
(let day′ (add1 day))
(let trans (lift (transitions features year day) n bs))
(let nreal (and pin (birdpos obs year day′)))
(← n′
(dist (if nreal (condition nreal categorical) categorical)
trans))
(for-each/monad
(λ (i)
(dist (condition (obs year day′ i) poisson)
(lift (λ (nv) (if (= i nv) 1 0)) n′)))
(iota n-cells 1))
(return n′)))
1
(iota (sub1 days) 1)))
(iota years 1))
(return bs)))
forM_ is for-each/monad, foldM_ is fold/monad, threading the bird's cell, liftA2 is lift, and the four abs <$> dist normal 0 10 are made by replicate/monad. The test uses upstream's
commented-out alternative
(B1:62-67), #:pin #t: on a day where the bird is seen its cell
is observed, elsewhere it is drawn. The posterior of
skipped.
The synthetic data set has 2 years of 6 days. Its features follow
the covariates of the problem's FAQ (l.430-443), with a wind drawn each night from
seed 17; its flights are simulated by running the generative half of the model,
with
(begin
(define (synthetic-features years days #!key (seed 17))
(define (lognpdf x mu sigma)
(/ (exp (- (/ (expt (- (log x) mu) 2) (* 2.0 sigma sigma))))
(* x sigma (sqrt (* 2.0 pi)))))
(define (cosine u v)
(let ((nu (sqrt (squared-distance u '(0 . 0))))
(nv (sqrt (squared-distance v '(0 . 0)))))
(if (or (zero? nu) (zero? nv))
0.0
(/ (+ (* (car u) (car v)) (* (cdr u) (cdr v))) (* nu nv)))))
(define (vector-to p q) (cons (- (car q) (car p)) (- (cdr q) (cdr p))))
(let* ((src (make-stdgen-source seed))
(winds (list->vector
(map-in-order
(λ (_)
(receive
(angle ll)
(distribution-sample (uniform 0.0 (* 2.0 pi)) src)
(cons (cos angle) (sin angle))))
(iota (* years days)))))
(top-right (cell->xy n-cells)))
(memoize
(λ (year day from to)
(if (= from to)
'(0.0 0.0 0.0 2.0)
(let* ((p (cell->xy from)) (v (vector-to p (cell->xy to))))
(list (* 4.0
(lognpdf (sqrt (squared-distance v '(0 . 0))) 1.0 1.0))
(cosine v (vector-to p top-right))
(cosine
v
(vector-ref winds (+ (* (sub1 year) days) (sub1 day))))
0.0)))))))
(define (bird-flights features bs #!key (years 30) (days 20))
(<$> collect
(traverse/monad
(λ (year)
(<$> (λ (path) (collect (reverse path)))
(fold/monad
(λ (path day)
(<$> (cut cons <> path)
(dist categorical
(lift (transitions features year day)
(car path)
bs))))
'(1)
(iota (sub1 days) 1))))
(iota years 1))))
(define ((indicator-counts paths missed?) year day cell)
(if (and (not (missed? day))
(= cell (list-ref (list-ref paths (sub1 year)) (sub1 day))))
1
0)))
((1 6 8 10 12 12) (1 11 8 15 16 13))
Every third day the watchers miss the bird (a Poisson(1) count is 0 with
probability
birdpos recovers the others. The model has
mcmC-fold over 3000 samples sums the
2605
(4.12945104050349 9.82328450023311 1.3251745427761 2.08709026263613)
(birds features obs #:years 0 #:days days), whose
(7.87600232652835 8.15640125395766 8.22387046673009 8.15729590109516)
hakaru10-birds.scm. Upstream's thesis run took 9 minutes for a
million samples, against BLOG's 49 minutes for a thousand (index.html:163-167).(define (test/birds/synthetic _))
((eta 1.466) (memory #(12582912 3609088 1048576)) (stdout "") (stderr ""))
test/regression: pass
C5. Bayesian linear regression, problem 4.1 of the PPAML small problems collection, quoting design.txt l.464-470:
❝The model is typeset on page 10 of the paper (design.txt l.471-486), here in ASCII Haskell; its helpers, which the paper describes in l.496-504, are Birds1.hs:103-114:
Given the set of training points (x_ij, y_i), i = 1..N, j = 1..k and the generative model y_i = Σ_j x_ij * w_j + noise_i find the posterior distribution on w_j. In the conventional linear regression language, we are given N observations of y assumed to be a linear combination of the controlled quantities x with the parameters w; we have to estimate w.
type RegrDatum = ([Double],Double) -- Xs and Y
model :: [RegrDatum] -> Model [Double]
model xsy = do
let mu = replicate dimK 0
w_mean <- normals mu 2
w <- normals w_mean 1
noise_sd <- (1/) <$> dist gamma 0.5 0.5
let make_cond (xs,y) = dist (y `condition` normal) (dot xs w) noise_sd
mapM make_cond xsy
return $ collect w-- normals means std
-- returns several independently distributed normal random variables
-- whose means are given by the list means, with the same std
normals :: [SExp Double] -> SExp Double -> MCMCM [SExp Double]
normals means std = mapM (\m -> dist normal m std) means
dot :: [Double] -> [SExp Double] -> SExp Double
dot xs ays = foldr1 (+) $ zipWith (\x ay -> (*x) <$> ay) xs ays
collect :: [SExp a] -> SExp [a]
collect = sequenceAIn Scheme collect is exported by (aux hakaru10), sexp+ and sexp* lift the arithmetic, reduce-right is foldr1, and mapM is traverse/monad, or for-each/monad where its results are dropped:
(begin
(define (normals means sd) (traverse/monad (λ (m) (dist normal m sd)) means))
(define (dot xs ays) (reduce-right sexp+ 0.0 (map sexp* ays xs)))
(define (regression data)
(do/monad
(← w-mean (normals (make-list (length (car (car data))) 0.0) 2.0))
(← w (normals w-mean 1.0))
(← inverse-noise-sd (dist gamma 0.5 0.5))
(let noise-sd (lift (cut / 1.0 <>) inverse-noise-sd))
(for-each/monad
(Λ ((,xs unquote y)) (dist (condition y normal) (dot xs w) noise-sd))
data)
(return (collect w)))))
The paper's instance has five hundred points and five parameters, a graph of
"511 vertices" (design.txt l.493-495; the hakaru10-language suite counts it).
Here twenty points with two features are simulated from
(define (regression-data xss w sd)
(<$> collect
(traverse/monad
(λ (xs)
(<$> (cut lift (cut cons xs <>) <>) (dist normal (dot xs w) sd)))
xss)))
The graph has
(1.60859970223401 -0.932442673713944)
(1.62928672635352 -0.926548718746949)
(define (least-squares/2 data)
(define (sum f) (fold (Λ ((,xs unquote y) ,acc) (+ acc (f xs y))) 0.0 data))
(let* ((a (sum (λ (xs y) (* (first xs) (first xs)))))
(b (sum (λ (xs y) (* (first xs) (second xs)))))
(d (sum (λ (xs y) (* (second xs) (second xs)))))
(u (sum (λ (xs y) (* (first xs) y))))
(v (sum (λ (xs y) (* (second xs) y))))
(det (- (* a d) (* b b))))
(list (/ (- (* d u) (* b v)) det) (/ (- (* a v) (* b u)) det))))
(define (test/regression _))
((eta 0.708) (memory #(12582912 3745624 1048576)) (stdout "") (stderr ""))
test/plot-mixture: pass
C6. The only use of plot upstream, Examples1.hs:166-167,
writes for gnuplot the histogram, in bins of 0.2, of the mixture of
Examples1.hs:159-164 (whose KL divergence from mixng the hakaru10-replay suite replays, X12): 5000 samples of a normal of mean 10
and sd 2 followed by 5000 of a gamma of shape 3 and scale 1/3:
_ =
let n = 10000 in plot (tabulate 0.2 $ mcmC (n `div` 2) (dist normal 10 2) ++ mcmC (n `div` 2) (dist gamma 3 (1/3)))Upstream writes /tmp/plot.dat; here plot's optional path is a temporary file, read back and deleted. It has the header
and one line per bin,
86
5.42921999999999
(define (test/plot-mixture _))
((eta 0.024) (memory #(12582912 3278520 1048576)) (stdout "") (stderr ""))