SFI.diagnostics package

SFI diagnostics — residual-consistency checks for inference results.

Top-level user-facing entry point:

from SFI.diagnostics import assess
report = assess(inferer, level="standard")
report.print_summary()

The module reuses the fitted force and the inferred constant diffusion to recompute standardised residuals (innovations) and test whether they look like an independent N(0, 1) sample:

  • mean, variance, skewness and kurtosis (pooled and per spatial component), plus the per-component covariance;

  • Ljung–Box autocorrelation of the residuals and their squares;

  • Kolmogorov–Smirnov normality against N(0, 1);

  • predicted-vs-realised normalised mean squared error of the force (a sampling-noise-aware chi-square check).

See SFI.diagnostics.residual_tests. Two preset levels are exposed:

  • "minimal" — residual moments only;

  • "standard" — adds autocorrelation, normality, and MSE consistency.

All backends (overdamped / underdamped, single / multi-particle, SPDE) share a unified residual definition: the Euler–Maruyama innovation \(r_t = \Delta x_t - F(x_t)\,\Delta t\) (overdamped) or the secant-velocity innovation \(r_t = \Delta v_t - F(x_t, v_t)\,\Delta t\) (underdamped), both whitened by \((2 \bar D \Delta t)^{-1/2}\).

References

Ljung & Box (1978); Diebold, Gunther & Tay (1998).

class SFI.diagnostics.DiagnosticsReport(residuals=<factory>, meta=<factory>)[source]

Bases: object

Container for residual-consistency test results.

Variables:
  • residuals (dict) – Test results. Always holds "moments"; at level="standard" also "autocorr", "normality" and "mse_consistency".

  • meta (dict) – Backend tag, regime, n_obs, n_particles, d, level.

Parameters:
  • residuals (dict)

  • meta (dict)

flag_issues(alpha=0.01, *, hints=True)[source]

List human-readable warnings.

Returns one line per test whose p-value is below alpha or whose statistic crosses a sane threshold (residual mean off zero, std far from one, MSE-consistency |z| > 5).

Parameters:
  • alpha (float) – Significance level for the p-value tests.

  • hints (bool) – When True (default), each message carries a one-line action hint (” — <what to do>”); set False for bare statistics (machine parsing).

Return type:

list[str]

meta: dict
print_summary(alpha=0.01, *, hints=True)[source]

Print a human-readable summary of the diagnostic report.

Each flagged issue carries a one-line action hint unless hints=False.

Parameters:
  • alpha (float)

  • hints (bool)

Return type:

None

residuals: dict
to_dict()[source]

Return a JSON-serialisable representation of the report.

Return type:

dict

to_json(indent=2)[source]

Serialise the report to a JSON string.

Parameters:

indent (int)

Return type:

str

class SFI.diagnostics.DynamicsOrderReport(verdict, fit=<factory>, scaling=<factory>, scan=<factory>, cross_check=None, meta=<factory>)[source]

Bases: object

Result of classify_dynamics().

Variables:
  • verdict (str) – "OD", "UD" or "inconclusive".

  • fit (dict) – Parametric fit output (params = sigma2, D, V, gamma, tau_v, stderr, V_z, delta_aicc and the raw SSR / AICc values).

  • scaling (dict) – Model-free statistics (rho2, K, beta).

  • scan (dict) – Lag-0/1/2 covariances across the dt scan (strides, dt, C0, C1, C2, n_eff).

  • cross_check (dict or None) – Overdamped-fit Ljung–Box result, or None if disabled.

  • meta (dict) – Sampling summary (d, n_datasets, strides, dt_min/max, gamma_dt_min).

Parameters:
  • verdict (str)

  • fit (dict)

  • scaling (dict)

  • scan (dict)

  • cross_check (dict | None)

  • meta (dict)

cross_check: dict | None = None
fit: dict
meta: dict
print_summary()[source]

Print a human-readable summary of the classification.

Return type:

None

scaling: dict
scan: dict
to_dict()[source]

JSON-serialisable representation of the report.

Return type:

dict

to_json(indent=2)[source]
Parameters:

indent (int)

Return type:

str

