Source code for spindoctor.nav_technique.dt_fitting.weights

"""Robust weighting and covariance for the DT fit.

The Holland-Welsch biweight the IRLS loop reweights with, and the
information-matrix inversion that turns a converged fit into a covariance.
"""

import math
from typing import cast

import numpy as np
from scipy.linalg import pinvh

from spindoctor.nav_technique.dt_fitting.constants import (
    DEFAULT_PINVH_RCOND,
    DEFAULT_TUKEY_C,
)
from spindoctor.support.types import NDArrayFloatType

__all__ = [
    'information_matrix_to_covariance',
    'tukey_biweight_weights',
]


[docs] def tukey_biweight_weights( residuals: NDArrayFloatType, *, c: float = DEFAULT_TUKEY_C, ) -> NDArrayFloatType: """Return Tukey biweight weights for a vector of residuals. The Holland-Welsch biweight is .. math:: w_i = \\begin{cases} \\bigl(1 - (r_i / c)^2\\bigr)^2 & |r_i| \\le c \\\\ 0 & \\text{otherwise} \\end{cases} so residuals beyond ``c`` are dropped completely. Callers are responsible for scaling residuals to the desired robust scale before invoking — typically dividing by an estimate of the residual standard deviation so that ``c = 4.685`` corresponds to the conventional 95 % asymptotic efficiency under Gaussian errors. Parameters: residuals: ``(N,)`` array of (already-scaled) residuals. May contain negative entries; only the magnitude is consulted. c: Strictly positive cutoff in residual units. Must be finite. Returns: ``(N,)`` float64 array of weights in ``[0, 1]``. Raises: ValueError: if ``c`` is not strictly positive or ``residuals`` is not 1-D. """ if not (c > 0.0) or not math.isfinite(c): raise ValueError(f'c must be a positive finite number; got {c!r}') arr = np.asarray(residuals, np.float64) if arr.ndim != 1: raise ValueError(f'residuals must be 1-D; got ndim={arr.ndim}') scaled = arr / c inside = np.abs(scaled) <= 1.0 weights = np.zeros_like(arr) inside_vals = scaled[inside] weights[inside] = (1.0 - inside_vals * inside_vals) ** 2 return cast(NDArrayFloatType, weights)
[docs] def information_matrix_to_covariance( jacobian: NDArrayFloatType, weights: NDArrayFloatType, *, rcond: float = DEFAULT_PINVH_RCOND, ) -> NDArrayFloatType: """Return the parameter covariance from a weighted Jacobian. The M-estimator information matrix is ``J^T diag(w) J``; the parameter covariance is its Moore-Penrose pseudoinverse via :func:`scipy.linalg.pinvh`, which gracefully handles rank-deficient inputs (a flat polyline produces a rank-1 information matrix; the returned covariance has unbounded variance along the unobservable null direction). The ``weights`` argument is the per-residual weight (Tukey biweight times any prior precision) and is multiplied into ``J`` before the matrix product, which both incorporates the IRLS reweighting and keeps the result symmetric within numerical tolerance. Parameters: jacobian: ``(N, P)`` Jacobian of the residual vector with respect to the parameter vector. weights: ``(N,)`` non-negative residual weights. rcond: Pseudoinverse cutoff; eigenvalues smaller than this relative to the largest are treated as null. Returns: ``(P, P)`` covariance matrix. Symmetric; positive semidefinite within ``rcond``. Raises: ValueError: if shapes disagree, ``weights`` contains negative entries, or ``jacobian`` is not 2-D. """ if jacobian.ndim != 2: raise ValueError(f'jacobian must be 2-D; got ndim={jacobian.ndim}') if weights.ndim != 1 or weights.shape[0] != jacobian.shape[0]: raise ValueError( 'weights must be a 1-D vector matching the Jacobian rows; ' f'got weights {weights.shape}, jacobian {jacobian.shape}' ) if (weights < 0).any(): raise ValueError('weights must be non-negative') sqrt_w = np.sqrt(weights) weighted_j = sqrt_w[:, None] * jacobian info = weighted_j.T @ weighted_j cov = pinvh(info, rtol=rcond) cov = 0.5 * (cov + cov.T) return cast(NDArrayFloatType, cov)