Stan¶
This page discharges the per-target obligations of Transpilation correctness for \(\mathsf{T} = \mathrm{Stan}\).
Semantics¶
Stan's denotational semantics is the factor-graph semantics of
Koller and Friedman (2009)
Chapter 4 instantiated by the Stan Reference Manual: a program
declares a finite set of random variables and a finite set of
factors, with ~ statements desugaring to target += <lpdf>
contributions to the log-density accumulator (Stan Reference
Manual §6.1, "Sampling Statements"). The log-density probe is
cmdstanpy.CmdStanModel.log_prob,
which evaluates the program's target at a parameter point on the
unconstrained scale.
The reference is Carpenter, Gelman, Hoffman, Lee, Goodrich, Betancourt, Brubaker, Guo, Li, and Riddell (2017).
Unconstrained-space change of variables¶
Stan is the only target in the supported set that uses a non-identity \(\Psi_{\mathsf{Stan}}\). Constrained-parameter declarations introduce the following changes of variables and Jacobians (Stan Reference Manual §10):
| Declaration | \(\Psi^{-1}_{\mathsf{Stan}}\) (constraint → unconstrained) | \(\log\bigl|\det J_{\Psi_{\mathsf{Stan}}}\bigr|\) |
|---|---|---|
| real<lower=0> | \(\theta = \exp(\widetilde\theta)\) | \(\widetilde\theta\) |
| real<lower=0, upper=1> | \(\theta = \mathrm{logit}^{-1}(\widetilde\theta)\) | \(\widetilde\theta - 2\log(1 + e^{\widetilde\theta})\) |
| vector<lower=0>[K] | per-element \(\exp\) | \(\sum_k \widetilde\theta_k\) |
| simplex[K] | stick-breaking | per Stan Reference Manual §10.6 |
| cov_matrix[K] | Cholesky factorization + log-Cholesky | per §10.9 |
| cholesky_factor_corr[K] | hyperspherical | per §10.10 |
| corr_matrix[K] | LKJ stick-breaking | per §10.10 |
The Stan renderer emits the constrained-space declaration; Stan's
runtime adds \(\log|\det J_{\Psi_{\mathsf{Stan}}}|\) to the target
accumulator automatically (cf. Stan Reference Manual §10.1, "Built-in
Transforms"). The renderer never emits the Jacobian term itself.
By Theorem 6.1 of the head page, the equality
holds at every joint point: the left-hand side is exactly Stan's
constrained-space log-density (Stan's target value minus the
Jacobian Stan added), which equals the QVR log-density on the
matching support.
Family parameterizations¶
Stan's families use the canonical parameterizations of the Stan Functions Reference. Most map identically from QVR's parameterization (Wikipedia canonical forms). The non-trivial cases:
| QVR family | Stan call | \(\pi_{F, \mathsf{Stan}}\) | \(c_{F, \mathsf{Stan}}\) |
|---|---|---|---|
Normal(μ, σ) |
normal(μ, σ) |
identity | 0 |
HalfNormal(σ) |
normal(0, σ) with <lower=0> declaration |
\(\sigma \mapsto (0, \sigma)\) | \(\log 2\) (the truncation normalizer) |
HalfCauchy(γ) |
cauchy(0, γ) with <lower=0> |
\(\gamma \mapsto (0, \gamma)\) | \(\log 2\) |
MultivariateNormal(μ, Σ) |
multi_normal(μ, Σ) |
identity | 0 |
LKJCorrCholesky(η) |
lkj_corr_cholesky(η) |
identity | 0 |
Dirichlet(α) |
dirichlet(α) |
identity (scalar broadcast via rep_vector) |
0 |
Categorical(p) |
categorical(p) |
identity (1-indexed) | 0 |
Bernoulli(p) |
bernoulli(p) |
identity | 0 |
LogitNormal(μ, σ) |
not supported (no native logit_normal_lpdf) |
— | raises family:LogitNormal |
| every other family in the registry | identical Stan name | identity | 0 |
Stan's 1-indexed categorical adds 1 to QVR's 0-indexed support;
the IntegerInterval(1, K) declaration on Stan's data side
reflects this. The renderer emits the index expression k
inside the for (k in 1:K) loop directly; no offset is added at
the call site.
The half-distribution support truncation contributes \(c_{F, \mathsf{Stan}} = \log 2\) per call site. This is collected into the per-program constant \(C_{\mathsf{Stan}}(M)\) and absorbed by Bayes-rule normalization.
Per-construct emit¶
SampleStep(x, F, args). top_var_decl_no_assign of type
\(\tau_F\) in parameters (with the right constraint per the
family's .support); sampling_statement \(x \sim F_{\mathsf{Stan}}(\mathrm{args})\)
in model. Soundness: by Stan's ~ desugaring, the contribution
to target is exactly \(\log f_{F, \mathsf{Stan}}(x \mid
\mathrm{args})\).
ObserveStep(y, F, args). \(y\) declared in data; same ~
statement in model contributing \(\log f_{F, \mathsf{Stan}}(y \mid
\mathrm{args})\).
Plate (Indexed Bind / Indexed Observe). Nested
for (m_<axis> in 1:N_<axis>) { ... } loops; the latent or
observation is declared as array[N_<axis>] <type>. Per Stan
Reference Manual §6.4, the for-loop's contribution to target is
the additive sum of per-iteration ~ contributions. Soundness
follows from the plate discussion.
MarginalizeStep. Stan emits the canonical log_sum_exp
enumeration. For a grouped marginalize with latent \(z : K\) on
group axis \(G\) and scope observe via fibration \(g : R \to G\):
{
array[|G|] vector[K] lps_z;
for (gi in 1:|G|)
for (k in 1:K)
lps_z[gi, k] = categorical_lpmf(k | F_args);
for (n in 1:|R|)
for (k in 1:K)
lps_z[g[n], k] += <scope_lpdf>(y[n] | args_obs(k));
for (gi in 1:|G|)
target += log_sum_exp(lps_z[gi]);
}
Soundness: the inner accumulation is exact arithmetic in
log-space; the outer log_sum_exp is the documented
log_sum_exp primitive that computes \(\log \sum_k \exp(\cdot)\)
accurately. Under the finite-enumeration strategy,
the emitted log-density equals the marginalized joint up to the
per-family constants of Lemma 5.1.1.
The Stan renderer raises
UnsupportedConstruct(["marginalize:non-finite-support:<family>"])
when finite_enumerable_at_call_site
returns False; cf.
ZIP regression for the
continuous-support workaround.
ScoreStep(name, expr). real <name> = <expr>; in
transformed_parameters, plus target += <name>; in model.
Contribution: \(\log w_{\mathrm{name}} = \mathrm{expr}\).
LetStep(name, expr). real <name> = <expr>; in
transformed_parameters. Denotationally inert.
Return(v_1, \dots, v_m). generated quantities { <type> v_i_value = v_i; }
for each return variable that is already a parameter (Stan rejects
re-declaration). Denotationally inert.
Acceptance¶
- Tier 1 pipeline composition. Structural checks cover the
expected Stan blocks and the
Lower >> StanRenderer >> EmitPretty(stan)pipeline. - Tier 2 external syntax.
stancaccepts the generated programs in the external-validation matrix. - Tier 3 numeric equivalence. For selected fixtures,
cmdstanpy.log_probis compared with the QVR reference on the shared finite grid and tolerance described in the test contract, after applying any documented backend-specific correction.