base⚓︎
Shared base for the analysis-step analyses.
An analysis computes the state update for one assimilation
iteration. The three shipped flavours -- approx, full and subspace
-- differ only in how the ensemble-approximated sensitivity is inverted; they
share their calling convention and their linear-algebra helpers.
This is the PIPT counterpart to popt.optimization_methods.subroutines:
small, focused numerical pieces the top-level scheme composes with, rather than
behaviour baked into the scheme's class name.
Historically these flavours were mixins combined into the scheme at class
definition time, producing a combinatorial explosion of names
(esmda_approx, esmda_full, esmda_subspace, lmenrml_approx, ...).
Every algorithm class now takes analysis as a constructor argument and
binds the matching analysis instead (see AnalysisBindingMixin). Mixing in still
works, for an analysis that genuinely cannot take this shape -- nothing shipped
here needs it any more, now that margis binds like the rest -- but doing
so is riskier than it looks: see AnalysisBindingMixin's module docstring for why
the scheme base usually has to be listed first, and what that can do to
method resolution.
Analysis contract
update(enX, enY, enE, **kwargs) -> AnalysisResult
Return the update as an :class:AnalysisResult: a state-space step
of shape (nx, ne) (a plain array is accepted and means the same), or
a step in ensemble-weight space, w_step or W_step (two
conventions, see the class). The scheme turns whichever it gets into a
trial state with propose_state; an analysis never writes its result
onto the scheme.
Analyses reach everything they need through self.scheme: the damping
parameter self.scheme.lam, self.scheme.trunc_energy,
self.scheme.localization, self.scheme.prior_enX,
self.scheme.cov_data, and so on. Some of those are the scheme's own
attributes and some belong to its ensemble, but the scheme exposes both as
properties (see :class:~pipt.update_schemes.core.AssimilationScheme),
so an analysis never has to know which -- and there is no forwarding
machinery on this side at all. A new flavour that needs a value no existing
one uses just reads self.scheme.<name>; if the scheme does not already
expose it, adding one property there is the whole change.
self.scheme resolves for both ways an analysis can be used:
- Bound --
self.schemeis the scheme it was constructed against. - Mixed in --
selfis the scheme, soself.schemeisself(see :attr:schemebelow). Nothing shipped here still needs this (margisbinds like the rest now); it remains supported for an analysis whose calling convention genuinely does not fit the bound shape.
State an analysis keeps between iterations -- full_update caches Am,
the weight-space flavours start current_W and keep their scaled
perturbations -- lives on self.scheme explicitly, the same way it is
read, not on self: nothing forwards a plain self.Am = ... to the
scheme.
AnalysisBase
⚓︎
Bases: ABC
Base class for analysis-step analyses.
Provides the linear-algebra helpers every flavour needs. Both accept either
a full 2-D matrix or a 1-D array holding just the diagonal, which is how
PIPT represents a diagonal data covariance without materialising nd x nd
zeros.
Two usages
Bound (what every algorithm class does, for every flavour in its
COMPATIBLE_ANALYSES) -- constructed against a scheme it holds a
reference to::
strategy = approx_update(scheme)
step = strategy.update(enX, enY, enE)
which is what lets analysis be a constructor argument of one scheme
class rather than picking which of several classes you get. Inside
update(), context is read explicitly off self.scheme -- there is
no delegation step to run first; self.scheme is just the object
passed to the constructor, and it exposes ensemble state as properties
of its own.
Mixed in -- nothing shipped here still needs this (margis binds
like the rest now); it remains supported for an analysis whose calling
convention genuinely does not fit the bound shape above::
class some_scheme(SomeAlgorithm, some_analysis): ...
self is the scheme here, so self.scheme (the :attr:scheme
property below) simply returns self -- self.scheme.lam and
self.lam are then the same read, resolved by ordinary inheritance.
An unbound, un-mixed-in analysis has self.scheme fall back to
self too, so a context read raises a plain AttributeError rather
than finding a half-initialised scheme.
scheme
⚓︎
The scheme to read context from and write results onto.
The bound value if there is one; otherwise self -- which is
exactly right when mixed in (self already is the scheme, so
self.scheme.x and self.x are the same read) and merely
produces a plain AttributeError from an unbound, un-mixed-in
analysis rather than a special-cased error path.
__init__(scheme=None)
⚓︎
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scheme
|
object
|
Scheme this analysis computes updates for. |
None
|
solve(A, B)
⚓︎
Apply A⁻¹ B, supporting both matrix (2-D) and diagonal (1-D) A.
np.ndim is used rather than A.ndim so that plain lists and
scalars -- which a covariance can still be when it comes straight from a
config file -- are handled instead of raising AttributeError.
sqrtm(A)
⚓︎
Matrix square root, supporting both matrix and diagonal inputs.
update(enX, enY, enE, **kwargs)
⚓︎
Compute the analysis update step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
enX
|
ndarray
|
State ensemble matrix, shape |
required |
enY
|
ndarray
|
Predicted data ensemble matrix, shape |
required |
enE
|
ndarray
|
Perturbed observation ensemble, shape |
required |
**kwargs
|
Analysis-specific extras, e.g. |
{}
|
Returns:
| Type | Description |
|---|---|
AnalysisResult
|
The update: a state-space |
AnalysisResult
⚓︎
What an analysis hands back to the scheme. Exactly one field is set.
step
Additive step in state space, (nx, ne); the trial state is
enX + scale * step. The multilevel analysis returns one array per
fidelity level.
w_step
Additive step to the weight matrix W of the ensemble subspace
formulation (Evensen et al. 2019), starting from W = 0; the trial
state is prior_enX @ (I + W / sqrt(ne - 1)).
W_step
Additive step to the ensemble transform W of the matrix
formulation (Raanes et al. 2019), starting from W = I; the trial
state is mean(prior_enX) + prior_anomalies * sqrt(ne - 1) @ W.
scale is the scheme's step length (GN-EnRML's gamma; 1 elsewhere)
and belongs to the scheme, which is why the analysis returns a step and
not a state.
coerce(value)
⚓︎
An AnalysisResult as given; a plain array or list as a state-space step.