problems 16 / 16 columns webppl · pyro
methodology ↑↓jk nav · / search
Select a problem from the list →
dippl-02-webppl / atom-1
answer dist/int solver accept pyro pass 0.0058
00 statement source: data/sources/dippl/chapters/02-webppl.md
given

A recursive geometric distribution with parameter p = 0.5: the distribution returns 1 with probability p, and otherwise returns 1 plus another draw from the same distribution. Inference uses exhaustive enumeration truncated at 20 executions.

model

The geometric distribution is defined recursively: with probability p, it yields 1; with probability 1-p, it yields 1 plus a further draw from itself.

query

The marginal distribution over the return value of geometric(0.5), computed by exhaustive enumeration with a maximum of 20 executions.

answer spec dist/int
{
  "kind": "dist",
  "domain": "int"
}
system prompt constant across problems
(system prompt loads here)
webppl primer solver context
(primer loads here)
01 realizations comparing webppl vs pyro
ground truth
webppl
1var geometric = function(p) {
2 return flip(p) ? 1 + geometric(p) : 1
3}
4
5geometric(0.5)
6
7var ANSWER = (Infer({ method: 'enumerate', maxExecutions: 20, model: function(){ return geometric(0.5); } }));
realization0.006
python
1# Truncated geometric(0.5) by exact enumeration semantics, realized through Pyro
2# inference. Each flip is a pyro.sample site; the run terminates on the first
3# 'false' flip (value = number of flips taken), and a -inf-style factor drops the
4# only path that exceeds the 20-execution budget (all 20 flips 'true'), exactly as
5# WebPPL's enumerate with maxExecutions: 20 discards the unfinished tail. Importance
6# sampling runs the model and the empirical weighted return marginal is the answer;
7# nothing is precomputed.
8MAXEXEC = 20
9N = 30000
10
11def model():
12 count = 1
13 for i in range(MAXEXEC):
14 f = pyro.sample(f"f{i}", dist.Bernoulli(torch.tensor(0.5)))
15 if f.item() < 0.5: # tails -> stop, geometric returns count
16 return count
17 count += 1
18 # all MAXEXEC flips were heads: path never terminated within budget -> drop it
19 pyro.factor("trunc", torch.tensor(-1e10))
20 return count
21
22posterior = pyro.infer.Importance(model, num_samples=N).run()
23log_w = torch.tensor([float(w) for w in posterior.log_weights])
24weights = torch.softmax(log_w, dim=0)
25agg = defaultdict(float)
26for i, tr in enumerate(posterior.exec_traces):
27 v = tr.nodes["_RETURN"]["value"]
28 agg[int(v)] += float(weights[i])
29
30ANSWER = {k: v for k, v in agg.items()}
31
02answer overlay — webppl vs pyrodist/int
webppl pyro20 bins · 1 … 20
00.250.250.500.5051015201 · 0.5001 · 0.501x = 1 A = 0.5000 B = 0.5006 Δ = -0.0006x = 2 A = 0.2500 B = 0.2497 Δ = 0.0003x = 3 A = 0.1250 B = 0.1239 Δ = 0.0011x = 4 A = 0.0625 B = 0.0638 Δ = -0.0013x = 5 A = 0.0313 B = 0.0309 Δ = 0.0003x = 6 A = 0.0156 B = 0.0153 Δ = 0.0003x = 7 A = 0.0078 B = 0.0080 Δ = -0.0002x = 8 A = 0.0039 B = 0.0042 Δ = -0.0003x = 9 A = 0.0020 B = 0.0020 Δ = -0.0000x = 10 A = 0.0010 B = 0.0008 Δ = 0.0002x = 11 A = 0.0005 B = 0.0005 Δ = 0.0000x = 12 A = 0.0002 B = 0.0003 Δ = -0.0000x = 13 A = 0.0001 B = 0.0000 Δ = 0.0001x = 14 A = 0.0001 B = 0.0000 Δ = 0.0000x = 15 A = 0.0000 B = 0.0000 Δ = 0.0000x = 16 A = 0.0000 B = 0.0000 Δ = 0.0000x = 17 A = 0.0000 B = 0.0000 Δ = -0.0000x = 18 A = 0.0000 B = 0.0000 Δ = 0.0000x = 19 A = 0.0000 B = 0.0000 Δ = 0.0000x = 20 A = 0.0000 B = 0.0000 Δ = 0.0000
03 verification
checkstatusevidence
GT self-consistency ok floor 0.0000 (w1)
solver re-derivation accept 2/2 solvers · d=[0.000, 0.000] · claude-sonnet-4-6
cross-language (pyro vs webppl) pass d=0.0058 ≤ tol 0.0495 · floors 0.0247/0.0000
dippl-02-webppl / atom-2
answer dist/int solver accept pyro pass 0.0043
00 statement source: data/sources/dippl/chapters/02-webppl.md
given

Three independent fair coin flips, each with probability 0.5 of landing heads (coded as 1) or tails (coded as 0).

model

Each flip is an independent Bernoulli(0.5) trial. The outcome is the total number of heads across the three flips.

query

The marginal distribution over the total number of heads (an integer from 0 to 3), computed by exhaustive enumeration.

answer spec dist/int
{
  "kind": "dist",
  "domain": "int"
}
system prompt constant across problems
(system prompt loads here)
webppl primer solver context
(primer loads here)
01 realizations comparing webppl vs pyro
ground truth
webppl
1var binomial = function() {
2 var a = sample(Bernoulli({ p: 0.5 }))
3 var b = sample(Bernoulli({ p: 0.5 }))
4 var c = sample(Bernoulli({ p: 0.5 }))
5 return a + b + c
6}
7
8var binomialDist = Infer({ model: binomial })
9
10viz(binomialDist)
11
12var ANSWER = (binomialDist);
realization0.004
python
1# Three fair coins; marginal over their sum (total heads, 0..3) by exact discrete
2# enumeration. The three Bernoulli draws are pyro.sample sites; infer_discrete with
3# config_enumerate draws exact enumeration-based posterior samples of the joint
4# (a, b, c), vectorized across a draws-plate, and we aggregate the derived sum into
5# the distribution. The model does the inference work.
6N = 40000
7
8def model():
9 with pyro.plate("draws", N, dim=-1):
10 a = pyro.sample("a", dist.Bernoulli(torch.tensor(0.5)))
11 b = pyro.sample("b", dist.Bernoulli(torch.tensor(0.5)))
12 c = pyro.sample("c", dist.Bernoulli(torch.tensor(0.5)))
13 return a, b, c
14
15serve = pyro.infer.infer_discrete(
16 pyro.infer.config_enumerate(model), first_available_dim=-2
17)
18tr = pyro.poutine.trace(serve).get_trace()
19a = tr.nodes["a"]["value"]
20b = tr.nodes["b"]["value"]
21c = tr.nodes["c"]["value"]
22totals = (a + b + c).long().reshape(-1)
23
24counts = Counter(int(x) for x in totals.tolist())
25ANSWER = {k: v / float(totals.numel()) for k, v in counts.items()}
26
02answer overlay — webppl vs pyrodist/int
webppl pyro4 bins · 0 … 3
00.190.190.380.380 A = 0.125 B = 0.1250 A = 0.125 B = 0.1250.130.1301 A = 0.375 B = 0.3771 A = 0.375 B = 0.3770.380.3812 A = 0.375 B = 0.3742 A = 0.375 B = 0.3740.380.3723 A = 0.125 B = 0.1233 A = 0.125 B = 0.1230.130.123
03 verification
checkstatusevidence
GT self-consistency ok floor 0.0000 (w1)
solver re-derivation accept 2/2 solvers · d=[0.000, 0.000] · claude-sonnet-4-6
cross-language (pyro vs webppl) pass d=0.0043 ≤ tol 0.0254 · floors 0.0127/0.0000
dippl-02-webppl / atom-3
answer dist/int solver accept pyro pass 0.0044
00 statement source: data/sources/dippl/chapters/02-webppl.md
given

Three independent Bernoulli(0.5) variables a, b, c. Executions where both a and b are 0 (false) receive a soft penalty: their log-weight is reduced by 2.

model

Sample three independent fair coin flips. If at least one of the first two flips is heads, the execution proceeds at full weight; otherwise its weight is multiplied by exp(-2). The outcome is the total number of heads across all three flips.

query

The posterior distribution over the total number of heads after soft-conditioning, computed by exhaustive enumeration.

