Drives an Iterative Ensemble Smoother entirely in R, using a user-supplied
forward model callable instead of the PEST++ .pst-file invocation cycle.
Each iteration:
Usage
pesto_ies_callback(
forward_model,
prior_ensemble,
obs,
obs_sd,
noptmax = 4L,
lambda = 1,
fidelity_schedule = NULL,
parcov = NULL,
eigthresh = 1e-06,
use_approx = TRUE,
inflation = NULL,
localisation = NULL,
on_failure = c("na", "stop"),
verbose = TRUE,
phi_tol = NULL,
obs_perturbation = c("mda", "none"),
mda_alpha = NULL,
seed = NULL
)Arguments
- forward_model
One of: a function with signature
function(theta) -> obs(wherethetais annreal x nparnumeric matrix andobsannreal x nobsnumeric matrix); apesto_forward_model()(the typed contract object, e.g. one carrying aparallel = "multicore"evaluation strategy); or apesto_multifidelity_model()(then seefidelity_schedule). A bare function is auto-wrapped viaas_forward_model()with this call'son_failure. Failed realisations may return rows ofNA; the driver tolerates them (seeon_failure). To run realisations in parallel, pass apesto_forward_modelbuilt withparallel = "multicore"rather than a bare function.- prior_ensemble
Matrix or data.table,
nreal x npar. Columns are parameters; an optionalreal_namecolumn is preserved if present. Column names supply parameter names. To warm-start, pass the parameter columns of a previous run'spar_ensembleas the prior of the next: a posterior ensemble is itself a valid prior ensemble.- obs
Named numeric vector. Target observations.
- obs_sd
Numeric scalar or vector of length
nobs. Observation standard deviation(s); the IES weights are1/obs_sd.- noptmax
Integer. Number of IES iterations (default 4).
- lambda
Numeric scalar or vector. Marquardt lambda per iteration. A scalar is recycled; a vector shorter than
noptmaxis right-padded with its last value (default 1.0). Under the defaultobs_perturbation = "mda"the Marquardt damping is the ES-MDA inflation (alpha = lambda + 1), so supplyinglambdathere fixes the inflation schedule and it must satisfysum(1 / (lambda + 1)) == 1; if it does not, the call errors rather than returning a mis-calibrated posterior. Leavelambdaalone and usemda_alphato choose the schedule in its natural units.- fidelity_schedule
Integer vector or
NULL. Only consulted whenforward_modelis apesto_multifidelity_model(): the fidelity level to evaluate at each iteration (recycled / right-padded tonoptmax).NULL(default) uses the highest fidelity every iteration. Ignored otherwise.- parcov
Numeric vector of length
npar, the diagonal of the prior parameter covariance. Defaults to the column-wise variance ofprior_ensemble; zero or negative entries are replaced with 1.0.- eigthresh
Numeric. SVD eigenvalue truncation threshold (default 1e-6).
- use_approx
Logical. If TRUE (default), skip the prior-scaling correction (upgrade_2); matches the typical
pestpp-iesdefault.- inflation
A
pesto_inflation()specification, orNULL(default) for no covariance inflation. Inflation counteracts ensemble under-dispersion – the progressive collapse of posterior spread a finite-ensemble smoother suffers.NULLleaves the update identical to the un-inflated smoother.- localisation
A
pesto_localisation()specification, orNULL(default) for no localisation. Localisation tapers the Kalman gain to suppress spurious finite-sample parameter-observation correlations; the active path usesensemble_solution_localised()(approximate / upgrade_1 form), so a non-NULLlocalisationwithuse_approx = FALSEwarns and drops the null-space correction.- on_failure
Character.
"na"(default) carries failed realisations forward unchanged and proceeds;"stop"aborts on any failure.- verbose
Logical. Print per-iteration phi summaries.
- phi_tol
Numeric scalar or
NULL. Optional convergence tolerance: when non-NULL, iteration stops early once the relative reduction in the mean objective function (phi) between successive iterations falls belowphi_tol– the phi-reduction stopping rule of White (2018).NULL(default) runs the fullnoptmaxiterations, leaving the update byte-identical to the unchecked smoother. Use a smallerphi_tolto demand more iterations, a larger one to stop sooner. Note that stopping early truncates the ES-MDA schedule, so underobs_perturbation = "mda"less than one likelihood is assimilated and the posterior comes back too wide; the driver warns when this happens.- obs_perturbation
Character,
"mda"(default) or"none". How the observations enter the update. See Posterior spread and observation perturbation."mda"gives each realisation its own perturbed data vector under the ensemble-smoother-with-multiple-data-assimilation scheme, which is what makes the returned ensemble a posterior sample."none"assimilates the same unperturbed vector into every realisation: the ensemble mean still converges to the maximum a posteriori fit, but the spread is not a posterior spread and must not be read as uncertainty.- mda_alpha
Numeric vector or
NULL. The ES-MDA inflation schedule \(\alpha_k\), one entry per iteration (recycled / right-padded).NULL(default) uses the uniform schedule \(\alpha_k = \)noptmaxof Emerick & Reynolds (2013). Any schedule must satisfy \(\sum_k 1/\alpha_k = 1\); the Marquardt lambda schedule is then derived as \(\lambda_k = \alpha_k - 1\). Only meaningful whenobs_perturbation = "mda".- seed
Integer or
NULL. When supplied,set.seed(seed)is called once before the run (so it advances the session stream exactly as a directset.seed()would) and the value is recorded on the result, from whereas_manifest()carries it into the manifest'sseedslot.NULL(default) draws the observation noise from the session stream and recordsNA; reproducibility is then the caller's ownset.seed().
Value
A list of class c("pesto_ies_callback_result", "pesto_ies_result")
with components:
- phi
data.table of per-realisation phi by iteration.
- par_ensemble
Final parameter ensemble (data.table).
- obs_ensemble
Final simulated-observation ensemble (data.table).
- obs_perturbation
Assimilation-scheme provenance:
scheme("mda"or"none"), the realised ES-MDA inflation schedulealpha, the runseed(NAwhen none was supplied), andobs_noise– thenobs x nrealnoise ensemble of the final assimilation step (NULLunder"none").- iterations
List of per-iteration metadata:
lambda,mda_alpha,mean_phi,n_failures, and the dispersion diagnosticsspread_ess/spread_ess_ratio(ensemble_spread_ess()), plusinflation_method/inflation_factor/retentionandlocalisation/loc_threshold/loc_frac_activewhen those countermeasures are active.- runtime_seconds
Total wall-clock runtime.
- n_forward_evals
Total number of realisation-level forward evaluations across all iterations (including the final refresh).
- failure_rate
Fraction of forward evaluations that returned NA.
- converged
Logical:
TRUEif the run stopped early on thephi_tolconvergence criterion;FALSEwhenphi_tolisNULLor the fullnoptmaxwas reached.- n_iterations
Number of IES iterations actually run (fewer than
noptmaxif the convergence checker stopped the run early).- fidelity
For a
pesto_multifidelity_model()run, a provenance listlist(type, schedule, final_level, n_levels, costs)recording the realised per-iteration fidelity schedule;NULLfor a single-fidelity run. Consumed byas_manifest()to populate the manifest contract.
Details
Evaluates
forward_model(par_ensemble)to obtain simulated observations.Forms parameter / observation anomalies and residuals.
Calls the C++ kernel
ensemble_solution()(GLM form, Chen & Oliver 2013) to compute annreal x nparupgrade.Adds the upgrade to the current ensemble.
The classic .pst-file path remains available via pesto_ies() for full
PEST++ compatibility. Use this callback driver when the forward model is
itself an R function (e.g. an apsimx wrapper from apsim_callback(),
a Python bridge, or a synthetic test problem) and the per-realisation
file-I/O overhead of the .pst path is the bottleneck.
Phase-1 behaviour: single lambda per iteration (or a user-supplied schedule).
A line-search over ies_lambda_mults matching pestpp-ies is a planned
Phase-2 enhancement; for the common case of a well-behaved forward model
with lambda = 1, the GLM update reduces phi reliably (see vignette
apsim-callback).
Posterior spread and observation perturbation
An ensemble smoother that assimilates the same, unperturbed observation
vector into every realisation does not return a posterior sample. Every
member feels an identical data pull, so the between-member spread is only
the prior variance the update failed to remove, and it does not shrink
towards the right answer as the ensemble grows. PESTO's default therefore
gives realisation \(j\) its own perturbed data,
$$d_j = d + \sqrt{\alpha_k}\,e_j, \qquad e_j \sim N(0, C_D),$$
and, because an iterative smoother assimilates the same data at every
step, inflates the measurement covariance by \(\alpha_k\) subject to
\(\sum_k 1/\alpha_k = 1\) so that exactly one likelihood is
assimilated in total. That is the ensemble smoother with multiple data
assimilation (ES-MDA) of Emerick & Reynolds (2013); the update step is
$$m_j \leftarrow m_j + C_{MD}(C_{DD} + \alpha_k C_D)^{-1}
(d + \sqrt{\alpha_k}\,e_j - g(m_j)).$$
PESTO's GLM kernel already computes that form: with \(C_D = W^{-2}\)
the Marquardt damping and the MDA inflation are the same number,
\(\alpha_k = \lambda_k + 1\), so the lambda schedule is derived from
mda_alpha rather than set independently.
On a linear-Gaussian problem the resulting ensemble covariance matches the
closed-form conjugate posterior (graded in
tests/testthat/test-posterior-calibration.R), and the nominal coverage of
its central intervals is realised. The reported phi is unaffected: it is
always computed against the unperturbed observations, so it remains the fit
to the data the user supplied.
obs_perturbation = "none" restores the unperturbed update. It is the
right choice when a single best-fit parameter vector is wanted and the
spread will be discarded, and the wrong choice for anything that reads the
ensemble as uncertainty.
Multi-fidelity
When forward_model is a pesto_multifidelity_model(), fidelity_schedule
selects which fidelity level each iteration evaluates – a recycled / padded
integer vector, one entry per iteration (default: the highest fidelity every
iteration, i.e. exactly the single-fidelity behaviour). This supports
fidelity ramping (cheap early iterations, expensive late ones); the final
ensemble refresh always uses the highest fidelity so the returned posterior
is at full resolution. The control-variate combiner mf_control_variate()
is the plug-in point for bias-corrected surrogate cascades.
References
Chen, Y. & Oliver, D.S. (2013). Levenberg-Marquardt forms of the iterative ensemble smoother for efficient history matching and uncertainty quantification. Computational Geosciences, 17(4), 689–703.
Emerick, A.A. & Reynolds, A.C. (2013). Ensemble smoother with multiple data assimilation. Computers & Geosciences, 55, 3–15.
Evensen, G. (2018). Analysis of iterative ensemble smoothers for solving inverse problems. Computational Geosciences, 22(3), 885–908.
White, J.T. (2018). A model-independent iterative ensemble smoother for efficient history-matching and uncertainty quantification in very high dimensions. Environmental Modelling & Software, 109, 191–201.
See also
pesto_ies() for the .pst-file path; apsim_callback()
for the apsimx adapter.
Examples
# Linear-Gaussian recovery toy
set.seed(1)
npar <- 3; nobs <- 6; nreal <- 80
G <- matrix(rnorm(nobs * npar), nobs, npar)
theta_true <- c(1.0, -0.5, 2.0)
y <- as.numeric(G %*% theta_true) + rnorm(nobs, sd = 0.05)
f <- function(theta) theta %*% t(G)
prior <- matrix(rnorm(nreal * npar), nreal, npar,
dimnames = list(NULL, paste0("p", 1:npar)))
fit <- pesto_ies_callback(
forward_model = f, prior_ensemble = prior,
obs = setNames(y, paste0("o", 1:nobs)), obs_sd = 0.05,
noptmax = 5, verbose = FALSE
)
colMeans(as.matrix(fit$par_ensemble[, -1])) # should approach theta_true
#> p1 p2 p3
#> 1.0156788 -0.4975924 1.9837106
