stan_code() writes the likelihood learned by nle() out as Stan source: a
functions block that recomputes \(\log q_\phi(x \mid \theta)\) in Stan's
own language, with the trained weights travelling alongside as data. Stan
differentiates the generated code itself, so NUTS gets exact gradients and
nothing has to be linked against torch at run time.
Usage
stan_code(fit, name = "nsbi_log_lik", model = TRUE)
write_stan_model(fit, file, name = "nsbi_log_lik", model = TRUE)
stan_data(fit, x_obs = NULL, model = TRUE)Arguments
- fit
An
nsbi_nleobject fromnle().- name
Prefix for the generated functions.
- model
Generate a complete, runnable model (the default) or only the
functionsblock, for#include-ing into a model of your own. Instan_data(), this must agree with themodela pairedstan_code()call used:model = TRUErequiresx_obs, because the generated model's data block declaresNandxand there is nothing to fill them with otherwise;model = FALSEmatches a functions-only export, which has noN/xto fill, sox_obsis optional there.- file
Path to write to.
- x_obs
Observation to put in the data list. Rows are independent observations. Required when
model = TRUE.
Value
stan_code() returns the Stan program as a single string;
write_stan_model() returns file invisibly; stan_data() returns a
named list ready for cmdstanr or rstan.
Why this is useful
The exported function is an ordinary Stan function of theta. That means
the surrogate likelihood stops being the whole model and becomes one term in
a model you write: put a hierarchical prior over the parameters, add
covariates, mix in a second data source with a likelihood you do know, and
let NUTS sample the lot. None of that is reachable from a posterior
estimator, which only ever knows the one conditional it was trained on.
The generated interface
Two entry points, where w is the packed weight vector from stan_data():
real nsbi_log_lik_lpdf(vector x, vector theta, vector w); // one observation
real nsbi_log_lik_sum_lpdf(matrix x, vector theta, vector w); // rows are i.i.d.Both take x and theta in the original units the simulator and prior use;
the standardization the estimator trained under is folded into the generated
code. Prefer the _sum form for repeated observations: for the MDN and the
linear-Gaussian estimator the conditional distribution depends on theta
alone, so it is computed once and reused across every row.
Why the code is generated rather than called
Python sbi hands its learned likelihood to Pyro or PyMC as a callable,
because everything lives in one process and the sampler can differentiate
the same graph the estimator was trained in. Stan cannot work that way: it
compiles to C++ and needs the gradient inside its own autodiff. The
alternative would be an external C++ header (--allow-undefined plus a
USER_HEADER) linking torch into every Stan compile, with the gradients
plumbed by hand. Generating source keeps the run-time dependency at zero and
makes the result something you can read, edit, and check – which is what the
package's own tests do, by evaluating the emitted functions and comparing
them against log_lik().
Supported estimators
"mdn", "maf" and "linear_gaussian". "nsf" is not exported – its
rational-quadratic spline transform would be a large and fragile block of
generated Stan. Refit with "maf" if you need a flow.
nre() fits are not exported at all. What gets transpiled here is a
density; a ratio estimator holds a classifier instead, and there is no
p(x | theta) in it to write out.
Supported priors
Only model = TRUE needs a prior, since only then is there a model block to
put one in. prior_uniform() and prior_normal() travel as data, so their
bounds and their mean and standard deviation come from stan_data() and the
compiled model does not care what they are. Every other named family (see
prior_families), including prior_independent() products and
prior_truncated() bounds, is written out as literal sampling statements
with T[,] where the support was cut down. A prior_custom() cannot be
written out at all: take stan_code(fit, model = FALSE) and write the model
block yourself.
Examples
prior <- prior_uniform(c(mu = -3), c(mu = 3))
fit <- nle(prior, function(mu) c(y = rnorm(1, mu, 0.5)),
n_simulations = 500, density_estimator = "linear_gaussian")
cat(substr(stan_code(fit), 1, 400))
#> // Generated by neuralsbi::stan_code(). Do not edit by hand.
#> // Surrogate likelihood q(x | theta) from an nle() fit (linear_gaussian, 500 simulations).
#> functions {
#> // conditional mean, given the standardized parameter
#> vector nsbi_log_lik_mean(vector ts, vector w) {
#> return (to_matrix(segment(w, 1, 2), 2, 1))' * append_row(1.0, ts);
#> }
#>
#> real nsbi_log_lik_lpdf(vector x, vector theta, vecto
str(stan_data(fit, matrix(rnorm(10), ncol = 1)), max.level = 1)
#> List of 6
#> $ nsbi_nw : int 3
#> $ nsbi_w : num [1:3] 9.32e-18 9.59e-01 2.83e-01
#> $ N : int 10
#> $ x : num [1:10, 1] 0.811 -1.586 -0.975 -0.782 1.202 ...
#> $ nsbi_low : num -3
#> $ nsbi_high: num 3
str(stan_data(fit, model = FALSE), max.level = 1)
#> List of 4
#> $ nsbi_nw : int 3
#> $ nsbi_w : num [1:3] 9.32e-18 9.59e-01 2.83e-01
#> $ nsbi_low : num -3
#> $ nsbi_high: num 3
