"""Generic Fisher information and Cramér-Rao lower bound (CRLB) tools."""
from collections.abc import Callable, Sequence
from typing import Any
import numpy as np
[docs]
def numerical_jacobian(
model: Callable[[np.ndarray], np.ndarray],
params: np.ndarray,
rel_step: float = 1e-4,
abs_step: float = 1e-8,
) -> np.ndarray:
"""
Central-difference Jacobian ``dmu/dtheta`` of a batched model.
Parameters
----------
model : Callable
``model(params)`` with `params` ``(P, K)`` returning channel means ``(P, T)``.
params : np.ndarray
Parameter vectors ``(P, K)`` (or ``(K,)`` for a single one).
rel_step, abs_step : float
Step for parameter ``k`` is ``max(rel_step * |theta_k|, abs_step)``.
Returns
-------
np.ndarray
Jacobian of shape ``(P, K, T)``.
"""
params = np.atleast_2d(np.asarray(params, dtype=float))
columns = []
for k in range(params.shape[1]):
h = np.maximum(rel_step * np.abs(params[:, k]), abs_step)
up, down = params.copy(), params.copy()
up[:, k] += h
down[:, k] -= h
columns.append((model(up) - model(down)) / (2.0 * h)[:, None])
return np.stack(columns, axis=1)
[docs]
def select_parameters(info: np.ndarray, indices: Sequence[int]) -> np.ndarray:
"""
Fisher sub-matrices for the parameters in `indices`, i.e. the information when
every other parameter is known exactly and not estimated.
"""
idx = np.asarray(indices)
return info[..., idx[:, None], idx[None, :]]
[docs]
def crlb_covariance(info: np.ndarray) -> np.ndarray:
"""
Cramér-Rao bound on the covariance, ``I^-1`` per batch entry. A (near-)singular
Fisher matrix -- an unidentifiable parameter -- falls back to the
pseudo-inverse.
"""
info = np.asarray(info, dtype=float)
try:
return np.linalg.inv(info)
except np.linalg.LinAlgError:
return np.linalg.pinv(info, hermitian=True)
[docs]
def crlb_std(info: np.ndarray, index: int) -> np.ndarray:
"""Smallest achievable standard deviation of parameter `index`, per batch entry."""
var = crlb_covariance(info)[..., index, index]
return np.sqrt(np.clip(var, 0.0, None))
[docs]
def crlb_cv(info: np.ndarray, value: Any, index: int) -> np.ndarray:
"""Smallest achievable coefficient of variation ``std / value`` of parameter `index`."""
return crlb_std(info, index) / np.asarray(value, dtype=float)
[docs]
def f_value(cv: Any, n_photons: Any) -> np.ndarray:
"""
Photon-economy F-value ``CV * sqrt(N)``: 1 for an ideal photon-counting
measurement, larger for any real instrument or estimator. Applies equally to a
measured CV and to a CRLB CV.
"""
return np.asarray(cv, dtype=float) * np.sqrt(np.asarray(n_photons, dtype=float))
[docs]
def estimator_efficiency(cv_measured: Any, cv_bound: Any) -> np.ndarray:
"""
Statistical efficiency ``(cv_bound / cv_measured) ** 2``: 1 when an estimator
reaches the CRLB, smaller otherwise (a biased estimator can exceed 1).
"""
cv_measured = np.asarray(cv_measured, dtype=float)
cv_bound = np.asarray(cv_bound, dtype=float)
with np.errstate(divide="ignore", invalid="ignore"):
return np.where(cv_measured > 0, (cv_bound / cv_measured) ** 2, np.nan)