#[derive(Debug, Clone)]
pub struct RobustLocationOptions {
pub max_iter: usize,
pub tol: f64,
pub coincidence_eps: f64,
}
impl Default for RobustLocationOptions {
fn default() -> Self {
Self {
max_iter: 200,
tol: 1e-10,
coincidence_eps: 1e-12,
}
}
}
impl RobustLocationOptions {
pub fn with_max_iter(mut self, v: usize) -> Self {
self.max_iter = v;
self
}
pub fn with_tol(mut self, v: f64) -> Self {
self.tol = v;
self
}
pub fn with_coincidence_eps(mut self, v: f64) -> Self {
self.coincidence_eps = v;
self
}
}
fn mean_2d(points: &[(f64, f64)]) -> Option<(f64, f64)> {
let n = points.len();
if n == 0 {
return None;
}
let mut sx = 0.0_f64;
let mut sy = 0.0_f64;
for &(x, y) in points {
sx += x;
sy += y;
}
let n_f = n as f64;
Some((sx / n_f, sy / n_f))
}
fn mean_3d(points: &[(f64, f64, f64)]) -> Option<(f64, f64, f64)> {
let n = points.len();
if n == 0 {
return None;
}
let mut sx = 0.0_f64;
let mut sy = 0.0_f64;
let mut sz = 0.0_f64;
for &(x, y, z) in points {
sx += x;
sy += y;
sz += z;
}
let n_f = n as f64;
Some((sx / n_f, sy / n_f, sz / n_f))
}
fn weiszfeld_step_2d(
cx: f64,
cy: f64,
points: &[(f64, f64)],
weights: &[f64],
coincidence_eps: f64,
) -> ((f64, f64), bool) {
let mut num_x = 0.0_f64;
let mut num_y = 0.0_f64;
let mut denom = 0.0_f64;
let mut coincident = false;
for (i, &(px, py)) in points.iter().enumerate() {
let w = weights[i];
let dx = px - cx;
let dy = py - cy;
let dist = (dx * dx + dy * dy).sqrt();
if dist < coincidence_eps {
coincident = true;
continue;
}
let inv_d = w / dist;
num_x += px * inv_d;
num_y += py * inv_d;
denom += inv_d;
}
if denom == 0.0 {
return ((cx, cy), true);
}
((num_x / denom, num_y / denom), coincident)
}
fn weiszfeld_step_3d(
cx: f64,
cy: f64,
cz: f64,
points: &[(f64, f64, f64)],
coincidence_eps: f64,
) -> ((f64, f64, f64), bool) {
let mut num_x = 0.0_f64;
let mut num_y = 0.0_f64;
let mut num_z = 0.0_f64;
let mut denom = 0.0_f64;
let mut coincident = false;
for &(px, py, pz) in points {
let dx = px - cx;
let dy = py - cy;
let dz = pz - cz;
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
if dist < coincidence_eps {
coincident = true;
continue;
}
let inv_d = 1.0 / dist;
num_x += px * inv_d;
num_y += py * inv_d;
num_z += pz * inv_d;
denom += inv_d;
}
if denom == 0.0 {
return ((cx, cy, cz), true);
}
((num_x / denom, num_y / denom, num_z / denom), coincident)
}
pub fn geometric_median(points: &[(f64, f64)]) -> Option<(f64, f64)> {
geometric_median_with_options(points, &RobustLocationOptions::default())
}
pub fn geometric_median_with_options(
points: &[(f64, f64)],
options: &RobustLocationOptions,
) -> Option<(f64, f64)> {
let n = points.len();
match n {
0 => return None,
1 => return Some(points[0]),
_ => {}
}
let weights: Vec<f64> = vec![1.0; n];
weighted_geometric_median(points, &weights, options)
}
pub fn weighted_geometric_median(
points: &[(f64, f64)],
weights: &[f64],
options: &RobustLocationOptions,
) -> Option<(f64, f64)> {
let n = points.len();
if n != weights.len() {
return None;
}
match n {
0 => return None,
1 => return Some(points[0]),
_ => {}
}
let total_w: f64 = weights.iter().sum();
let (mut cx, mut cy) = if total_w == 0.0 {
mean_2d(points)?
} else {
let mut sx = 0.0_f64;
let mut sy = 0.0_f64;
for (i, &(px, py)) in points.iter().enumerate() {
sx += weights[i] * px;
sy += weights[i] * py;
}
(sx / total_w, sy / total_w)
};
for _ in 0..options.max_iter {
let ((nx, ny), coincident) =
weiszfeld_step_2d(cx, cy, points, weights, options.coincidence_eps);
if coincident {
let ((nx2, ny2), _) = weiszfeld_step_2d(
cx + options.coincidence_eps,
cy,
points,
weights,
options.coincidence_eps,
);
let dx = nx2 - cx;
let dy = ny2 - cy;
let step = (dx * dx + dy * dy).sqrt();
cx = nx2;
cy = ny2;
if step < options.tol {
break;
}
continue;
}
let dx = nx - cx;
let dy = ny - cy;
let step = (dx * dx + dy * dy).sqrt();
cx = nx;
cy = ny;
if step < options.tol {
break;
}
}
Some((cx, cy))
}
pub fn geometric_median_3d(
points: &[(f64, f64, f64)],
options: &RobustLocationOptions,
) -> Option<(f64, f64, f64)> {
let n = points.len();
match n {
0 => return None,
1 => return Some(points[0]),
_ => {}
}
let (mut cx, mut cy, mut cz) = mean_3d(points)?;
for _ in 0..options.max_iter {
let ((nx, ny, nz), coincident) =
weiszfeld_step_3d(cx, cy, cz, points, options.coincidence_eps);
if coincident {
let ((nx2, ny2, nz2), _) = weiszfeld_step_3d(
cx + options.coincidence_eps,
cy,
cz,
points,
options.coincidence_eps,
);
let dx = nx2 - cx;
let dy = ny2 - cy;
let dz = nz2 - cz;
let step = (dx * dx + dy * dy + dz * dz).sqrt();
cx = nx2;
cy = ny2;
cz = nz2;
if step < options.tol {
break;
}
continue;
}
let dx = nx - cx;
let dy = ny - cy;
let dz = nz - cz;
let step = (dx * dx + dy * dy + dz * dz).sqrt();
cx = nx;
cy = ny;
cz = nz;
if step < options.tol {
break;
}
}
Some((cx, cy, cz))
}
pub fn l1_median(points: &[(f64, f64)]) -> Option<(f64, f64)> {
let n = points.len();
if n == 0 {
return None;
}
let mut xs: Vec<f64> = points.iter().map(|&(x, _)| x).collect();
let mut ys: Vec<f64> = points.iter().map(|&(_, y)| y).collect();
xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
ys.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median_x = coordinate_median(&xs);
let median_y = coordinate_median(&ys);
Some((median_x, median_y))
}
fn coordinate_median(sorted: &[f64]) -> f64 {
let n = sorted.len();
debug_assert!(n > 0, "coordinate_median called on empty slice");
if n % 2 == 1 {
sorted[n / 2]
} else {
(sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
}
}
pub fn spatial_mean(points: &[(f64, f64)]) -> Option<(f64, f64)> {
mean_2d(points)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unit_mean_2d_empty() {
assert!(mean_2d(&[]).is_none());
}
#[test]
fn unit_mean_2d_single() {
let r = mean_2d(&[(3.0, 4.0)]).expect("non-empty slice should return Some");
assert!((r.0 - 3.0).abs() < 1e-12 && (r.1 - 4.0).abs() < 1e-12);
}
#[test]
fn unit_mean_2d_symmetric() {
let pts = [(-1.0, 0.0), (1.0, 0.0), (0.0, -1.0), (0.0, 1.0)];
let r = mean_2d(&pts).expect("non-empty slice should return Some");
assert!(r.0.abs() < 1e-12 && r.1.abs() < 1e-12);
}
#[test]
fn unit_coordinate_median_odd() {
let v = [1.0, 3.0, 5.0];
assert!((coordinate_median(&v) - 3.0).abs() < 1e-12);
}
#[test]
fn unit_coordinate_median_even() {
let v = [1.0, 3.0];
assert!((coordinate_median(&v) - 2.0).abs() < 1e-12);
}
#[test]
fn unit_geometric_median_options_builder() {
let opts = RobustLocationOptions::default()
.with_max_iter(50)
.with_tol(1e-8)
.with_coincidence_eps(1e-6);
assert_eq!(opts.max_iter, 50);
assert!((opts.tol - 1e-8).abs() < f64::EPSILON);
assert!((opts.coincidence_eps - 1e-6).abs() < f64::EPSILON);
}
}