oxigdal_algorithms/vector/
robust_location.rs1#[derive(Debug, Clone)]
16pub struct RobustLocationOptions {
17 pub max_iter: usize,
19
20 pub tol: f64,
22
23 pub coincidence_eps: f64,
27}
28
29impl Default for RobustLocationOptions {
30 fn default() -> Self {
31 Self {
32 max_iter: 200,
33 tol: 1e-10,
34 coincidence_eps: 1e-12,
35 }
36 }
37}
38
39impl RobustLocationOptions {
40 pub fn with_max_iter(mut self, v: usize) -> Self {
42 self.max_iter = v;
43 self
44 }
45
46 pub fn with_tol(mut self, v: f64) -> Self {
48 self.tol = v;
49 self
50 }
51
52 pub fn with_coincidence_eps(mut self, v: f64) -> Self {
54 self.coincidence_eps = v;
55 self
56 }
57}
58
59fn mean_2d(points: &[(f64, f64)]) -> Option<(f64, f64)> {
67 let n = points.len();
68 if n == 0 {
69 return None;
70 }
71 let mut sx = 0.0_f64;
72 let mut sy = 0.0_f64;
73 for &(x, y) in points {
74 sx += x;
75 sy += y;
76 }
77 let n_f = n as f64;
78 Some((sx / n_f, sy / n_f))
79}
80
81fn mean_3d(points: &[(f64, f64, f64)]) -> Option<(f64, f64, f64)> {
85 let n = points.len();
86 if n == 0 {
87 return None;
88 }
89 let mut sx = 0.0_f64;
90 let mut sy = 0.0_f64;
91 let mut sz = 0.0_f64;
92 for &(x, y, z) in points {
93 sx += x;
94 sy += y;
95 sz += z;
96 }
97 let n_f = n as f64;
98 Some((sx / n_f, sy / n_f, sz / n_f))
99}
100
101fn weiszfeld_step_2d(
105 cx: f64,
106 cy: f64,
107 points: &[(f64, f64)],
108 weights: &[f64],
109 coincidence_eps: f64,
110) -> ((f64, f64), bool) {
111 let mut num_x = 0.0_f64;
112 let mut num_y = 0.0_f64;
113 let mut denom = 0.0_f64;
114 let mut coincident = false;
115
116 for (i, &(px, py)) in points.iter().enumerate() {
117 let w = weights[i];
118 let dx = px - cx;
119 let dy = py - cy;
120 let dist = (dx * dx + dy * dy).sqrt();
121 if dist < coincidence_eps {
122 coincident = true;
123 continue;
126 }
127 let inv_d = w / dist;
128 num_x += px * inv_d;
129 num_y += py * inv_d;
130 denom += inv_d;
131 }
132
133 if denom == 0.0 {
134 return ((cx, cy), true);
136 }
137
138 ((num_x / denom, num_y / denom), coincident)
139}
140
141fn weiszfeld_step_3d(
145 cx: f64,
146 cy: f64,
147 cz: f64,
148 points: &[(f64, f64, f64)],
149 coincidence_eps: f64,
150) -> ((f64, f64, f64), bool) {
151 let mut num_x = 0.0_f64;
152 let mut num_y = 0.0_f64;
153 let mut num_z = 0.0_f64;
154 let mut denom = 0.0_f64;
155 let mut coincident = false;
156
157 for &(px, py, pz) in points {
158 let dx = px - cx;
159 let dy = py - cy;
160 let dz = pz - cz;
161 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
162 if dist < coincidence_eps {
163 coincident = true;
164 continue;
165 }
166 let inv_d = 1.0 / dist;
167 num_x += px * inv_d;
168 num_y += py * inv_d;
169 num_z += pz * inv_d;
170 denom += inv_d;
171 }
172
173 if denom == 0.0 {
174 return ((cx, cy, cz), true);
175 }
176
177 ((num_x / denom, num_y / denom, num_z / denom), coincident)
178}
179
180pub fn geometric_median(points: &[(f64, f64)]) -> Option<(f64, f64)> {
194 geometric_median_with_options(points, &RobustLocationOptions::default())
195}
196
197pub fn geometric_median_with_options(
201 points: &[(f64, f64)],
202 options: &RobustLocationOptions,
203) -> Option<(f64, f64)> {
204 let n = points.len();
205 match n {
206 0 => return None,
207 1 => return Some(points[0]),
208 _ => {}
209 }
210
211 let weights: Vec<f64> = vec![1.0; n];
213 weighted_geometric_median(points, &weights, options)
214}
215
216pub fn weighted_geometric_median(
226 points: &[(f64, f64)],
227 weights: &[f64],
228 options: &RobustLocationOptions,
229) -> Option<(f64, f64)> {
230 let n = points.len();
231 if n != weights.len() {
232 return None;
233 }
234 match n {
235 0 => return None,
236 1 => return Some(points[0]),
237 _ => {}
238 }
239
240 let total_w: f64 = weights.iter().sum();
242 let (mut cx, mut cy) = if total_w == 0.0 {
243 mean_2d(points)?
244 } else {
245 let mut sx = 0.0_f64;
246 let mut sy = 0.0_f64;
247 for (i, &(px, py)) in points.iter().enumerate() {
248 sx += weights[i] * px;
249 sy += weights[i] * py;
250 }
251 (sx / total_w, sy / total_w)
252 };
253
254 for _ in 0..options.max_iter {
256 let ((nx, ny), coincident) =
257 weiszfeld_step_2d(cx, cy, points, weights, options.coincidence_eps);
258
259 if coincident {
260 let ((nx2, ny2), _) = weiszfeld_step_2d(
263 cx + options.coincidence_eps,
264 cy,
265 points,
266 weights,
267 options.coincidence_eps,
268 );
269 let dx = nx2 - cx;
270 let dy = ny2 - cy;
271 let step = (dx * dx + dy * dy).sqrt();
272 cx = nx2;
273 cy = ny2;
274 if step < options.tol {
275 break;
276 }
277 continue;
278 }
279
280 let dx = nx - cx;
281 let dy = ny - cy;
282 let step = (dx * dx + dy * dy).sqrt();
283 cx = nx;
284 cy = ny;
285 if step < options.tol {
286 break;
287 }
288 }
289
290 Some((cx, cy))
291}
292
293pub fn geometric_median_3d(
302 points: &[(f64, f64, f64)],
303 options: &RobustLocationOptions,
304) -> Option<(f64, f64, f64)> {
305 let n = points.len();
306 match n {
307 0 => return None,
308 1 => return Some(points[0]),
309 _ => {}
310 }
311
312 let (mut cx, mut cy, mut cz) = mean_3d(points)?;
314
315 for _ in 0..options.max_iter {
317 let ((nx, ny, nz), coincident) =
318 weiszfeld_step_3d(cx, cy, cz, points, options.coincidence_eps);
319
320 if coincident {
321 let ((nx2, ny2, nz2), _) = weiszfeld_step_3d(
323 cx + options.coincidence_eps,
324 cy,
325 cz,
326 points,
327 options.coincidence_eps,
328 );
329 let dx = nx2 - cx;
330 let dy = ny2 - cy;
331 let dz = nz2 - cz;
332 let step = (dx * dx + dy * dy + dz * dz).sqrt();
333 cx = nx2;
334 cy = ny2;
335 cz = nz2;
336 if step < options.tol {
337 break;
338 }
339 continue;
340 }
341
342 let dx = nx - cx;
343 let dy = ny - cy;
344 let dz = nz - cz;
345 let step = (dx * dx + dy * dy + dz * dz).sqrt();
346 cx = nx;
347 cy = ny;
348 cz = nz;
349 if step < options.tol {
350 break;
351 }
352 }
353
354 Some((cx, cy, cz))
355}
356
357pub fn l1_median(points: &[(f64, f64)]) -> Option<(f64, f64)> {
373 let n = points.len();
374 if n == 0 {
375 return None;
376 }
377
378 let mut xs: Vec<f64> = points.iter().map(|&(x, _)| x).collect();
379 let mut ys: Vec<f64> = points.iter().map(|&(_, y)| y).collect();
380
381 xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
384 ys.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
385
386 let median_x = coordinate_median(&xs);
387 let median_y = coordinate_median(&ys);
388
389 Some((median_x, median_y))
390}
391
392fn coordinate_median(sorted: &[f64]) -> f64 {
401 let n = sorted.len();
402 debug_assert!(n > 0, "coordinate_median called on empty slice");
403 if n % 2 == 1 {
404 sorted[n / 2]
405 } else {
406 (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
407 }
408}
409
410pub fn spatial_mean(points: &[(f64, f64)]) -> Option<(f64, f64)> {
418 mean_2d(points)
419}
420
421#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[test]
431 fn unit_mean_2d_empty() {
432 assert!(mean_2d(&[]).is_none());
433 }
434
435 #[test]
436 fn unit_mean_2d_single() {
437 let r = mean_2d(&[(3.0, 4.0)]).expect("non-empty slice should return Some");
438 assert!((r.0 - 3.0).abs() < 1e-12 && (r.1 - 4.0).abs() < 1e-12);
439 }
440
441 #[test]
442 fn unit_mean_2d_symmetric() {
443 let pts = [(-1.0, 0.0), (1.0, 0.0), (0.0, -1.0), (0.0, 1.0)];
444 let r = mean_2d(&pts).expect("non-empty slice should return Some");
445 assert!(r.0.abs() < 1e-12 && r.1.abs() < 1e-12);
446 }
447
448 #[test]
449 fn unit_coordinate_median_odd() {
450 let v = [1.0, 3.0, 5.0];
451 assert!((coordinate_median(&v) - 3.0).abs() < 1e-12);
452 }
453
454 #[test]
455 fn unit_coordinate_median_even() {
456 let v = [1.0, 3.0];
457 assert!((coordinate_median(&v) - 2.0).abs() < 1e-12);
458 }
459
460 #[test]
461 fn unit_geometric_median_options_builder() {
462 let opts = RobustLocationOptions::default()
463 .with_max_iter(50)
464 .with_tol(1e-8)
465 .with_coincidence_eps(1e-6);
466 assert_eq!(opts.max_iter, 50);
467 assert!((opts.tol - 1e-8).abs() < f64::EPSILON);
468 assert!((opts.coincidence_eps - 1e-6).abs() < f64::EPSILON);
469 }
470}