A simulator in neuralsbi is called once per parameter set and returns
one simulated observation. Everything that calls a simulator –
npe(), simulate_for_sbi(), npe_sequential(), sbc(), tarp(),
posterior_predictive() – uses this contract.
How parameters arrive
Two signatures are accepted, and which one applies is decided once per run
from formals(simulator). If every parameter name appears among the
simulator's formals, the parameters are passed by name, one scalar each:
prior <- prior_uniform(low = c(mu = -5, sigma = 0.1),
high = c(mu = 5, sigma = 3))
simulator <- function(mu, sigma) {
y <- rnorm(100, mean = mu, sd = sigma)
c(mean = mean(y), sd = sd(y))
}Otherwise the whole named parameter vector goes to the first argument:
simulator <- function(theta) {
y <- rnorm(100, mean = theta["mu"], sd = theta["sigma"])
c(mean = mean(y), sd = sd(y))
}An unnamed prior always takes the vector form, and so does a prior whose
names are not syntactic R names ("beta[1]"), since those can never match a
formal.
Everything else the simulator needs
Observed data, a time grid, a population size, a design matrix, a solver
tolerance: anything that is not a calibrated parameter goes in sim_args, a
named list forwarded to every simulator call.
simulator <- function(alpha, beta, sigma, x_grid) {
rnorm(length(x_grid), mean = alpha + beta * x_grid, sd = sigma)
}
fit <- npe(prior, simulator, n_simulations = 10000,
sim_args = list(x_grid = seq(-1, 1, length.out = 50)))A list is used rather than ... because every one of npe()'s own
arguments would otherwise be exposed to R's partial matching: x, theta,
n and seed are all natural names for a simulator argument and all of
them would be captured silently.
What the simulator returns
One observation per call: a numeric vector of length d, a scalar, or a
one-row matrix or data frame. Names on the vector, or colnames on the
matrix or data frame, become the outcome names used in summaries and plots.
Every draw must agree on d. Lists, character or factor columns, and
anything with more than one row are rejected by name.
One sharp edge: arithmetic on a single named parameter keeps the name, so
theta["beta"] / theta["gamma"] returns a scalar named beta and would
quietly become an outcome called beta. Vectors are unaffected –
theta["alpha"] + theta["beta"] * x comes back unnamed. Use unname(), or
name the outcome deliberately with c(r0 = ...).
Failed simulations
A draw whose output contains NA, NaN or an infinite value is dropped,
together with its parameters, and one warning per run reports the count and
the rate. A single non-finite value would otherwise poison the training loss
for the whole fit and surface much later as a NaN validation loss.
Dropping conditions on the simulator having succeeded. When failure depends
on the parameters – an ODE that diverges for large beta, a model that
returns NA outside a stability region – the surviving draws are no longer
a sample from the prior, and the fit targets the posterior given success
rather than the posterior. A high drop rate is a modelling signal, and the
honest fix is usually to handle the failure inside the simulator.
See also
nsbi_parallel for how the calls are spread across workers.
