Source code for pycif.plugins.obsoperators.onnx.obsoper
import datetime
import numpy as np
from logging import info, warning
from ....utils.check.errclass import CifIOError
[docs]
def obsoper(
self,
controlvect,
obsvect,
mode,
run_id=0,
datei=datetime.datetime(1979, 1, 1),
datef=datetime.datetime(2100, 1, 1),
workdir="./",
reload_results=False,
check_transforms=False,
ignore_exceptions=False,
force_fetch_results=False,
**kwargs,
):
"""Apply the ONNX surrogate observation operator.
**Forward** (``mode='fwd'``):
.. math::
\\mathbf{y}_\\text{sim} = \\mathcal{N}(\\mathbf{x})
Sets ``obsvect.ysim`` and returns *obsvect*.
**Tangent-linear** (``mode='tl'``):
.. math::
\\delta\\mathbf{y} \\approx
\\frac{\\mathcal{N}(\\mathbf{x}+\\varepsilon\\,\\delta\\mathbf{x})
- \\mathcal{N}(\\mathbf{x}-\\varepsilon\\,\\delta\\mathbf{x})}
{2\\varepsilon}
Requires 2 forward passes (central finite differences).
Sets both ``obsvect.ysim`` and ``obsvect.dy``; returns *obsvect*.
**Adjoint** (``mode='adj'``):
.. math::
\\delta\\mathbf{x} \\approx \\mathbf{J}(\\mathbf{x})^\\top\\,\\delta\\mathbf{y}
where :math:`\\mathbf{J}` is built column-by-column via central finite
differences (:math:`2 n_\\text{ctl}` forward passes). The Jacobian is
cached and reused for repeated adjoint calls at the same
:math:`\\mathbf{x}`.
Args:
self: Obs-operator plugin instance. Must have ``_ort_operator`` set
by a prior call to ``ini_data``.
controlvect: Control-vector object. ``controlvect.x`` is read in
``fwd`` / ``tl`` modes; ``controlvect.dx`` is read in ``tl`` mode
and written in ``adj`` mode.
obsvect: Observation-vector object. ``obsvect.ysim`` is written in
``fwd`` / ``tl`` modes; ``obsvect.dy`` is read in ``adj`` mode
and written in ``tl`` mode.
mode: One of ``'fwd'``, ``'tl'``, or ``'adj'``.
run_id: Run identifier (unused; present for interface compatibility).
datei: Simulation start date (unused).
datef: Simulation end date (unused).
workdir: Working directory (unused).
reload_results: Reload pre-computed results (unused).
check_transforms: Validate transform consistency (unused).
ignore_exceptions: Swallow non-fatal errors (unused).
force_fetch_results: If ``True``, raise :exc:`CifIOError` immediately.
**kwargs: Additional keyword arguments (ignored).
Returns:
*obsvect* in ``'fwd'`` and ``'tl'`` modes; *controlvect* in ``'adj'``.
Raises:
CifIOError: if ``force_fetch_results`` is ``True``.
"""
if force_fetch_results:
raise CifIOError
H = self._ort_operator.forward
eps = self.fd_epsilon
x = np.asarray(controlvect.x, dtype=np.float64)
if mode == "fwd":
obsvect.ysim = H(x)
# Invalidate Jacobian cache — x has changed
self._adj_cache = None
self._adj_cache_x = None
return obsvect
elif mode == "tl":
dx = np.asarray(controlvect.dx, dtype=np.float64)
obsvect.ysim = H(x)
obsvect.dy = (H(x + eps * dx) - H(x - eps * dx)) / (2.0 * eps)
return obsvect
elif mode == "adj":
dy = np.asarray(obsvect.dy, dtype=np.float64)
n_state = len(x)
# Recompute Jacobian only when x has changed
if self._adj_cache is None or not np.array_equal(self._adj_cache_x, x):
if n_state > 500:
warning(
f"ONNX adjoint: computing {n_state}-column finite-difference "
f"Jacobian ({2 * n_state} forward passes). "
"Consider a backend that supports analytic gradients."
)
n_obs = len(dy)
J = np.empty((n_obs, n_state), dtype=np.float64)
for i in range(n_state):
ei = np.zeros(n_state, dtype=np.float64)
ei[i] = eps
J[:, i] = (H(x + ei) - H(x - ei)) / (2.0 * eps)
self._adj_cache = J
self._adj_cache_x = x.copy()
controlvect.dx = self._adj_cache.T @ dy
return controlvect