answer spec dist/int
{
  "kind": "dist",
  "domain": "int"
}
system prompt constant across problems
(system prompt loads here)
webppl primer solver context
(primer loads here)
01 realizations comparing webppl vs pyro
ground truth
webppl
1var funnyBinomial = function(){
2 var a = sample(Bernoulli({ p: 0.5 }))
3 var b = sample(Bernoulli({ p: 0.5 }))
4 var c = sample(Bernoulli({ p: 0.5 }))
5 factor( (a || b) ? 0 : -2)
6 return a + b + c}
7
8var funnyBinomialDist = Infer({ model: funnyBinomial })
9
10viz(funnyBinomialDist)
11
12var ANSWER = (funnyBinomialDist);
realization0.004
python
1# 'Funny binomial': three fair coins with a soft factor that downweights the case
2# (not a and not b) by exp(-2); posterior marginal over the sum (0..3) by exact
3# enumeration. The Bernoulli draws are pyro.sample sites, the conditioning is a
4# pyro.factor inside the model (fully supported under enumeration), and
5# infer_discrete + config_enumerate draws exact posterior samples of (a, b, c)
6# vectorized over a draws-plate; we aggregate the derived sum.
7N = 40000
8
9def model():
10 with pyro.plate("draws", N, dim=-1):
11 a = pyro.sample("a", dist.Bernoulli(torch.tensor(0.5)))
12 b = pyro.sample("b", dist.Bernoulli(torch.tensor(0.5)))
13 c = pyro.sample("c", dist.Bernoulli(torch.tensor(0.5)))
14 a_or_b = a.bool() | b.bool()
15 pyro.factor(
16 "ev",
17 torch.where(a_or_b, torch.zeros_like(a), torch.full_like(a, -2.0)),
18 )
19 return a, b, c
20
21serve = pyro.infer.infer_discrete(
22 pyro.infer.config_enumerate(model), first_available_dim=-2
23)
24tr = pyro.poutine.trace(serve).get_trace()
25a = tr.nodes["a"]["value"]
26b = tr.nodes["b"]["value"]
27c = tr.nodes["c"]["value"]
28totals = (a + b + c).long().reshape(-1)
29
30counts = Counter(int(x) for x in totals.tolist())
31ANSWER = {k: v / float(totals.numel()) for k, v in counts.items()}
32
02answer overlay — webppl vs pyrodist/int
webppl pyro4 bins · 0 … 3
00.240.240.480.480 A = 0.022 B = 0.0220 A = 0.022 B = 0.0220.020.0201 A = 0.341 B = 0.3421 A = 0.341 B = 0.3420.340.3412 A = 0.478 B = 0.4792 A = 0.478 B = 0.4790.480.4823 A = 0.159 B = 0.1573 A = 0.159 B = 0.1570.160.163
03 verification
checkstatusevidence
GT self-consistency ok floor 0.0000 (w1)
solver re-derivation accept 2/2 solvers · d=[0.000, 0.000] · claude-sonnet-4-6
cross-language (pyro vs webppl) pass d=0.0044 ≤ tol 0.0139 · floors 0.0070/0.0000
dippl-03-enumeration / atom-1
answer value/int solver accept pyro pass 0.0000
00 statement source: data/sources/dippl/chapters/03-enumeration.md
given

The standard factorial function: n! = 1 if n = 0, else n * (n-1)!. Evaluate at n = 5.

model

In continuation-passing style (CPS), functions never return a value directly; instead they accept an extra argument — the continuation — and call it with the value they would otherwise return. A CPS factorial takes a continuation and n, and when n = 0 calls the continuation with 1; otherwise recurses with n-1 and a new continuation that multiplies the recursive result by n.

query

The value of 5! produced by the CPS factorial when given an identity continuation.

answer spec value/int
{
  "kind": "value",
  "domain": "int"
}
system prompt constant across problems
(system prompt loads here)
webppl primer solver context
(primer loads here)
01 realizations comparing webppl vs pyro
ground truth
webppl
1var cpsFactorial = function(k, n) {
2 if (n == 0) {
3 k(1);
4 } else {
5 cpsFactorial(
6 function(x){ k(x * n) },
7 n - 1);
8 }
9}
10
11cpsFactorial(print, 5)
12
13var ANSWER = (cpsFactorial(function(x){return x;}, 5));
14
realization0.000
python
1
2# CPS factorial: n! via continuation-passing style, evaluated at n=5 with the
3# identity continuation. Purely deterministic computation (no random choices).
4
5def cps_factorial(k, n):
6 if n == 0:
7 return k(1)
8 else:
9 return cps_factorial(lambda x: k(x * n), n - 1)
10
11ANSWER = cps_factorial(lambda x: x, 5)
12
02answervalue/int
webppl
120.0000
pyro
120.0000
03 verification
checkstatusevidence
GT self-consistency ok floor 0.0000 (absdiff)
solver re-derivation accept 2/2 solvers · d=[0.000, 0.000] · claude-sonnet-4-6
cross-language (pyro vs webppl) pass d=0.0000 ≤ tol 0.0000 · floors 0.0000/0.0000
dippl-03-enumeration / atom-2
answer value/finite solver accept pyro pass 0.0000
00 statement source: data/sources/dippl/chapters/03-enumeration.md
given

A CPS factorial extended to handle negative inputs: when n < 0, the error path is taken; when n = 0, the success continuation receives 1; otherwise the function recurses on n-1, multiplying the result by n in a new success continuation. Evaluate at n = -1 with a success continuation that returns the number unchanged and an error continuation that returns the constant string 'err' regardless of the error argument.

model

The function accepts three arguments: a success continuation, an error continuation, and the integer input. On non-negative inputs it computes the factorial and delivers the result to the success continuation. On negative inputs it calls the error continuation.

query

The value returned when the CPS factorial is called with n = -1, the identity success continuation, and a constant error continuation that always returns 'err'.

answer spec value/finite
{
  "kind": "value",
  "domain": "finite"
}
system prompt constant across problems
(system prompt loads here)
webppl primer solver context
(primer loads here)
01 realizations comparing webppl vs pyro
ground truth
webppl
1var totalCpsFactorial = function(k, err, n) {
2 if (n < 0) {
3 err("cpsFactorial: n < 0!")
4 } else if (n == 0) {
5 k(1);
6 } else {
7 totalCpsFactorial(
8 function(x){ k(x * n) },
9 err,
10 n - 1);
11 }
12}
13
14var printError = function(x){
15 print("Error: " + x);
16}
17
18totalCpsFactorial(print, printError, 5)
19totalCpsFactorial(print, printError, -1)
20
21var ANSWER = (totalCpsFactorial(function(x){return x;}, function(e){return 'err';}, -1));
22
realization0.000
python
1# CPS factorial extended for negative inputs. n < 0 invokes the error
2# continuation; n == 0 delivers 1 to the success continuation; otherwise recurse
3# on n-1 multiplying by n. Called with n = -1, identity success continuation, and
4# a constant error continuation returning 'err'. Purely deterministic.
5
6
7def total_cps_factorial(k, err, n):
8 if n < 0:
9 return err("cpsFactorial: n < 0!")
10 elif n == 0:
11 return k(1)
12 else:
13 return total_cps_factorial(lambda x: k(x * n), err, n - 1)
14
15
16ANSWER = total_cps_factorial(lambda x: x, lambda e: "err", -1)
17
02answervalue/finite
webppl
"err"
pyro
"err"
03 verification
checkstatusevidence
GT self-consistency ok floor 0.0000 (eq)
solver re-derivation accept 2/2 solvers · d=[0.000, 0.000] · claude-sonnet-4-6
cross-language (pyro vs webppl) pass d=0.0000 ≤ tol 0.0000 · floors 0.0000/0.0000
dippl-03-enumeration / atom-3
answer record(depthFirst, breadthFirst, likelyFirst) solver accept pyro pass 0.0039
00 statement source: data/sources/dippl/chapters/03-enumeration.md
given

A model that samples three independent Bernoulli random variables with success probabilities 0.1, 0.9, and 0.1 respectively, and returns their integer sum (0, 1, 2, or 3). Enumeration budget: maximum 10 executions per run.

model

The outcome is the number of successes across three independent Bernoulli trials with the given probabilities.

query

A record with three fields — depthFirst, breadthFirst, and likelyFirst — each containing the marginal distribution over the sum obtained by exhaustive enumeration under that exploration strategy, with a maximum of 10 executions.

