fdars_core/frechet/spaces/
correlation.rs1use crate::error::FdarError;
19use crate::frechet::MetricSpace;
20use crate::helpers::NUMERICAL_EPS;
21
22#[derive(Debug, Clone, PartialEq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct CorrelationMatrixSpace {
26 pub d: usize,
28}
29
30impl CorrelationMatrixSpace {
31 pub fn new(d: usize) -> Result<Self, FdarError> {
36 if d < 1 {
37 return Err(FdarError::InvalidParameter {
38 parameter: "d",
39 message: "matrix dimension must be >= 1".to_string(),
40 });
41 }
42 Ok(Self { d })
43 }
44
45 fn check_len(&self, obj: &[f64], name: &'static str) -> Result<(), FdarError> {
46 let dd = self.d * self.d;
47 if obj.len() != dd {
48 return Err(FdarError::InvalidDimension {
49 parameter: name,
50 expected: format!("{dd} elements (d*d)"),
51 actual: format!("{} elements", obj.len()),
52 });
53 }
54 Ok(())
55 }
56}
57
58impl MetricSpace for CorrelationMatrixSpace {
59 type Object = Vec<f64>;
60
61 fn distance(&self, a: &Self::Object, b: &Self::Object) -> Result<f64, FdarError> {
62 self.check_len(a, "a")?;
63 self.check_len(b, "b")?;
64 Ok(a.iter()
65 .zip(b.iter())
66 .map(|(x, y)| (x - y) * (x - y))
67 .sum::<f64>()
68 .sqrt())
69 }
70
71 fn weighted_frechet_mean(
72 &self,
73 objects: &[Self::Object],
74 weights: &[f64],
75 ) -> Result<Self::Object, FdarError> {
76 if objects.is_empty() {
77 return Err(FdarError::InvalidDimension {
78 parameter: "objects",
79 expected: "at least 1 object".to_string(),
80 actual: "0 objects".to_string(),
81 });
82 }
83 if weights.len() != objects.len() {
84 return Err(FdarError::InvalidDimension {
85 parameter: "weights",
86 expected: format!("{} weights (matching objects)", objects.len()),
87 actual: format!("{} weights", weights.len()),
88 });
89 }
90 let dd = self.d * self.d;
91 for (i, o) in objects.iter().enumerate() {
92 if o.len() != dd {
93 return Err(FdarError::InvalidDimension {
94 parameter: "objects",
95 expected: format!("each object has {dd} elements"),
96 actual: format!("object {i} has {} elements", o.len()),
97 });
98 }
99 }
100 let sw: f64 = weights.iter().sum();
101 if sw.abs() < NUMERICAL_EPS {
102 return Err(FdarError::ComputationFailed {
103 operation: "CorrelationMatrixSpace::weighted_frechet_mean",
104 detail: "sum of weights is ~0; cannot normalize the barycenter".to_string(),
105 });
106 }
107 let mut m = vec![0.0f64; dd];
109 for (o, &w) in objects.iter().zip(weights.iter()) {
110 for (k, mk) in m.iter_mut().enumerate() {
111 *mk += w * o[k];
112 }
113 }
114 for x in &mut m {
115 *x /= sw;
116 }
117 let d = self.d;
119 for i in 0..d {
120 if m[i + i * d] <= 0.0 {
121 return Err(FdarError::ComputationFailed {
122 operation: "CorrelationMatrixSpace::weighted_frechet_mean",
123 detail: format!(
124 "averaged diagonal entry {i} is non-positive ({}); cannot renormalize to a correlation matrix",
125 m[i + i * d]
126 ),
127 });
128 }
129 }
130 let mut result = vec![0.0f64; dd];
131 for i in 0..d {
132 for j in 0..d {
133 result[i + j * d] = m[i + j * d] / (m[i + i * d] * m[j + j * d]).sqrt();
134 }
135 }
136 Ok(result)
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 fn corr2(r: f64) -> Vec<f64> {
146 vec![1.0, r, r, 1.0]
147 }
148
149 #[test]
150 fn correlation_distance_of_identical_is_zero() {
151 let s = CorrelationMatrixSpace::new(2).unwrap();
152 let a = corr2(0.4);
153 assert!(s.distance(&a, &a).unwrap() < 1e-12);
154 }
155
156 #[test]
157 fn correlation_mean_of_identical_recovers() {
158 let s = CorrelationMatrixSpace::new(2).unwrap();
159 let a = corr2(0.5);
160 let m = s
161 .weighted_frechet_mean(&[a.clone(), a.clone()], &[0.5, 0.5])
162 .unwrap();
163 for (x, y) in m.iter().zip(a.iter()) {
164 assert!((x - y).abs() < 1e-10, "x={x} y={y}");
165 }
166 }
167
168 #[test]
169 fn correlation_mean_has_unit_diagonal() {
170 let s = CorrelationMatrixSpace::new(2).unwrap();
171 let m = s
172 .weighted_frechet_mean(&[corr2(0.2), corr2(0.8)], &[0.3, 0.7])
173 .unwrap();
174 assert!((m[0] - 1.0).abs() < 1e-10);
175 assert!((m[3] - 1.0).abs() < 1e-10);
176 }
177
178 #[test]
179 fn correlation_rejects_non_positive_diagonal() {
180 let s = CorrelationMatrixSpace::new(2).unwrap();
182 let a = corr2(0.1);
183 let b = corr2(0.2);
184 let degen = vec![0.0, 0.0, 0.0, 1.0];
189 assert!(matches!(
190 s.weighted_frechet_mean(&[degen, a, b], &[1.0, 0.0, 0.0]),
191 Err(FdarError::ComputationFailed { .. })
192 ));
193 }
194
195 #[test]
196 fn correlation_rejects_dimension_mismatch() {
197 let s = CorrelationMatrixSpace::new(2).unwrap();
198 let a = corr2(0.3);
199 let bad = vec![1.0, 0.0, 0.0];
200 assert!(matches!(
201 s.distance(&a, &bad),
202 Err(FdarError::InvalidDimension { .. })
203 ));
204 }
205}