Skip to content

enrml⚓︎

EnRML type schemes

GNEnRML ⚓︎

Bases: IterativeEnRML

Gauss-Newton Ensemble Randomized Maximum Likelihood (GN-EnRML).

Solves the same randomized maximum likelihood problem as :class:LMEnRML, but takes undamped Gauss-Newton steps scaled by a step length :math:\gamma \in (0, 1] rather than inflating the Hessian:

.. math::

m \leftarrow m + \gamma \, C_{md} (C_d + C_{dd})^{-1}
(d_{obs} - g(m))

Steps are accepted or rejected on the mean data misfit as in LM-EnRML. On acceptance :math:\gamma is relaxed towards gamma_max; on rejection it is divided by gamma_factor and the step re-solved, in the same within-:meth:update_step loop LM-EnRML uses for :math:\lambda.

Parameters:

Name Type Description Default
keys_da dict

Parsed dataassim configuration. Besides the keys every scheme reads -- data, datavar, obsname, truedataindex -- the ones this scheme acts on are listed under Notes.

required
keys_en dict

Parsed ensemble configuration: ensemble size ne, the state variable names, and the prior_<name> blocks describing each.

required
sim object

Forward simulator instance, e.g. simulator.opm.flow.

required
analysis (approx, full, subspace)

Analysis flavour, i.e. how the ensemble-approximated sensitivity is inverted. Defaults to the analysis key in keys_da, falling back to 'approx'. The flavours differ in cost and in how they handle a rank-deficient ensemble; they solve the same update equation.

'approx'

Attributes:

Name Type Description
ensemble AssimilationEnsemble

Collaborator holding the state realisations, observed data and simulator. Its state is exposed as properties on the scheme, so scheme.enX and scheme.keys_da read straight through.

analysis AnalysisBase

The bound analysis object. Note the constructor takes analysis as a name and this attribute holds the resulting object, the way Model(optimizer="adam").optimizer is an optimizer instance.

analysis_name str

The flavour name that was resolved, e.g. 'approx'.

iteration int

Accepted iterations completed so far.

data_misfit, prior_data_misfit float

Current and initial mean data misfit.

Notes

Configured through the iteration block of keys_da:

max_iter Maximum accepted iterations. gamma Initial step length (default 0.2). gamma_max Value the step length relaxes towards on success (default 0.5). gamma_factor Divisor applied to the step length on rejection (default 2.5). max_inner_iter Step-length attempts one iteration may make before the run gives up (default 10). There is no gamma_min, so this is what bounds it. data_misfit_tol Relative misfit change treated as converged (default 0.01).

The margis flavour is backed by margIS_update, ported from an older layout. It returns a matrix-form ensemble transform step (AnalysisResult(W_step=...), starting from W = I) rather than the weight step most other flavours use; propose_state reconstructs the state for either. Run against real data it produces a large, sensible misfit reduction, but is still one run on one case with no committed reference pinning it -- see its module docstring (:mod:pipt.update_schemes.analysis.margis) for what was fixed in the port and what remains a modelling choice rather than a bug.

Examples:

>>> result = GNEnRML.assimilate(keys_da, keys_en, flow(keys_sim))
References

Chen and Oliver chen2013; see also Raanes, Stordal and Evensen, Revising the stochastic iterative ensemble smoother raanes2019, and Evensen et al. evensen2019.

See Also

IterativeEnRML : The loop, scoring and bookkeeping both schemes share. LMEnRML : Levenberg-Marquardt form, damped via the Hessian.

log_columns(prior_run=False) ⚓︎

GN-EnRML reports the step length the logged iteration took.

IterativeEnRML ⚓︎

Bases: AssimilationScheme

What LM-EnRML and GN-EnRML share: everything but the control parameter.

Both solve the randomized maximum likelihood problem by repeated linearisation, accept or reject each step on the mean data misfit, retry a rejected step from the same state inside :meth:update_step, and stop on the relative misfit change, on max_inner_iter failed attempts in one iteration, or on max_iter. They differ only in the control parameter that reacts to an attempt: LM-EnRML's damping :math:\lambda inflates the Hessian and grows on rejection; GN-EnRML's step length :math:\gamma scales the step and shrinks on rejection. A subclass supplies that behaviour through the hooks below and nothing else.

Hooks

_read_damping_options(options) Read the control parameter(s) from the iteration block. _step_scale() Factor applied to the analysis step: 1 for LM-EnRML, :math:\gamma for GN-EnRML. _record_control() Remember the control the attempt ran with, for the run table. _control_exhausted() and _exhausted_message() Whether the control itself says stop (LM-EnRML: :math:\lambda \ge lambda_max), and the stop reason to report then. _why_stop_control() The control's entries in why_stop. _on_improved() Accepted with a smaller misfit spread: relax the control. _on_rejected() Rejected: tighten the control. _give_up_message(attempt) Stop reason when max_inner_iter attempts all failed. log_columns() The control's column in the run table.

__init__(keys_da, keys_en, sim, analysis=None, ensemble=None) ⚓︎

Build the ensemble from the config (or take the one given) and bind the analysis.

See the subclass docstrings for the parameters; ensemble is a ready-made collaborator to run on instead of building one.

calc_analysis() ⚓︎

Compute the trial state: the analysis step, scaled and clipped.

check_convergence() ⚓︎

Report the verdict reached by the preceding :meth:score_and_commit.

score_and_commit() ⚓︎

Score the forecast, decide on the attempt, and adjust the control.

Returns:

Name Type Description
why_stop dict

The convergence criteria with their values, including the control's own entries.

update_step() ⚓︎

Run one iteration, retrying until an attempt improves the misfit.

