fdars_core/alignment/
elastic_depth.rs1use super::pairwise::{amplitude_self_distance_matrix, phase_self_distance_matrix};
8use crate::error::FdarError;
9use crate::matrix::FdMatrix;
10
11#[derive(Debug, Clone, PartialEq)]
13#[non_exhaustive]
14pub struct ElasticDepthResult {
15 pub amplitude_depth: Vec<f64>,
17 pub phase_depth: Vec<f64>,
19 pub combined_depth: Vec<f64>,
21 pub amplitude_distances: FdMatrix,
23 pub phase_distances: FdMatrix,
25}
26
27#[must_use = "expensive computation whose result should not be discarded"]
42pub fn elastic_depth(
43 data: &FdMatrix,
44 argvals: &[f64],
45 lambda: f64,
46) -> Result<ElasticDepthResult, FdarError> {
47 let (n, m) = data.shape();
48
49 if argvals.len() != m {
50 return Err(FdarError::InvalidDimension {
51 parameter: "argvals",
52 expected: format!("{m}"),
53 actual: format!("{}", argvals.len()),
54 });
55 }
56 if n < 2 {
57 return Err(FdarError::InvalidDimension {
58 parameter: "data",
59 expected: "at least 2 rows".to_string(),
60 actual: format!("{n} rows"),
61 });
62 }
63
64 let amplitude_distances = amplitude_self_distance_matrix(data, argvals, lambda);
66 let phase_distances = phase_self_distance_matrix(data, argvals, lambda);
67
68 let amplitude_depth = distance_matrix_to_depth(&litude_distances, n);
70 let phase_depth = distance_matrix_to_depth(&phase_distances, n);
71
72 let combined_depth: Vec<f64> = amplitude_depth
74 .iter()
75 .zip(phase_depth.iter())
76 .map(|(&a, &p)| (a * p).sqrt())
77 .collect();
78
79 Ok(ElasticDepthResult {
80 amplitude_depth,
81 phase_depth,
82 combined_depth,
83 amplitude_distances,
84 phase_distances,
85 })
86}
87
88fn distance_matrix_to_depth(dist: &FdMatrix, n: usize) -> Vec<f64> {
90 (0..n)
91 .map(|i| {
92 let sum: f64 = (0..n).filter(|&j| j != i).map(|j| dist[(i, j)]).sum();
93 let mean_dist = sum / (n - 1) as f64;
94 1.0 / (1.0 + mean_dist)
95 })
96 .collect()
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::test_helpers::uniform_grid;
103
104 fn make_sine_data(n: usize, m: usize) -> (FdMatrix, Vec<f64>) {
105 let t = uniform_grid(m);
106 let mut data_vec = vec![0.0; n * m];
107 for i in 0..n {
108 let phase = 0.05 * i as f64;
109 for j in 0..m {
110 data_vec[i + j * n] = ((t[j] + phase) * 4.0).sin();
111 }
112 }
113 let data = FdMatrix::from_column_major(data_vec, n, m).unwrap();
114 (data, t)
115 }
116
117 #[test]
118 fn elastic_depth_dimensions() {
119 let (data, t) = make_sine_data(4, 20);
120 let result = elastic_depth(&data, &t, 0.0).unwrap();
121 assert_eq!(result.amplitude_depth.len(), 4);
122 assert_eq!(result.phase_depth.len(), 4);
123 assert_eq!(result.combined_depth.len(), 4);
124 assert_eq!(result.amplitude_distances.shape(), (4, 4));
125 assert_eq!(result.phase_distances.shape(), (4, 4));
126 }
127
128 #[test]
129 fn elastic_depth_nonneg() {
130 let (data, t) = make_sine_data(4, 20);
131 let result = elastic_depth(&data, &t, 0.0).unwrap();
132 for &d in &result.amplitude_depth {
133 assert!((0.0..=1.0).contains(&d), "amplitude depth {d} out of [0,1]");
134 }
135 for &d in &result.phase_depth {
136 assert!((0.0..=1.0).contains(&d), "phase depth {d} out of [0,1]");
137 }
138 for &d in &result.combined_depth {
139 assert!((0.0..=1.0).contains(&d), "combined depth {d} out of [0,1]");
140 }
141 }
142
143 #[test]
144 fn elastic_depth_identical_curves_highest() {
145 let m = 20;
147 let t = uniform_grid(m);
148 let n = 4;
149 let mut data_vec = vec![0.0; n * m];
150 for j in 0..m {
151 let v = (t[j] * 4.0).sin();
152 data_vec[j * n] = v;
154 data_vec[1 + j * n] = v;
155 data_vec[2 + j * n] = v;
156 data_vec[3 + j * n] = (t[j] * 12.0).cos() * 3.0;
158 }
159 let data = FdMatrix::from_column_major(data_vec, n, m).unwrap();
160 let result = elastic_depth(&data, &t, 0.0).unwrap();
161
162 let min_identical = result.combined_depth[0]
164 .min(result.combined_depth[1])
165 .min(result.combined_depth[2]);
166 assert!(
167 min_identical > result.combined_depth[3],
168 "identical curves depth ({min_identical}) should exceed outlier depth ({})",
169 result.combined_depth[3]
170 );
171 }
172}