fdars_core/frechet/spaces/
network.rs1use crate::error::FdarError;
17use crate::frechet::MetricSpace;
18use crate::helpers::NUMERICAL_EPS;
19
20#[derive(Debug, Clone, PartialEq)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub struct NetworkSpace {
24 pub d: usize,
26}
27
28impl NetworkSpace {
29 pub fn new(d: usize) -> Result<Self, FdarError> {
34 if d < 1 {
35 return Err(FdarError::InvalidParameter {
36 parameter: "d",
37 message: "number of nodes must be >= 1".to_string(),
38 });
39 }
40 Ok(Self { d })
41 }
42
43 fn check_len(&self, obj: &[f64], name: &'static str) -> Result<(), FdarError> {
44 let dd = self.d * self.d;
45 if obj.len() != dd {
46 return Err(FdarError::InvalidDimension {
47 parameter: name,
48 expected: format!("{dd} elements (d*d)"),
49 actual: format!("{} elements", obj.len()),
50 });
51 }
52 Ok(())
53 }
54}
55
56impl MetricSpace for NetworkSpace {
57 type Object = Vec<f64>;
58
59 fn distance(&self, a: &Self::Object, b: &Self::Object) -> Result<f64, FdarError> {
60 self.check_len(a, "a")?;
61 self.check_len(b, "b")?;
62 Ok(a.iter()
63 .zip(b.iter())
64 .map(|(x, y)| (x - y) * (x - y))
65 .sum::<f64>()
66 .sqrt())
67 }
68
69 fn weighted_frechet_mean(
70 &self,
71 objects: &[Self::Object],
72 weights: &[f64],
73 ) -> Result<Self::Object, FdarError> {
74 if objects.is_empty() {
75 return Err(FdarError::InvalidDimension {
76 parameter: "objects",
77 expected: "at least 1 object".to_string(),
78 actual: "0 objects".to_string(),
79 });
80 }
81 if weights.len() != objects.len() {
82 return Err(FdarError::InvalidDimension {
83 parameter: "weights",
84 expected: format!("{} weights (matching objects)", objects.len()),
85 actual: format!("{} weights", weights.len()),
86 });
87 }
88 let dd = self.d * self.d;
89 for (i, o) in objects.iter().enumerate() {
90 if o.len() != dd {
91 return Err(FdarError::InvalidDimension {
92 parameter: "objects",
93 expected: format!("each object has {dd} elements"),
94 actual: format!("object {i} has {} elements", o.len()),
95 });
96 }
97 }
98 let sw: f64 = weights.iter().sum();
99 if sw.abs() < NUMERICAL_EPS {
100 return Err(FdarError::ComputationFailed {
101 operation: "NetworkSpace::weighted_frechet_mean",
102 detail: "sum of weights is ~0; cannot normalize the barycenter".to_string(),
103 });
104 }
105 let mut m = vec![0.0f64; dd];
106 for (o, &w) in objects.iter().zip(weights.iter()) {
107 for (k, mk) in m.iter_mut().enumerate() {
108 *mk += w * o[k];
109 }
110 }
111 for x in &mut m {
112 *x /= sw;
113 }
114 Ok(m)
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 fn laplacian_path3() -> Vec<f64> {
124 vec![1.0, -1.0, 0.0, -1.0, 2.0, -1.0, 0.0, -1.0, 1.0]
126 }
127
128 fn laplacian_triangle3() -> Vec<f64> {
129 vec![2.0, -1.0, -1.0, -1.0, 2.0, -1.0, -1.0, -1.0, 2.0]
131 }
132
133 fn row_sums(m: &[f64], d: usize) -> Vec<f64> {
134 (0..d)
135 .map(|i| (0..d).map(|j| m[i + j * d]).sum::<f64>())
136 .collect()
137 }
138
139 #[test]
140 fn network_distance_of_identical_is_zero() {
141 let s = NetworkSpace::new(3).unwrap();
142 let a = laplacian_path3();
143 assert!(s.distance(&a, &a).unwrap() < 1e-12);
144 }
145
146 #[test]
147 fn network_mean_of_identical_recovers() {
148 let s = NetworkSpace::new(3).unwrap();
149 let a = laplacian_path3();
150 let m = s
151 .weighted_frechet_mean(&[a.clone(), a.clone()], &[0.5, 0.5])
152 .unwrap();
153 for (x, y) in m.iter().zip(a.iter()) {
154 assert!((x - y).abs() < 1e-10);
155 }
156 }
157
158 #[test]
159 fn network_mean_preserves_row_sums() {
160 let s = NetworkSpace::new(3).unwrap();
161 let m = s
162 .weighted_frechet_mean(&[laplacian_path3(), laplacian_triangle3()], &[0.4, 0.6])
163 .unwrap();
164 for rs in row_sums(&m, 3) {
165 assert!(rs.abs() < 1e-10, "row sum {rs} != 0");
166 }
167 }
168
169 #[test]
170 fn network_rejects_dimension_mismatch() {
171 let s = NetworkSpace::new(3).unwrap();
172 let a = laplacian_path3();
173 let bad = vec![0.0; 4];
174 assert!(matches!(
175 s.distance(&a, &bad),
176 Err(FdarError::InvalidDimension { .. })
177 ));
178 }
179}