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 #[inline]
129 pub fn into_array(self) -> Array2<f64> {
130 self.0
131 }
132}
133
134impl From<Array2<f64>> for PhiScaledCovariance {
135 #[inline]
136 fn from(cov: Array2<f64>) -> Self {
137 Self(cov)
138 }
139}
140
141impl From<PhiScaledCovariance> for Array2<f64> {
142 #[inline]
143 fn from(cov: PhiScaledCovariance) -> Self {
144 cov.0
145 }
146}
147
148impl Deref for PhiScaledCovariance {
149 type Target = Array2<f64>;
150 #[inline]
151 fn deref(&self) -> &Array2<f64> {
152 &self.0
153 }
154}
155
156impl DerefMut for PhiScaledCovariance {
157 #[inline]
158 fn deref_mut(&mut self) -> &mut Array2<f64> {
159 &mut self.0
160 }
161}
162
163#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
168#[serde(transparent)]
169pub struct UnscaledPrecision(pub Array2<f64>);
170
171impl UnscaledPrecision {
172 #[inline]
176 pub fn wrap(hessian: Array2<f64>) -> Self {
177 Self(hessian)
178 }
179
180 #[inline]
182 pub fn as_array(&self) -> &Array2<f64> {
183 &self.0
184 }
185
186 #[inline]
188 pub fn into_array(self) -> Array2<f64> {
189 self.0
190 }
191}
192
193impl From<Array2<f64>> for UnscaledPrecision {
194 #[inline]
195 fn from(h: Array2<f64>) -> Self {
196 Self(h)
197 }
198}
199
200impl From<UnscaledPrecision> for Array2<f64> {
201 #[inline]
202 fn from(h: UnscaledPrecision) -> Self {
203 h.0
204 }
205}
206
207impl Deref for UnscaledPrecision {
208 type Target = Array2<f64>;
209 #[inline]
210 fn deref(&self) -> &Array2<f64> {
211 &self.0
212 }
213}
214
215impl DerefMut for UnscaledPrecision {
216 #[inline]
217 fn deref_mut(&mut self) -> &mut Array2<f64> {
218 &mut self.0
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use ndarray::array;
226
227 #[test]
230 fn se_from_diagonal_matrix_is_sqrt_of_diagonal() {
231 let cov = array![[4.0_f64, 0.0], [0.0, 9.0]];
233 let se = se_from_covariance(&cov).unwrap();
234 assert_eq!(se.len(), 2);
235 assert!((se[0] - 2.0).abs() < 1e-14);
236 assert!((se[1] - 3.0).abs() < 1e-14);
237 }
238
239 #[test]
240 fn se_snaps_only_backward_error_scale_negative_diagonal() {
241 let cov = array![[1.0_f64, 0.0], [0.0, -4.0 * f64::EPSILON]];
242 let se = se_from_covariance(&cov).unwrap();
243 assert_eq!(se[1], 0.0);
244 let materially_indefinite = array![[1.0_f64, 0.0], [0.0, -1e-8]];
245 assert!(matches!(
246 se_from_covariance(&materially_indefinite),
247 Err(CovarianceStandardErrorError::NegativeDiagonal { index: 1, .. })
248 ));
249 }
250
251 #[test]
254 fn phi_scaled_covariance_wrap_and_as_array_round_trip() {
255 let m = array![[1.0_f64, 2.0], [3.0, 4.0]];
256 let wrapped = PhiScaledCovariance::wrap(m.clone());
257 assert_eq!(*wrapped.as_array(), m);
258 }
259
260 #[test]
261 fn phi_scaled_covariance_deref_gives_array2() {
262 let m = array![[5.0_f64]];
263 let wrapped = PhiScaledCovariance::wrap(m.clone());
264 assert_eq!(wrapped.nrows(), 1);
265 assert_eq!(wrapped[[0, 0]], 5.0);
266 }
267
268 #[test]
269 fn phi_scaled_covariance_into_array_consumes() {
270 let m = array![[7.0_f64]];
271 let wrapped = PhiScaledCovariance::wrap(m.clone());
272 assert_eq!(wrapped.into_array(), m);
273 }
274
275 #[test]
278 fn unscaled_precision_wrap_and_as_array_round_trip() {
279 let h = array![[2.0_f64, 0.0], [0.0, 3.0]];
280 let wrapped = UnscaledPrecision::wrap(h.clone());
281 assert_eq!(*wrapped.as_array(), h);
282 }
283}