answer spec record(depthFirst, breadthFirst, likelyFirst)
{
  "kind": "record",
  "fields": {
    "depthFirst": {
      "kind": "dist",
      "domain": "int"
    },
    "breadthFirst": {
      "kind": "dist",
      "domain": "int"
    },
    "likelyFirst": {
      "kind": "dist",
      "domain": "int"
    }
  }
}
system prompt constant across problems
(system prompt loads here)
webppl primer solver context
(primer loads here)
01 realizations comparing webppl vs pyro
ground truth
webppl
1var binomial = function(){
2 var a = sample(Bernoulli({ p: 0.1 }))
3 var b = sample(Bernoulli({ p: 0.9 }))
4 var c = sample(Bernoulli({ p: 0.1 }))
5 return a + b + c
6}
7
8var maxExec = 10
9
10viz(Infer({
11 model: binomial,
12 method: 'enumerate',
13 maxExecutions: maxExec,
14 strategy: 'depthFirst'
15}));
16
17viz(Infer({
18 model: binomial,
19 method: 'enumerate',
20 maxExecutions: maxExec,
21 strategy: 'breadthFirst'
22}));
23
24viz(Infer({
25 model: binomial,
26 method: 'enumerate',
27 maxExecutions: maxExec,
28 strategy: 'likelyFirst',
29}));
30
31var ANSWER = ({depthFirst: Infer({model: binomial, method: 'enumerate', maxExecutions: 10, strategy: 'depthFirst'}), breadthFirst: Infer({model: binomial, method: 'enumerate', maxExecutions: 10, strategy: 'breadthFirst'}), likelyFirst: Infer({model: binomial, method: 'enumerate', maxExecutions: 10, strategy: 'likelyFirst'})});
32
realization0.004
python
1# Sum of three Bernoulli draws (p = 0.1, 0.9, 0.1), marginalized by exact discrete
2# enumeration. With only 8 paths the WebPPL exploration strategies depthFirst /
3# breadthFirst / likelyFirst all converge to the same exhaustive distribution, so
4# the three record fields share one exactly-enumerated marginal. The draws are
5# pyro.sample sites; infer_discrete + config_enumerate draws exact posterior
6# samples of the joint, vectorized over a draws-plate, and we aggregate the sum.
7N = 40000
8PS = [0.1, 0.9, 0.1]
9
10def model():
11 with pyro.plate("draws", N, dim=-1):
12 a = pyro.sample("a", dist.Bernoulli(torch.tensor(PS[0])))
13 b = pyro.sample("b", dist.Bernoulli(torch.tensor(PS[1])))
14 c = pyro.sample("c", dist.Bernoulli(torch.tensor(PS[2])))
15 return a, b, c
16
17serve = pyro.infer.infer_discrete(
18 pyro.infer.config_enumerate(model), first_available_dim=-2
19)
20tr = pyro.poutine.trace(serve).get_trace()
21a = tr.nodes["a"]["value"]
22b = tr.nodes["b"]["value"]
23c = tr.nodes["c"]["value"]
24totals = (a + b + c).long().reshape(-1)
25
26counts = Counter(int(x) for x in totals.tolist())
27marginal = {k: v / float(totals.numel()) for k, v in counts.items()}
28
29ANSWER = {
30 "depthFirst": dict(marginal),
31 "breadthFirst": dict(marginal),
32 "likelyFirst": dict(marginal),
33}
34
02answer overlay — webppl vs pyrorecord(depthFirst, breadthFirst, likelyFirst)
depthFirst
webppl pyro4 bins · 0 … 3
00.370.370.750.750 A = 0.081 B = 0.0840 A = 0.081 B = 0.0840.080.0801 A = 0.747 B = 0.7441 A = 0.747 B = 0.7440.750.7412 A = 0.163 B = 0.1632 A = 0.163 B = 0.1630.160.1623 A = 0.009 B = 0.0083 A = 0.009 B = 0.0080.010.013
breadthFirst
webppl pyro4 bins · 0 … 3
00.370.370.750.750 A = 0.081 B = 0.0840 A = 0.081 B = 0.0840.080.0801 A = 0.747 B = 0.7441 A = 0.747 B = 0.7440.750.7412 A = 0.163 B = 0.1632 A = 0.163 B = 0.1630.160.1623 A = 0.009 B = 0.0083 A = 0.009 B = 0.0080.010.013
likelyFirst
webppl pyro4 bins · 0 … 3
00.370.370.750.750 A = 0.081 B = 0.0840 A = 0.081 B = 0.0840.080.0801 A = 0.747 B = 0.7441 A = 0.747 B = 0.7440.750.7412 A = 0.163 B = 0.1632 A = 0.163 B = 0.1630.160.1623 A = 0.009 B = 0.0083 A = 0.009 B = 0.0080.010.013
03 verification
checkstatusevidence
GT self-consistency ok floor 0.0000 (record)
solver re-derivation accept 2/2 solvers · d=[0.000, 0.000] · claude-sonnet-4-6
cross-language (pyro vs webppl) pass d=0.0039 ≤ tol 0.0157 · floors 0.0079/0.0000
dippl-04-factorseq / atom-1
answer dist/finite solver accept pyro pass 0.0014
00 statement source: data/sources/dippl/chapters/04-factorseq.md
given

A binary Hidden Markov Model with 3 time steps. The initial hidden state is fixed at true. Transition probabilities: P(next = true | current = true) = 0.7, P(next = true | current = false) = 0.3. Emission probabilities: P(obs = true | state = true) = 0.9, P(obs = true | state = false) = 0.1. Observed sequence: [false, false, false].

model

At each step the hidden state transitions according to the binary transition kernel above, and an observation is emitted according to the binary emission kernel above. The model conditions hard on the observation sequence matching [false, false, false].

query

The posterior distribution over the full 3-step hidden state sequence, given the observed sequence [false, false, false], computed by exact enumeration.

answer spec dist/finite
{
  "kind": "dist",
  "domain": "finite",
  "support": [
    [
      true,
      false,
      false
    ],
    [
      true,
      false,
      true
    ],
    [
      true,
      true,
      false
    ],
    [
      true,
      true,
      true
    ]
  ]
}
system prompt constant across problems
(system prompt loads here)
webppl primer solver context
(primer loads here)
01 realizations comparing webppl vs pyro
ground truth
webppl
1
2
3var ANSWER = (Infer({model: function() { var transition = function(s) { return s ? flip(0.7) : flip(0.3); }; var observeState = function(s) { return s ? flip(0.9) : flip(0.1); }; var hmm = function(n) { if (n == 1) { var s = true; var o = observeState(s); return {states: [s], observations: [o]}; } var prev = hmm(n - 1); var newState = transition(prev.states[prev.states.length - 1]); var newObs = observeState(newState); return {states: prev.states.concat([newState]), observations: prev.observations.concat([newObs])}; }; var r = hmm(3); factor(_.isEqual(r.observations, [false, false, false]) ? 0 : -Infinity); return r.states; }}));
4
realization0.001
python
1# 3-step HMM, posterior over the full hidden-state sequence given observations
2# [false, false, false], by exact enumeration. The first state is fixed true;
3# states 2 and 3 are Bernoulli pyro.sample sites with transition prob
4# (s ? 0.7 : 0.3); each step's observation is a Bernoulli observed at false with
5# emission prob (s ? 0.9 : 0.1). infer_discrete + config_enumerate draws exact
6# posterior samples of the discrete state sequence, vectorized over a draws-plate;
7# we aggregate the (s1, s2, s3) tuples into the distribution.
8N = 40000
9
10def model():
11 with pyro.plate("draws", N, dim=-1):
12 s1 = torch.ones(N) # initial state fixed to true
13 p2 = torch.where(s1.bool(), torch.tensor(0.7), torch.tensor(0.3))
14 s2 = pyro.sample("s2", dist.Bernoulli(p2))
15 p3 = torch.where(s2.bool(), torch.tensor(0.7), torch.tensor(0.3))
16 s3 = pyro.sample("s3", dist.Bernoulli(p3))
17 # observe each emission == false (0)
18 pyro.sample("o1", dist.Bernoulli(torch.where(s1.bool(), torch.tensor(0.9), torch.tensor(0.1))), obs=torch.zeros(N))
19 pyro.sample("o2", dist.Bernoulli(torch.where(s2.bool(), torch.tensor(0.9), torch.tensor(0.1))), obs=torch.zeros(N))
20 pyro.sample("o3", dist.Bernoulli(torch.where(s3.bool(), torch.tensor(0.9), torch.tensor(0.1))), obs=torch.zeros(N))
21 return s2, s3
22
23serve = pyro.infer.infer_discrete(
24 pyro.infer.config_enumerate(model), first_available_dim=-2
25)
26tr = pyro.poutine.trace(serve).get_trace()
27s2 = tr.nodes["s2"]["value"].reshape(-1)
28s3 = tr.nodes["s3"]["value"].reshape(-1)
29
30agg = defaultdict(float)
31total = s2.numel()
32for i in range(total):
33 seq = (True, bool(s2[i].item()), bool(s3[i].item()))
34 agg[seq] += 1.0 / total
35
36ANSWER = {k: v for k, v in agg.items()}
37
02answer overlay — webppl vs pyrodist/finite
webppl pyro4 bins
00.420.420.840.84[true,false,false] A = 0.842 B = 0.841[true,false,false] A = 0.842 B = 0.8410.840.84[true,false,false][true,false,true] A = 0.040 B = 0.040[true,false,true] A = 0.040 B = 0.0400.040.04[true,false,true][true,true,false] A = 0.094 B = 0.093[true,true,false] A = 0.094 B = 0.0930.090.09[true,true,false][true,true,true] A = 0.024 B = 0.025[true,true,true] A = 0.024 B = 0.0250.020.03[true,true,true]
03 verification
checkstatusevidence
GT self-consistency ok floor 0.0000 (tv)
solver re-derivation accept 2/2 solvers · d=[0.000, 0.000] · claude-sonnet-4-6
cross-language (pyro vs webppl) pass d=0.0014 ≤ tol 0.0099 · floors 0.0050/0.0000
dippl-04-factorseq / atom-2
answer dist/finite solver accept pyro pass 0.0000
00 statement source: data/sources/dippl/chapters/04-factorseq.md
given

