Developing the OEM interface
The user guide is in Configuring an optimal-estimation retrieval and the mathematical formulation is in Optimal estimation. This page separates the initial interface cleanup from further work on numerical algorithms and public APIs. The statistical objective and existing method names should remain stable unless a change is explicitly documented and tested.
Responsibilities and regression baseline
src/m_oem.cc validates inputs and connects workspace inputs and outputs
to the inverse problem. src/oem.h contains the agenda adapter, solver
wrappers, and logging. 3rdparty/invlib/src/invlib/map.cpp drives
the optimization and computes costs and convergence measures. The LM
step and linear solvers live below that layer. Covariance storage and
inverse operations are implemented in
src/core/jacobian/covariance_matrix.cc; retrieval covariance setup
also involves src/m_covmat.cc.
The first cleanup centralizes method selection and LM configuration while
retaining the public defaults. Direct and CG LM methods now use all six
settings, the supplied stop_dx, and the same diagonal of prior precision
for damping. Direct measurement-space li_m and gn_m reuse MFORM
with DirectMeasurementSolver. State normalization is rejected for measurement-space
methods instead of applying state scales to a measurement-space system.
Measurement-space convergence uses the state-space Hessian metric
(Rodgers 5.30), so it remains well-defined when the numbers of measurements
and states differ. Diagnostics come from the formulation that actually
ran. LM history is collected independently of terminal output, and aliases
have the same initial-cost behavior. Final nonlinear Jacobians are refreshed
at the returned state when needed for the requested gain. Outputs that would
otherwise retain an earlier run’s gain or errors are cleared, and the
clear_matrices policy also applies to a skipped retrieval.
src/tests/test_oem_methods.cc exercises the workspace interface using
small deterministic forward models. CTest registers the individual cases
as cpp.fast.oem.<method>, plus settings and validation cases. Each
supported spelling, including ml and ml_cg, is part of the contract.
These tests run without the optional MATLAB integration required by the
legacy invlib-level tests in src/tests/test_oem.cc. They complement
the atmospheric retrieval examples under tests/core/vmr. Exact initial
solutions and underdetermined problems are covered; a zero right-hand side
now returns a zero CG solution directly.
The numerical reference for an affine model should come from an independently formed linear posterior solution. Check the state, fitted measurement, cost decomposition, gain, and output dimensions; agreement between two methods alone can preserve a shared bug. Include correlated covariances, unequal state and measurement dimensions, a non-prior starting state, and equivalent scaled and unscaled systems. Nonlinear cases need to test accepted-state Jacobians and meaningful damping changes, rather than only checking that a residual decreased.
Forward-model reuse and agenda ownership
AgendaWrapper tracks the exact state of its most recent successful
simulation separately from the state of its stored Jacobian. It borrows
the workspace fit and Jacobian storage; only the state tags need additional
storage proportional to the state size. Matching an earlier state
approximately is not sufficient for reuse. Initial evaluations, accepted
LM trial values, and final Jacobians must not be repeated when the required
result is already current. A value-only trial preserves the last Jacobian,
but changes the physical model fields. Returning to that Jacobian’s state
therefore still requires a value-only evaluation to restore the fields.
Invalidate cache entries before executing an agenda and publish new state
tags only after successful execution and dimension checks.
The criterion_needs_measurement trait identifies the built-in
Rodgers530 and Rodgers531 types. Their state-step tests allow a continuing Gauss–Newton iteration to obtain
its fit and Jacobian together, without a preceding value-only evaluation.
Unknown convergence criteria, including overrides derived from these
classes, default to requiring the new measurement. Retain that conservative
behavior when extending the trait. At the final
iteration, obtain the fit for costs and diagnostics, then refresh a nonlinear
Jacobian only if it is needed for retained matrix outputs.
clear_matrices=1 skips that final derivative refresh as well as gain
construction. Preserve evaluation-count regressions alongside numerical
oracles, including rejected trials, zero steps, iteration limits, failed
evaluations, and both matrix-output policies.
The generated agenda executor borrows explicit inputs and outputs through
non-owning Wsv wrappers. Ordinary workspace sharing copies handles,
not field or matrix payloads. Agenda::finalize identifies internally
modified inputs that need isolation; copy_workspace copies those
values, and Agenda.document() lists them. Nested agendas have their
own copy lists. Literal values stored in agenda methods are also copied
when executed. Do not retain an executed local workspace blindly:
copy_only_workspace can copy its already modified scratch values,
changing initialization between trials. Any future scratch reuse must
preserve fresh-call initialization and failure behavior.
Python tuple-style operators have additional argument and return conversion
costs. Their generated C++ adapters move the owning return tuple’s elements
into outputs; never move from Python-owned objects or borrowed inputs.
CallbackOperator instead supplies a restricted workspace sharing its
declared variables, allowing outputs to be updated in place. Keep the
native allocation-transfer regression in test_agenda_operator.cc and
the Python retained-object regression in
tests/core/agenda/operator_return_values.py when changing these bridges.
State mapping and derivative selection
AgendaWrapper always passes the complete target set as
model_state_targets. It selects a const reference to either that same
set or a local empty set for jac_targets before entering the agenda.
Both objects outlive the synchronous agenda execution. Do not copy the
populated targets or clear an agenda input to implement value-only calls.
Methods mapping trial states into physical fields use model_state_targets.
Radiative transfer and Jacobian transformations use jac_targets.
Measurement-error values and derivatives likewise use the full mapping and
derivative targets respectively. This distinction preserves trial-state
updates and error values when derivatives are disabled. The predefined
agendas must not list either target set among their copied inputs.
Bounded inner iterations
An outer max_iter does not bound the work of an inner linear solve or
LM trial search. Both enforce independent limits. CG returns its current
iterate on exhaustion and invokes an optional warning callback. OEM installs
this callback and records one warning per OEM call in oem.diagnostics.errors; the outer
optimizer continues and retains its own diagnostic status. LM trial
exhaustion still raises an error. OEM maps errors caught
during inversion to status Error and records the explanation in oem.diagnostics.errors.
Exhausting the existing LM damping range retains its status DampingLimit behavior.
Gauss–Newton must propagate linear-solver exceptions with their nested
cause. Returning an empty step after catching an error hides the failure
and can cause invalid vector operations in the measurement-space formulation.
The native ConjugateGradient constructor and both preconditioned
variants accept max_iterations after verbosity. Zero selects
\(\max(1000,2d)\) for a system of dimension \(d\); a positive value
sets an explicit bound. Custom convergence predicates cannot disable it.
cg_tolerance and cg_max_iter are validated at the OEM boundary.
Static native solver preconditions remain assertions. Nonpositive curvature
or a nonpositive preconditioned residual inner product is a runtime numerical
breakdown, reported through the OEM error diagnostics.
CG records its stop reason on every solve, including a zero right-hand side.
GN refuses a truncated step, preserving the last state and reporting
LinearSolverLimit. LM rejects it and retries with increased damping;
if the final attempt is still truncated, it reports the same status.
Do not let the measurement-space mapping apply its prior offset when a step
was rejected. Regressions cover all formulations and final-state Jacobian reuse.
The native LM optimizer provides get_maximum_trials() and
set_maximum_trials(); the positive trial limit defaults to 100 per
outer iteration. A local counter bounds rejected-step retries, and a
damping update must make progress before another trial is attempted.
This catches multiplication that rounds back to the current damping.
The trial budget counts linear solves, including any additional undamped
solve used to check stationarity.
The LM trial limit is exposed as settings.lm.maximum_trials (default 100). Preserve the termination regressions when
changing convergence predicates, trial acceptance, or damping updates.
LM acceptance and stop outcomes
LMStopReason records termination independently of the damping value.
None means the optimizer can continue; Stationary is successful
termination. DampingLimit returns a zero step and maps to workspace
status DampingLimit. TrialLimit, DampingStalled, LinearSolverFailure, and
NumericalFailure accompany exceptions, which OEM maps to status Error
when caught during inversion. Ordinary convergence after an accepted
step still uses the configured convergence criterion and damping gate.
Both that criterion and Stationary map to status Converged; outer iteration
exhaustion retains status IterationLimit.
All MAP formulations consult the optimizer’s explicit outcome before
testing the returned step for convergence. A zero step returned after
damping exhaustion must not count as convergence. Rejected trials must
never update the accepted state, and damping history must contain physical
damping values, not a numerical sentinel such as maximum + 1. At a
large finite maximum, that addition can round back to the maximum itself.
The ordinary convergence gate returns a zero tolerance while damping
exceeds its limit, so the strict comparison also rejects a rounded-zero
state change.
The generic helper in optimization/minimize.h also honors optional
stop_iteration() and converged() hooks. A reported optimizer
failure returns before testing the criterion or updating the state. A
stationary optimizer’s verified step is applied, but success still requires
J.criterion to meet the helper’s own tolerance. Iteration exhaustion
returns one. Retain native regressions for these outcomes and for custom
minimizers without either hook.
MAPBase::model_cost_scale() returns two: MAP reports the full quadratic
cost, while its normal equations use the half-gradient and half-Hessian.
The LM prediction includes this scale; an exact affine model therefore
has an actual-to-predicted reduction ratio of one. Generic objectives
without this optional method retain scale one and must provide mutually
consistent cost, gradient, and Hessian definitions. Keep regressions for
both conventions when changing the reduction formula or acceptance
thresholds.
LM compares reductions against 32 * epsilon * max(abs(old_cost),
abs(new_cost)) before dividing them. If actual or predicted reduction
is unresolved at that scale, it checks the undamped normal equations once
within the current step. Numerical stationarity requires a finite,
nonnegative undamped decrement below n * stop_dx, a finite,
nonnegative predicted reduction within the current cost’s roundoff
scale, and a candidate cost indistinguishable from the current cost at
that scale. This step can converge independently of the damping gate:
its size is checked without damping. An exactly zero gradient is also
stationary. These checks must not substitute the damped step for the
undamped decrement; very strong damping can hide a large remaining error.
Resolved acceptance requires finite, positive actual and predicted
reductions and a true reduction ratio of at least 0.5; damping is decreased
when the ratio exceeds 0.75. These thresholds use consistently scaled costs,
not the inflated ratio in the older implementation.
Keep workspace regressions for all four LM spellings at damping
1e20, tight affine tolerances, and an exactly stationary state with
nonzero cost. Native tests also cover explicit stop reasons, objective
scaling, rejected steps, and bounded or stalled damping updates. Check
the state, fitted measurement, costs, gain, and damping history as well
as the final status.
Remaining numerical work
The following items require separate implementation and regression work.
Expose inner-solver controls and diagnostics. Report linear iterations and residuals. Relative CG tolerance and the linear iteration budget are already exposed. Keep these separate from outer
max_iterand the six damping controls. Extend numerical coverage to nearly zero right-hand sides and ill-conditioned positive-definite systems; bounded termination alone does not establish the accuracy of a difficult solve.Extend public stop diagnostics. The workspace retains its numeric status mapping; native LM stop reasons are not a separate workspace output. A future result could expose the precise stop reason and distinguish successful completion of a one-step linear solve from exhaustion of an iterative convergence budget.
Replace dense normalization and explicit gain inversion. Store state scales as a vector and apply diagonal products without allocating an \(n\times n\) dense scaling matrix. Compute the gain by solving the posterior precision system for its right-hand sides. Measure peak memory as well as runtime, and retain the option to skip gain production. Consider factorization reuse and a preconditioner appropriate to each formulation only after correctness is established.
Make agenda state and failure behavior explicit. Validation, covariance inversion, and the initial agenda evaluation can still throw directly; status
Errorcovers errors caught during inversion. Trial forward model evaluations mutate atmosphere, sensor, and surface workspace data. Define which state and diagnostics remain valid after an exception or a rejected trial. A future result should distinguish the last accepted state from a failed trial without treating NaNs as the only status record.
Covariance validation and construction
CovarianceMatrix.validate(expected_size=-1, relative_tolerance=1e-10,
max_dense_elements=10_000_000) checks an existing physical covariance
without modifying it. Validation covers block dimensions and ranges,
coverage, unique upper-triangular block identifiers, non-null matrices,
finite values, positive variances, symmetry, and positive definiteness.
Symmetry and supplied-inverse comparisons use variance-scaled coordinates
so mixed units do not let a large-variance quantity hide an invalid
small-variance block. Positive definiteness is checked over connected
components of the complete covariance, including cross-block correlations.
Each represented inverse component must be complete and consistent with
its connected covariance component. Whole independent components may
remain uncached during validation. In normalized coordinates, the matrix-product
residual is compared with relative_tolerance times the component size;
the diagonal fast path uses relative_tolerance directly.
compute_inverse uses the same checks and inverts uncached independent
components. This catches invalid
physical covariances on the existing OEM inversion path without imposing
the standalone analysis’s memory limit on existing retrievals. It preserves
the inverse-only representation used internally for LM damping; that
representation is not accepted as a physical covariance by validate
or the standalone information report.
Replacing blocks or accessing mutable blocks invalidates cached inverses.
Adding a covariance block invalidates the affected connected components
while retaining independent cached components. Shared matrix aliases
can still outlive those access points; validation before inversion must
continue to detect a stale supplied inverse. Do not silently overwrite
asymmetric covariance entries during inversion. The covariance-addition
helper in src/m_covmat.cc now inserts the optional supplied inverse
as an inverse and validates its inputs before mutation.
Sparse diagonal validation and inversion retain diagonal storage.
Explicit validation of non-diagonal connected components requires dense
work; check
max_dense_elements before allocating each component matrix. This
guard bounds individual dense arrays, not total peak memory. Keep
regressions for diagonal and correlated blocks, mixed scales, incomplete
coverage, inverse-only inputs, stale caches, and supplied inverse
consistency. Validation must not silently repair a statistical model.
Useful construction helpers would accept standard deviations and an optional correlation matrix or named correlation kernel, then construct covariance blocks. Parameter names should state whether they accept a standard deviation, variance, covariance, or precision. A report could identify each block by retrieval target and include its units, variance range, and factorization or conditioning diagnostics. Covariance repair, such as adding a diagonal term or changing correlations, must be an explicit user choice because it changes the inference.
Standalone information analysis
python/src/pyarts3/retrieval.py provides information and
information_from_workspace independently of the OEM optimizer.
The workspace adapter reads the current Jacobian and the two physical
covariances. Neither entry point executes a forward agenda, runs OEM,
updates a workspace, or selects new covariance values. This separation
lets a user compare proposed measurement and uncertainty models with a
fixed Jacobian before a retrieval, and examine local sensitivities after
one. The mathematical definitions belong in Information carried by the measurements;
usage and interpretation belong in Correlating temperature and log-water on the same grid.
The report uses covariance factors to obtain a dimensionless Jacobian,
then an exact singular value decomposition. singular_values and
mode_variance_reduction include all state directions, padding the
unobserved modes with zero singular values when measurements are fewer
than states. state_modes contains physical state directions scaled
to unit prior uncertainty; measurement_modes refers to whitened
measurement coordinates. Retain the distinction between mode and
marginal variance reduction. Mode signs and bases inside a degenerate
subspace are not stable identifiers for comparisons between runs.
Use factor solves rather than forming covariance inverses for this
analysis. Diagonal measurement covariances must not allocate a full
measurement-square matrix. Guard dense work with max_dense_elements;
even a diagonal prior still needs a state-square basis when all modes
are returned. The analysis guards an estimate of 3*m*n + 4*n*n
elements as well as the cumulative dense factor sizes; LAPACK can need
additional work arrays. Future truncated or operator-based analyses must state
which parts of the spectrum and uncertainty they approximate, and must
not label retained-mode information as the complete information content.
information requires both an explicit measurement and
prior_prediction for innovation_chi_square.
information_from_workspace reads the workspace measurement only when
given prior_prediction; it does not accept a measurement override.
Do not infer the prior prediction from a workspace’s fitted measurement:
its generating state is not known to this API. The
chi-squared reference distribution assumes a linear model, a prediction
at the prior mean, and the stated independent Gaussian error sources.
The spectrum itself is independent of the measured residual.
Regression oracles should include diagonal analytic systems, correlated covariances, unequal dimensions, exact null directions, consistent coordinate changes, and posterior covariance reconstructed from the returned modes. Cross-check the innovation statistic against an independently formed innovation covariance. Keep workspace immutability and dense-memory guards covered. Plot uncertainty ratios when combining state elements with different units; retain absolute standard deviations and labels in the numerical report.
Helping users choose settings
Named LM settings are implemented by
LevenbergMarquardtSettings. Its keyword-only constructor and
mutable fields expose initial_damping, decrease_factor,
increase_factor, maximum_damping, damping_threshold, and
convergence_damping_limit. The native
settings representation provides shared validation for the Python object
and the optimizers used by oemCalc. Both direct and CG optimizers must
continue to use the same named controls.
The type lives in src/core/jacobian/oem_settings.h and
src/core/jacobian/oem_settings.cc without an invlib dependency;
src/python_interface/py_retrieval.cpp binds the Python interface.
OptimalEstimationSettings owns the method enum, outer iteration controls,
CG limits, nested LM controls and output flags. Both workspace calculations
validate it once before covariance preparation, then pass it by const reference
to the shared iteration dispatcher. Keep normalization and bases in
OptimalEstimationData because their dimensions belong to the problem.
The settings type uses aggregate XML serialization; extend the Python
constructor, properties, pickle state and round-trip regressions together when
adding fields. The six-value LM shorthand initializes damping only; the
separate maximum_trials field keeps its default.
The Python LM constructor validates all seven LM settings, and each field setter validates a temporary copy before replacing the stored object. An invalid edit must preserve the previous values and report the affected setting. This also avoids losing useful validation errors through nanobind’s implicit-conversion error handling. Related field changes must either keep each intermediate configuration valid or use a replacement object constructed with the desired keyword arguments together.
validate() checks a configured object at the OEM boundary. describe()
explains its fields. Both settings types have XML storage;
OEM uses the named defaults when omitted. The outer settings are a workspace
group; the nested LM type is registered as a group friend. The C++ API remains named.
The Python binding accepts std::array<Numeric, 6> through its implicit
constructor, which validates the six-field input.
There is no as_vector() output conversion.
Do not describe the threshold as a
hard minimum: it controls both restart after rejection and switching a
proposed decrease to zero. The convergence damping limit gates the
existing state-step criterion using the updated damping. Preserve the
accepted/rejected-step regressions when changing either behavior.
A preset claiming particular convergence or performance properties needs
documented assumptions and representative retrieval benchmarks.
src/tests/test_oem_methods.cc tests the native settings and solver
behavior. tests/core/jac/oem_lm_settings.py covers the Python interface
through the actual workspace call. Keep named settings covered for all four LM spellings, including correlated
priors and nonlinear rejected trials; comparing complete results and
damping histories catches changes that an endpoint-only test would miss.
Further API work should keep existing scripts usable:
Represent iteration family, linear solver, and formulation separately internally. A single method descriptor table can drive parsing, supported-name documentation, and test enumeration. Existing string spellings and aliases can remain as the public compatibility layer.
Provide a setup report with state and measurement sizes, estimated matrix storage, chosen coordinates and scales, covariance diagnostics, and solver settings. Recommend a method with a reason while leaving the explicit choice available. Dimensions can suggest a formulation; they cannot establish linearity or predict nonlinear convergence.
Add an optional local Jacobian check using representative perturbations in prior-scaled coordinates, and report discrepancies by measurement and retrieval target. Start with a deterministic small-model example before applying it to expensive radiative-transfer agendas.
Extend
OptimalEstimationDiagnosticswith accepted iteration costs, state-step measures, damping, trial rejections, linear residuals, and forward-model call counts. There is no legacy positional adapter. A user can then distinguish poor model linearity, a difficult linear solve, an exhausted iteration budget, and a mismatch in assumed errors.
Method recommendations should be assessed on linear, weakly nonlinear, strongly nonlinear, correlated, poorly scaled, and large sparse cases. Compare final objective and state as well as runtime, peak memory, and forward-model calls. Automatic changes to prior or measurement covariance require scientific assumptions that cannot be inferred from solver progress alone. Keep those assumptions visible in both the setup and uncertainty reports.
Matching-grid covariance helper
oemStateCovmatCorrelateConstant resolves finalized atmospheric target keys
and checks actual coordinate grids, not only vector lengths. It assumes
pointwise retrieval coordinates; custom mappings that mix grid points are
outside its contract. It constructs an upper-triangular sparse cross block
from existing marginal standard deviations. It validates a candidate
containing all existing pairs before assignment, preserving the original
on failure and discarding inverse caches on success. Extensions must keep
these guarantees and must not silently rescale transformed covariances.
The correlation method accepts two independent shared-pointer input variants in one implementation. Each argument is converted separately to the common atmospheric key type. The regression exercises all atmospheric-key/species combinations and the generated workspace dispatch through an agenda.
Measurement-space noise scaling
When measurement_vec_normalization is nonempty, the measurement-space
direct and CG paths solve
Here \(\mathbf{J}\) is the measurement Jacobian and \(D_{ii}\) are
the supplied scales. Empty scales disable the transformation. The helper
computes the suggested noise scales \(\sqrt{S_{\epsilon,ii}}\);
OEM does not select them automatically. NoiseScaledSystem applies this
operation without materializing \(\mathbf{M}\) or \(\mathbf{D}\).
The existing state-sized normalization remains exclusive to state-space
solvers. The relative CG tolerance is measured in the scaled system.
oemMeasurementCovmatNormalization exposes the same standard
deviations as a generic Vector output. Correlated noise is not fully whitened.
Regression tests compare with the affine analytic solution and check state
and cost invariance under independent measurement-unit changes.
Direct measurement-space assembly
DirectMeasurementSolver opts into dense_measurement_system. GaussNewton
forwards this compile-time policy to MFORM; other optimizers/solvers default
to lazy evaluation. MFORM materializes \(\mathbf{J}^{\top}\) and computes
It reuses \(\mathbf{B}\) for the state update. Here \(\mathbf{H}\) is
the measurement-space system, not the state-space half-Hessian.
The direct solver copies \(\mathbf{H}\) for optional scaling and
factorization. This uses additional \(n\times m\) temporary
storage but avoids repeated covariance applications. CG remains lazy.
The ARTS adapters expose transpose_view() as a non-owning const strided
view. Dense MFORM passes this view directly to covariance multiplication,
avoiding an additional \(n\times m\) Jacobian copy. The source Jacobian
must remain alive and its storage stable until multiplication finishes.
Owning transpose() remains available for invlib expression materialization;
other backends retain that fallback. Gain postprocessing remains state-space and must be included
in performance comparisons. New methods share the affine, nonlinear,
underdetermined, normalization and failure regression fixtures.
Fused covariance addition and GEMM
The ARTS reference-matrix adapter provides multiply_add(B, Se) for the
dense MFORM path. It initializes the result to zero, adds the covariance
blocks directly (including off-diagonal blocks), and calls mult with
\(\alpha=1\) and \(\beta=1\). Thus
\(\mathbf{H}=\mathbf{J}\mathbf{B}+\mathbf{S}_\epsilon\) needs one GEMM and no separate dense
covariance temporary or post-GEMM addition pass. Other invlib backends keep
the multiplication-plus-addition fallback. The transpose-view path removes the explicit Jacobian transpose copy.
The direct solver’s scaling copy remains.
Diagonal covariance regressions
Every method-specific OEM CTest also runs test_diagonal_covariances.
It uses \(\mathbf{S}_a=\operatorname{diag}(4,2)\) and
\(\mathbf{S}_\epsilon=\operatorname{diag}(1,2,1/2)\) with the affine fixture,
testing all four combinations of dense/sparse prior and noise storage,
with normalization enabled and disabled. The reference state, gain, total
cost and measurement cost are rational values derived independently from
the two-state normal equations. Linear methods must take exactly one step.
These checks run as ordinary CTest regressions.
Exact diagonal covariance operations
Covariance matrix multiplication now detects exact diagonal structure across
independent blocks and scales the destination directly. mult_inv and
vector solve divide by covariance diagonal entries without requiring an
inverse cache. Sparse diagonals are inspected in \(\mathcal{O}(\mathrm{nnz})\); dense blocks require
an exact off-diagonal scan. Structure is not cached because callers can hold
mutable/shared block storage. Detection uses no threshold and never discards
small correlations. These paths use \(\mathcal{O}(n)\) scratch space rather than a full
matrix-result temporary.
Component solves now use an internal variant of diagonal values and dense
Cholesky factors, grouped by exact block connectivity. Ordinary covariance
mult_inv and solve calls prepare/reuse this representation without
forming inverse blocks. Matrix and Sparse remain the public input types.
Adding an off-diagonal block merges components; replacing/removing blocks
can split them. Coupled sparse components currently use dense Cholesky,
matching the former dense inverse component storage; sparse Cholesky and
banded factorization are not implemented.
OEM calls CovarianceMatrix::prepared once for each covariance before
iteration. Preparation checks exact values and block layout, validates changed
inputs, and publishes a detached, read-only snapshot. Its block storage belongs
to the snapshot, so subsequent edits through aliases of the source do not change
a running retrieval. Unchanged inputs reuse the prepared snapshot. Mutable
covariance copies have independent preparation holders. A completed
compute_inverse records the exact validated storage; preparation reuses
that validation only if all covariance and inverse values, shapes and block
metadata still match. This avoids duplicate validation in cold calls with
explicit inverse preparation.
Preparation publication is protected by a mutex. Published snapshots support concurrent solves; callers must synchronize source mutation with preparation and must not mutate storage obtained from a prepared snapshot. Iterations do not compare snapshots, validate covariances, or build inverse caches. Standalone operations on mutable covariances retain their defensive checks. Concurrent solves, inverse construction, explicit validation and preparation synchronize cache publication, retaining factors while arithmetic runs outside the lock. External block edits still require synchronization. Prefer prepared snapshots for repeated concurrent solves to avoid cache checks and locking. Moved-from matrices remain valid empty objects and can be reused. New or changed supplied inverses still receive the full deterministic consistency check; only identical previously validated contents skip that cubic work.
prepared(true) requests explicit precision for consumers that need it.
OEM requests this for state-space methods and gain output. Measurement error
covariance stays diagonal or factorized unless inverse blocks were supplied.
Any supplied inverse currently selects inverse application for the entire
covariance, with missing component inverses materialized; mixed inverse/factor
preparation within one covariance is not implemented.
Calling an explicit precision consumer on prepared(false) without inverse
blocks throws; request the required representation before entering the loop.
CG versus direct method selection remains the user’s choice.
Preparation adds detached storage and a linear comparison once per OEM call. It is not a zero-copy interface. This cost avoids repeated validation and alias checks inside iterative solvers while preserving detection of source changes between retrievals.
Regression tests cover mixed Matrix/Sparse inputs, joining/splitting components, retained-reference mutation after factorization, copies, multiple RHS and left/right solves. Existing supplied-inverse and all-method OEM regressions remain applicable.
Diagnostics interface
OptimalEstimationDiagnostics owns the LM history and error/warning list.
Reset the complete object for each OEM call. iterations is an Index
starting at zero; only unavailable costs use NaN. Translate invlib’s return
code at the OEM boundary to OptimalEstimationStatus, defined in
src/core/options/arts_options.cc. Its formatting, XML, and Python support
come from the standard options machinery. Both diagnostics
and LM settings use their aggregate XML representation; status is serialized
by name, independently of enum ordinals.
ReducedOEM adapter
oemBasisCalc prepares model_state_basis_mat,
measurement_basis_mat and oem_basis_singular_values from
the full Jacobian and covariances. By default both bases are square; neither null
space is discarded. full_matrices=0 instead retains \(\min(m,n)\) modes
on each side, omitting only the extra null-space vectors of the larger side. The spectrum contains \(\min(m,n)\) values,
including zeros. Additional directions of the larger square basis have
zero information. Keep these three outputs matched, including their mode
ordering. Mixing equally sized decompositions cannot be detected by
dimension checks.
oemBasisReduce reads the basis matrices and full spectrum and
truncates the matrices in place. It also sets oem_basis_lost_dofs and
oem_basis_lost_information_bits relative to the original decomposition.
The spectrum remains intact. Selection may be repeated to remove more modes,
but cannot restore removed modes; requests requiring unavailable directions
fail before modifying either matrix. Restore saved copies or recalculate to
increase rank. Both slices are materialized before either input is replaced.
No current Jacobian or covariance is needed during selection.
oemCalcReduced reads these same matrices as ordinary workspace inputs.
Both bases use BlockMatrix so explicit projections can remain
Sparse through forward-vector, Jacobian and gain multiplication.
oemMeasurementBasisCalc constructs this input alone. It groups exactly
matching full row directions after normalization by a signed pivot, retaining
per-channel amplitudes separately. No tolerance-based grouping is implicit.
Diagonal noise permits one stored projection entry per channel. For correlated
noise, a single prepared covariance solve constructs
\(\mathbf C=\mathbf T^\top\mathbf S_\epsilon^{-1}\); treating these
correlations as independent would change the retrieval.
The projection is rectangular and has no ordinary inverse. Projected noise
is constructed and prepared before iteration, then reuses CovarianceMatrix
solve caches. Sparse projections with diagonal or entirely sparse covariance
blocks use sparse products, including transposed off-diagonal blocks, without
a dense measurement-square temporary. General dense covariance blocks use the
dense preparation path; sparse projection application inside iteration still
uses sparse multiplication. Do not attach an inverse cache to BlockMatrix
or silently materialize sparse projections in the agenda adapter.
Grouping outputs do not have the SVD ordering required by
oemBasisReduce. Tests exercise proportional rows, correlations,
storage dispatch, all OEM methods, and a large collection of independent groups.
CovarianceSquareRoot validates and detaches covariance components, then
uses the existing diagonal/Cholesky component machinery to apply
\(\mathbf{L}\), \(\mathbf{L}^{\top}\),
\(\mathbf{L}^{-1}\), or \(\mathbf{L}^{-\top}\). Source inverse caches
are checked and preserved. No factor is published into the input covariance;
square-root operations use the component factors directly.
Matpack’s svd calls LAPACK DGESVD. Basis construction requests full left
and right singular vectors to preserve both null spaces. The full bases
require \(n^2+m^2\) dense elements in addition to SVD working storage
and intermediates. Selecting fewer modes afterwards does not reduce this
preparation cost. The economical option must be selected before the SVD to
avoid those allocations. It does not reproduce the full prior when \(m<n\).
Whitening is
applied directly to the Jacobian; avoid replacing this with eigenanalysis
of normal equations, which squares its condition number. Generate the
measurement basis with a transposed factor solve on the left vectors, not
division by singular values: zero-information modes must also work.
Both bases and the spectrum are published only after successful preparation.
Selection also prepares its outputs before publishing them. These methods
invalidate checked because changing basis dimensions can invalidate a
previously accepted normalization vector.
Tests reconstruct both original covariances, check rectangular and null
cases, and reselect ranks without a new decomposition. They compare
covariance metrics, subspaces and gains because singular-vector signs
and rotations within repeated values are not unique.
Rank selection and its reported losses sum DOFS and bit contributions from
the weakest singular value upwards. Automatic selection stops before either
supplied loss budget would be exceeded.
Use scaled formulas for strong singular values to avoid squaring overflow;
never subtract a weak tail from the total information. No relative cutoff
against the largest singular value is applied, since a very strong mode
does not make another mode less informative. Without limits the bit budget
is zero. Retain one mode in the all-zero case to satisfy the current
oemCalcReduced dimension contract. Basis generation has no configured
allocation cutoff; covariance validation still checks mathematical validity.
oemCalcReduced validates both reduction matrices and prepares reduced
covariances once at the workspace-method boundary. It projects the starting
state using the prior metric and rejects an explicit start outside the
affine subspace. No target metadata is fabricated or resized.
oem::ReducedAgendaWrapper wraps the ordinary AgendaWrapper.
Every physical agenda execution receives a full state and the real original
targets. Value-only trials use the existing empty derivative targets.
The reduced adapter expands the state, projects the measurement vector,
and computes \(\mathbf{J}_r=\mathbf{C}\mathbf{J}\mathbf{B}\).
It borrows both reduction matrices and reuses its full-state buffer,
\(m\times r\) Jacobian intermediate, and \(q\times r\)
reduced Jacobian. The full Jacobian stays in the workspace output.
A state tag avoids repeating projection of an unchanged Jacobian.
The common oem_compute dispatch serves full and reduced OEM for every LI,
GN and LM method. There is no nested callback agenda or second workspace
method invocation. Reduced LM receives
\(\mathbf{B}^{\top}\operatorname{diag}(\mathbf{S}_a^{-1})\mathbf{B}\)
as its damping matrix. This must not be replaced by identity or by the
diagonal of the reduced prior precision for an arbitrary basis.
The reduced prior is \((\mathbf{B}^{\top}\mathbf{S}_a^{-1}\mathbf{B})^{-1}\); the noise is \(\mathbf{C}\mathbf{S}_\epsilon\mathbf{C}^{\top}\). Reduced covariance matrices can be dense even if the original covariances are sparse. Preparation applies the full covariance’s structured solve/multiply paths. Only reduced state matrices are inverted to construct the reduced covariance and gain; the full prior is never densified for the reduction. The ordinary LM inverse-diagonal preparation is still needed for damping.
Final outputs are restored to the accepted full state, including after LI or a rejected trial. Existing state tags avoid redundant full agenda calls. Final-state restoration failures clear matrices and set the existing error diagnostics. The gain is \(\mathbf{B}\mathbf{G}_r\mathbf{C}\). Diagnostic costs are recomputed in full coordinates; solver progress uses the compressed objective. Keep this distinction when extending diagnostics.
InformationReport.reduction selects a rank from reverse cumulative
information sums, avoiding cancellation when weak tails accompany strong
leading modes. It produces \(\mathbf{B}=\mathbf{L}_a\mathbf{V}_r\) and
\(\mathbf{C}=\mathbf{U}_q^{\top}\mathbf{L}_\epsilon^{-1}\),
with \(q=\min(r,m)\). The report retains the block noise factors to apply
the latter solve when requested; it does not store a full whitening matrix.
Full posterior covariance is allocated only on request by scaling the state
mode columns before multiplying. Discarded columns retain unit prior variance.
tests/core/jac/reduced_oem.py runs through CTest. It compares all methods
against independent affine solutions with both reductions, exercises channel
selection, correlated errors, arbitrary basis scaling, reduced normalization,
and nonlinear GN/LM. It also checks full output restoration, diagnostics,
rank validation, and local lossless compression. A future directional
Jacobian implementation would require a different agenda contract; this
adapter does not avoid computing the full Jacobian.
State-basis storage
An exact identity state basis bypasses the prior transformation, projected LM damping and intermediate state-Jacobian product. It shares the prepared original prior. Other sparse bases use sparse multiplication for expansion, Jacobian projection and gain expansion. With diagonal prior precision, row scaling and \(\mathbf B^\top\mathbf S_a^{-1}\mathbf B\) remain sparse; a diagonal reduced precision is inverted by reciprocating its diagonal. General correlated priors may require dense preparation. Projected LM damping preserves sparse storage when the supplied state basis is sparse. All such preparation occurs before iteration. Basis truncation preserves storage type.
Owning workspace data
OptimalEstimationData is the workspace group of oem. Numerical OEM
inputs and outputs belong to this object; physical fields, sensor definitions
and finalized Jacobian targets remain separate workspace data. oemCalc
and oemCalcReduced bind references to its members and use the same solver
code. AgendaWrapper passes the owned fit/Jacobian buffers directly to the
ordinary inversion agenda interface. It does not copy the object or move its
members back to primitive workspace variables during iteration.
oemInit resets the OEM object and workspace Jacobian targets. All
oemAdd methods take both objects; pending covariance blocks belong to
oem.covmat_diagonal_blocks, not a separate workspace variable.
oemFinalizeDiagonal resolves target offsets and replaces the owned state
covariance with the assembled blocks, then calls check(&jac_targets).
The prior, observations and measurement covariance must already be present.
Failure leaves the object unchecked; success marks it checked. Call
jac_targetsFinalize separately when mapping is needed before numerical
setup is complete. Repeating finalization does not append duplicate blocks. Clearing auxiliary data releases the pending map without
changing the assembled covariance; add targets again before rebuilding it.
oemInitFromData checks dimensions before consuming the four numerical
inputs: prior, observations and their covariances. It constructs a fresh,
unchecked object separately before replacement, allowing inputs to refer to
members of the object being replaced. Fit and Jacobian are not input data.
oemSetApriori and oemSetMeasurement consume only their respective vectors,
retaining all other members. Only size changes invalidate checked. They do
not reset the previous fitted state; starting a new calculation from the prior
requires an empty current state. All consuming methods use std::exchange
to leave sources empty. The caller can obtain the prior with
model_state_vecFromData; the setters have no physical-model inputs.
Prepared covariance caches move with the covariance objects.
oemRestoreApriori takes const OEM data and updates only physical fields
through the workspace target mappings. It leaves all previous results intact.
Per-call reduced matrices and solver scratch retain automatic lifetimes and
are released on return. Reusable covariance factors and snapshots belong to
CovarianceMatrix. clear_auxiliary releases those caches together with
all recomputable outputs, but retains the selected bases and current state.
It does not reset diagnostics. Direct edits to a state or physical model
require clearing its cached fit/Jacobian before a new calculation; dimensions
alone cannot establish the linearization point of externally supplied data.
Both basis constructors and selection operate on oem. Spectrum and loss
members are auxiliary; the retained matrices are required inputs to
oemCalcReduced. Clearing the spectrum deliberately prevents subsequent
mode selection until a new decomposition is computed.
Python attribute guards
OptimalEstimationData::check marks the object checked only after collecting
and reporting numerical-input validation failures. Optional finalized targets
are checked for state-size agreement. The workspace method oemCheck calls
this same implementation with the workspace targets, declaring oem as
an input/output and jac_targets as a const input. Python exposes the flag read-only and
uses guarded property setters. Reads still return references; in-place edits
through those references are explicitly permitted and are not tracked.
uncheck changes only the flag. Target-adding methods and covariance-layout
changes invalidate it; shape-preserving correlation edits retain it.
Python clear requires unchecked data. clear_auxiliary remains callable
on checked data and preserves the flag: required inputs are unchanged. Workspace
methods remain trusted mutation paths. Both calculation entry points call
ensure_checked(jac_targets) before covariance preparation or output changes.
It calls check only when the flag is false; successful validation is reused.
Explicit oemCheck and oemFinalizeDiagonal still validate unconditionally. Preflight accepts either full-space or supplied
basis dimensions for normalization vectors; the chosen calculation enforces
its own normalization dimensions. Method-specific checks remain at the OEM boundary. XML stores the same
numerical fields as before but never persists the checked flag. Loading into
an unchecked object leaves it unchecked, and loading into a checked object
requires uncheck first.
When adding a persistent numerical member, update oem_xml_members in
oem_settings.cc and its Python guarded property. Validation state is
intentionally excluded from this serialization tuple.