#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CovMatrix2x2 {
pub var_x: f64,
pub var_y: f64,
pub cov_xy: f64,
}
impl CovMatrix2x2 {
pub fn diagonal(var_x: f64, var_y: f64) -> Self {
Self {
var_x,
var_y,
cov_xy: 0.0,
}
}
pub fn new(var_x: f64, var_y: f64, cov_xy: f64) -> Self {
Self {
var_x,
var_y,
cov_xy,
}
}
pub fn std_x(&self) -> f64 {
self.var_x.max(0.0).sqrt()
}
pub fn std_y(&self) -> f64 {
self.var_y.max(0.0).sqrt()
}
pub fn propagate(&self, j00: f64, j01: f64, j10: f64, j11: f64) -> Self {
let a00 = j00 * self.var_x + j01 * self.cov_xy;
let a01 = j00 * self.cov_xy + j01 * self.var_y;
let a10 = j10 * self.var_x + j11 * self.cov_xy;
let a11 = j10 * self.cov_xy + j11 * self.var_y;
let var_x_out = a00 * j00 + a01 * j01;
let cov_xy_out = a00 * j10 + a01 * j11;
let var_y_out = a10 * j10 + a11 * j11;
Self {
var_x: var_x_out,
var_y: var_y_out,
cov_xy: cov_xy_out,
}
}
pub fn propagate_through_jacobian(
&self,
dxdlam: f64,
dxdphi: f64,
dydlam: f64,
dydphi: f64,
) -> Self {
self.propagate(dxdlam, dxdphi, dydlam, dydphi)
}
pub fn is_psd(&self) -> bool {
self.var_x >= 0.0
&& self.var_y >= 0.0
&& self.var_x * self.var_y >= self.cov_xy * self.cov_xy
}
pub fn det(&self) -> f64 {
self.var_x * self.var_y - self.cov_xy * self.cov_xy
}
pub fn propagate_through_hessian_factors(
&self,
factors: &crate::hessian_factors::HessianFactors,
) -> Self {
self.propagate(
factors.dxdlam,
factors.dxdphi,
factors.dydlam,
factors.dydphi,
)
}
}
impl core::fmt::Display for CovMatrix2x2 {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"Cov([[{:.6e}, {:.6e}], [{:.6e}, {:.6e}]])",
self.var_x, self.cov_xy, self.cov_xy, self.var_y
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_jacobian_preserves_covariance() {
let sigma = CovMatrix2x2::new(4.0, 9.0, 1.5);
let out = sigma.propagate(1.0, 0.0, 0.0, 1.0);
assert!((out.var_x - 4.0).abs() < 1e-12);
assert!((out.var_y - 9.0).abs() < 1e-12);
assert!((out.cov_xy - 1.5).abs() < 1e-12);
}
#[test]
fn scale_jacobian_scales_variance() {
let sigma = CovMatrix2x2::diagonal(1.0, 1.0);
let s = 2.5;
let out = sigma.propagate(s, 0.0, 0.0, s);
assert!((out.var_x - s * s).abs() < 1e-12);
assert!((out.var_y - s * s).abs() < 1e-12);
assert!(out.cov_xy.abs() < 1e-12);
}
#[test]
fn rotation_jacobian_preserves_trace() {
let sigma = CovMatrix2x2::diagonal(4.0, 4.0);
let angle = 0.7_f64;
let c = angle.cos();
let s = angle.sin();
let out = sigma.propagate(c, -s, s, c);
let trace = out.var_x + out.var_y;
assert!((trace - 8.0).abs() < 1e-10, "trace={}", trace);
}
#[test]
fn psd_check() {
assert!(CovMatrix2x2::diagonal(1.0, 1.0).is_psd());
assert!(CovMatrix2x2::new(4.0, 1.0, 1.0).is_psd()); assert!(!CovMatrix2x2::new(1.0, 1.0, 2.0).is_psd()); }
#[test]
fn std_deviations() {
let cov = CovMatrix2x2::diagonal(9.0, 16.0);
assert!((cov.std_x() - 3.0).abs() < 1e-12);
assert!((cov.std_y() - 4.0).abs() < 1e-12);
}
#[test]
fn propagate_through_jacobian_same_as_propagate() {
let sigma = CovMatrix2x2::new(2.0, 3.0, 0.5);
let (j00, j01, j10, j11) = (1.5, 0.3, 0.2, 2.0);
let a = sigma.propagate(j00, j01, j10, j11);
let b = sigma.propagate_through_jacobian(j00, j01, j10, j11);
assert_eq!(a, b);
}
#[test]
fn mercator_covariance_propagation() {
let sigma_input = CovMatrix2x2::diagonal(1e-6, 1e-6); let out = sigma_input.propagate(1.0, 0.0, 0.0, 1.0);
assert!(out.is_psd());
assert!((out.var_x - 1e-6).abs() < 1e-15);
}
#[test]
fn propagate_preserves_psd_and_matches_independent_product() {
let mut state: u64 = 0x00C0_FFEE_1234_5678;
let mut next = || {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(state >> 11) as f64 / (1u64 << 53) as f64
};
for _ in 0..1000 {
let l00 = next() * 3.0 + 0.1;
let l10 = next() * 4.0 - 2.0;
let l11 = next() * 3.0 + 0.1;
let var_x = l00 * l00;
let cov_xy = l00 * l10;
let var_y = l10 * l10 + l11 * l11;
let sigma = CovMatrix2x2::new(var_x, var_y, cov_xy);
assert!(sigma.is_psd(), "constructed input must be PSD");
let (j00, j01, j10, j11) = (
next() * 4.0 - 2.0,
next() * 4.0 - 2.0,
next() * 4.0 - 2.0,
next() * 4.0 - 2.0,
);
let out = sigma.propagate(j00, j01, j10, j11);
let b00 = var_x * j00 + cov_xy * j01;
let b01 = var_x * j10 + cov_xy * j11;
let b10 = cov_xy * j00 + var_y * j01;
let b11 = cov_xy * j10 + var_y * j11;
let o00 = j00 * b00 + j01 * b10;
let o01 = j00 * b01 + j01 * b11;
let o10 = j10 * b00 + j11 * b10;
let o11 = j10 * b01 + j11 * b11;
let tol = |ref_val: f64| 1e-9 * (1.0 + ref_val.abs());
assert!((out.var_x - o00).abs() <= tol(o00), "var_x");
assert!((out.var_y - o11).abs() <= tol(o11), "var_y");
assert!((out.cov_xy - o01).abs() <= tol(o01), "cov from o01");
assert!(
(out.cov_xy - o10).abs() <= tol(o10),
"cov from o10 (symmetry)"
);
assert!((o01 - o10).abs() <= tol(o01), "result must be symmetric");
let det = out.det();
assert!(
det >= -1e-6 * (1.0 + out.var_x.abs() * out.var_y.abs()),
"Σ_out must stay PSD: det={det}"
);
}
}
#[cfg(feature = "serde")]
#[test]
fn cov_matrix_serde_round_trip() {
let original = CovMatrix2x2::new(4.0, 9.0, 1.5);
let json = serde_json::to_string(&original).expect("serialize");
let restored: CovMatrix2x2 = serde_json::from_str(&json).expect("deserialize");
assert_eq!(original, restored);
}
}