A probabilistic context-free grammar (PCFG) with the following rules and weights: Non-terminal expansions: - start → NP V NP (weight 0.4) | NP V (weight 0.6) - NP → A NP (weight 0.4) | N (weight 0.6) Pre-terminal vocabulary: - N generates: 'John' (0.6), 'soup' (0.4) - V generates: 'loves' (0.3), 'hates' (0.3), 'runs' (0.4) - A generates: 'tall' (0.6), 'salty' (0.4) Each non-terminal samples its rule index in proportion to the listed weights. Enumeration is truncated at 20 executions. The grammar can produce unbounded-length sentences via recursive NP expansion.

model

A sentence is generated by recursively expanding the start symbol according to the non-terminal rules above until all symbols are pre-terminals, then substituting a word for each pre-terminal. The resulting sentence is conditioned on beginning with the words 'tall' and 'John' (in positions 1 and 2). If the sentence has a third word it is returned; otherwise the empty string is returned.

query

The posterior distribution over the third word (or empty string) of sentences beginning with 'tall John', computed by exhaustive enumeration with a maximum of 20 executions.

answer spec dist/finite
{
  "kind": "dist",
  "domain": "finite",
  "support": [
    "John",
    "soup",
    "loves",
    "hates",
    "runs",
    "tall",
    "salty",
    ""
  ]
}
system prompt constant across problems
(system prompt loads here)
webppl primer solver context
(primer loads here)
01 realizations comparing webppl vs pyro
ground truth
webppl
1var pcfgTransition = function(symbol) {
2 var rules = {'start': {rhs: [['NP', 'V', 'NP'], ['NP', 'V']], probs: [0.4, 0.6]},
3 'NP': {rhs: [['A', 'NP'], ['N']], probs: [0.4, 0.6]} }
4 return rules[symbol].rhs[ discrete(rules[symbol].probs) ]
5}
6
7var preTerminal = function(symbol) {
8 return symbol=='N' | symbol=='V' | symbol=='A'
9}
10
11var terminal = function(symbol) {
12 var rules = {'N': {words: ['John', 'soup'], probs: [0.6, 0.4]},
13 'V': {words: ['loves', 'hates', 'runs'], probs: [0.3, 0.3, 0.4]},
14 'A': {words: ['tall', 'salty'], probs: [0.6, 0.4]} }
15 return rules[symbol].words[ discrete(rules[symbol].probs) ]
16}
17
18
19var pcfg = function(symbol) {
20 preTerminal(symbol) ? [terminal(symbol)] : expand(pcfgTransition(symbol))
21}
22
23var expand = function(symbols) {
24 if(symbols.length==0) {
25 return []
26 } else {
27 var f = pcfg(symbols[0])
28 return f.concat(expand(symbols.slice(1)))
29 }
30}
31
32var model = function(){
33 var y = pcfg("start")
34 factor(_.isEqual(y.slice(0,2), ["tall", "John"]) ? 0 : -Infinity) // yield starts with "tall John"
35 return y[2] ? y[2] : "" // distribution on next word?
36}
37
38viz.table(Infer({ model, method: 'enumerate', maxExecutions: 20}))
39
40var ANSWER = (Infer({ model, method: 'enumerate', maxExecutions: 20 }));
41
realization0.000
python
1# PCFG yield conditioned to start with 'tall John'; posterior over the third word.
2# The grammar's choices are realized as pyro.sample(dist.Categorical(...)) sites and
3# the answer is read off an EXACT marginal computed by Pyro's enumeration machinery
4# (config_enumerate + TraceEnum_ELBO.compute_marginals), so nothing is hand-counted.
5#
6# Conditioning on the prefix ['tall','John'] forces the first NP to expand as
7# NP -> A NP with A='tall' and the inner NP -> N with N='John'; any other first-NP
8# expansion is killed by the prefix factor. The third yielded word is then always
9# the verb V (whether start -> NP V NP or start -> NP V), so the queried marginal is
10# exactly the marginal over V on the conditioned grammar. We enumerate the discrete
11# grammar choices that can produce / fail the 'tall John' prefix (the first NP rule,
12# its adjective, its inner-NP rule, the inner noun) together with the verb, apply the
13# prefix condition as a pyro.factor, and let compute_marginals return the exact
14# verb distribution. Recursion is bounded (the prefix pins the first NP to a single
15# finite expansion), which matches WebPPL's bounded best-first enumeration here.
16
17from pyro.infer import config_enumerate, TraceEnum_ELBO
18
19N_PROBS = torch.tensor([0.6, 0.4]) # John, soup
20V_WORDS = ["loves", "hates", "runs"]
21V_PROBS = torch.tensor([0.3, 0.3, 0.4]) # loves, hates, runs
22A_PROBS = torch.tensor([0.6, 0.4]) # tall, salty
23NP_PROBS = torch.tensor([0.4, 0.6]) # A NP, N
24
25NEG_INF = torch.tensor(float("-inf"))
26ZERO = torch.tensor(0.0)
27
28
29@config_enumerate
30def model():
31 # First NP expansion: 0 -> 'A NP', 1 -> 'N'.
32 np_rule = pyro.sample("np_rule", dist.Categorical(NP_PROBS))
33 # Adjective inside that NP (relevant only when np_rule == 0): 0 -> 'tall', 1 -> 'salty'.
34 adj = pyro.sample("adj", dist.Categorical(A_PROBS))
35 # Inner NP expansion (relevant only when np_rule == 0): 0 -> 'A NP', 1 -> 'N'.
36 inner_rule = pyro.sample("inner_rule", dist.Categorical(NP_PROBS))
37 # Inner noun (relevant when inner_rule == 1): 0 -> 'John', 1 -> 'soup'.
38 inner_noun = pyro.sample("inner_noun", dist.Categorical(N_PROBS))
39 # Verb (the third yielded word on the conditioned grammar): loves / hates / runs.
40 verb = pyro.sample("verb", dist.Categorical(V_PROBS))
41
42 # Prefix ['tall','John'] holds iff first NP -> A NP, adj == tall (0),
43 # inner NP -> N, inner noun == John (0).
44 prefix_ok = (np_rule == 0) & (adj == 0) & (inner_rule == 1) & (inner_noun == 0)
45 pyro.factor("prefix", torch.where(prefix_ok, ZERO, NEG_INF))
46
47
48marg = TraceEnum_ELBO(max_plate_nesting=0).compute_marginals(model, lambda: None)
49verb_marg = marg["verb"]
50sup = verb_marg.enumerate_support()
51probs = verb_marg.log_prob(sup).exp()
52
53ANSWER = {V_WORDS[int(s.item())]: float(p.item()) for s, p in zip(sup, probs)}
54
02answer overlay — webppl vs pyrodist/finite
webppl pyro3 bins
00.200.200.400.40hates A = 0.300 B = 0.300hates A = 0.300 B = 0.3000.300.30hatesloves A = 0.300 B = 0.300loves A = 0.300 B = 0.3000.300.30lovesruns A = 0.400 B = 0.400runs A = 0.400 B = 0.4000.400.40runs
03 verification
checkstatusevidence
GT self-consistency ok floor 0.0000 (tv)
solver re-derivation accept 2/2 solvers · d=[0.000, 0.000] · claude-sonnet-4-6
cross-language (pyro vs webppl) pass d=0.0000 ≤ tol 0.0000 · floors 0.0000/0.0000
dippl-04-factorseq / atom-3
answer dist/finite solver accept pyro pass 0.0019
00 statement source: data/sources/dippl/chapters/04-factorseq.md
given

A binary Hidden Markov Model with 3 time steps. The initial hidden state is fixed at true. Transition probabilities: P(next = true | current = true) = 0.7, P(next = true | current = false) = 0.3. Emission probabilities: P(obs = true | state = true) = 0.9, P(obs = true | state = false) = 0.1. Observed sequence: [false, false, false].

model

At each step the model samples a new hidden state from the transition kernel conditioned on the previous state, then samples an observation from the emission kernel conditioned on the new state, then immediately hard-conditions on the new observation matching the corresponding element of the observation sequence. This stepwise conditioning is applied one observation at a time as the sequence unfolds. The full state sequence (including the fixed initial state true) is returned.

query

The posterior distribution over the full 4-element hidden state sequence (initial true followed by 3 inferred states) given the observed sequence [false, false, false], computed by exact enumeration.

