1use super::karcher::karcher_mean;
12use super::pairwise::elastic_distance;
13use super::set::align_to_target;
14use super::srsf::srsf_single;
15use crate::error::FdarError;
16use crate::matrix::FdMatrix;
17
18#[non_exhaustive]
22#[derive(Debug, Clone, PartialEq)]
23pub struct RobustKarcherConfig {
24 pub max_iter: usize,
26 pub tol: f64,
28 pub lambda: f64,
30 pub trim_fraction: f64,
32}
33
34impl Default for RobustKarcherConfig {
35 fn default() -> Self {
36 Self {
37 max_iter: 20,
38 tol: 1e-3,
39 lambda: 0.0,
40 trim_fraction: 0.1,
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq)]
47#[non_exhaustive]
48pub struct RobustKarcherResult {
49 pub mean: Vec<f64>,
51 pub mean_srsf: Vec<f64>,
53 pub gammas: FdMatrix,
55 pub aligned_data: FdMatrix,
57 pub weights: Vec<f64>,
59 pub n_iter: usize,
61 pub converged: bool,
63}
64
65#[must_use = "expensive computation whose result should not be discarded"]
89pub fn karcher_median(
90 data: &FdMatrix,
91 argvals: &[f64],
92 config: &RobustKarcherConfig,
93) -> Result<RobustKarcherResult, FdarError> {
94 let (n, m) = data.shape();
95 validate_inputs(n, m, argvals)?;
96
97 let init = karcher_mean(data, argvals, 1, config.tol, config.lambda);
99 let mut current_mean = init.mean;
100
101 let mut converged = false;
102 let mut n_iter = 0;
103 let mut weights = vec![1.0 / n as f64; n];
104 let mut alignment_result = align_to_target(data, ¤t_mean, argvals, config.lambda);
105
106 for iter in 0..config.max_iter {
108 n_iter = iter + 1;
109
110 let distances: Vec<f64> = (0..n)
112 .map(|i| {
113 let fi = data.row(i);
114 elastic_distance(¤t_mean, &fi, argvals, config.lambda)
115 })
116 .collect();
117
118 let epsilon = 1e-10;
120 let raw_weights: Vec<f64> = distances.iter().map(|&d| 1.0 / d.max(epsilon)).collect();
121 let w_sum: f64 = raw_weights.iter().sum();
122 weights = raw_weights.iter().map(|&w| w / w_sum).collect();
123
124 let mut new_mean = vec![0.0; m];
126 for i in 0..n {
127 for j in 0..m {
128 new_mean[j] += weights[i] * alignment_result.aligned_data[(i, j)];
129 }
130 }
131
132 let old_srsf = srsf_single(¤t_mean, argvals);
134 let new_srsf = srsf_single(&new_mean, argvals);
135 let rel = relative_srsf_change(&old_srsf, &new_srsf);
136
137 current_mean = new_mean;
138
139 if rel < config.tol {
140 converged = true;
141 alignment_result = align_to_target(data, ¤t_mean, argvals, config.lambda);
143 break;
144 }
145
146 alignment_result = align_to_target(data, ¤t_mean, argvals, config.lambda);
148 }
149
150 let mean_srsf = srsf_single(¤t_mean, argvals);
151
152 Ok(RobustKarcherResult {
153 mean: current_mean,
154 mean_srsf,
155 gammas: alignment_result.gammas,
156 aligned_data: alignment_result.aligned_data,
157 weights,
158 n_iter,
159 converged,
160 })
161}
162
163#[must_use = "expensive computation whose result should not be discarded"]
180pub fn robust_karcher_mean(
181 data: &FdMatrix,
182 argvals: &[f64],
183 config: &RobustKarcherConfig,
184) -> Result<RobustKarcherResult, FdarError> {
185 let (n, m) = data.shape();
186 validate_inputs(n, m, argvals)?;
187
188 if !(0.0..1.0).contains(&config.trim_fraction) {
189 return Err(FdarError::InvalidParameter {
190 parameter: "trim_fraction",
191 message: format!("must be in [0, 1), got {}", config.trim_fraction),
192 });
193 }
194
195 let initial_mean = karcher_mean(data, argvals, config.max_iter, config.tol, config.lambda);
197
198 let distances: Vec<f64> = (0..n)
200 .map(|i| {
201 let fi = data.row(i);
202 elastic_distance(&initial_mean.mean, &fi, argvals, config.lambda)
203 })
204 .collect();
205
206 let n_trim = ((n as f64) * config.trim_fraction).ceil() as usize;
208 let n_keep = n.saturating_sub(n_trim).max(2); let mut indexed_distances: Vec<(usize, f64)> =
211 distances.iter().enumerate().map(|(i, &d)| (i, d)).collect();
212 indexed_distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
213
214 let kept_indices: Vec<usize> = indexed_distances
215 .iter()
216 .take(n_keep)
217 .map(|&(i, _)| i)
218 .collect();
219
220 let mut weights = vec![0.0; n];
222 for &idx in &kept_indices {
223 weights[idx] = 1.0;
224 }
225
226 let kept_data = subset_rows_from_indices(data, &kept_indices);
228 let robust_mean = karcher_mean(
229 &kept_data,
230 argvals,
231 config.max_iter,
232 config.tol,
233 config.lambda,
234 );
235
236 let final_alignment = align_to_target(data, &robust_mean.mean, argvals, config.lambda);
238
239 let mean_srsf = srsf_single(&robust_mean.mean, argvals);
240
241 Ok(RobustKarcherResult {
242 mean: robust_mean.mean,
243 mean_srsf,
244 gammas: final_alignment.gammas,
245 aligned_data: final_alignment.aligned_data,
246 weights,
247 n_iter: robust_mean.n_iter,
248 converged: robust_mean.converged,
249 })
250}
251
252fn validate_inputs(n: usize, m: usize, argvals: &[f64]) -> Result<(), FdarError> {
254 if argvals.len() != m {
255 return Err(FdarError::InvalidDimension {
256 parameter: "argvals",
257 expected: format!("{m}"),
258 actual: format!("{}", argvals.len()),
259 });
260 }
261 if n < 2 {
262 return Err(FdarError::InvalidDimension {
263 parameter: "data",
264 expected: "at least 2 rows".to_string(),
265 actual: format!("{n} rows"),
266 });
267 }
268 Ok(())
269}
270
271fn relative_srsf_change(q_old: &[f64], q_new: &[f64]) -> f64 {
273 let diff_norm: f64 = q_old
274 .iter()
275 .zip(q_new.iter())
276 .map(|(&a, &b)| (a - b).powi(2))
277 .sum::<f64>()
278 .sqrt();
279 let old_norm: f64 = q_old.iter().map(|&v| v * v).sum::<f64>().sqrt().max(1e-10);
280 diff_norm / old_norm
281}
282
283use crate::cv::subset_rows as subset_rows_from_indices;
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use crate::test_helpers::uniform_grid;
289
290 fn make_sine_data(n: usize, m: usize) -> (FdMatrix, Vec<f64>) {
291 let t = uniform_grid(m);
292 let mut data_vec = vec![0.0; n * m];
293 for i in 0..n {
294 let phase = 0.03 * i as f64;
295 for j in 0..m {
296 data_vec[i + j * n] = ((t[j] + phase) * 4.0).sin();
297 }
298 }
299 let data = FdMatrix::from_column_major(data_vec, n, m).unwrap();
300 (data, t)
301 }
302
303 #[test]
304 fn karcher_median_basic() {
305 let (data, t) = make_sine_data(5, 20);
306 let config = RobustKarcherConfig {
307 max_iter: 5,
308 ..Default::default()
309 };
310 let result = karcher_median(&data, &t, &config).unwrap();
311 assert_eq!(result.mean.len(), 20);
312 assert_eq!(result.mean_srsf.len(), 20);
313 assert_eq!(result.gammas.shape(), (5, 20));
314 assert_eq!(result.aligned_data.shape(), (5, 20));
315 assert_eq!(result.weights.len(), 5);
316 assert!(result.n_iter >= 1);
317 }
318
319 #[test]
320 fn karcher_median_robust_to_outlier() {
321 let m = 20;
322 let t = uniform_grid(m);
323 let n = 6;
324 let mut data_vec = vec![0.0; n * m];
325
326 for i in 0..5 {
328 let phase = 0.02 * i as f64;
329 for j in 0..m {
330 data_vec[i + j * n] = ((t[j] + phase) * 4.0).sin();
331 }
332 }
333 for j in 0..m {
335 data_vec[5 + j * n] = (t[j] * 20.0).cos() * 5.0;
336 }
337 let data = FdMatrix::from_column_major(data_vec, n, m).unwrap();
338
339 let std_mean = karcher_mean(&data, &t, 5, 1e-3, 0.0);
341 let median_config = RobustKarcherConfig {
342 max_iter: 5,
343 ..Default::default()
344 };
345 let median_result = karcher_median(&data, &t, &median_config).unwrap();
346
347 let clean_data = subset_rows_from_indices(&data, &[0, 1, 2, 3, 4]);
349 let clean_mean = karcher_mean(&clean_data, &t, 5, 1e-3, 0.0);
350
351 let d_std = pointwise_l2(&std_mean.mean, &clean_mean.mean);
353 let d_median = pointwise_l2(&median_result.mean, &clean_mean.mean);
354 assert!(
355 d_median <= d_std + 1e-6,
356 "median distance to clean ({d_median:.4}) should be <= standard mean distance ({d_std:.4})"
357 );
358 }
359
360 #[test]
361 fn robust_trimmed_removes_outliers() {
362 let m = 20;
363 let t = uniform_grid(m);
364 let n = 6;
365 let mut data_vec = vec![0.0; n * m];
366
367 for i in 0..5 {
369 let phase = 0.02 * i as f64;
370 for j in 0..m {
371 data_vec[i + j * n] = ((t[j] + phase) * 4.0).sin();
372 }
373 }
374 for j in 0..m {
376 data_vec[5 + j * n] = (t[j] * 20.0).cos() * 5.0;
377 }
378 let data = FdMatrix::from_column_major(data_vec, n, m).unwrap();
379
380 let config = RobustKarcherConfig {
381 max_iter: 5,
382 trim_fraction: 0.2, ..Default::default()
384 };
385 let result = robust_karcher_mean(&data, &t, &config).unwrap();
386
387 assert!(
389 result.weights[5] < 1e-10,
390 "outlier weight should be 0, got {}",
391 result.weights[5]
392 );
393
394 let n_kept: usize = result.weights.iter().filter(|&&w| w > 0.5).count();
396 assert!(n_kept >= 4, "should keep at least 4 curves, got {n_kept}");
397 }
398
399 #[test]
400 fn robust_config_default() {
401 let cfg = RobustKarcherConfig::default();
402 assert_eq!(cfg.max_iter, 20);
403 assert!((cfg.tol - 1e-3).abs() < f64::EPSILON);
404 assert!((cfg.lambda - 0.0).abs() < f64::EPSILON);
405 assert!((cfg.trim_fraction - 0.1).abs() < f64::EPSILON);
406 }
407
408 fn pointwise_l2(a: &[f64], b: &[f64]) -> f64 {
410 a.iter()
411 .zip(b.iter())
412 .map(|(&x, &y)| (x - y).powi(2))
413 .sum::<f64>()
414 .sqrt()
415 }
416}