The retry loop is here rather than in the base loop: one call is one iteration, and the attempts it took to get there are this scheme's business. That mirrors popt, where EnOpt.update_step backtracks over its own step length and returns only once it has an improving step or has run out of attempts.

Each attempt re-solves the analysis with the current control parameter, forecasts the proposal and scores it. A worse misfit tightens the control (:meth:_on_rejected) and tries again from the same state -- nothing was committed -- so the retries cost forecasts, not correctness.

Returns:

Type Description
StepReport

accepted is whether an attempt improved the misfit. It is False only when the scheme has also decided to stop, which :meth:check_convergence then reports to the loop.

LMEnRML ⚓︎

Bases: IterativeEnRML

Levenberg-Marquardt Ensemble Randomized Maximum Likelihood (LM-EnRML).

An iterative ensemble smoother that solves the randomized maximum likelihood problem by repeated linearisation, with a Levenberg-Marquardt damping parameter :math:\lambda controlling the step size. The damped update inflates the Hessian approximation:

.. math::

m \leftarrow m + C_{md} \big((1 + \lambda) C_d + C_{dd}\big)^{-1}
(d_{obs} - g(m))

Unlike ES-MDA, steps are accepted or rejected. A step that increases the mean data misfit is discarded, :math:\lambda is multiplied by lambda_factor and the step re-solved from the same state; one that decreases it is kept and :math:\lambda reduced. That retry loop lives inside :meth:update_step, so one iteration is one call however many attempts it takes -- the shape popt's optimizers have. The run stops when the relative misfit change falls below data_misfit_tol, when :math:\lambda reaches lambda_max, when a single iteration exhausts max_inner_iter attempts, or on max_iter.

Parameters:

Name Type Description Default
keys_da dict

Parsed dataassim configuration. Besides the keys every scheme reads -- data, datavar, obsname, truedataindex -- the ones this scheme acts on are listed under Notes.

required
keys_en dict

Parsed ensemble configuration: ensemble size ne, the state variable names, and the prior_<name> blocks describing each.

required
sim object

Forward simulator instance, e.g. simulator.opm.flow.

required
analysis (approx, full, subspace)

Analysis flavour, i.e. how the ensemble-approximated sensitivity is inverted. Defaults to the analysis key in keys_da, falling back to 'approx'. The flavours differ in cost and in how they handle a rank-deficient ensemble; they solve the same update equation.

'approx'

Attributes:

Name Type Description
ensemble AssimilationEnsemble

Collaborator holding the state realisations, observed data and simulator. Its state is exposed as properties on the scheme, so scheme.enX and scheme.keys_da read straight through.

analysis AnalysisBase

The bound analysis object. Note the constructor takes analysis as a name and this attribute holds the resulting object, the way Model(optimizer="adam").optimizer is an optimizer instance.

analysis_name str

The flavour name that was resolved, e.g. 'approx'.

iteration int

Accepted iterations completed so far.

data_misfit, prior_data_misfit float

Current and initial mean data misfit.

Notes

Configured through the iteration block of keys_da:

max_iter Maximum accepted iterations. lambda Initial damping parameter (default 100). 'auto' derives it from the prior data misfit. lambda_factor Factor by which damping grows on rejection and shrinks on acceptance (default 5). Held as lam_factor -- not gamma, which is GN-EnRML's step length, a different quantity entirely. lambda_max, lambda_min Bounds on the damping parameter. max_inner_iter Damping attempts one iteration may make before the run gives up (default 10). lambda_max normally stops it first. data_misfit_tol Relative misfit change treated as converged (default 0.01).

Examples:

>>> result = LMEnRML.assimilate(keys_da, keys_en, flow(keys_sim))
>>> result.message
'Maximum number of iterations reached'

success distinguishes the two ways a run can end: True when a convergence criterion fired, False when max_iter was reached first. Both are ordinary outcomes -- check prior_data_misfit against data_misfit to judge whether the run achieved anything.

References

Chen and Oliver, Levenberg-Marquardt forms of the iterative ensemble smoother for efficient history matching and uncertainty quantification chen2013.

See Also

IterativeEnRML : The loop, scoring and bookkeeping both schemes share. GNEnRML : Gauss-Newton form, damped by a step length instead. ESMDA : Fixed schedule rather than convergence-driven iteration.

log_columns(prior_run=False) ⚓︎

LM-EnRML reports the damping the logged iteration ran with.

score(pred_data=None) ⚓︎

Data misfit, sizing lambda='auto' the first time there is one.

:math:\lambda_0 = \Phi_{prior} / 2 N_d is defined against the prior misfit, so it cannot be settled in __init__. The first score of a run is the prior's, which makes this the earliest point it can be resolved -- and everything downstream needs a number: the prior row reports λ, and the prior QA/QC pass computes with it.

co_lm_enrml ⚓︎

Bases: LMEnRML

Approximate LM-EnRML of Chen and Oliver (2013), under its historical name.

This is LMEnRML(..., analysis="approx") and nothing more: the class only ever differed from LM-EnRML by mixing in the approximate analysis, which is a constructor argument now. It stays so that configs written as scheme = "co_lm_enrml" and code importing the name keep working. New code should say LMEnRML with analysis="approx".

gn_enrml ⚓︎

Bases: GNEnRML

Gauss-Newton stochastic IES of Raanes et al. (2019), under its historical name.

This is GNEnRML(..., analysis="subspace"): the weight-space update this class used to carry inline is the subspace analysis, and the step-length schedule it called lambda is GN-EnRML's gamma schedule. It stays so that configs written as scheme = "gn_enrml" and code importing the name keep working. New code should say GNEnRML with analysis="subspace".