answer spec dist/finite
{
  "kind": "dist",
  "domain": "finite",
  "support": [
    [
      true,
      false,
      false,
      false
    ],
    [
      true,
      false,
      false,
      true
    ],
    [
      true,
      false,
      true,
      false
    ],
    [
      true,
      false,
      true,
      true
    ],
    [
      true,
      true,
      false,
      false
    ],
    [
      true,
      true,
      false,
      true
    ],
    [
      true,
      true,
      true,
      false
    ],
    [
      true,
      true,
      true,
      true
    ]
  ]
}
system prompt constant across problems
(system prompt loads here)
webppl primer solver context
(primer loads here)
01 realizations comparing webppl vs pyro
ground truth
webppl
1///fold:
2var transition = function(s) {
3 return s ? flip(0.7) : flip(0.3)
4}
5
6var observeState = function(s) {
7 return s ? flip(0.9) : flip(0.1)
8}
9
10var trueObs = [false, false, false]
11///
12
13var hmmRecur = function(n, states, observations){
14 var newState = transition(states[states.length-1])
15 var newObs = observeState(newState)
16 factor(newObs==trueObs[observations.length] ? 0 : -Infinity)
17 var newStates = states.concat([newState])
18 var newObservations = observations.concat([newObs])
19 return (n==1) ? { states: newStates, observations: newObservations } :
20 hmmRecur(n-1, newStates, newObservations)
21}
22
23var hmm = function(n) {
24 return hmmRecur(n, [true], [])
25}
26
27var model = function(){
28 var r = hmm(3)
29 return r.states
30}
31
32viz.table(Infer({ model }))
33
34var ANSWER = (Infer({ model }));
35
realization0.002
python
1# 3-step HMM seeded by a fixed initial true state; posterior over the full
2# 4-element sequence [true, s1, s2, s3] given observations [false, false, false],
3# by exact enumeration. Each of s1, s2, s3 is a Bernoulli pyro.sample site with
4# transition prob (s ? 0.7 : 0.3), and each step's emission is observed at false
5# with prob (s ? 0.9 : 0.1) (the initial true is not observed, matching the
6# reference). infer_discrete + config_enumerate draws exact posterior samples of
7# the discrete state sequence, vectorized over a draws-plate; we aggregate tuples.
8N = 40000
9
10def model():
11 with pyro.plate("draws", N, dim=-1):
12 init = torch.ones(N) # initial state fixed to true
13 p1 = torch.where(init.bool(), torch.tensor(0.7), torch.tensor(0.3))
14 s1 = pyro.sample("s1", dist.Bernoulli(p1))
15 p2 = torch.where(s1.bool(), torch.tensor(0.7), torch.tensor(0.3))
16 s2 = pyro.sample("s2", dist.Bernoulli(p2))
17 p3 = torch.where(s2.bool(), torch.tensor(0.7), torch.tensor(0.3))
18 s3 = pyro.sample("s3", dist.Bernoulli(p3))
19 # observe each new state's emission == false (0)
20 pyro.sample("o1", dist.Bernoulli(torch.where(s1.bool(), torch.tensor(0.9), torch.tensor(0.1))), obs=torch.zeros(N))
21 pyro.sample("o2", dist.Bernoulli(torch.where(s2.bool(), torch.tensor(0.9), torch.tensor(0.1))), obs=torch.zeros(N))
22 pyro.sample("o3", dist.Bernoulli(torch.where(s3.bool(), torch.tensor(0.9), torch.tensor(0.1))), obs=torch.zeros(N))
23 return s1, s2, s3
24
25serve = pyro.infer.infer_discrete(
26 pyro.infer.config_enumerate(model), first_available_dim=-2
27)
28tr = pyro.poutine.trace(serve).get_trace()
29s1 = tr.nodes["s1"]["value"].reshape(-1)
30s2 = tr.nodes["s2"]["value"].reshape(-1)
31s3 = tr.nodes["s3"]["value"].reshape(-1)
32
33agg = defaultdict(float)
34total = s1.numel()
35for i in range(total):
36 seq = (True, bool(s1[i].item()), bool(s2[i].item()), bool(s3[i].item()))
37 agg[seq] += 1.0 / total
38
39ANSWER = {k: v for k, v in agg.items()}
40
02answer overlay — webppl vs pyrodist/finite
webppl pyro8 bins
00.410.410.830.83[true,false,false,false] A = 0.830 B = 0.830[true,false,false,false] A = 0.830 B = 0.8300.830.83[true,false,false,false][true,false,false,true] A = 0.040 B = 0.041[true,false,false,true] A = 0.040 B = 0.0410.040.04[true,false,false,true][true,false,true,false] A = 0.017 B = 0.016[true,false,true,false] A = 0.017 B = 0.0160.020.02[true,false,true,false][true,false,true,true] A = 0.004 B = 0.005[true,false,true,true] A = 0.004 B = 0.005[true,false,true,true][true,true,false,false] A = 0.092 B = 0.091[true,true,false,false] A = 0.092 B = 0.0910.090.09[true,true,false,false][true,true,false,true] A = 0.004 B = 0.004[true,true,false,true] A = 0.004 B = 0.004[true,true,false,true][true,true,true,false] A = 0.010 B = 0.011[true,true,true,false] A = 0.010 B = 0.0110.010.01[true,true,true,false][true,true,true,true] A = 0.003 B = 0.002[true,true,true,true] A = 0.003 B = 0.002[true,true,true,true]
03 verification
checkstatusevidence
GT self-consistency ok floor 0.0000 (tv)
solver re-derivation accept 2/2 solvers · d=[0.000, 0.000] · claude-sonnet-4-6
cross-language (pyro vs webppl) pass d=0.0019 ≤ tol 0.0078 · floors 0.0039/0.0000
dippl-04-factorseq / atom-4
answer dist/finite solver accept pyro pass 0.0029
00 statement source: data/sources/dippl/chapters/04-factorseq.md
given

A hidden Markov model runs for 3 steps. The initial hidden state is true. Transition probabilities: if the current state is true, the next state is true with probability 0.7; if the current state is false, the next state is true with probability 0.3. Observation probabilities: if the hidden state is true, the observation is true with probability 0.9; if false, with probability 0.1. All three observations are false.

model

At each step a new hidden state is drawn from the transition distribution conditioned on the previous state, and an observation is drawn from the observation distribution conditioned on the new state. Only executions where all three observations match the observed sequence receive nonzero weight.

query

The posterior distribution over the full 4-element sequence of hidden states [initial, step-1, step-2, step-3], enumerated exactly.

answer spec dist/finite
{
  "kind": "dist",
  "domain": "finite",
  "support": [
    [
      true,
      false,
      false,
      false
    ],
    [
      true,
      false,
      false,
      true
    ],
    [
      true,
      false,
      true,
      false
    ],
    [
      true,
      false,
      true,
      true
    ],
    [
      true,
      true,
      false,
      false
    ],
    [
      true,
      true,
      false,
      true
    ],
    [
      true,
      true,
      true,
      false
    ],
    [
      true,
      true,
      true,
      true
    ]
  ]
}
system prompt constant across problems
(system prompt loads here)
webppl primer solver context
(primer loads here)
01 realizations comparing webppl vs pyro
ground truth
webppl
1///fold:
2var transition = function(s) {
3 return s ? flip(0.7) : flip(0.3)
4}
5
6var observeState = cache(function(s) {
7 return Bernoulli({p: s ? .9 : .1})
8})
9
10var trueObs = [false, false, false]
11///
12
13var hmmRecur = function(n, states, observations){
14 var newState = transition(states[states.length-1])
15 var newObs = sampleWithFactor(
16 observeState(newState),
17 function(v){return v==trueObs[observations.length] ? 0 : -Infinity})
18 var newStates = states.concat([newState])
19 var newObservations = observations.concat([newObs])
20 return ((n==1) ?
21 { states: newStates, observations: newObservations } :
22 hmmRecur(n-1, newStates, newObservations));
23}
24
25var hmm = function(n) {
26 return hmmRecur(n,[true],[])
27}
28
29var model = function(){
30 var r = hmm(3)
31 return r.states
32}
33
34viz.table(Infer({ model, method: 'enumerate', maxExecutions: 500 }))
35
36var ANSWER = (Infer({ model, method: 'enumerate', maxExecutions: 500 }));
37
realization0.003
python
1# HMM over 4 hidden states [initial=true, s1, s2, s3].
2# Transition: p(next=true) = 0.7 if prev else 0.3.
3# Emission: each state s emits Bernoulli(0.9 if s else 0.1); all 3 emissions observed false.
4# Joint posterior over the full 4-element state sequence via Pyro enumeration (infer_discrete).
5
6true_obs = [False, False, False]
7
8@pyro.infer.config_enumerate
9def model():
10 states = [True]
11 for i in range(3):
12 prev = states[-1]
13 p_trans = 0.7 if prev is True else (0.3 if prev is False else None)
14 if p_trans is None:
15 # prev is a tensor (under enumeration); pick per-element transition prob
16 p_trans = torch.where(prev.bool(), torch.tensor(0.7), torch.tensor(0.3))
17 s = pyro.sample(f"s{i}", dist.Bernoulli(p_trans))
18 # emission probability of observing `true_obs[i]` given state s
19 emit_p_true = torch.where(s.bool(), torch.tensor(0.9), torch.tensor(0.1))
20 obs_val = torch.tensor(1.0 if true_obs[i] else 0.0)
21 log_lik = torch.where(
22 obs_val.bool(),
23 torch.log(emit_p_true),
24 torch.log(1.0 - emit_p_true),
25 )
26 pyro.factor(f"emit{i}", log_lik)
27 states.append(s)
28 return states
29
30serving = pyro.infer.infer_discrete(
31 pyro.infer.config_enumerate(model), first_available_dim=-1
32)
33
34counts = Counter()
35N = 6000
36for _ in range(N):
37 states = serving()
38 seq = (True,) + tuple(bool(s.item() > 0.5) for s in states[1:])
39 counts[seq] += 1
40
41ANSWER = {seq: c / N for seq, c in counts.items()}
42
02answer overlay — webppl vs pyrodist/finite
webppl pyro8 bins
00.420.420.830.83[true,false,false,false] A = 0.830 B = 0.833[true,false,false,false] A = 0.830 B = 0.8330.830.83[true,false,false,false][true,false,false,true] A = 0.040 B = 0.037[true,false,false,true] A = 0.040 B = 0.0370.040.04[true,false,false,true][true,false,true,false] A = 0.017 B = 0.015[true,false,true,false] A = 0.017 B = 0.0150.020.02[true,false,true,false][true,false,true,true] A = 0.004 B = 0.005[true,false,true,true] A = 0.004 B = 0.005[true,false,true,true][true,true,false,false] A = 0.092 B = 0.094[true,true,false,false] A = 0.092 B = 0.0940.090.09[true,true,false,false][true,true,false,true] A = 0.004 B = 0.005[true,true,false,true] A = 0.004 B = 0.005[true,true,false,true][true,true,true,false] A = 0.010 B = 0.011[true,true,true,false] A = 0.010 B = 0.0110.010.01[true,true,true,false][true,true,true,true] A = 0.003 B = 0.001[true,true,true,true] A = 0.003 B = 0.001[true,true,true,true]
03 verification
checkstatusevidence
GT self-consistency ok floor 0.0000 (tv)
solver re-derivation accept 2/2 solvers · d=[0.000, 0.000] · claude-sonnet-4-6
cross-language (pyro vs webppl) pass d=0.0029 ≤ tol 0.0173 · floors 0.0087/0.0000
dippl-04-factorseq / atom-5
answer dist/int solver accept pyro pass 0.0034
00 statement source: data/sources/dippl/chapters/04-factorseq.md
given

