Generated from the full canonical file for this source snapshot. Line numbers match the library source.
Source SHA256: 160bcbf5fd3fd7b9d1807d1063deb01f092e9c660a153a4bb9b68344f6093115
1"""Bounded offline sensitivity diagnostics with explicit parameter and noise units.23The caller supplies a small Jacobian from a declared diagnostic experiment. This4module does not download images or retain Jacobians inside a production solve.5NumPy, supplied by the optional scientific environment, is loaded only on use.6"""78from __future__ import annotations910import importlib11import math12from collections.abc import Sequence13from dataclasses import dataclass14from typing import Any1516from dpt.contracts import ContractError, NumericalError, finite_scalar171819@dataclass(frozen=True, slots=True)20class SensitivitySpectrum:21singular_values: tuple[float, ...]22right_directions: tuple[tuple[float, ...], ...]23rank: int24threshold: float25condition_number: float | None26observations: int27parameters: int282930def _scaled_entry(value: float, unit: float, weight: float) -> float:31value = finite_scalar(value, "Jacobian entry")32root_weight = math.sqrt(finite_scalar(weight, "precision weight", minimum=0))33if value == 0 or root_weight == 0:34return 0.035mantissa, exponent = 1.0, 036for factor in (value, unit, root_weight):37coefficient, power = math.frexp(factor)38mantissa *= coefficient39exponent += power40try:41return math.ldexp(mantissa, exponent)42except OverflowError as error:43raise NumericalError("scaled sensitivity exceeds binary64 range") from error444546# region book:scaled-sensitivity-diagnostics47def sensitivity_spectrum(48jacobian_rows: Sequence[Sequence[float]],49parameter_scales: Sequence[float],50*,51precision_weights: Sequence[float] | None = None,52relative_threshold: float = 1e-8,53absolute_threshold: float = 0.0,54) -> SensitivitySpectrum:55"""SVD of sqrt(W) J S; right directions use dimensionless chart coordinates.5657Singular values and an explicitly chosen threshold describe local sensitivity,58not global identifiability or clinical recovery. Duplicate views add no new59independent directions, although weighting can change threshold-defined rank.60Noise correlations require an explicitly prewhitened Jacobian; diagonal61precision weights must not stand in for an unknown covariance model.62"""63rows, columns = len(jacobian_rows), len(parameter_scales)64if not 1 <= rows <= 4096 or not 1 <= columns <= 32:65raise ContractError("offline diagnostic is bounded to 4096 observations and 32 parameters")66if any(len(row) != columns for row in jacobian_rows):67raise ContractError("every Jacobian row must contain each active parameter")68units = tuple(finite_scalar(value, "parameter scale", minimum=0) for value in parameter_scales)69if min(units) == 0:70raise ContractError("parameter scales must be positive")71relative = finite_scalar(relative_threshold, "relative threshold", minimum=0)72absolute = finite_scalar(absolute_threshold, "absolute threshold", minimum=0)73if relative >= 1:74raise ContractError("relative rank threshold must be less than one")75weights = tuple(precision_weights) if precision_weights is not None else (1.0,) * rows76if len(weights) != rows:77raise ContractError("one fixed precision weight is required per observation")78scaled = [79[_scaled_entry(value, units[column], weights[index]) for column, value in enumerate(row)]80for index, row in enumerate(jacobian_rows)81]82if not all(math.isfinite(value) for row in scaled for value in row):83raise NumericalError("scaled sensitivity exceeds binary64 range")84# Zero rows preserve the missing directions when observations < parameters,85# without requesting the large square left-singular-vector matrix.86scaled.extend([[0.0] * columns for _ in range(max(0, columns - rows))])87np: Any = importlib.import_module("numpy")88_, values, right = np.linalg.svd(np.asarray(scaled, dtype=np.float64), full_matrices=False)89singular = tuple(float(value) for value in values)90if not all(map(math.isfinite, singular)):91raise NumericalError("sensitivity decomposition produced nonfinite singular values")92threshold = max(absolute, relative * singular[0])93rank = sum(value > threshold for value in singular)94condition = singular[0] / singular[-1] if rank == columns else None95if condition is not None and not math.isfinite(condition):96condition = None97return SensitivitySpectrum(98singular,99tuple(tuple(float(value) for value in row) for row in right),100rank,101threshold,102condition,103rows,104columns,105)106107108# endregion book:scaled-sensitivity-diagnostics109