1mod bayesian;
15mod closed;
16mod clustering;
17mod constrained;
18mod diagnostics;
19mod elastic_depth;
20mod fpns;
21mod generative;
22mod geodesic;
23mod karcher;
24mod lambda_cv;
25mod multires;
26mod nd;
27mod outlier;
28mod pairwise;
29mod partial_match;
30mod persistence;
31mod phase_boxplot;
32mod quality;
33mod robust_karcher;
34mod set;
35mod shape;
36mod shape_ci;
37mod srsf;
38mod transfer;
39mod tsrvf;
40mod warp_stats;
41
42#[cfg(test)]
43mod tests;
44
45pub use bayesian::{bayesian_align_pair, BayesianAlignConfig, BayesianAlignmentResult};
47pub use closed::{
48 elastic_align_pair_closed, elastic_distance_closed, karcher_mean_closed, ClosedAlignmentResult,
49 ClosedKarcherMeanResult,
50};
51pub use clustering::{
52 cut_dendrogram, hierarchical_from_distances, kmedoids_from_distances, Dendrogram,
53 KMedoidsConfig, KMedoidsResult, Linkage,
54};
55pub use constrained::{
56 elastic_align_pair_constrained, elastic_align_pair_with_landmarks, ConstrainedAlignmentResult,
57};
58pub use diagnostics::{
59 diagnose_alignment, diagnose_pairwise, AlignmentDiagnostic, AlignmentDiagnosticSummary,
60 DiagnosticConfig,
61};
62pub use elastic_depth::{elastic_depth, ElasticDepthResult};
63pub use fpns::{horiz_fpns, FpnsResult};
64pub use generative::{gauss_model, joint_gauss_model, GenerativeModelResult};
65pub use geodesic::{curve_geodesic, curve_geodesic_nd, GeodesicPath, GeodesicPathNd};
66pub use karcher::{karcher_mean, karcher_mean_banded, karcher_mean_with_band};
67pub use lambda_cv::{lambda_cv, LambdaCvConfig, LambdaCvResult};
68pub use multires::{elastic_align_pair_multires, MultiresConfig};
69pub use nd::{
70 elastic_align_pair_nd, elastic_distance_nd, srsf_inverse_nd, srsf_transform_nd,
71 AlignmentResultNd,
72};
73pub use nd::{karcher_covariance_nd, karcher_mean_nd, pca_nd, KarcherMeanResultNd, PcaNdResult};
74pub use outlier::{elastic_outlier_detection, ElasticOutlierConfig, ElasticOutlierResult};
75pub use pairwise::{
76 amplitude_distance, amplitude_self_distance_matrix, elastic_align_pair,
77 elastic_align_pair_banded, elastic_align_pair_penalized, elastic_cross_distance_matrix,
78 elastic_cross_distance_matrix_banded, elastic_cross_distance_matrix_with_band,
79 elastic_distance, elastic_distance_banded, elastic_self_distance_matrix,
80 elastic_self_distance_matrix_banded, elastic_self_distance_matrix_with_band,
81 phase_distance_pair, phase_self_distance_matrix, WarpPenaltyType,
82};
83pub use partial_match::{elastic_partial_match, PartialMatchConfig, PartialMatchResult};
84pub use persistence::{peak_persistence, PersistenceDiagramResult};
85pub use phase_boxplot::{phase_boxplot, PhaseBoxplot};
86pub use quality::{
87 alignment_quality, pairwise_consistency, warp_complexity, warp_smoothness, AlignmentQuality,
88};
89pub use robust_karcher::{
90 karcher_median, robust_karcher_mean, RobustKarcherConfig, RobustKarcherResult,
91};
92pub use set::{align_to_target, elastic_decomposition, DecompositionResult};
93pub use shape::{
94 orbit_representative, shape_distance, shape_mean, shape_self_distance_matrix,
95 OrbitRepresentative, ShapeDistanceResult, ShapeMeanResult, ShapeQuotient,
96};
97pub use shape_ci::{shape_confidence_interval, ShapeCiConfig, ShapeCiResult};
98pub use srsf::{
99 compose_warps, invert_warp, reparameterize_curve, srsf_inverse, srsf_transform,
100 warp_inverse_error,
101};
102pub use transfer::{transfer_alignment, TransferAlignConfig, TransferAlignResult};
103pub use tsrvf::{
104 tsrvf_from_alignment, tsrvf_from_alignment_with_method, tsrvf_inverse, tsrvf_transform,
105 tsrvf_transform_with_method, TransportMethod, TsrvfResult,
106};
107pub use warp_stats::{warp_statistics, WarpStatistics};
108
109pub(crate) use karcher::sqrt_mean_inverse;
111
112use crate::helpers::linear_interp;
113use crate::matrix::FdMatrix;
114use crate::warping::normalize_warp;
115use std::cell::RefCell;
116
117#[derive(Debug, Clone, PartialEq)]
121#[non_exhaustive]
122pub struct AlignmentResult {
123 pub gamma: Vec<f64>,
125 pub f_aligned: Vec<f64>,
127 pub distance: f64,
129}
130
131#[derive(Debug, Clone, PartialEq)]
133#[non_exhaustive]
134pub struct AlignmentSetResult {
135 pub gammas: FdMatrix,
137 pub aligned_data: FdMatrix,
139 pub distances: Vec<f64>,
141}
142
143#[derive(Debug, Clone, PartialEq)]
145#[non_exhaustive]
146pub struct KarcherMeanResult {
147 pub mean: Vec<f64>,
149 pub mean_srsf: Vec<f64>,
151 pub gammas: FdMatrix,
153 pub aligned_data: FdMatrix,
155 pub n_iter: usize,
157 pub converged: bool,
159 pub aligned_srsfs: Option<FdMatrix>,
162}
163
164impl KarcherMeanResult {
165 pub fn new(
167 mean: Vec<f64>,
168 mean_srsf: Vec<f64>,
169 gammas: FdMatrix,
170 aligned_data: FdMatrix,
171 n_iter: usize,
172 converged: bool,
173 aligned_srsfs: Option<FdMatrix>,
174 ) -> Self {
175 Self {
176 mean,
177 mean_srsf,
178 gammas,
179 aligned_data,
180 n_iter,
181 converged,
182 aligned_srsfs,
183 }
184 }
185}
186
187pub trait AlignmentOutput {
192 fn mean(&self) -> &[f64];
194 fn mean_srsf(&self) -> &[f64];
196 fn aligned_data(&self) -> &FdMatrix;
198 fn gammas(&self) -> &FdMatrix;
200 fn converged(&self) -> bool;
202 fn n_iter(&self) -> usize;
204}
205
206impl AlignmentOutput for KarcherMeanResult {
207 fn mean(&self) -> &[f64] {
208 &self.mean
209 }
210 fn mean_srsf(&self) -> &[f64] {
211 &self.mean_srsf
212 }
213 fn aligned_data(&self) -> &FdMatrix {
214 &self.aligned_data
215 }
216 fn gammas(&self) -> &FdMatrix {
217 &self.gammas
218 }
219 fn converged(&self) -> bool {
220 self.converged
221 }
222 fn n_iter(&self) -> usize {
223 self.n_iter
224 }
225}
226
227impl AlignmentOutput for RobustKarcherResult {
228 fn mean(&self) -> &[f64] {
229 &self.mean
230 }
231 fn mean_srsf(&self) -> &[f64] {
232 &self.mean_srsf
233 }
234 fn aligned_data(&self) -> &FdMatrix {
235 &self.aligned_data
236 }
237 fn gammas(&self) -> &FdMatrix {
238 &self.gammas
239 }
240 fn converged(&self) -> bool {
241 self.converged
242 }
243 fn n_iter(&self) -> usize {
244 self.n_iter
245 }
246}
247
248impl From<RobustKarcherResult> for KarcherMeanResult {
251 fn from(r: RobustKarcherResult) -> Self {
252 Self {
253 mean: r.mean,
254 mean_srsf: r.mean_srsf,
255 gammas: r.gammas,
256 aligned_data: r.aligned_data,
257 n_iter: r.n_iter,
258 converged: r.converged,
259 aligned_srsfs: None,
260 }
261 }
262}
263
264#[rustfmt::skip]
271const COPRIME_NBHD_7: [(usize, usize); 35] = [
272 (1,1),(1,2),(1,3),(1,4),(1,5),(1,6),(1,7),
273 (2,1), (2,3), (2,5), (2,7),
274 (3,1),(3,2), (3,4),(3,5), (3,7),
275 (4,1), (4,3), (4,5), (4,7),
276 (5,1),(5,2),(5,3),(5,4), (5,6),(5,7),
277 (6,1), (6,5), (6,7),
278 (7,1),(7,2),(7,3),(7,4),(7,5),(7,6),
279];
280
281#[inline]
289pub(super) fn dp_edge_weight(
290 q1: &[f64],
291 q2: &[f64],
292 argvals: &[f64],
293 sc: usize,
294 tc: usize,
295 sr: usize,
296 tr: usize,
297) -> f64 {
298 if tc == sc || tr == sr {
299 return f64::INFINITY;
300 }
301 let rslope = ((argvals[tr] - argvals[sr]) / (argvals[tc] - argvals[sc])).sqrt();
303 let span = argvals[tc] - argvals[sc];
304 dp_edge_weight_core(q1, q2, sc, tc, sr, tr, rslope, span)
305}
306
307#[inline]
320pub(super) fn dp_edge_weight_core(
321 q1: &[f64],
322 q2: &[f64],
323 sc: usize,
324 tc: usize,
325 sr: usize,
326 tr: usize,
327 rslope: f64,
328 span: f64,
329) -> f64 {
330 let n1 = tc - sc;
331 let n2 = tr - sr;
332 if n1 == 0 || n2 == 0 {
333 return f64::INFINITY;
334 }
335
336 let mut weight_scaled = 0.0;
337 let mut i1 = 0usize; let mut i2 = 0usize; while i1 < n1 && i2 < n2 {
341 let left = (i1 * n2).max(i2 * n1);
342 let right1 = (i1 + 1) * n2;
343 let right2 = (i2 + 1) * n1;
344 let right = right1.min(right2);
345 let dt = right - left;
346
347 if dt > 0 {
348 let diff = q1[sc + i1] - rslope * q2[sr + i2];
349 weight_scaled += diff * diff * dt as f64;
350 }
351
352 if right1 < right2 {
354 i1 += 1;
355 } else if right2 < right1 {
356 i2 += 1;
357 } else {
358 i1 += 1;
359 i2 += 1;
360 }
361 }
362
363 weight_scaled / (n1 * n2) as f64 * span
365}
366
367fn uniform_spacing(argvals: &[f64]) -> Option<f64> {
373 let m = argvals.len();
374 if m < 2 {
375 return None;
376 }
377 let h = (argvals[m - 1] - argvals[0]) / (m - 1) as f64;
378 if h.is_nan() || h <= 0.0 {
379 return None;
380 }
381 let tol = h * 1e-9;
382 for k in 1..m {
383 if (argvals[k] - argvals[k - 1] - h).abs() > tol {
384 return None;
385 }
386 }
387 Some(h)
388}
389
390#[inline]
392pub(super) fn dp_lambda_penalty(
393 argvals: &[f64],
394 sc: usize,
395 tc: usize,
396 sr: usize,
397 tr: usize,
398 lambda: f64,
399) -> f64 {
400 if lambda > 0.0 {
401 let dt = argvals[tc] - argvals[sc];
402 let slope = (argvals[tr] - argvals[sr]) / dt;
403 lambda * (slope - 1.0).powi(2) * dt
404 } else {
405 0.0
406 }
407}
408
409fn dp_traceback(parent: &[u32], nrows: usize, ncols: usize) -> Vec<(usize, usize)> {
413 let mut path = Vec::with_capacity(nrows + ncols);
414 let mut cur = (nrows - 1) * ncols + (ncols - 1);
415 loop {
416 path.push((cur / ncols, cur % ncols));
417 if cur == 0 || parent[cur] == u32::MAX {
418 break;
419 }
420 cur = parent[cur] as usize;
421 }
422 path.reverse();
423 path
424}
425
426#[inline]
428fn dp_relax_cell<F>(
429 e: &mut [f64],
430 parent: &mut [u32],
431 ncols: usize,
432 tr: usize,
433 tc: usize,
434 edge_cost: &F,
435) where
436 F: Fn(usize, usize, usize, usize) -> f64,
437{
438 let idx = tr * ncols + tc;
439 for &(dr, dc) in &COPRIME_NBHD_7 {
440 if dr > tr || dc > tc {
441 continue;
442 }
443 let sr = tr - dr;
444 let sc = tc - dc;
445 let src_idx = sr * ncols + sc;
446 if e[src_idx] == f64::INFINITY {
447 continue;
448 }
449 let cost = e[src_idx] + edge_cost(sr, sc, tr, tc);
450 if cost < e[idx] {
451 e[idx] = cost;
452 parent[idx] = src_idx as u32;
453 }
454 }
455}
456
457thread_local! {
458 static DP_SCRATCH: RefCell<(Vec<f64>, Vec<u32>)> = const { RefCell::new((Vec::new(), Vec::new())) };
467}
468
469pub(super) fn dp_grid_solve<F>(nrows: usize, ncols: usize, edge_cost: F) -> Vec<(usize, usize)>
475where
476 F: Fn(usize, usize, usize, usize) -> f64,
477{
478 dp_grid_solve_banded(nrows, ncols, None, edge_cost)
479}
480
481pub(super) fn dp_grid_solve_banded<F>(
491 nrows: usize,
492 ncols: usize,
493 band: Option<usize>,
494 edge_cost: F,
495) -> Vec<(usize, usize)>
496where
497 F: Fn(usize, usize, usize, usize) -> f64,
498{
499 DP_SCRATCH.with(|cell| {
500 let mut scratch = cell.borrow_mut();
501 let (e, parent) = &mut *scratch;
502 let size = nrows * ncols;
503 e.clear();
504 e.resize(size, f64::INFINITY);
505 parent.clear();
506 parent.resize(size, u32::MAX);
507 e[0] = 0.0;
508
509 for tr in 0..nrows {
510 for tc in 0..ncols {
511 if tr == 0 && tc == 0 {
512 continue;
513 }
514 if let Some(r) = band {
515 if tr.abs_diff(tc) > r {
516 continue;
517 }
518 }
519 dp_relax_cell(e, parent, ncols, tr, tc, &edge_cost);
520 }
521 }
522
523 dp_traceback(parent, nrows, ncols)
524 })
525}
526
527pub(super) fn band_radius(band_frac: f64, m: usize) -> Option<usize> {
535 if band_frac > 0.0 && band_frac < 1.0 {
536 Some(((band_frac * m as f64).ceil() as usize).max(1))
537 } else {
538 None
539 }
540}
541
542pub(super) fn dp_path_to_gamma(path: &[(usize, usize)], argvals: &[f64]) -> Vec<f64> {
544 let path_tc: Vec<f64> = path.iter().map(|&(_, c)| argvals[c]).collect();
545 let path_tr: Vec<f64> = path.iter().map(|&(r, _)| argvals[r]).collect();
546 let mut gamma: Vec<f64> = argvals
547 .iter()
548 .map(|&t| linear_interp(&path_tc, &path_tr, t))
549 .collect();
550 normalize_warp(&mut gamma, argvals);
551 gamma
552}
553
554pub(crate) fn dp_alignment_core(q1: &[f64], q2: &[f64], argvals: &[f64], lambda: f64) -> Vec<f64> {
560 dp_alignment_core_banded(q1, q2, argvals, lambda, None)
561}
562
563pub(crate) fn dp_alignment_core_banded(
569 q1: &[f64],
570 q2: &[f64],
571 argvals: &[f64],
572 lambda: f64,
573 band: Option<usize>,
574) -> Vec<f64> {
575 let m = argvals.len();
576 if m < 2 {
577 return argvals.to_vec();
578 }
579
580 let norm1 = q1.iter().map(|&v| v * v).sum::<f64>().sqrt().max(1e-10);
581 let norm2 = q2.iter().map(|&v| v * v).sum::<f64>().sqrt().max(1e-10);
582 let q1n: Vec<f64> = q1.iter().map(|&v| v / norm1).collect();
583 let q2n: Vec<f64> = q2.iter().map(|&v| v / norm2).collect();
584
585 let path = if let Some(h) = uniform_spacing(argvals) {
586 let mut rslope_tab = [[0.0f64; 8]; 8];
591 for (dr, row) in rslope_tab.iter_mut().enumerate().skip(1) {
592 for (dc, cell) in row.iter_mut().enumerate().skip(1) {
593 *cell = (dr as f64 / dc as f64).sqrt();
594 }
595 }
596 dp_grid_solve_banded(m, m, band, |sr, sc, tr, tc| {
597 let dr = tr - sr;
598 let dc = tc - sc;
599 dp_edge_weight_core(
600 &q1n,
601 &q2n,
602 sc,
603 tc,
604 sr,
605 tr,
606 rslope_tab[dr][dc],
607 dc as f64 * h,
608 ) + dp_lambda_penalty(argvals, sc, tc, sr, tr, lambda)
609 })
610 } else {
611 dp_grid_solve_banded(m, m, band, |sr, sc, tr, tc| {
612 dp_edge_weight(&q1n, &q2n, argvals, sc, tc, sr, tr)
613 + dp_lambda_penalty(argvals, sc, tc, sr, tr, lambda)
614 })
615 };
616
617 dp_path_to_gamma(&path, argvals)
618}
619
620#[cfg(test)]
622pub(super) fn gcd(a: usize, b: usize) -> usize {
623 if b == 0 {
624 a
625 } else {
626 gcd(b, a % b)
627 }
628}
629
630#[cfg(test)]
633pub(super) fn generate_coprime_nbhd(nbhd_dim: usize) -> Vec<(usize, usize)> {
634 let mut pairs = Vec::new();
635 for i in 1..=nbhd_dim {
636 for j in 1..=nbhd_dim {
637 if gcd(i, j) == 1 {
638 pairs.push((i, j));
639 }
640 }
641 }
642 pairs
643}