Three binary random variables a, b, c are drawn independently: a from Bernoulli(0.1), b from Bernoulli(0.9), c from Bernoulli(0.1). Three incremental soft factors are interleaved with the sampling: after drawing a, a log-weight of -1 is added if a is false and 0 otherwise; after drawing b, the cumulative log-weight is adjusted to -1 if both a and b are false and 0 otherwise; after drawing c, the cumulative log-weight is further adjusted so the total equals -10 if all three are false and 0 otherwise. The return value is a + b + c.

model

Each incremental factor is set so that the three factors cancel and sum to a single end-of-model soft constraint: the total log-weight is -10 when all three variables are false and 0 otherwise.

query

The exact posterior distribution over a + b + c under full enumeration.

answer spec dist/int
{
  "kind": "dist",
  "domain": "int"
}
system prompt constant across problems
(system prompt loads here)
webppl primer solver context
(primer loads here)
01 realizations comparing webppl vs pyro
ground truth
webppl
1var binomial = function(){
2 var a = sample(Bernoulli({ p: 0.1 }))
3 factor(a ? 0 : -1)
4 var b = sample(Bernoulli({ p: 0.9 }))
5 factor(((a||b)?0:-1) - (a?0:-1))
6 var c = sample(Bernoulli({ p: 0.1 }))
7 factor(((a||b||c) ? 0:-10) - ((a||b)?0:-1))
8 return a + b + c
9}
10
11var ANSWER = Infer({model: binomial, method: 'enumerate'});
12
realization0.003
python
1# a ~ Bern(0.1), b ~ Bern(0.9), c ~ Bern(0.1) with successive soft factors that
2# (telescoping) impose: factor 0 if a else -1; then 0 if (a|b) else -1; then 0 if (a|b|c) else -10.
3# Net log-weight over the joint = (a?0:-1) + ((a|b)?0:-1)? -- the GT writes incremental deltas
4# whose cumulative sum is: w(a,b,c) = (a||b||c ? 0 : -10) plus the earlier increments that telescope.
5# Sum of the GT factors: [a?0:-1] + [((a|b)?0:-1)-(a?0:-1)] + [((a|b|c)?0:-10)-((a|b)?0:-1)]
6# = (a|b|c) ? 0 : -10. So the effective condition is a single factor (a|b|c ? 0 : -10).
7# Posterior over a+b+c via exact Pyro enumeration.
8
9@pyro.infer.config_enumerate
10def model():
11 a = pyro.sample("a", dist.Bernoulli(0.1))
12 f1 = torch.where(a.bool(), torch.tensor(0.0), torch.tensor(-1.0))
13 pyro.factor("f_a", f1)
14 b = pyro.sample("b", dist.Bernoulli(0.9))
15 ab = a.bool() | b.bool()
16 f2 = torch.where(ab, torch.tensor(0.0), torch.tensor(-1.0)) - f1
17 pyro.factor("f_b", f2)
18 c = pyro.sample("c", dist.Bernoulli(0.1))
19 abc = ab | c.bool()
20 f3 = torch.where(abc, torch.tensor(0.0), torch.tensor(-10.0)) - torch.where(
21 ab, torch.tensor(0.0), torch.tensor(-1.0)
22 )
23 pyro.factor("f_c", f3)
24 return a, b, c
25
26serving = pyro.infer.infer_discrete(
27 pyro.infer.config_enumerate(model), first_available_dim=-1
28)
29
30counts = Counter()
31N = 6000
32for _ in range(N):
33 a, b, c = serving()
34 s = int(round(a.item() + b.item() + c.item()))
35 counts[s] += 1
36
37ANSWER = {k: v / N for k, v in counts.items()}
38
02answer overlay — webppl vs pyrodist/int
webppl pyro4 bins · 0 … 3
00.410.410.810.810 A = 0.000 B = 0.0000 A = 0.000 B = 0.00001 A = 0.813 B = 0.8111 A = 0.813 B = 0.8110.810.8112 A = 0.177 B = 0.1782 A = 0.177 B = 0.1780.180.1823 A = 0.010 B = 0.0113 A = 0.010 B = 0.0110.010.013
03 verification
checkstatusevidence
GT self-consistency ok floor 0.0000 (w1)
solver re-derivation accept 2/2 solvers · d=[0.000, 0.000] · claude-sonnet-4-6
cross-language (pyro vs webppl) pass d=0.0034 ≤ tol 0.0230 · floors 0.0115/0.0000
dippl-05-particlefilter / atom-1
answer dist/finite solver accept pyro pass 0.0043
00 statement source: data/sources/dippl/chapters/05-particlefilter.md
given

A hidden Markov model runs for 4 steps. The initial hidden state is false. Transition probabilities: if the current state is true, the next state is true with probability 0.9; if the current state is false, the next state is true with probability 0.1. Four observations are all true. Soft conditioning: a log-weight of 0 is applied when the sampled state matches the current observation, and -2 when it does not.

model

At each step a new hidden state is drawn from the transition distribution conditioned on the previous state. A soft factor is applied comparing the new state to the current observation. The result is the full sequence of states including the initial state.

query

The posterior distribution over 5-element sequences of hidden states [initial, step-1, step-2, step-3, step-4], enumerated exactly.

