use ndarray::{Array1, Array2};
use serde::{Deserialize, Serialize};
use std::ops::{Deref, DerefMut};
use thiserror::Error;
pub use crate::Dispersion;
#[derive(Clone, Copy, Debug, Error, PartialEq)]
pub enum CovarianceStandardErrorError {
#[error("covariance must be square, got {rows}x{cols}")]
NotSquare { rows: usize, cols: usize },
#[error("covariance entry ({row}, {col}) is non-finite: {value}")]
NonFinite { row: usize, col: usize, value: f64 },
#[error("covariance dimension {dimension} is too large for a sub-unit rounding-error bound")]
DimensionTooLarge { dimension: usize },
#[error(
"covariance diagonal {index} is materially negative: {value} (backward-error tolerance {tolerance})"
)]
NegativeDiagonal {
index: usize,
value: f64,
tolerance: f64,
},
}
pub fn se_from_covariance(cov: &Array2<f64>) -> Result<Array1<f64>, CovarianceStandardErrorError> {
let (rows, cols) = cov.dim();
if rows != cols {
return Err(CovarianceStandardErrorError::NotSquare { rows, cols });
}
let mut max_abs = 0.0_f64;
for ((row, col), &value) in cov.indexed_iter() {
if !value.is_finite() {
return Err(CovarianceStandardErrorError::NonFinite { row, col, value });
}
max_abs = max_abs.max(value.abs());
}
let relative_error = 16.0 * (rows.max(1) as f64) * f64::EPSILON;
let tolerance = if relative_error < 1.0 {
max_abs / relative_error.recip()
} else {
return Err(CovarianceStandardErrorError::DimensionTooLarge { dimension: rows });
};
let mut standard_errors = Array1::zeros(rows);
for (index, &value) in cov.diag().iter().enumerate() {
standard_errors[index] = if value == 0.0 {
0.0
} else if value > 0.0 {
value.sqrt()
} else if -value <= tolerance {
0.0
} else {
return Err(CovarianceStandardErrorError::NegativeDiagonal {
index,
value,
tolerance,
});
};
}
Ok(standard_errors)
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
#[serde(transparent)]
pub struct PhiScaledCovariance(pub Array2<f64>);
impl PhiScaledCovariance {
#[inline]
pub fn wrap(cov: Array2<f64>) -> Self {
Self(cov)
}
#[inline]
pub fn as_array(&self) -> &Array2<f64> {
&self.0
}
}
impl From<Array2<f64>> for PhiScaledCovariance {
#[inline]
fn from(cov: Array2<f64>) -> Self {
Self(cov)
}
}
impl From<PhiScaledCovariance> for Array2<f64> {
#[inline]
fn from(cov: PhiScaledCovariance) -> Self {
cov.0
}
}
impl Deref for PhiScaledCovariance {
type Target = Array2<f64>;
#[inline]
fn deref(&self) -> &Array2<f64> {
&self.0
}
}
impl DerefMut for PhiScaledCovariance {
#[inline]
fn deref_mut(&mut self) -> &mut Array2<f64> {
&mut self.0
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
#[serde(transparent)]
pub struct UnscaledPrecision(pub Array2<f64>);
impl UnscaledPrecision {
#[inline]
pub fn wrap(hessian: Array2<f64>) -> Self {
Self(hessian)
}
#[inline]
pub fn as_array(&self) -> &Array2<f64> {
&self.0
}
}
impl From<Array2<f64>> for UnscaledPrecision {
#[inline]
fn from(h: Array2<f64>) -> Self {
Self(h)
}
}
impl From<UnscaledPrecision> for Array2<f64> {
#[inline]
fn from(h: UnscaledPrecision) -> Self {
h.0
}
}
impl Deref for UnscaledPrecision {
type Target = Array2<f64>;
#[inline]
fn deref(&self) -> &Array2<f64> {
&self.0
}
}
impl DerefMut for UnscaledPrecision {
#[inline]
fn deref_mut(&mut self) -> &mut Array2<f64> {
&mut self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
#[test]
fn se_from_diagonal_matrix_is_sqrt_of_diagonal() {
let cov = array![[4.0_f64, 0.0], [0.0, 9.0]];
let se = se_from_covariance(&cov).unwrap();
assert_eq!(se.len(), 2);
assert!((se[0] - 2.0).abs() < 1e-14);
assert!((se[1] - 3.0).abs() < 1e-14);
}
#[test]
fn se_snaps_only_backward_error_scale_negative_diagonal() {
let cov = array![[1.0_f64, 0.0], [0.0, -4.0 * f64::EPSILON]];
let se = se_from_covariance(&cov).unwrap();
assert_eq!(se[1], 0.0);
let materially_indefinite = array![[1.0_f64, 0.0], [0.0, -1e-8]];
assert!(matches!(
se_from_covariance(&materially_indefinite),
Err(CovarianceStandardErrorError::NegativeDiagonal { index: 1, .. })
));
}
#[test]
fn phi_scaled_covariance_wrap_and_as_array_round_trip() {
let m = array![[1.0_f64, 2.0], [3.0, 4.0]];
let wrapped = PhiScaledCovariance::wrap(m.clone());
assert_eq!(*wrapped.as_array(), m);
}
#[test]
fn phi_scaled_covariance_deref_gives_array2() {
let m = array![[5.0_f64]];
let wrapped = PhiScaledCovariance::wrap(m.clone());
assert_eq!(wrapped.nrows(), 1);
assert_eq!(wrapped[[0, 0]], 5.0);
}
#[test]
fn unscaled_precision_wrap_and_as_array_round_trip() {
let h = array![[2.0_f64, 0.0], [0.0, 3.0]];
let wrapped = UnscaledPrecision::wrap(h.clone());
assert_eq!(*wrapped.as_array(), h);
}
}