Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
e867a47
Add PyMC v5 transpiled models: blr, earn_height, wells_dist, radon_po…
claude Mar 11, 2026
daa3110
Merge pull request #1 from twiecki/pymc-models
twiecki Mar 11, 2026
944b7fd
Merge branch 'stan-dev:master' into master
twiecki Mar 11, 2026
c9d0f6e
Add PyMC port: Rate_1_model
claude Mar 11, 2026
3bee9dc
Add PyMC port: nes_logit_model
claude Mar 11, 2026
874d835
Add PyMC port: kidscore_momhs
claude Mar 11, 2026
7518764
Add PyMC port: kidscore_momiq
claude Mar 11, 2026
222681c
Add PyMC port: logearn_height
claude Mar 11, 2026
f445daf
Add PyMC port: wells_dist100_model
claude Mar 11, 2026
25c1ba5
Add PyMC port: Rate_3_model
claude Mar 11, 2026
d369dac
Add PyMC port: kidscore_momhsiq
claude Mar 11, 2026
fe0443c
Add PyMC port: wells_dist100ars_model
claude Mar 11, 2026
17542db
Add PyMC port: low_dim_gauss_mix_collapse
claude Mar 11, 2026
4fa9f97
Add PyMC port: normal_mixture_k
claude Mar 11, 2026
5ca66a1
Add PyMC port: low_dim_gauss_mix
claude Mar 11, 2026
8152dc4
Add PyMC port: wells_dae_model
claude Mar 11, 2026
8a8ce7e
Add PyMC port: wells_interaction_model
claude Mar 11, 2026
b2b3ee0
Add PyMC port: logmesquite_logvolume
claude Mar 11, 2026
fc4244d
Add PyMC port: kidscore_interaction
claude Mar 11, 2026
74609d5
Add PyMC port: mesquite
claude Mar 11, 2026
e23be1e
Add PyMC port: kidscore_mom_work
claude Mar 11, 2026
0345ead
Add PyMC port: wells_interaction_c_model
claude Mar 11, 2026
75f03d8
Add PyMC port: kidscore_interaction_c
claude Mar 11, 2026
37e22d7
Add PyMC port: kidscore_interaction_c2
claude Mar 11, 2026
ab34e52
Add PyMC port: wells_dae_c_model
claude Mar 11, 2026
27b2a89
Add PyMC port: normal_mixture
claude Mar 11, 2026
3a4441c
Add PyMC port: kidscore_interaction_z
claude Mar 11, 2026
95c1cee
Add PyMC port: wells_daae_c_model
claude Mar 11, 2026
b3e62f3
Add PyMC port: Rate_5_model
claude Mar 11, 2026
571b820
Add PyMC port: logmesquite_logva
claude Mar 11, 2026
7a2109d
Add PyMC port: radon_variable_slope_centered
claude Mar 11, 2026
2f67c2e
Add PyMC port: wells_dae_inter_model
claude Mar 11, 2026
ae7fd45
Add PyMC port: radon_variable_slope_noncentered
claude Mar 11, 2026
28651dc
Merge pull request #2 from twiecki/pymc-batch-transpile
twiecki Mar 11, 2026
da57fd7
Add PyMC ports: log10earn_height, logearn_height_male, logearn_intera…
claude Mar 11, 2026
a8fed56
Add PyMC ports: radon_county_intercept, radon_hierarchical_intercept_…
claude Mar 11, 2026
1ef01ec
Add 15 PyMC ports: radon variants, Rate, mesquite, GLM, diamonds, nes…
claude Mar 11, 2026
8673a62
Add 11 PyMC ports: capture-recapture (M0, Mb, Mt), dogs, bones, rats,…
claude Mar 11, 2026
c576e02
Add PyMC ports: accel_gp, accel_splines
claude Mar 11, 2026
4426a98
Add PyMC port: bym2_offset_only; update error log
claude Mar 11, 2026
2238094
Update transpile error log with latest batch failures
claude Mar 11, 2026
75f86ef
Update error log: batches 9-17 complete, remaining models are complex…
claude Mar 11, 2026
ab520ea
Update error log: batch 12 (dugongs, IRT, losscurve, ODE all failed)
claude Mar 11, 2026
1551389
Update error log with remaining batch failures
claude Mar 11, 2026
8f700a2
Add 18 newly transpiled PyMC models (offset-tolerant validation)
claude Mar 11, 2026
8d45f0b
Add dugongs_model, losscurve_sislob, prophet PyMC transpilations
claude Mar 11, 2026
21d1d40
Add Mh_model and lsat_model PyMC transpilations
claude Mar 11, 2026
6f88308
Update transpile error log with retry results
claude Mar 11, 2026
c714274
Add one_comp_mm_elim_abs PyMC transpilation (ODE model)
claude Mar 11, 2026
98e11f8
Add irt_2pl PyMC transpilation
claude Mar 11, 2026
8fa9158
Add Mth_model PyMC transpilation
claude Mar 11, 2026
8b7bc1e
Add state_space_stochastic_level_stochastic_seasonal PyMC transpilation
claude Mar 11, 2026
6257258
Add unit tests comparing transpiled PyMC models against Stan via Brid…
claude Mar 11, 2026
b43a0aa
Merge pull request #3 from twiecki/transpile-batch-1
twiecki Mar 11, 2026
ac9795b
Vectorize all PyMC models: eliminate Python for-loops for faster comp…
claude Mar 11, 2026
9f2a8a4
Merge pull request #4 from twiecki/claude/optimize-pymc-posteriordb-D…
twiecki Mar 11, 2026
2ea59ab
Add gradient comparison tests for PyMC vs Stan models
claude Mar 11, 2026
b246447
Merge pull request #5 from twiecki/claude/add-gradient-tests-PXrCg
twiecki Mar 11, 2026
ed9de52
Test re-transpile of Mh_model and Mth_model with updated compiler
claude Mar 11, 2026
34a8ca7
Log failed transpile attempt (missing API key)
claude Mar 11, 2026
d1ae16e
Add 8 new PyMC model transpilations (104/120 total)
twiecki Mar 13, 2026
3439c59
Merge pull request #8 from twiecki/pymc-retranspile-batch2
twiecki Mar 13, 2026
94a87d8
Add porting guide for remaining 16 Stan models and retranspile script
twiecki Mar 13, 2026
e67c028
Add PyMC-to-Rust compilation pipeline and first 6 compiled models
twiecki Mar 13, 2026
fcd9bb1
Merge pull request #10 from twiecki/rust-compilation-batch1
twiecki Mar 13, 2026
8ef3f11
Add 4 PyMC models with simplex parameters (LDA, HMM, hierarchical GP)
twiecki Mar 14, 2026
00430ee
Improve PyMC model idiomaticity based on review feedback
twiecki Mar 14, 2026
a3b9de9
Add 4 PyMC models with simplex parameters (LDA, HMM, hierarchical GP)
twiecki Mar 14, 2026
ec56d05
Merge pull request #9 from twiecki/porting-guide-remaining-16
twiecki Mar 14, 2026
af19729
Merge pull request #11 from twiecki/pymc-simplex-models
twiecki Mar 15, 2026
a563882
Fix sampling failures in 5 transpiled PyMC models
twiecki Mar 16, 2026
33ce5aa
Fix import: pymc_rust_compiler → transpailer
twiecki Mar 16, 2026
eb3d357
Merge pull request #12 from twiecki/fix/sampling-failures-initvals
twiecki Mar 16, 2026
c375656
Remove redundant initvals, keep only where needed
twiecki Mar 16, 2026
9281b68
Merge pull request #13 from twiecki/fix/sampling-failures-initvals
twiecki Mar 17, 2026
d1f1c66
Add PyMC vs Stan benchmark: 101 posteriordb models compared
twiecki Mar 17, 2026
5f7fb6c
Add benchmark analysis: PyMC/nutpie vs Stan on 101 posteriordb models
twiecki Mar 17, 2026
7a8525c
Update benchmark: use total sec/ESS as primary metric
twiecki Mar 17, 2026
8ba99c7
Fix 4 poorly transpiled PyMC models, re-run benchmark
twiecki Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ TODO.tdl
*.exe
*.hpp
*.dll
*.so
tmp*
__pycache__/
.pytest_cache/
posterior_database/models/stan/*/
295 changes: 295 additions & 0 deletions NEXT_STEPS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
# Porting the Remaining 16 posteriordb Models to PyMC

## Context

We have successfully transpiled 104/120 Stan models from posteriordb to PyMC v5.
All transpiled models are validated against BridgeStan for gradient equivalence.
The transpiler uses an agentic loop (Claude + BridgeStan validation) defined in
`/Users/twiecki/projects/transpailer/transpailer/stan_to_pymc.py`.

The remaining 16 models failed automatic transpilation because they require
PyMC-ecosystem-specific APIs that the transpiler doesn't know about yet.
This document describes how to port each one, grouped by the PyMC tool needed.

## Setup

```bash
# Posteriordb repo (on pymc-retranspile-batch2 branch)
cd /Users/twiecki/projects/posteriordb

# Transpailer (Stan→PyMC transpiler)
cd /Users/twiecki/projects/transpailer

# Run transpilation for a single model:
cd /Users/twiecki/projects/transpailer && BRIDGESTAN=$HOME/.bridgestan/bridgestan-2.7.0 \
uv run python /Users/twiecki/projects/posteriordb/run_retranspile.py <model_name>

# All models follow: make_model(data: dict) -> pm.Model
# Output goes to: posterior_database/models/pymc/<model_name>.py
```

## Strategy

For each group below, the approach is:
1. Add the relevant PyMC API patterns to the transpailer skill (`transpailer/skills/stan_to_pymc.md`)
2. Re-run the transpiler for those models
3. If the transpiler still fails, port manually and validate with `tests/test_pymc_gradients.py`

---

## Group 1: ODE Models (3 models)

**Models:** `sir`, `covid19imperial_v2`, `covid19imperial_v3`
**Also possibly:** `soil_incubation` (if it uses ODEs internally)

### Tool: `sunode` (recommended) or `pymc.ode.DifferentialEquation` (fallback)

**sunode** wraps SUNDIALS solvers and is ~200x faster than the built-in `pymc.ode`:

```python
import sunode
import sunode.wrappers.as_pytensor

def sir_rhs(t, y, p):
return {
'S': -p.beta * y.S * y.I,
'I': p.beta * y.S * y.I - p.gamma * y.I,
'R': p.gamma * y.I,
}

with pm.Model() as model:
beta = pm.LogNormal("beta", mu=0, sigma=1)
gamma = pm.LogNormal("gamma", mu=0, sigma=1)

y_hat, _, problem, solver, _, _ = sunode.wrappers.as_pytensor.solve_ivp(
y0={'S': (S0, ()), 'I': (I0, ()), 'R': (R0, ())},
params={'beta': (beta, ()), 'gamma': (gamma, ())},
rhs=sir_rhs,
tvals=times,
t0=t0,
)
# y_hat is a dict of tensors: y_hat['S'], y_hat['I'], y_hat['R']
pm.Normal("obs", mu=y_hat['I'], sigma=sigma, observed=cases)
```

**Fallback** (`pymc.ode.DifferentialEquation`):
```python
from pymc.ode import DifferentialEquation

def sir_func(y, t, p):
beta, gamma = p[0], p[1]
S, I, R = y[0], y[1], y[2]
return [-beta * S * I, beta * S * I - gamma * I, gamma * I]

ode = DifferentialEquation(func=sir_func, times=times, n_states=3, n_theta=2, t0=0)
y_hat = ode(y0=[S0, I0, R0], theta=[beta, gamma])
```

### Experiment plan
1. Try `sunode` first for `sir` (simplest ODE model)
2. If sunode works, port `covid19imperial_v2/v3` (more complex SEIR with interventions)
3. For `soil_incubation`, check the Stan model to see if it's actually ODE-based
4. Note: The current `lotka_volterra` port uses a custom Op without gradients - consider
re-porting it with sunode too for proper gradient support

### Key difference from current approach
The existing `one_comp_mm_elim_abs.py` and `lotka_volterra.py` use a custom PyTensor `Op`
with `scipy.solve_ivp` in `perform()`. This works for logp but has NO gradients, meaning
NUTS won't work efficiently. `sunode` provides proper adjoint gradients.

---

## Group 2: HMMs (3 models)

**Models:** `hmm_gaussian`, `hmm_drive_1`, `iohmm_reg`
**Already ported:** `hmm_example`, `hmm_drive_0` (manual forward algorithm)

### Tool: `pymc_extras.distributions.DiscreteMarkovChain` + `pymc_extras.marginalize()`

This implements the forward algorithm automatically:

```python
import pymc_extras as pmx
from pymc_extras.distributions import DiscreteMarkovChain

with pm.Model() as model:
# Transition matrix
P = pm.Dirichlet("P", a=np.ones(K), shape=(K, K))
# Initial state distribution
init_dist = pm.Categorical.dist(p=np.ones(K) / K)
# Discrete Markov chain (will be marginalized out)
states = DiscreteMarkovChain("states", P=P, init_dist=init_dist, steps=T-1)

# Emission parameters
mu = pm.Normal("mu", 0, 10, shape=K) # or ordered for identifiability
sigma = pm.HalfNormal("sigma", 5, shape=K)

# Observations conditioned on hidden states
y = pm.Normal("y", mu=mu[states], sigma=sigma[states], observed=data)

# Marginalize out the discrete states (forward algorithm)
marginalized_model = pmx.marginalize(model, ["states"])

# Sample from the marginalized model
with marginalized_model:
idata = pm.sample()

# Recover posterior over discrete states
idata = pmx.recover_marginals(marginalized_model, idata)
```

### Per-model notes
- **`hmm_gaussian`**: Standard first-order Gaussian HMM. Direct fit with above pattern.
Use `ordered[K] mu` for identifiability (Stan model does this).
- **`hmm_drive_1`**: Same structure as `hmm_drive_0` (which was ported successfully with
manual forward algorithm). Try `DiscreteMarkovChain` approach instead.
- **`iohmm_reg`**: Input-Output HMM where transition probabilities depend on covariates.
May need time-varying transition matrix. Check if `DiscreteMarkovChain` supports
3D P tensors (P[t, i, j]). If not, may need manual forward algorithm with
`pytensor.scan`.

### Validation note
The Stan HMM models implement the forward algorithm manually and marginalize over
discrete states. The logp values should match exactly since both approaches compute
the same marginal likelihood. Gradient comparison is the definitive test.

---

## Group 3: Gaussian Processes (2 models)

**Models:** `hierarchical_gp`, `kronecker_gp`

### Tool: `pm.gp.Marginal`, `pm.gp.MarginalKron`, `pm.gp.cov.*`

The transpailer already has GP skills (`skills/gp.md`, `skills/gp_accelerate.md`,
`skills/gp_cuda.md`) but those are for Rust compilation, NOT for PyMC GP API.

**Add to `stan_to_pymc.md` skill:**

```python
# Standard GP regression
with pm.Model() as model:
ls = pm.InverseGamma("ls", 5, 5)
eta = pm.HalfCauchy("eta", beta=2)
cov = eta**2 * pm.gp.cov.ExpQuad(input_dim=1, ls=ls)

gp = pm.gp.Marginal(cov_func=cov)
sigma = pm.HalfCauchy("sigma", beta=3)
y_ = gp.marginal_likelihood("y", X=X, y=y, sigma=sigma)

# Kronecker-structured GP (grid data)
with pm.Model() as model:
cov1 = pm.gp.cov.ExpQuad(input_dim=1, ls=ls1)
cov2 = pm.gp.cov.ExpQuad(input_dim=1, ls=ls2)
gp = pm.gp.MarginalKron(cov_funcs=[cov1, cov2])
sigma = pm.HalfNormal("sigma", 1)
y_ = gp.marginal_likelihood("y", Xs=[X1, X2], y=y, sigma=sigma)
```

### Per-model notes
- **`kronecker_gp`**: Stan model manually implements Kronecker product eigendecomposition.
`pm.gp.MarginalKron` does this automatically. Map Stan's custom functions
(`kron_mvprod`, `calculate_eigenvalues`) to the built-in API.
- **`hierarchical_gp`**: Multi-level GP with shared hyperpriors across groups. Use
multiple `pm.gp.Marginal` or `pm.gp.Latent` instances sharing lengthscale/amplitude
priors. May also work with `pm.gp.HSGP` for scalability.

### Covariance function mapping (Stan → PyMC)
| Stan | PyMC |
|------|------|
| `cov_exp_quad(x, alpha, rho)` | `alpha**2 * pm.gp.cov.ExpQuad(D, ls=rho)` |
| `cov_matern32(x, alpha, rho)` | `alpha**2 * pm.gp.cov.Matern32(D, ls=rho)` |
| `cov_matern52(x, alpha, rho)` | `alpha**2 * pm.gp.cov.Matern52(D, ls=rho)` |
| `diag_matrix(rep_vector(sigma^2, N))` | `pm.gp.cov.WhiteNoise(sigma=sigma)` |

---

## Group 4: Topic Models / LDA (2 models)

**Models:** `ldaK2`, `ldaK5`

### Approach: Manual marginalization with `pt.logsumexp`

Stan marginalizes per-word topic assignments using `log_sum_exp`. PyMC equivalent:

```python
with pm.Model() as model:
# Topic-word distributions: phi[k] is word distribution for topic k
phi = pm.Dirichlet("phi", a=beta_prior, shape=(K, V))
# Document-topic distributions: theta[m] is topic distribution for doc m
theta = pm.Dirichlet("theta", a=alpha_prior, shape=(M, K))

# Marginalized likelihood (log_sum_exp over topics for each word)
# For word n in document doc[n]:
# p(w[n] | theta, phi) = sum_k theta[doc[n],k] * phi[k, w[n]]
log_lik = pt.logsumexp(
pt.log(theta[doc_idx, :]) + pt.log(phi[:, word_idx]).T,
axis=1
)
pm.Potential("log_lik", pt.sum(log_lik))
```

This directly mirrors Stan's approach. Do NOT use `pmx.marginalize()` for LDA -
the number of discrete variables (one per word token) is far too large.

### Note on indexing
Stan's `doc` and `w` arrays are 1-based. Convert: `doc_idx = data['doc'] - 1`,
`word_idx = data['w'] - 1`.

---

## Group 5: Remaining Models (6 models)

### `dogs_log`, `dogs_nonhierarchical`
These are avoidance learning models with cumulative sums. The transpiler failed
on validation (logp mismatch after 30 attempts). The pattern is:
- Cumulative count of avoidances and shocks per dog
- `inv_logit(beta[1] * n_avoid + beta[2] * n_shock)`
- Bernoulli likelihood

The `dogs` model (without `_log`) was successfully ported. Check what differs.
The `_log` variant uses `log(p)` parameterization. Try increasing `max_turns`
or manually adjusting the transpiled code.

### `soil_incubation`
Check if this is ODE-based (see Group 1) or a simpler compartment model.
Read the Stan source to determine.

### `gpcm_latent_reg_irt`
Generalized Partial Credit Model. Similar to `grsm_latent_reg_irt` which was
successfully ported. Compare the two Stan models and adapt the working port.

### `nn_rbm1bJ10`, `nn_rbm1bJ100`
Restricted Boltzmann Machine models. These have discrete binary latent units
that need marginalization. For small J (J=10), `pmx.marginalize()` with
`pm.Bernoulli` might work. For J=100, manual marginalization is needed.

Check if the Stan model uses the standard RBM free energy:
`F(v) = -b'v - sum_j log(1 + exp(c_j + W_j v))`
This can be computed directly with `pt.log1pexp` without enumerating states.

---

## Adding Skills to the Transpailer

To teach the transpiler these patterns, add sections to
`/Users/twiecki/projects/transpailer/transpailer/skills/stan_to_pymc.md`:

1. **ODE Models** section with sunode pattern
2. **HMM / Discrete Marginalization** section with `pmx.DiscreteMarkovChain`
3. **GP Models** section with `pm.gp.*` API mapping
4. **Topic Models** section with `pt.logsumexp` marginalization pattern

Then re-run the transpiler. Models that still fail should be ported manually.

## Validation

All ports must pass:
```bash
cd /Users/twiecki/projects/transpailer && BRIDGESTAN=$HOME/.bridgestan/bridgestan-2.7.0 \
uv run python -m pytest /Users/twiecki/projects/posteriordb/tests/test_pymc_gradients.py \
-k "<model_name>" -v
```

For models using sunode or pmx.marginalize, gradient comparison against BridgeStan
is still the gold standard. The gradients should match within 1e-3 relative error.
Loading