verdict: str
SFI.diagnostics.assess(inferer, *, level='standard', n_lags=None, data=None)[source]

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 (str) – "minimal" — pooled residual moments only; "standard" (default) — adds autocorrelation, normality, and predicted-vs-realised MSE consistency.

  • n_lags (int | None) – Number of autocorrelation lags. Default min(20, n_eff // 5).

  • data – Optional independent TrajectoryCollection on which to evaluate the residuals (held-out diagnostics). Default: the training data attached to the inferer.

Returns:

Container with a residuals section and meta.

Return type:

DiagnosticsReport

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.

SFI.diagnostics.classify_dynamics(data, *, strides=None, cross_check=True)[source]

Classify trajectory data as overdamped, underdamped, or inconclusive.

Robust to high localization noise (lag >= 2 covariances are noise-immune) and reports inconclusive at coarse sampling where the momentum is unresolved (gamma * dt >~ 1), rather than guessing.

Parameters:
  • data (TrajectoryCollection) – Raw trajectories (positions). Velocities are not required — the test reconstructs the dynamics order from position increments alone.

  • strides (sequence of int, optional) – Coarse-graining factors for the dt scan. Default: a geometric set 1, 2, 4, ... capped so the coarsest still has enough samples.

  • cross_check (bool) – Run the Layer-3 overdamped-fit residual-autocorrelation corroboration (default True).

Return type:

DynamicsOrderReport

Notes

Assumes white localization noise; spatially uniform, isotropic pooling of components. Strong memory (a generalized Langevin / viscoelastic bath) can also produce velocity persistence without inertia — out of scope; the test detects trajectory smoothness, which inertia produces.

Examples

>>> report = classify_dynamics(collection)
>>> report.verdict
'UD'
SFI.diagnostics.parametric_four_point_diagnostic(data, drift_fn, dt, n_substeps=4)[source]

Four-point diagnostic: test that Cov[r_i, r_{i+2}] = 0.

Under correct model specification and i.i.d. measurement noise, midpoint flow residuals separated by two steps share no measurement noise source, so their cross-covariance should vanish. Deviations indicate model misspecification, correlated measurement noise, or higher-order effects.

Parameters:
  • data (TrajectoryCollection or TrajectoryDataset) – Observed trajectory data.

  • drift_fn (callable (d,) (d,)) – Drift function with parameters already closed over.

  • dt (float) – Observation time step.

  • n_substeps (int) – RK4 micro-steps per interval.

Returns:

resultC_02 : (d, d) empirical cross-covariance of r_0 and r_2. frobenius_norm : ||C_02||_F. n_quadruplets : number of quadruplets used.

Return type:

dict

SFI.diagnostics.plot_dynamics_order(report, *, axes=None)[source]

Visualise an OD-vs-UD classification (classify_dynamics()).

Two panels versus the sampling step dt:

  • lag-2 persistence rho2 = C2/(C0+2C1) (noise-immune): tends to 0 for overdamped data, to a positive plateau for inertia;

  • apparent kinetic energy K = (C0+2C1)/dt^2 on log-log axes, with reference slopes -1 (overdamped, K ~ 2D/dt) and 0 (underdamped); the fitted log-log slope beta is annotated.

The parametric-fit prediction is overlaid on both panels. Accepts a DynamicsOrderReport.

SFI.diagnostics.plot_qq(report_or_inferer, *, ax=None, level='standard')[source]

Normal Q–Q plot of pooled whitened residuals.

Parameters:

level (str)

SFI.diagnostics.plot_residual_acf(report_or_inferer, *, ax=None, level='standard')[source]

Autocorrelation of the residuals (and of $z^2$).

Reads the ACF computed by autocorrelation_tests() (stored on the report) rather than recomputing it.

Parameters:

level (str)

SFI.diagnostics.plot_residual_histogram(report_or_inferer, *, ax=None, bins=60, level='standard')[source]

Histogram of pooled residuals overlaid with N(0,1) density.

Parameters:
  • bins (int)

  • level (str)

SFI.diagnostics.plot_summary(report_or_inferer, *, level='standard', figsize=(13.0, 4.0))[source]

1×3 summary figure: Q–Q plot, residual histogram, and ACF.

Returns the matplotlib Figure.

Parameters:

level (str)

Submodules