1use super::pairwise::elastic_align_pair;
8use super::srsf::{reparameterize_curve, srsf_single};
9use super::{dp_alignment_core, AlignmentResult};
10use crate::error::FdarError;
11use crate::helpers::{l2_distance, linear_interp, simpsons_weights};
12use crate::warping::normalize_warp;
13
14#[non_exhaustive]
20#[derive(Debug, Clone, PartialEq)]
21pub struct MultiresConfig {
22 pub coarsen_factor: usize,
25 pub n_refine_steps: usize,
28 pub step_size: f64,
31 pub lambda: f64,
33}
34
35impl Default for MultiresConfig {
36 fn default() -> Self {
37 Self {
38 coarsen_factor: 4,
39 n_refine_steps: 10,
40 step_size: 0.01,
41 lambda: 0.0,
42 }
43 }
44}
45
46#[must_use = "expensive computation whose result should not be discarded"]
67pub fn elastic_align_pair_multires(
68 f1: &[f64],
69 f2: &[f64],
70 argvals: &[f64],
71 config: &MultiresConfig,
72) -> Result<AlignmentResult, FdarError> {
73 let m = f1.len();
74
75 if m != f2.len() || m != argvals.len() {
76 return Err(FdarError::InvalidDimension {
77 parameter: "f1/f2/argvals",
78 expected: format!("equal lengths, f1 has {m}"),
79 actual: format!("f2 has {}, argvals has {}", f2.len(), argvals.len()),
80 });
81 }
82 if m < 2 {
83 return Err(FdarError::InvalidDimension {
84 parameter: "f1",
85 expected: "length >= 2".to_string(),
86 actual: format!("length {m}"),
87 });
88 }
89 if config.coarsen_factor < 2 {
90 return Err(FdarError::InvalidParameter {
91 parameter: "coarsen_factor",
92 message: format!("must be >= 2, got {}", config.coarsen_factor),
93 });
94 }
95
96 if m < 2 * config.coarsen_factor {
98 let result = elastic_align_pair(f1, f2, argvals, config.lambda);
99 return Ok(result);
100 }
101
102 let q1 = srsf_single(f1, argvals);
103 let q2 = srsf_single(f2, argvals);
104
105 let m_coarse = (m / config.coarsen_factor).max(4);
107 let coarse_argvals = subsample_grid(argvals, m_coarse);
108 let coarse_q1 = subsample_values(&q1, argvals, &coarse_argvals);
109 let coarse_q2 = subsample_values(&q2, argvals, &coarse_argvals);
110
111 let coarse_gamma = dp_alignment_core(&coarse_q1, &coarse_q2, &coarse_argvals, config.lambda);
112
113 let mut gamma: Vec<f64> = argvals
115 .iter()
116 .map(|&t| linear_interp(&coarse_argvals, &coarse_gamma, t))
117 .collect();
118 normalize_warp(&mut gamma, argvals);
119
120 for _ in 0..config.n_refine_steps {
122 let f2_warped = reparameterize_curve(f2, argvals, &gamma);
124 let q2_warped = srsf_single(&f2_warped, argvals);
125
126 let h = 1.0 / (m as f64 * 10.0);
129 let weights = simpsons_weights(argvals);
130 let _current_dist = l2_distance(&q1, &q2_warped, &weights);
131
132 let mut improved = false;
133 for j in 1..m - 1 {
134 let orig = gamma[j];
136
137 gamma[j] = orig + h;
138 if gamma[j] <= gamma[j - 1] || gamma[j] >= gamma[j + 1] {
140 gamma[j] = orig;
141 continue;
142 }
143
144 let f2_pert = reparameterize_curve(f2, argvals, &gamma);
145 let q2_pert = srsf_single(&f2_pert, argvals);
146 let dist_plus = l2_distance(&q1, &q2_pert, &weights);
147
148 gamma[j] = orig - h;
149 if gamma[j] <= gamma[j - 1] || gamma[j] >= gamma[j + 1] {
150 gamma[j] = orig;
151 continue;
152 }
153
154 let f2_pert2 = reparameterize_curve(f2, argvals, &gamma);
155 let q2_pert2 = srsf_single(&f2_pert2, argvals);
156 let dist_minus = l2_distance(&q1, &q2_pert2, &weights);
157
158 let grad = (dist_plus - dist_minus) / (2.0 * h);
160
161 let new_val = orig - config.step_size * grad;
163 let lo = gamma[j - 1] + 1e-12;
165 let hi = gamma[j + 1] - 1e-12;
166 gamma[j] = new_val.clamp(lo, hi);
167
168 if (gamma[j] - orig).abs() > 1e-15 {
169 improved = true;
170 }
171 }
172
173 if !improved {
174 break;
175 }
176
177 normalize_warp(&mut gamma, argvals);
178 }
179
180 let f_aligned = reparameterize_curve(f2, argvals, &gamma);
182 let q_aligned = srsf_single(&f_aligned, argvals);
183 let weights = simpsons_weights(argvals);
184 let distance = l2_distance(&q1, &q_aligned, &weights);
185
186 Ok(AlignmentResult {
187 gamma,
188 f_aligned,
189 distance,
190 })
191}
192
193fn subsample_grid(argvals: &[f64], m_coarse: usize) -> Vec<f64> {
197 let m = argvals.len();
198 if m_coarse >= m {
199 return argvals.to_vec();
200 }
201 (0..m_coarse)
202 .map(|i| {
203 let idx_f = i as f64 * (m - 1) as f64 / (m_coarse - 1) as f64;
204 let lo = idx_f.floor() as usize;
205 let hi = idx_f.ceil().min((m - 1) as f64) as usize;
206 let frac = idx_f - lo as f64;
207 argvals[lo] * (1.0 - frac) + argvals[hi] * frac
208 })
209 .collect()
210}
211
212fn subsample_values(values: &[f64], fine_grid: &[f64], coarse_grid: &[f64]) -> Vec<f64> {
214 coarse_grid
215 .iter()
216 .map(|&t| linear_interp(fine_grid, values, t))
217 .collect()
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use crate::test_helpers::uniform_grid;
224
225 #[test]
226 fn multires_identity() {
227 let m = 50;
228 let t = uniform_grid(m);
229 let f: Vec<f64> = t.iter().map(|&x| (x * 6.0).sin()).collect();
230
231 let config = MultiresConfig::default();
232 let result = elastic_align_pair_multires(&f, &f, &t, &config).unwrap();
233
234 assert!(
235 result.distance < 0.5,
236 "identical curves should have near-zero distance, got {}",
237 result.distance
238 );
239 }
240
241 #[test]
242 fn multires_phase_shifted() {
243 let m = 60;
244 let t = uniform_grid(m);
245 let f1: Vec<f64> = t.iter().map(|&x| (x * 6.0).sin()).collect();
246 let f2: Vec<f64> = t.iter().map(|&x| ((x + 0.1) * 6.0).sin()).collect();
247
248 let config = MultiresConfig::default();
249 let result = elastic_align_pair_multires(&f1, &f2, &t, &config).unwrap();
250
251 let standard = elastic_align_pair(&f1, &f2, &t, 0.0);
253 assert!(
255 result.distance < standard.distance * 2.0 + 0.5,
256 "multi-res distance ({}) should be comparable to standard ({})",
257 result.distance,
258 standard.distance,
259 );
260 }
261
262 #[test]
263 fn multires_falls_back_short_curves() {
264 let m = 6;
265 let t = uniform_grid(m);
266 let f1: Vec<f64> = t.iter().map(|&x| x * x).collect();
267 let f2: Vec<f64> = t.iter().map(|&x| x * x + 0.1).collect();
268
269 let config = MultiresConfig {
270 coarsen_factor: 4,
271 ..Default::default()
272 };
273 let result = elastic_align_pair_multires(&f1, &f2, &t, &config).unwrap();
274 assert_eq!(result.gamma.len(), m);
275 assert_eq!(result.f_aligned.len(), m);
276 }
277
278 #[test]
279 fn multires_rejects_bad_coarsen_factor() {
280 let t = uniform_grid(20);
281 let f: Vec<f64> = t.to_vec();
282 let config = MultiresConfig {
283 coarsen_factor: 1,
284 ..Default::default()
285 };
286 assert!(elastic_align_pair_multires(&f, &f, &t, &config).is_err());
287 }
288
289 #[test]
290 fn multires_config_default() {
291 let config = MultiresConfig::default();
292 assert_eq!(config.coarsen_factor, 4);
293 assert_eq!(config.n_refine_steps, 10);
294 assert!((config.step_size - 0.01).abs() < f64::EPSILON);
295 assert!((config.lambda - 0.0).abs() < f64::EPSILON);
296 }
297}