///|
/// Namespace object for covariance and observation diagnostics.
pub enum KalmanDiagnostics {
Token
} derive(Debug, Eq)
///|
pub enum CovarianceHealth {
Healthy
NonFinite
NotSquare
NotSymmetric
NotPositiveSemidefinite
IllConditioned
} derive(Debug, Eq)
///|
pub struct CovarianceReport {
health : CovarianceHealth
dimension : Int
symmetry_error : Double
minimum_eigenvalue : Double
maximum_eigenvalue : Double
condition_estimate : Double
finite : Bool
positive_diagonal : Bool
} derive(Debug)
///|
pub fn CovarianceReport::health(self : CovarianceReport) -> CovarianceHealth {
self.health
}
///|
pub fn CovarianceReport::dimension(self : CovarianceReport) -> Int {
self.dimension
}
///|
pub fn CovarianceReport::symmetry_error(self : CovarianceReport) -> Double {
self.symmetry_error
}
///|
pub fn CovarianceReport::minimum_eigenvalue(self : CovarianceReport) -> Double {
self.minimum_eigenvalue
}
///|
pub fn CovarianceReport::maximum_eigenvalue(self : CovarianceReport) -> Double {
self.maximum_eigenvalue
}
///|
pub fn CovarianceReport::condition_estimate(self : CovarianceReport) -> Double {
self.condition_estimate
}
///|
pub fn CovarianceReport::is_healthy(self : CovarianceReport) -> Bool {
self.health is Healthy
}
///|
pub fn CovarianceReport::finite(self : CovarianceReport) -> Bool {
self.finite
}
///|
pub fn CovarianceReport::positive_diagonal(self : CovarianceReport) -> Bool {
self.positive_diagonal
}
///|
pub fn KalmanDiagnostics::new() -> KalmanDiagnostics {
Token
}
///|
/// Inspect symmetry, finite values, diagonal signs, approximate eigenvalues,
/// and conditioning of a covariance matrix.
pub fn KalmanDiagnostics::report(
self : KalmanDiagnostics,
covariance : Matrix,
) -> CovarianceReport {
let _ = self
let finite = covariance.is_finite()
let square = covariance.is_square()
let mut symmetry_error = 0.0
if square {
for i in 0.. symmetry_error {
symmetry_error = difference
}
}
}
}
let mut positive_diagonal = square
if square {
for i in 0.. 0 {
minimum = eigenvalues[0]
maximum = eigenvalues[0]
for value in eigenvalues {
if value < minimum {
minimum = value
}
if value > maximum {
maximum = value
}
}
}
let condition = if minimum > 0.000000000001 { maximum / minimum } else { 0.0 }
let health = if !finite {
NonFinite
} else if !square {
NotSquare
} else if symmetry_error > 0.000001 {
NotSymmetric
} else if !positive_diagonal || minimum < -0.000001 {
NotPositiveSemidefinite
} else if condition > 100000000.0 {
IllConditioned
} else {
Healthy
}
{
health,
dimension: covariance.rows(),
symmetry_error,
minimum_eigenvalue: minimum,
maximum_eigenvalue: maximum,
condition_estimate: condition,
finite,
positive_diagonal,
}
}
///|
/// Compatibility helper for the original API.
pub fn KalmanDiagnostics::check_covariance(
self : KalmanDiagnostics,
covariance : Array[Array[Double]],
) -> Bool {
self.report(Matrix::from_rows(covariance)).is_healthy()
}
///|
/// Return whether a matrix is symmetric positive semidefinite within a
/// caller-supplied tolerance.
pub fn covariance_is_psd(covariance : Matrix, tolerance : Double) -> Bool {
let report = KalmanDiagnostics::new().report(covariance)
report.symmetry_error() <= tolerance &&
report.minimum_eigenvalue() >= -tolerance
}
///|
pub fn covariance_is_positive_definite(
covariance : Matrix,
tolerance : Double,
) -> Bool {
let report = KalmanDiagnostics::new().report(covariance)
report.symmetry_error() <= tolerance &&
report.minimum_eigenvalue() > tolerance
}
///|
pub fn covariance_project_psd(covariance : Matrix, floor : Double) -> Matrix {
let safe_floor = if floor < 0.0 { 0.0 } else { floor }
let result = covariance.symmetric_part()
for i in 0.. ignore
}
}
result
}
///|
pub struct InnovationDiagnostics {
innovation : Array[Double]
covariance : Matrix
nis : Double
whitened_norm : Double
accepted : Bool
} derive(Debug)
///|
pub fn InnovationDiagnostics::innovation(
self : InnovationDiagnostics,
) -> Array[Double] {
self.innovation.copy()
}
///|
pub fn InnovationDiagnostics::covariance(
self : InnovationDiagnostics,
) -> Matrix {
self.covariance.copy()
}
///|
pub fn InnovationDiagnostics::nis(self : InnovationDiagnostics) -> Double {
self.nis
}
///|
pub fn InnovationDiagnostics::whitened_norm(
self : InnovationDiagnostics,
) -> Double {
self.whitened_norm
}
///|
pub fn InnovationDiagnostics::accepted(self : InnovationDiagnostics) -> Bool {
self.accepted
}
///|
pub fn make_innovation_diagnostics(
innovation : Array[Double],
covariance : Matrix,
gate_threshold : Double,
) -> InnovationDiagnostics {
let nis = match covariance.inverse() {
None => 0.0
Some(inverse) => vector_dot(innovation, inverse.multiply_vector(innovation))
}
let safe_threshold = if gate_threshold < 0.0 { 0.0 } else { gate_threshold }
{
innovation: innovation.copy(),
covariance: covariance.copy(),
nis,
whitened_norm: nis.sqrt(),
accepted: nis <= safe_threshold,
}
}
///|
pub(all) enum MissingObservationAction {
PredictOnly
InflateAndPredict
HoldLastEstimate
} derive(Debug, Eq)
///|
pub struct MissingObservationPolicy {
max_consecutive : Int
inflate_factor : Double
action : MissingObservationAction
} derive(Debug)
///|
pub fn MissingObservationPolicy::new(
max_consecutive : Int,
inflate_factor : Double,
action : MissingObservationAction,
) -> MissingObservationPolicy {
{
max_consecutive: if max_consecutive < 0 {
0
} else {
max_consecutive
},
inflate_factor: if inflate_factor < 1.0 {
1.0
} else {
inflate_factor
},
action,
}
}
///|
pub fn MissingObservationPolicy::max_consecutive(
self : MissingObservationPolicy,
) -> Int {
self.max_consecutive
}
///|
pub fn MissingObservationPolicy::inflate_factor(
self : MissingObservationPolicy,
) -> Double {
self.inflate_factor
}
///|
pub fn MissingObservationPolicy::action(
self : MissingObservationPolicy,
) -> MissingObservationAction {
self.action
}
///|
pub fn handle_missing_observation() -> Unit {
// Compatibility entry point: a missing sample means predict-only. The
// policy type above lets applications make the decision explicit.
}
///|
pub fn missing_action(
policy : MissingObservationPolicy,
consecutive : Int,
) -> MissingObservationAction {
if consecutive > policy.max_consecutive {
HoldLastEstimate
} else {
match policy.action {
PredictOnly => PredictOnly
InflateAndPredict => InflateAndPredict
HoldLastEstimate => HoldLastEstimate
}
}
}
///|
pub fn covariance_trace(covariance : Matrix) -> Double {
covariance.trace()
}
///|
pub fn covariance_average_variance(covariance : Matrix) -> Double {
if !covariance.is_square() || covariance.rows() == 0 {
return 0.0
}
covariance.trace() / covariance.rows().to_double()
}
///|
pub fn covariance_relative_change(
previous : Matrix,
current : Matrix,
) -> Double {
let denominator = previous.frobenius_norm()
if denominator <= 0.000000000001 {
current.frobenius_norm()
} else {
previous.sub(current).frobenius_norm() / denominator
}
}
///|
pub fn state_has_valid_uncertainty(
state : Array[Double],
covariance : Matrix,
) -> Bool {
vector_is_finite(state) &&
covariance.rows() == state.length() &&
covariance.cols() == state.length() &&
covariance_is_psd(covariance, 0.000001)
}