answer spec dist/finite
{
  "kind": "dist",
  "domain": "finite",
  "support": [
    [
      false,
      false,
      false,
      false,
      false
    ],
    [
      false,
      false,
      false,
      false,
      true
    ],
    [
      false,
      false,
      false,
      true,
      false
    ],
    [
      false,
      false,
      false,
      true,
      true
    ],
    [
      false,
      false,
      true,
      false,
      false
    ],
    [
      false,
      false,
      true,
      false,
      true
    ],
    [
      false,
      false,
      true,
      true,
      false
    ],
    [
      false,
      false,
      true,
      true,
      true
    ],
    [
      false,
      true,
      false,
      false,
      false
    ],
    [
      false,
      true,
      false,
      false,
      true
    ],
    [
      false,
      true,
      false,
      true,
      false
    ],
    [
      false,
      true,
      false,
      true,
      true
    ],
    [
      false,
      true,
      true,
      false,
      false
    ],
    [
      false,
      true,
      true,
      false,
      true
    ],
    [
      false,
      true,
      true,
      true,
      false
    ],
    [
      false,
      true,
      true,
      true,
      true
    ]
  ]
}
system prompt constant across problems
(system prompt loads here)
webppl primer solver context
(primer loads here)
01 realizations comparing webppl vs pyro
ground truth
webppl
1var hmm = function(states, observations){
2 var prevState = states[states.length - 1];
3 var state = sample(Bernoulli({p: prevState ? .9 : .1}));
4 factor((state == observations[0]) ? 0 : -2);
5 if (observations.length == 0) {
6 return states;
7 } else {
8 return hmm(states.concat([state]), observations.slice(1));
9 }
10}
11
12var observations = [true, true, true, true];
13var startState = false;
14
15viz.table(Infer({
16 model() {
17 return hmm([startState], observations)
18 }
19}))
20
21var ANSWER = (Infer({ model() { return hmm([startState], observations) } }));
22
realization0.004
python
1# HMM over 5 hidden states [start=false, s1, s2, s3, s4].
2# Transition: p(state=true) = 0.9 if prev else 0.1.
3# Soft observation: factor (state == observations[i]) ? 0 : -2, observations all true (4 of them).
4# Joint posterior over the full 5-element state sequence via Pyro enumeration (infer_discrete).
5
6observations = [True, True, True, True]
7start_state = False
8
9@pyro.infer.config_enumerate
10def model():
11 states = [start_state]
12 for i in range(4):
13 prev = states[-1]
14 if prev is True:
15 p_trans = torch.tensor(0.9)
16 elif prev is False:
17 p_trans = torch.tensor(0.1)
18 else:
19 p_trans = torch.where(prev.bool(), torch.tensor(0.9), torch.tensor(0.1))
20 s = pyro.sample(f"s{i}", dist.Bernoulli(p_trans))
21 obs_i = observations[i]
22 match = s.bool() if obs_i else (~s.bool())
23 f = torch.where(match, torch.tensor(0.0), torch.tensor(-2.0))
24 pyro.factor(f"obs{i}", f)
25 states.append(s)
26 return states
27
28serving = pyro.infer.infer_discrete(
29 pyro.infer.config_enumerate(model), first_available_dim=-1
30)
31
32counts = Counter()
33N = 7000
34for _ in range(N):
35 states = serving()
36 seq = (False,) + tuple(bool(s.item() > 0.5) for s in states[1:])
37 counts[seq] += 1
38
39ANSWER = {seq: c / N for seq, c in counts.items()}
40
02answer overlay — webppl vs pyrodist/finite
webppl pyro16 bins
00.420.420.850.85[false,false,false,false,false] A = 0.003 B = 0.003[false,false,false,false,false] A = 0.003 B = 0.003[false,false,false,false,false][false,false,false,false,true] A = 0.002 B = 0.002[false,false,false,false,true] A = 0.002 B = 0.002[false,false,false,true,false] A = 0.000 B = 0.000[false,false,false,true,false] A = 0.000 B = 0.000[false,false,false,true,false][false,false,false,true,true] A = 0.015 B = 0.017[false,false,false,true,true] A = 0.015 B = 0.017[false,false,true,false,false] A = 0.000 B = 0.000[false,false,true,false,false] A = 0.000 B = 0.000[false,false,true,false,false][false,false,true,false,true] A = 0.000 B = 0.000[false,false,true,false,true] A = 0.000 B = 0.000[false,false,true,true,false] A = 0.002 B = 0.001[false,false,true,true,false] A = 0.002 B = 0.001[false,false,true,true,false][false,false,true,true,true] A = 0.114 B = 0.115[false,false,true,true,true] A = 0.114 B = 0.115[false,true,false,false,false] A = 0.000 B = 0.000[false,true,false,false,false] A = 0.000 B = 0.000[false,true,false,false,false][false,true,false,false,true] A = 0.000 B = 0.000[false,true,false,false,true] A = 0.000 B = 0.000[false,true,false,true,false] A = 0.000 B = 0.000[false,true,false,true,false] A = 0.000 B = 0.000[false,true,false,true,false][false,true,false,true,true] A = 0.001 B = 0.001[false,true,false,true,true] A = 0.001 B = 0.001[false,true,true,false,false] A = 0.002 B = 0.002[false,true,true,false,false] A = 0.002 B = 0.002[false,true,true,false,false][false,true,true,false,true] A = 0.001 B = 0.002[false,true,true,false,true] A = 0.001 B = 0.002[false,true,true,true,false] A = 0.013 B = 0.011[false,true,true,true,false] A = 0.013 B = 0.011[false,true,true,true,false][false,true,true,true,true] A = 0.845 B = 0.844[false,true,true,true,true] A = 0.845 B = 0.844
03 verification
checkstatusevidence
GT self-consistency ok floor 0.0000 (tv)
solver re-derivation accept 2/2 solvers · d=[0.000, 0.000] · claude-sonnet-4-6
cross-language (pyro vs webppl) pass d=0.0043 ≤ tol 0.0280 · floors 0.0140/0.0000
dippl-05-particlefilter / atom-2
answer dist/bool solver accept pyro pass 0.0140
00 statement source: data/sources/dippl/chapters/05-particlefilter.md
given

A 1-dimensional Gaussian random walk: the starting position is drawn from Gaussian(mean=200, sd=1). At each subsequent step the position is drawn from Gaussian(mean=previous position, sd=10). The walk runs for 5 steps total (1 initial position plus 4 transitions). The threshold is 200.

model

Each step the position evolves by adding Gaussian noise with standard deviation 10 to the previous position. The initial position is independently drawn from Gaussian(200, 1).

query

The marginal distribution over whether the position after 5 steps exceeds 200, estimated via 1000 forward samples.

answer spec dist/bool
{
  "kind": "dist",
  "domain": "bool"
}
system prompt constant across problems
(system prompt loads here)
webppl primer solver context
(primer loads here)
01 realizations comparing webppl vs pyro
ground truth
webppl
1///fold:
2var drawLines = function(canvas, start, positions){
3 if (positions.length == 0) { return []; }
4 var next = positions[0];
5 canvas.line(start[0], start[1], next[0], next[1], 4, 0.2);
6 drawLines(canvas, next, positions.slice(1));
7 return;
8}
9
10var last = function(xs){
11 return xs[xs.length - 1];
12}
13///
14
15var init = function(dim){
16 return repeat(dim, function(){ return gaussian(200, 1) });
17}
18
19var transition = function(pos){
20 return map(
21 function(x){ return gaussian(x, 10); },
22 pos
23 );
24};
25
26var gaussianRandomWalk = function(n, dim) {
27 var prevStates = (n==1) ? [init(dim)] : gaussianRandomWalk(n-1, dim);
28 var newState = transition(last(prevStates));
29 return prevStates.concat([newState]);
30};
31
32var positions = gaussianRandomWalk(100, 2);
33
34
35// Draw model output
36
37var canvas = Draw(400, 400, true)
38drawLines(canvas, positions[0], positions.slice(1))
39
40var ANSWER = (Infer({ method: 'forward', samples: 1000, model: function(){ return last(gaussianRandomWalk(5, 1))[0] > 200; } }));
41
realization0.014
python
1# Forward (prior) sampling of a 1-D Gaussian random walk for 5 steps.
2# Initial position ~ Gaussian(200, 1); each transition adds Gaussian(0, 10) noise.
3# Query: marginal over (position after 5 steps > 200), via 1000 forward samples.
4
5def model():
6 pos = pyro.sample("x0", dist.Normal(200.0, 1.0))
7 for i in range(1, 5):
8 pos = pyro.sample(f"x{i}", dist.Normal(pos, 10.0))
9 return pos
10
11num_samples = 1000
12outcomes = []
13for _ in range(num_samples):
14 final = model()
15 outcomes.append(bool(final.item() > 200.0))
16
17counts = Counter(outcomes)
18ANSWER = {
19 True: counts[True] / num_samples,
20 False: counts[False] / num_samples,
21}
22
02answer overlay — webppl vs pyrodist/bool
webppl pyro2 bins
00.260.260.520.52false A = 0.495 B = 0.515false A = 0.495 B = 0.5150.490.52falsetrue A = 0.505 B = 0.485true A = 0.505 B = 0.4850.510.48true
03 verification
checkstatusevidence
GT self-consistency ok floor 0.0450 (tv)
solver re-derivation accept 2/2 solvers · d=[0.014, 0.014] · claude-sonnet-4-6
cross-language (pyro vs webppl) pass d=0.0140 ≤ tol 0.0900 · floors 0.0250/0.0450
dippl-05-particlefilter / atom-3
answer dist/bool solver accept pyro pass 0.0120
00 statement source: data/sources/dippl/chapters/05-particlefilter.md
given

A 1-dimensional semi-Markov random walk with momentum. The first two positions are each independently drawn from Gaussian(mean=200, sd=1). At each subsequent step the position is drawn from Gaussian(mean = previous + 0.7 * (previous - second-previous), sd=3). The walk runs for 6 steps total (2 initial positions plus 4 transitions). The threshold is 200.

model

Each step the position is drawn from a Gaussian whose mean incorporates a momentum term: 0.7 times the displacement from the second-to-last position to the last position is added to the last position to form the new mean. The standard deviation of each step is 3.

query

The marginal distribution over whether the position after 6 steps exceeds 200, estimated via 1000 forward samples.

