1mod clustering;
15mod constrained;
16mod diagnostics;
17mod karcher;
18mod lambda_cv;
19mod nd;
20mod pairwise;
21mod phase_boxplot;
22mod quality;
23mod set;
24mod shape;
25mod srsf;
26mod tsrvf;
27mod warp_stats;
28
29#[cfg(test)]
30mod tests;
31
32pub use clustering::{
34 cut_dendrogram, elastic_hierarchical, elastic_kmeans, ElasticClusterConfig,
35 ElasticClusterMethod, ElasticClusterResult, ElasticDendrogram,
36};
37pub use constrained::{
38 elastic_align_pair_constrained, elastic_align_pair_with_landmarks, ConstrainedAlignmentResult,
39};
40pub use diagnostics::{
41 diagnose_alignment, diagnose_pairwise, AlignmentDiagnostic, AlignmentDiagnosticSummary,
42 DiagnosticConfig,
43};
44pub use karcher::karcher_mean;
45pub use lambda_cv::{lambda_cv, LambdaCvConfig, LambdaCvResult};
46pub use nd::{
47 elastic_align_pair_nd, elastic_distance_nd, srsf_inverse_nd, srsf_transform_nd,
48 AlignmentResultNd,
49};
50pub use pairwise::{
51 amplitude_distance, amplitude_self_distance_matrix, elastic_align_pair,
52 elastic_align_pair_penalized, elastic_cross_distance_matrix, elastic_distance,
53 elastic_self_distance_matrix, phase_distance_pair, phase_self_distance_matrix, WarpPenaltyType,
54};
55pub use phase_boxplot::{phase_boxplot, PhaseBoxplot};
56pub use quality::{
57 alignment_quality, pairwise_consistency, warp_complexity, warp_smoothness, AlignmentQuality,
58};
59pub use set::{align_to_target, elastic_decomposition, DecompositionResult};
60pub use shape::{
61 orbit_representative, shape_distance, shape_mean, shape_self_distance_matrix,
62 OrbitRepresentative, ShapeDistanceResult, ShapeMeanResult, ShapeQuotient,
63};
64pub use srsf::{
65 compose_warps, invert_warp, reparameterize_curve, srsf_inverse, srsf_transform,
66 warp_inverse_error,
67};
68pub use tsrvf::{
69 tsrvf_from_alignment, tsrvf_from_alignment_with_method, tsrvf_inverse, tsrvf_transform,
70 tsrvf_transform_with_method, TransportMethod, TsrvfResult,
71};
72pub use warp_stats::{warp_statistics, WarpStatistics};
73
74pub(crate) use karcher::sqrt_mean_inverse;
76
77use crate::helpers::linear_interp;
78use crate::matrix::FdMatrix;
79use crate::warping::normalize_warp;
80
81#[derive(Debug, Clone, PartialEq)]
85#[non_exhaustive]
86pub struct AlignmentResult {
87 pub gamma: Vec<f64>,
89 pub f_aligned: Vec<f64>,
91 pub distance: f64,
93}
94
95#[derive(Debug, Clone, PartialEq)]
97#[non_exhaustive]
98pub struct AlignmentSetResult {
99 pub gammas: FdMatrix,
101 pub aligned_data: FdMatrix,
103 pub distances: Vec<f64>,
105}
106
107#[derive(Debug, Clone, PartialEq)]
109#[non_exhaustive]
110pub struct KarcherMeanResult {
111 pub mean: Vec<f64>,
113 pub mean_srsf: Vec<f64>,
115 pub gammas: FdMatrix,
117 pub aligned_data: FdMatrix,
119 pub n_iter: usize,
121 pub converged: bool,
123 pub aligned_srsfs: Option<FdMatrix>,
126}
127
128impl KarcherMeanResult {
129 pub fn new(
131 mean: Vec<f64>,
132 mean_srsf: Vec<f64>,
133 gammas: FdMatrix,
134 aligned_data: FdMatrix,
135 n_iter: usize,
136 converged: bool,
137 aligned_srsfs: Option<FdMatrix>,
138 ) -> Self {
139 Self {
140 mean,
141 mean_srsf,
142 gammas,
143 aligned_data,
144 n_iter,
145 converged,
146 aligned_srsfs,
147 }
148 }
149}
150
151#[rustfmt::skip]
158const COPRIME_NBHD_7: [(usize, usize); 35] = [
159 (1,1),(1,2),(1,3),(1,4),(1,5),(1,6),(1,7),
160 (2,1), (2,3), (2,5), (2,7),
161 (3,1),(3,2), (3,4),(3,5), (3,7),
162 (4,1), (4,3), (4,5), (4,7),
163 (5,1),(5,2),(5,3),(5,4), (5,6),(5,7),
164 (6,1), (6,5), (6,7),
165 (7,1),(7,2),(7,3),(7,4),(7,5),(7,6),
166];
167
168#[inline]
176pub(super) fn dp_edge_weight(
177 q1: &[f64],
178 q2: &[f64],
179 argvals: &[f64],
180 sc: usize,
181 tc: usize,
182 sr: usize,
183 tr: usize,
184) -> f64 {
185 let n1 = tc - sc;
186 let n2 = tr - sr;
187 if n1 == 0 || n2 == 0 {
188 return f64::INFINITY;
189 }
190
191 let slope = (argvals[tr] - argvals[sr]) / (argvals[tc] - argvals[sc]);
192 let rslope = slope.sqrt();
193
194 let mut weight = 0.0;
196 let mut i1 = 0usize; let mut i2 = 0usize; while i1 < n1 && i2 < n2 {
200 let left1 = i1 as f64 / n1 as f64;
202 let right1 = (i1 + 1) as f64 / n1 as f64;
203 let left2 = i2 as f64 / n2 as f64;
204 let right2 = (i2 + 1) as f64 / n2 as f64;
205
206 let left = left1.max(left2);
207 let right = right1.min(right2);
208 let dt = right - left;
209
210 if dt > 0.0 {
211 let diff = q1[sc + i1] - rslope * q2[sr + i2];
212 weight += diff * diff * dt;
213 }
214
215 if right1 < right2 {
217 i1 += 1;
218 } else if right2 < right1 {
219 i2 += 1;
220 } else {
221 i1 += 1;
222 i2 += 1;
223 }
224 }
225
226 weight * (argvals[tc] - argvals[sc])
228}
229
230#[inline]
232pub(super) fn dp_lambda_penalty(
233 argvals: &[f64],
234 sc: usize,
235 tc: usize,
236 sr: usize,
237 tr: usize,
238 lambda: f64,
239) -> f64 {
240 if lambda > 0.0 {
241 let dt = argvals[tc] - argvals[sc];
242 let slope = (argvals[tr] - argvals[sr]) / dt;
243 lambda * (slope - 1.0).powi(2) * dt
244 } else {
245 0.0
246 }
247}
248
249fn dp_traceback(parent: &[u32], nrows: usize, ncols: usize) -> Vec<(usize, usize)> {
253 let mut path = Vec::with_capacity(nrows + ncols);
254 let mut cur = (nrows - 1) * ncols + (ncols - 1);
255 loop {
256 path.push((cur / ncols, cur % ncols));
257 if cur == 0 || parent[cur] == u32::MAX {
258 break;
259 }
260 cur = parent[cur] as usize;
261 }
262 path.reverse();
263 path
264}
265
266#[inline]
268fn dp_relax_cell<F>(
269 e: &mut [f64],
270 parent: &mut [u32],
271 ncols: usize,
272 tr: usize,
273 tc: usize,
274 edge_cost: &F,
275) where
276 F: Fn(usize, usize, usize, usize) -> f64,
277{
278 let idx = tr * ncols + tc;
279 for &(dr, dc) in &COPRIME_NBHD_7 {
280 if dr > tr || dc > tc {
281 continue;
282 }
283 let sr = tr - dr;
284 let sc = tc - dc;
285 let src_idx = sr * ncols + sc;
286 if e[src_idx] == f64::INFINITY {
287 continue;
288 }
289 let cost = e[src_idx] + edge_cost(sr, sc, tr, tc);
290 if cost < e[idx] {
291 e[idx] = cost;
292 parent[idx] = src_idx as u32;
293 }
294 }
295}
296
297pub(super) fn dp_grid_solve<F>(nrows: usize, ncols: usize, edge_cost: F) -> Vec<(usize, usize)>
303where
304 F: Fn(usize, usize, usize, usize) -> f64,
305{
306 let mut e = vec![f64::INFINITY; nrows * ncols];
307 let mut parent = vec![u32::MAX; nrows * ncols];
308 e[0] = 0.0;
309
310 for tr in 0..nrows {
311 for tc in 0..ncols {
312 if tr == 0 && tc == 0 {
313 continue;
314 }
315 dp_relax_cell(&mut e, &mut parent, ncols, tr, tc, &edge_cost);
316 }
317 }
318
319 dp_traceback(&parent, nrows, ncols)
320}
321
322pub(super) fn dp_path_to_gamma(path: &[(usize, usize)], argvals: &[f64]) -> Vec<f64> {
324 let path_tc: Vec<f64> = path.iter().map(|&(_, c)| argvals[c]).collect();
325 let path_tr: Vec<f64> = path.iter().map(|&(r, _)| argvals[r]).collect();
326 let mut gamma: Vec<f64> = argvals
327 .iter()
328 .map(|&t| linear_interp(&path_tc, &path_tr, t))
329 .collect();
330 normalize_warp(&mut gamma, argvals);
331 gamma
332}
333
334pub(crate) fn dp_alignment_core(q1: &[f64], q2: &[f64], argvals: &[f64], lambda: f64) -> Vec<f64> {
340 let m = argvals.len();
341 if m < 2 {
342 return argvals.to_vec();
343 }
344
345 let norm1 = q1.iter().map(|&v| v * v).sum::<f64>().sqrt().max(1e-10);
346 let norm2 = q2.iter().map(|&v| v * v).sum::<f64>().sqrt().max(1e-10);
347 let q1n: Vec<f64> = q1.iter().map(|&v| v / norm1).collect();
348 let q2n: Vec<f64> = q2.iter().map(|&v| v / norm2).collect();
349
350 let path = dp_grid_solve(m, m, |sr, sc, tr, tc| {
351 dp_edge_weight(&q1n, &q2n, argvals, sc, tc, sr, tr)
352 + dp_lambda_penalty(argvals, sc, tc, sr, tr, lambda)
353 });
354
355 dp_path_to_gamma(&path, argvals)
356}
357
358#[cfg(test)]
360pub(super) fn gcd(a: usize, b: usize) -> usize {
361 if b == 0 {
362 a
363 } else {
364 gcd(b, a % b)
365 }
366}
367
368#[cfg(test)]
371pub(super) fn generate_coprime_nbhd(nbhd_dim: usize) -> Vec<(usize, usize)> {
372 let mut pairs = Vec::new();
373 for i in 1..=nbhd_dim {
374 for j in 1..=nbhd_dim {
375 if gcd(i, j) == 1 {
376 pairs.push((i, j));
377 }
378 }
379 }
380 pairs
381}