use ndarray::Array2;
pub mod starfinders;
pub fn sigma_clip(
data: &Array2<f64>,
sigma: f64,
maxiters: Option<usize>,
use_median: bool,
) -> Array2<f64> {
let maxiters = maxiters.unwrap_or(usize::MAX);
let mut result = data.clone();
let mut iteration = 0;
let mut n_changed = 1;
while (n_changed > 0 || iteration == 0) && iteration < maxiters {
let valid_data: Vec<f64> = result.iter().filter(|&&x| x.is_finite()).cloned().collect();
if valid_data.is_empty() {
break;
}
let center = if use_median {
median(&valid_data)
} else {
valid_data.iter().sum::<f64>() / valid_data.len() as f64
};
let sum_sq: f64 = valid_data.iter().map(|&x| (x - center).powi(2)).sum();
let std_dev = (sum_sq / valid_data.len() as f64).sqrt();
let lower_bound = center - (sigma * std_dev);
let upper_bound = center + (sigma * std_dev);
n_changed = 0;
for val in result.iter_mut() {
if !val.is_finite() {
continue;
}
if *val < lower_bound {
*val = lower_bound;
n_changed += 1;
} else if *val > upper_bound {
*val = upper_bound;
n_changed += 1;
}
}
iteration += 1;
}
result
}
fn median(data: &[f64]) -> f64 {
let mut sorted_data = data.to_vec();
sorted_data.sort_by(|a, b| a.partial_cmp(b).unwrap());
let len = sorted_data.len();
if len.is_multiple_of(2) {
(sorted_data[len / 2 - 1] + sorted_data[len / 2]) / 2.0
} else {
sorted_data[len / 2]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_median() {
let data_odd = vec![5.0, 3.0, 1.0, 4.0, 2.0];
assert_eq!(median(&data_odd), 3.0);
let data_even = vec![5.0, 3.0, 1.0, 4.0, 2.0, 6.0];
assert_eq!(median(&data_even), 3.5);
let data_repeated = vec![1.0, 1.0, 2.0, 2.0, 3.0];
assert_eq!(median(&data_repeated), 2.0);
let data_single = vec![42.0];
assert_eq!(median(&data_single), 42.0);
}
#[test]
fn test_sigma_clip_mean() {
let mut data = Array2::zeros((3, 3));
data[[0, 0]] = 1.0;
data[[0, 1]] = 2.0;
data[[0, 2]] = 3.0;
data[[1, 0]] = 4.0;
data[[1, 1]] = 100.0; data[[1, 2]] = 6.0;
data[[2, 0]] = 7.0;
data[[2, 1]] = 8.0;
data[[2, 2]] = 9.0;
let clipped = sigma_clip(&data, 2.0, Some(1), false);
for row in 0..3 {
for col in 0..3 {
if row == 1 && col == 1 {
assert!(clipped[[row, col]] < 80.0);
} else {
assert_eq!(clipped[[row, col]], data[[row, col]]);
}
}
}
}
#[test]
fn test_sigma_clip_median() {
let mut data = Array2::zeros((3, 3));
data[[0, 0]] = 1.0;
data[[0, 1]] = 2.0;
data[[0, 2]] = 3.0;
data[[1, 0]] = 4.0;
data[[1, 1]] = 100.0; data[[1, 2]] = 6.0;
data[[2, 0]] = 7.0;
data[[2, 1]] = 8.0;
data[[2, 2]] = 9.0;
let clipped = sigma_clip(&data, 2.0, Some(1), true);
for row in 0..3 {
for col in 0..3 {
if row == 1 && col == 1 {
assert!(clipped[[row, col]] < 70.0);
} else {
assert_eq!(clipped[[row, col]], data[[row, col]]);
}
}
}
}
#[test]
fn test_sigma_clip_multiple_iterations() {
let mut data = Array2::zeros((3, 3));
data[[0, 0]] = 1.0;
data[[0, 1]] = 2.0;
data[[0, 2]] = 3.0;
data[[1, 0]] = 4.0;
data[[1, 1]] = 30.0; data[[1, 2]] = 6.0;
data[[2, 0]] = 7.0;
data[[2, 1]] = 8.0;
data[[2, 2]] = 1000.0;
let clipped = sigma_clip(&data, 2.0, Some(3), false);
assert!(
clipped[[2, 2]] < 1000.0,
"Expected clipped[2,2] < 1000.0, got {}",
clipped[[2, 2]]
);
assert_eq!(clipped[[0, 0]], 1.0);
assert_eq!(clipped[[0, 1]], 2.0);
assert_eq!(clipped[[0, 2]], 3.0);
assert_eq!(clipped[[1, 0]], 4.0);
assert_eq!(clipped[[1, 2]], 6.0);
assert_eq!(clipped[[2, 0]], 7.0);
assert_eq!(clipped[[2, 1]], 8.0);
}
#[test]
fn test_sigma_clip_gaussian_data() {
let mut data = Array2::zeros((3, 3));
data[[0, 0]] = 10.0;
data[[0, 1]] = 11.0;
data[[0, 2]] = 9.0;
data[[1, 0]] = 12.0;
data[[1, 1]] = 10.5;
data[[1, 2]] = 9.5;
data[[2, 0]] = 11.5;
data[[2, 1]] = 10.0;
data[[2, 2]] = 50.0;
let clipped = sigma_clip(&data, 2.0, Some(1), false);
assert!(
clipped[[2, 2]] < 50.0,
"Expected outlier to be clipped, got {}",
clipped[[2, 2]]
);
for row in 0..3 {
for col in 0..3 {
if row == 2 && col == 2 {
continue; }
assert_eq!(clipped[[row, col]], data[[row, col]]);
}
}
}
#[test]
fn test_sigma_clip_with_nan_values() {
let mut data = Array2::zeros((3, 3));
data[[0, 0]] = 1.0;
data[[0, 1]] = 2.0;
data[[0, 2]] = f64::NAN;
data[[1, 0]] = 4.0;
data[[1, 1]] = 100.0; data[[1, 2]] = 6.0;
data[[2, 0]] = 7.0;
data[[2, 1]] = 8.0;
data[[2, 2]] = 9.0;
let clipped = sigma_clip(&data, 2.0, Some(1), false);
assert!(clipped[[0, 2]].is_nan());
assert!(clipped[[1, 1]] < data[[1, 1]]);
assert_eq!(clipped[[0, 0]], 1.0);
assert_eq!(clipped[[0, 1]], 2.0);
assert_eq!(clipped[[1, 0]], 4.0);
assert_eq!(clipped[[1, 2]], 6.0);
assert_eq!(clipped[[2, 0]], 7.0);
assert_eq!(clipped[[2, 1]], 8.0);
assert_eq!(clipped[[2, 2]], 9.0);
}
}