Source code for SFI.diagnostics.assess
"""Top-level diagnostic dispatcher.
See :func:`assess` for the user-facing entry point.
"""
from __future__ import annotations
from typing import Optional
from .report import DiagnosticsReport
from .residual_tests import (
autocorrelation_tests,
mse_consistency,
normality_test,
residual_moments,
)
from .residuals import build_residuals
_LEVELS = ("minimal", "standard")
[docs]
def assess(
inferer,
*,
level: str = "standard",
n_lags: Optional[int] = None,
data=None,
) -> DiagnosticsReport:
"""Run residual-consistency checks on a fitted inference object.
Recomputes the standardised residuals (Euler--Maruyama innovations,
whitened by the inferred constant diffusion) and tests whether they
look like an independent ``N(0, 1)`` sample.
Parameters
----------
inferer :
A fitted ``OverdampedLangevinInference`` or
``UnderdampedLangevinInference``. Must have a callable
``force_inferred`` (run e.g. ``infer_force_linear`` first) and an
inferred constant diffusion (``A_inv``).
level :
``"minimal"`` — pooled residual moments only;
``"standard"`` (default) — adds autocorrelation, normality, and
predicted-vs-realised MSE consistency.
n_lags :
Number of autocorrelation lags. Default ``min(20, n_eff // 5)``.
data :
Optional independent :class:`~SFI.trajectory.TrajectoryCollection`
on which to evaluate the residuals (held-out diagnostics).
Default: the training data attached to the inferer.
Returns
-------
DiagnosticsReport
Container with a ``residuals`` section and ``meta``.
Notes
-----
The Euler-style residual is exact for the linear estimators; for the
parametric estimators it is an approximation that
is still asymptotically consistent under correct specification, so
the autocorrelation and normality tests remain valid for flagging
misspecification.
"""
if level not in _LEVELS:
raise ValueError(f"level must be one of {_LEVELS!r}; got {level!r}.")
bundle = build_residuals(inferer, data=data)
residuals: dict = {"moments": residual_moments(bundle)}
if level == "standard":
residuals["autocorr"] = autocorrelation_tests(bundle, n_lags=n_lags)
residuals["normality"] = normality_test(bundle)
residuals["mse_consistency"] = mse_consistency(inferer, bundle)
meta = {
"backend": bundle.backend,
"regime": bundle.regime,
"n_obs": bundle.n_obs,
"n_particles": bundle.n_particles,
"d": bundle.d,
"level": level,
"inferer_class": type(inferer).__name__,
}
return DiagnosticsReport(residuals=residuals, meta=meta)