answer spec dist/bool
{
  "kind": "dist",
  "domain": "bool"
}
system prompt constant across problems
(system prompt loads here)
webppl primer solver context
(primer loads here)
01 realizations comparing webppl vs pyro
ground truth
webppl
1///fold:
2var drawLines = function(canvas, start, positions){
3 if (positions.length == 0) { return []; }
4 var next = positions[0];
5 canvas.line(start[0], start[1], next[0], next[1], 4, 0.2);
6 drawLines(canvas, next, positions.slice(1));
7 return;
8}
9///
10
11var init = function(dim){
12 return repeat(dim, function(){ return gaussian(200, 1) });
13}
14
15var transition = function(lastPos, secondLastPos){
16 return map2(
17 function(lastX, secondLastX){
18 var momentum = (lastX - secondLastX) * .7;
19 return gaussian(lastX + momentum, 3);
20 },
21 lastPos,
22 secondLastPos
23 );
24};
25
26var semiMarkovWalk = function(n, dim) {
27 var prevStates = (n==2) ? [init(dim), init(dim)] : semiMarkovWalk(n-1, dim);
28 var newState = transition(last(prevStates), secondLast(prevStates));
29 return prevStates.concat([newState]);
30};
31
32var positions = semiMarkovWalk(80, 2);
33
34
35// Draw model output
36
37var canvas = Draw(400, 400, true)
38drawLines(canvas, positions[0], positions.slice(1))
39
40var ANSWER = (Infer({ method: 'forward', samples: 1000, model: function(){ return last(semiMarkovWalk(6, 1))[0] > 200; } }));
41
realization0.012
python
1
2# 1-D semi-Markov walk with momentum. First two positions ~ Normal(200, 1).
3# Each subsequent step: Normal(prev + 0.7*(prev - prev_prev), 3). 6 steps total
4# (2 initial + 4 transitions). Forward (prior) sampling, 1000 samples.
5def model():
6 p_prev2 = pyro.sample("pos_0", dist.Normal(200.0, 1.0))
7 p_prev1 = pyro.sample("pos_1", dist.Normal(200.0, 1.0))
8 for step in range(4):
9 momentum = 0.7 * (p_prev1 - p_prev2)
10 new_pos = pyro.sample(f"pos_{step + 2}",
11 dist.Normal(p_prev1 + momentum, 3.0))
12 p_prev2 = p_prev1
13 p_prev1 = new_pos
14 return bool((p_prev1 > 200.0).item())
15
16counts = Counter()
17for _ in range(1000):
18 tr = pyro.poutine.trace(model).get_trace()
19 counts[tr.nodes["_RETURN"]["value"]] += 1
20total = sum(counts.values())
21ANSWER = {True: counts[True] / total, False: counts[False] / total}
22
02answer overlay — webppl vs pyrodist/bool
webppl pyro2 bins
00.260.260.520.52false A = 0.492 B = 0.480false A = 0.492 B = 0.4800.490.48falsetrue A = 0.508 B = 0.520true A = 0.508 B = 0.5200.510.52true
03 verification
checkstatusevidence
GT self-consistency ok floor 0.0540 (tv)
solver re-derivation accept 2/2 solvers · d=[0.018, 0.018] · claude-sonnet-4-6
cross-language (pyro vs webppl) pass d=0.0120 ≤ tol 0.1080 · floors 0.0280/0.0540
dippl-06-mcmc / atom-1
answer dist/int solver accept pyro pass 0.0097
00 statement source: data/sources/dippl/chapters/06-mcmc.md
given

Three binary random variables a, b, c are drawn independently from Bernoulli(0.5). A log-weight of -1 is applied when both a and b are false (i.e., when a OR b is false); otherwise the log-weight is 0.

model

The three variables are sampled independently. A soft factor downweights executions where neither a nor b is true by a factor of exp(-1). The model returns the sum a + b + c.

query

The exact posterior distribution over a + b + c under full enumeration.

answer spec dist/int
{
  "kind": "dist",
  "domain": "int"
}
system prompt constant across problems
(system prompt loads here)
webppl primer solver context
(primer loads here)
01 realizations comparing webppl vs pyro
ground truth
webppl
1var skewBinomial = function(){
2 var a = sample(Bernoulli({p: 0.5}))
3 var b = sample(Bernoulli({p: 0.5}))
4 var c = sample(Bernoulli({p: 0.5}))
5 factor( (a|b)?0:-1 )
6 return a + b + c
7}
8
9viz(Infer({ model: skewBinomial }))
10
11var ANSWER = (Infer({ model: skewBinomial }));
12
realization0.010
python
1# a, b, c ~ Bernoulli(0.5) i.i.d.; soft factor (a|b) ? 0 : -1.
2# Posterior over a+b+c via exact Pyro enumeration.
3
4@pyro.infer.config_enumerate
5def model():
6 a = pyro.sample("a", dist.Bernoulli(0.5))
7 b = pyro.sample("b", dist.Bernoulli(0.5))
8 c = pyro.sample("c", dist.Bernoulli(0.5))
9 ab = a.bool() | b.bool()
10 pyro.factor("cond", torch.where(ab, torch.tensor(0.0), torch.tensor(-1.0)))
11 return a, b, c
12
13serving = pyro.infer.infer_discrete(
14 pyro.infer.config_enumerate(model), first_available_dim=-1
15)
16
17counts = Counter()
18N = 6000
19for _ in range(N):
20 a, b, c = serving()
21 s = int(round(a.item() + b.item() + c.item()))
22 counts[s] += 1
23
24ANSWER = {k: v / N for k, v in counts.items()}
25
02answer overlay — webppl vs pyrodist/int
webppl pyro4 bins · 0 … 3
00.220.220.450.450 A = 0.055 B = 0.0540 A = 0.055 B = 0.0540.050.0501 A = 0.352 B = 0.3501 A = 0.352 B = 0.3500.350.3512 A = 0.445 B = 0.4462 A = 0.445 B = 0.4460.450.4523 A = 0.148 B = 0.1513 A = 0.148 B = 0.1510.150.153
03 verification
checkstatusevidence
GT self-consistency ok floor 0.0000 (w1)
solver re-derivation accept 2/2 solvers · d=[0.000, 0.000] · claude-sonnet-4-6
cross-language (pyro vs webppl) pass d=0.0097 ≤ tol 0.0533 · floors 0.0267/0.0000
dippl-06-mcmc / atom-2
answer dist/int solver accept pyro pass 0.0097
00 statement source: data/sources/dippl/chapters/06-mcmc.md
given

Three independent fair coins are drawn, each from a Bernoulli distribution with p=0.5. Call their values a, b, c. Executions in which (a OR b) is false (i.e. both a and b are zero) are downweighted by a factor of exp(-1); all other executions receive a factor of exp(0)=1.

model

The model samples three independent fair coin flips a, b, c. Soft conditioning via a factor reduces the probability mass of executions where both a and b are false by a factor of e^{-1} relative to executions where at least one of a or b is true. The quantity of interest is the sum a+b+c.

query

The posterior distribution over the integer-valued sum a+b+c (ranging from 0 to 3) after the soft conditioning.

answer spec dist/int
{
  "kind": "dist",
  "domain": "int"
}
system prompt constant across problems
(system prompt loads here)
webppl primer solver context
(primer loads here)
01 realizations comparing webppl vs pyro
ground truth
webppl
1var skewBinomial = function(){
2 var a = sample(Bernoulli({p: 0.5}))
3 var b = sample(Bernoulli({p: 0.5}))
4 var c = sample(Bernoulli({p: 0.5}))
5 factor( (a|b)?0:-1 )
6 return a + b + c
7}
8
9var ANSWER = Infer({ model: skewBinomial });
realization0.010
python
1# a, b, c ~ Bernoulli(0.5) i.i.d.; soft factor (a|b) ? 0 : -1.
2# Posterior over the integer sum a+b+c (0..3) via exact Pyro enumeration.
3
4@pyro.infer.config_enumerate
5def model():
6 a = pyro.sample("a", dist.Bernoulli(0.5))
7 b = pyro.sample("b", dist.Bernoulli(0.5))
8 c = pyro.sample("c", dist.Bernoulli(0.5))
9 ab = a.bool() | b.bool()
10 pyro.factor("cond", torch.where(ab, torch.tensor(0.0), torch.tensor(-1.0)))
11 return a, b, c
12
13serving = pyro.infer.infer_discrete(
14 pyro.infer.config_enumerate(model), first_available_dim=-1
15)
16
17counts = Counter()
18N = 6000
19for _ in range(N):
20 a, b, c = serving()
21 s = int(round(a.item() + b.item() + c.item()))
22 counts[s] += 1
23
24ANSWER = {k: v / N for k, v in counts.items()}
25
02answer overlay — webppl vs pyrodist/int
webppl pyro4 bins · 0 … 3
00.220.220.450.450 A = 0.055 B = 0.0540 A = 0.055 B = 0.0540.050.0501 A = 0.352 B = 0.3501 A = 0.352 B = 0.3500.350.3512 A = 0.445 B = 0.4462 A = 0.445 B = 0.4460.450.4523 A = 0.148 B = 0.1513 A = 0.148 B = 0.1510.150.153
03 verification
checkstatusevidence
GT self-consistency ok floor 0.0000 (w1)
solver re-derivation accept 2/2 solvers · d=[0.000, 0.000] · claude-sonnet-4-6
cross-language (pyro vs webppl) pass d=0.0097 ≤ tol 0.0533 · floors 0.0267/0.0000