gam_problem/
dispersion_cov.rs1use ndarray::{Array1, Array2};
33use serde::{Deserialize, Serialize};
34use std::ops::{Deref, DerefMut};
35use thiserror::Error;
36
37pub use crate::Dispersion;
38
39#[derive(Clone, Copy, Debug, Error, PartialEq)]
40pub enum CovarianceStandardErrorError {
41 #[error("covariance must be square, got {rows}x{cols}")]
42 NotSquare { rows: usize, cols: usize },
43 #[error("covariance entry ({row}, {col}) is non-finite: {value}")]
44 NonFinite { row: usize, col: usize, value: f64 },
45 #[error("covariance dimension {dimension} is too large for a sub-unit rounding-error bound")]
46 DimensionTooLarge { dimension: usize },
47 #[error(
48 "covariance diagonal {index} is materially negative: {value} (backward-error tolerance {tolerance})"
49 )]
50 NegativeDiagonal {
51 index: usize,
52 value: f64,
53 tolerance: f64,
54 },
55}
56
57pub fn se_from_covariance(cov: &Array2<f64>) -> Result<Array1<f64>, CovarianceStandardErrorError> {
64 let (rows, cols) = cov.dim();
65 if rows != cols {
66 return Err(CovarianceStandardErrorError::NotSquare { rows, cols });
67 }
68 let mut max_abs = 0.0_f64;
69 for ((row, col), &value) in cov.indexed_iter() {
70 if !value.is_finite() {
71 return Err(CovarianceStandardErrorError::NonFinite { row, col, value });
72 }
73 max_abs = max_abs.max(value.abs());
74 }
75 let relative_error = 16.0 * (rows.max(1) as f64) * f64::EPSILON;
76 let tolerance = if relative_error < 1.0 {
77 max_abs / relative_error.recip()
78 } else {
79 return Err(CovarianceStandardErrorError::DimensionTooLarge { dimension: rows });
80 };
81 let mut standard_errors = Array1::zeros(rows);
82 for (index, &value) in cov.diag().iter().enumerate() {
83 standard_errors[index] = if value == 0.0 {
84 0.0
85 } else if value > 0.0 {
86 value.sqrt()
87 } else if -value <= tolerance {
88 0.0
89 } else {
90 return Err(CovarianceStandardErrorError::NegativeDiagonal {
91 index,
92 value,
93 tolerance,
94 });
95 };
96 }
97 Ok(standard_errors)
98}
99
100#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
110#[serde(transparent)]
111pub struct PhiScaledCovariance(pub Array2<f64>);
112
113impl PhiScaledCovariance {
114 #[inline]
117 pub fn wrap(cov: Array2<f64>) -> Self {
118 Self(cov)
119 }
120
121 #[inline]
123 pub fn as_array(&self) -> &Array2<f64> {
124 &self.0
125 }
126
127}
128
129impl From<Array2<f64>> for PhiScaledCovariance {
130 #[inline]
131 fn from(cov: Array2<f64>) -> Self {
132 Self(cov)
133 }
134}
135
136impl From<PhiScaledCovariance> for Array2<f64> {
137 #[inline]
138 fn from(cov: PhiScaledCovariance) -> Self {
139 cov.0
140 }
141}
142
143impl Deref for PhiScaledCovariance {
144 type Target = Array2<f64>;
145 #[inline]
146 fn deref(&self) -> &Array2<f64> {
147 &self.0
148 }
149}
150
151impl DerefMut for PhiScaledCovariance {
152 #[inline]
153 fn deref_mut(&mut self) -> &mut Array2<f64> {
154 &mut self.0
155 }
156}
157
158#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
163#[serde(transparent)]
164pub struct UnscaledPrecision(pub Array2<f64>);
165
166impl UnscaledPrecision {
167 #[inline]
171 pub fn wrap(hessian: Array2<f64>) -> Self {
172 Self(hessian)
173 }
174
175 #[inline]
177 pub fn as_array(&self) -> &Array2<f64> {
178 &self.0
179 }
180
181}
182
183impl From<Array2<f64>> for UnscaledPrecision {
184 #[inline]
185 fn from(h: Array2<f64>) -> Self {
186 Self(h)
187 }
188}
189
190impl From<UnscaledPrecision> for Array2<f64> {
191 #[inline]
192 fn from(h: UnscaledPrecision) -> Self {
193 h.0
194 }
195}
196
197impl Deref for UnscaledPrecision {
198 type Target = Array2<f64>;
199 #[inline]
200 fn deref(&self) -> &Array2<f64> {
201 &self.0
202 }
203}
204
205impl DerefMut for UnscaledPrecision {
206 #[inline]
207 fn deref_mut(&mut self) -> &mut Array2<f64> {
208 &mut self.0
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use ndarray::array;
216
217 #[test]
220 fn se_from_diagonal_matrix_is_sqrt_of_diagonal() {
221 let cov = array![[4.0_f64, 0.0], [0.0, 9.0]];
223 let se = se_from_covariance(&cov).unwrap();
224 assert_eq!(se.len(), 2);
225 assert!((se[0] - 2.0).abs() < 1e-14);
226 assert!((se[1] - 3.0).abs() < 1e-14);
227 }
228
229 #[test]
230 fn se_snaps_only_backward_error_scale_negative_diagonal() {
231 let cov = array![[1.0_f64, 0.0], [0.0, -4.0 * f64::EPSILON]];
232 let se = se_from_covariance(&cov).unwrap();
233 assert_eq!(se[1], 0.0);
234 let materially_indefinite = array![[1.0_f64, 0.0], [0.0, -1e-8]];
235 assert!(matches!(
236 se_from_covariance(&materially_indefinite),
237 Err(CovarianceStandardErrorError::NegativeDiagonal { index: 1, .. })
238 ));
239 }
240
241 #[test]
244 fn phi_scaled_covariance_wrap_and_as_array_round_trip() {
245 let m = array![[1.0_f64, 2.0], [3.0, 4.0]];
246 let wrapped = PhiScaledCovariance::wrap(m.clone());
247 assert_eq!(*wrapped.as_array(), m);
248 }
249
250 #[test]
251 fn phi_scaled_covariance_deref_gives_array2() {
252 let m = array![[5.0_f64]];
253 let wrapped = PhiScaledCovariance::wrap(m.clone());
254 assert_eq!(wrapped.nrows(), 1);
255 assert_eq!(wrapped[[0, 0]], 5.0);
256 }
257
258 #[test]
261 fn unscaled_precision_wrap_and_as_array_round_trip() {
262 let h = array![[2.0_f64, 0.0], [0.0, 3.0]];
263 let wrapped = UnscaledPrecision::wrap(h.clone());
264 assert_eq!(*wrapped.as_array(), h);
265 }
266}