"""Kalman covariance initialization, informed by classification uncertainty.
A Kalman filter needs a process-noise covariance ``Q``, a measurement-noise
covariance ``R``, and an initial state covariance ``P0``. When the modes were
classified with uncertainty quantification, that uncertainty is propagated into
``P0``: a mode identified with low confidence is given a larger initial variance,
so the filter trusts well-identified modes more than ambiguous ones.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
import numpy as np
if TYPE_CHECKING:
from collections.abc import Sequence
import numpy.typing as npt
__all__ = [
"CovarianceSet",
"covariances_from_confidence",
"initialize_covariances",
]
[docs]
@dataclass
class CovarianceSet:
"""Kalman-filter covariance matrices.
Parameters
----------
q : npt.NDArray[np.float64]
Process-noise covariance, shape ``(n_states, n_states)``.
r : npt.NDArray[np.float64]
Measurement-noise covariance, shape ``(n_outputs, n_outputs)``.
p0 : npt.NDArray[np.float64]
Initial state covariance, shape ``(n_states, n_states)``.
"""
q: npt.NDArray[np.float64]
r: npt.NDArray[np.float64]
p0: npt.NDArray[np.float64]
[docs]
def initialize_covariances(
n_states: int,
n_outputs: int,
*,
process_noise: float = 1e-4,
measurement_noise: float = 1e-2,
initial_variance: float = 1.0,
) -> CovarianceSet:
"""Build uniform diagonal covariance matrices.
Parameters
----------
n_states : int
Number of states.
n_outputs : int
Number of measured outputs.
process_noise, measurement_noise, initial_variance : float, optional
Diagonal values for ``Q``, ``R``, and ``P0`` respectively; all must be
non-negative.
Returns
-------
CovarianceSet
The diagonal covariance matrices.
Raises
------
ValueError
If a dimension is negative or a noise value is negative.
"""
_validate_nonnegative(n_states, n_outputs, process_noise, measurement_noise)
if initial_variance < 0.0:
msg = "initial_variance must be non-negative"
raise ValueError(msg)
return CovarianceSet(
q=process_noise * np.eye(n_states, dtype=np.float64),
r=measurement_noise * np.eye(n_outputs, dtype=np.float64),
p0=initial_variance * np.eye(n_states, dtype=np.float64),
)
[docs]
def covariances_from_confidence(
confidences: Sequence[float],
n_outputs: int,
*,
process_noise: float = 1e-4,
measurement_noise: float = 1e-2,
base_variance: float = 1.0,
uncertainty_scale: float = 10.0,
) -> CovarianceSet:
"""Build covariances whose ``P0`` grows with classification uncertainty.
Each mode maps to two modal states (real and imaginary parts); the initial
variance of those states is ``base_variance * (1 + uncertainty_scale * (1 -
confidence))``, so a low-confidence mode starts with a larger covariance.
Parameters
----------
confidences : Sequence[float]
Per-mode classification confidence in ``[0, 1]``; values are clipped to
that range.
n_outputs : int
Number of measured outputs.
process_noise, measurement_noise : float, optional
Diagonal values for ``Q`` and ``R``; must be non-negative.
base_variance : float, optional
Initial variance of a fully confident mode; must be non-negative.
uncertainty_scale : float, optional
How strongly uncertainty inflates the initial variance; must be
non-negative.
Returns
-------
CovarianceSet
The covariance matrices, with ``2 * len(confidences)`` states.
Raises
------
ValueError
If a noise value, ``base_variance``, ``uncertainty_scale``, or
``n_outputs`` is negative.
"""
_validate_nonnegative(n_outputs, n_outputs, process_noise, measurement_noise)
if base_variance < 0.0 or uncertainty_scale < 0.0:
msg = "base_variance and uncertainty_scale must be non-negative"
raise ValueError(msg)
clipped = np.clip(np.asarray(confidences, dtype=np.float64), 0.0, 1.0)
per_mode = base_variance * (1.0 + uncertainty_scale * (1.0 - clipped))
p0_diagonal = np.repeat(per_mode, 2)
n_states = p0_diagonal.shape[0]
return CovarianceSet(
q=process_noise * np.eye(n_states, dtype=np.float64),
r=measurement_noise * np.eye(n_outputs, dtype=np.float64),
p0=np.diag(p0_diagonal),
)
def _validate_nonnegative(
n_states: int, n_outputs: int, process_noise: float, measurement_noise: float
) -> None:
"""Validate dimension and noise non-negativity."""
if n_states < 0 or n_outputs < 0:
msg = "State and output counts must be non-negative"
raise ValueError(msg)
if process_noise < 0.0 or measurement_noise < 0.0:
msg = "Noise values must be non-negative"
raise ValueError(msg)