Stan Coding Guidelines
Use this skill when writing or modifying Stan programs to ensure clean, efficient code.
Program Structure
Use canonical block order: functions, data, transformed data, parameters, transformed parameters, model, generated quantities.
Follow Stan style:
- Two-space indents, no tabs, ≤80 character lines
- Opening braces at end of line:
for (n in 1:N) {
- Spaces around operators and after commas
- Variable names: lowercase with underscores (
sigma_y, mu_group)
- Dimension constants: single uppercase letters (
N, K, J)
- Declare locals close to use; scalars inside loops, reused containers outside
Types and Containers
Use appropriate types:
- Linear algebra:
matrix, vector, row_vector with matrix operations (x * beta)
- Indexing/containers:
array[N] real y (not legacy real y[N])
- Repeated row access:
array[M] row_vector[N] x over matrix[M, N]
- Heterogeneous returns:
tuple(...) for multiple values
- Sum-to-zero:
sum_to_zero_vector, sum_to_zero_matrix instead of manual constraints
Memory layout: matrices are column-major, arrays are row-major.
Distributions and Vectorization
Always use log form:
- Write
y ~ normal(mu, sigma) or target += normal_lpdf(y | mu, sigma)
- Vectorize:
y ~ normal(mu, sigma) for arrays, not loops
- Use GLM functions:
bernoulli_logit_glm, poisson_log_glm, normal_id_glm
- Precompute shared expressions: compute
mu = X * beta once, reuse
- Finite mixtures: use
log_sum_exp on log scale
Parameterization
Use constrained types over manual checks:
<lower=0>, <upper=...>, ordered, positive_ordered, simplex, unit_vector
- Covariance (K≥3):
cholesky_factor_corr[K] L_Omega with multi_normal_cholesky
- Sum-to-zero: use built-in types, not "last element = minus sum"
For custom transforms, use built-in *_constrain, *_unconstrain, *_jacobian functions.
Parallelization
For large-N models with independent terms, use reduce_sum:
- Write partial sum function that takes data slice and returns log-density contribution
- Keep partial sum vectorized internally
- No side effects (no printing, no mutation)
Functions
Modularize complex logic in functions block:
- Reused operations, complex math, custom likelihoods
- Signature: data arguments first, then parameters, then tuning constants
- Use
tuple returns for multiple heterogeneous outputs
Preventing Crashes
Both compilation and sampling can crash or OOM.
Defensive Stan patterns:
- Always use tight bounds:
int<lower=1, upper=K> id[N]
- Guard math: check parameters before
log, sqrt, division
- Add explicit bounds for dispersion:
real<lower=0.01> phi (never exactly 0)
Execution:
- Wrap
CmdStanModel() and model.sample() in try-except
- Probe with short runs before full sampling
On crash/OOM:
- Reduce
parallel_chains (4 → 2 → 1)
- Reduce
max_treedepth (10 → 8)
- Subsample data or simplify model
ArviZ Integration
Design Stan programs for downstream ArviZ workflow:
Generated quantities:
- Always include pointwise log-likelihood:
vector[N] log_lik - required for model comparison and downstream workflow
- Always include posterior predictive draws:
vector[N] y_rep - required for all predictive checks
- For multiple observed variables, use one vector per variable:
log_lik_y1, log_lik_y2
- This will incur modest overhead, but might be worth workflow simplicity
Transformed parameters:
- Put reusable intermediate quantities here (e.g.,
vector[N] mu = alpha + X * beta)
- Avoids recomputation in Python and makes them available in posterior samples
Extending without refitting:
- To add new derived quantities, use
generate_quantities mode with original posterior draws
- Write new Stan file with same data/parameters/transformed parameters but extended generated quantities
- Call
model.generate_quantities(data=data, mcmc_sample=fit) - orders of magnitude faster than refitting
Save and cache:
- Convert to InferenceData:
az.from_cmdstanpy(fit, log_likelihood="log_lik", posterior_predictive=["y_rep"])
- Save as NetCDF:
idata.to_netcdf("posterior.nc") - makes all downstream analysis instant
- Use consistent coords/dims for all models in the workflow
References
If stuck on Stan patterns or ArviZ usage, search these resources:
Use WebSearch or WebFetch to find specific examples.
1---2name: stan-coding-23description: Best practices for writing efficient, clean Stan programs4---56# Stan Coding Guidelines78Use this skill when writing or modifying Stan programs to ensure clean, efficient code.910## Program Structure1112Use canonical block order: `functions`, `data`, `transformed data`, `parameters`, `transformed parameters`, `model`, `generated quantities`.1314Follow Stan style:15- Two-space indents, no tabs, ≤80 character lines16- Opening braces at end of line: `for (n in 1:N) {`17- Spaces around operators and after commas18- Variable names: lowercase with underscores (`sigma_y`, `mu_group`)19- Dimension constants: single uppercase letters (`N`, `K`, `J`)20- Declare locals close to use; scalars inside loops, reused containers outside2122## Types and Containers2324Use appropriate types:25- Linear algebra: `matrix`, `vector`, `row_vector` with matrix operations (`x * beta`)26- Indexing/containers: `array[N] real y` (not legacy `real y[N]`)27- Repeated row access: `array[M] row_vector[N] x` over `matrix[M, N]`28- Heterogeneous returns: `tuple(...)` for multiple values29- Sum-to-zero: `sum_to_zero_vector`, `sum_to_zero_matrix` instead of manual constraints3031Memory layout: matrices are column-major, arrays are row-major.3233## Distributions and Vectorization3435Always use log form:36- Write `y ~ normal(mu, sigma)` or `target += normal_lpdf(y | mu, sigma)`37- Vectorize: `y ~ normal(mu, sigma)` for arrays, not loops38- Use GLM functions: `bernoulli_logit_glm`, `poisson_log_glm`, `normal_id_glm`39- Precompute shared expressions: compute `mu = X * beta` once, reuse40- Finite mixtures: use `log_sum_exp` on log scale4142## Parameterization4344Use constrained types over manual checks:45- `<lower=0>`, `<upper=...>`, `ordered`, `positive_ordered`, `simplex`, `unit_vector`46- Covariance (K≥3): `cholesky_factor_corr[K] L_Omega` with `multi_normal_cholesky`47- Sum-to-zero: use built-in types, not "last element = minus sum"4849For custom transforms, use built-in `*_constrain`, `*_unconstrain`, `*_jacobian` functions.5051## Parallelization5253For large-N models with independent terms, use `reduce_sum`:54- Write partial sum function that takes data slice and returns log-density contribution55- Keep partial sum vectorized internally56- No side effects (no printing, no mutation)5758## Functions5960Modularize complex logic in `functions` block:61- Reused operations, complex math, custom likelihoods62- Signature: data arguments first, then parameters, then tuning constants63- Use `tuple` returns for multiple heterogeneous outputs6465## Preventing Crashes6667Both compilation and sampling can crash or OOM.6869**Defensive Stan patterns:**70- Always use tight bounds: `int<lower=1, upper=K> id[N]`71- Guard math: check parameters before `log`, `sqrt`, division72- Add explicit bounds for dispersion: `real<lower=0.01> phi` (never exactly 0)7374**Execution:**75- Wrap `CmdStanModel()` and `model.sample()` in try-except76- Probe with short runs before full sampling7778**On crash/OOM:**79- Reduce `parallel_chains` (4 → 2 → 1)80- Reduce `max_treedepth` (10 → 8)81- Subsample data or simplify model8283## ArviZ Integration8485Design Stan programs for downstream ArviZ workflow:8687**Generated quantities:**88- Always include pointwise log-likelihood: `vector[N] log_lik` - required for model comparison and downstream workflow89- Always include posterior predictive draws: `vector[N] y_rep` - required for all predictive checks90- For multiple observed variables, use one vector per variable: `log_lik_y1`, `log_lik_y2`91- This will incur modest overhead, but might be worth workflow simplicity9293**Transformed parameters:**94- Put reusable intermediate quantities here (e.g., `vector[N] mu = alpha + X * beta`)95- Avoids recomputation in Python and makes them available in posterior samples9697**Extending without refitting:**98- To add new derived quantities, use `generate_quantities` mode with original posterior draws99- Write new Stan file with same data/parameters/transformed parameters but extended generated quantities100- Call `model.generate_quantities(data=data, mcmc_sample=fit)` - orders of magnitude faster than refitting101102**Save and cache:**103- Convert to InferenceData: `az.from_cmdstanpy(fit, log_likelihood="log_lik", posterior_predictive=["y_rep"])`104- Save as NetCDF: `idata.to_netcdf("posterior.nc")` - makes all downstream analysis instant105- Use consistent coords/dims for all models in the workflow106107## References108109If stuck on Stan patterns or ArviZ usage, search these resources:110- Stan case studies: https://mc-stan.org/learn-stan/case-studies.html111- ArviZ API documentation: https://python.arviz.org/en/latest/api/index.html112113Use WebSearch or WebFetch to find specific examples.