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 shift;
38mod srsf;
39mod transfer;
40mod tsrvf;
41mod warp_stats;
42
43#[cfg(test)]
44mod tests;
45
46pub use bayesian::{bayesian_align_pair, BayesianAlignConfig, BayesianAlignmentResult};
48pub use closed::{
49 elastic_align_pair_closed, elastic_distance_closed, karcher_mean_closed, ClosedAlignmentResult,
50 ClosedKarcherMeanResult,
51};
52pub use clustering::{
53 cut_dendrogram, hierarchical_from_distances, kmedoids_from_distances, Dendrogram,
54 KMedoidsConfig, KMedoidsResult, Linkage,
55};
56pub use constrained::{
57 elastic_align_pair_constrained, elastic_align_pair_with_landmarks, ConstrainedAlignmentResult,
58};
59pub use diagnostics::{
60 diagnose_alignment, diagnose_pairwise, AlignmentDiagnostic, AlignmentDiagnosticSummary,
61 DiagnosticConfig,
62};
63pub use elastic_depth::{elastic_depth, ElasticDepthResult};
64pub use fpns::{horiz_fpns, FpnsResult};
65pub use generative::{gauss_model, joint_gauss_model, GenerativeModelResult};
66pub use geodesic::{curve_geodesic, curve_geodesic_nd, GeodesicPath, GeodesicPathNd};
67pub use karcher::{karcher_mean, karcher_mean_banded, karcher_mean_with_band};
68pub use lambda_cv::{lambda_cv, LambdaCvConfig, LambdaCvResult};
69pub use multires::{elastic_align_pair_multires, MultiresConfig};
70pub use nd::{
71 elastic_align_pair_nd, elastic_distance_nd, srsf_inverse_nd, srsf_transform_nd,
72 AlignmentResultNd,
73};
74pub use nd::{karcher_covariance_nd, karcher_mean_nd, pca_nd, KarcherMeanResultNd, PcaNdResult};
75pub use outlier::{elastic_outlier_detection, ElasticOutlierConfig, ElasticOutlierResult};
76pub use pairwise::{
77 amplitude_distance, amplitude_self_distance_matrix, elastic_align_pair,
78 elastic_align_pair_banded, elastic_align_pair_penalized, elastic_cross_distance_matrix,
79 elastic_cross_distance_matrix_banded, elastic_cross_distance_matrix_with_band,
80 elastic_distance, elastic_distance_banded, elastic_self_distance_matrix,
81 elastic_self_distance_matrix_banded, elastic_self_distance_matrix_with_band,
82 phase_distance_pair, phase_self_distance_matrix, WarpPenaltyType,
83};
84pub use partial_match::{elastic_partial_match, PartialMatchConfig, PartialMatchResult};
85pub use persistence::{peak_persistence, PersistenceDiagramResult};
86pub use phase_boxplot::{phase_boxplot, PhaseBoxplot};
87pub use quality::{
88 alignment_quality, least_squares_score, pairwise_consistency, pairwise_correlation_score,
89 sobolev_least_squares_score, warp_complexity, warp_smoothness, AlignmentQuality,
90};
91pub use robust_karcher::{
92 karcher_median, robust_karcher_mean, RobustKarcherConfig, RobustKarcherResult,
93};
94pub use set::{align_to_target, elastic_decomposition, DecompositionResult};
95pub use shape::{
96 orbit_representative, shape_distance, shape_mean, shape_self_distance_matrix,
97 OrbitRepresentative, ShapeDistanceResult, ShapeMeanResult, ShapeQuotient,
98};
99pub use shape_ci::{shape_confidence_interval, ShapeCiConfig, ShapeCiResult};
100pub use shift::{
101 least_squares_shift_registration, ShiftRegistrationResult, DEFAULT_MAX_SHIFT_FRACTION,
102};
103pub use srsf::{
104 compose_warps, invert_warp, reparameterize_curve, srsf_inverse, srsf_transform,
105 warp_inverse_error,
106};
107pub use transfer::{transfer_alignment, TransferAlignConfig, TransferAlignResult};
108pub use tsrvf::{
109 tsrvf_from_alignment, tsrvf_from_alignment_with_method, tsrvf_inverse, tsrvf_transform,
110 tsrvf_transform_with_method, TransportMethod, TsrvfResult,
111};
112pub use warp_stats::{warp_statistics, WarpStatistics};
113
114pub(crate) use karcher::sqrt_mean_inverse;
116
117use crate::helpers::linear_interp;
118use crate::matrix::FdMatrix;
119use crate::warping::normalize_warp;
120use std::cell::RefCell;
121
122#[derive(Debug, Clone, PartialEq)]
126#[non_exhaustive]
127pub struct AlignmentResult {
128 pub gamma: Vec<f64>,
130 pub f_aligned: Vec<f64>,
132 pub distance: f64,
134}
135
136#[derive(Debug, Clone, PartialEq)]
138#[non_exhaustive]
139pub struct AlignmentSetResult {
140 pub gammas: FdMatrix,
142 pub aligned_data: FdMatrix,
144 pub distances: Vec<f64>,
146}
147
148#[derive(Debug, Clone, PartialEq)]
150#[non_exhaustive]
151pub struct KarcherMeanResult {
152 pub mean: Vec<f64>,
154 pub mean_srsf: Vec<f64>,
156 pub gammas: FdMatrix,
158 pub aligned_data: FdMatrix,
160 pub n_iter: usize,
162 pub converged: bool,
164 pub aligned_srsfs: Option<FdMatrix>,
167}
168
169impl KarcherMeanResult {
170 pub fn new(
172 mean: Vec<f64>,
173 mean_srsf: Vec<f64>,
174 gammas: FdMatrix,
175 aligned_data: FdMatrix,
176 n_iter: usize,
177 converged: bool,
178 aligned_srsfs: Option<FdMatrix>,
179 ) -> Self {
180 Self {
181 mean,
182 mean_srsf,
183 gammas,
184 aligned_data,
185 n_iter,
186 converged,
187 aligned_srsfs,
188 }
189 }
190}
191
192pub trait AlignmentOutput {
197 fn mean(&self) -> &[f64];
199 fn mean_srsf(&self) -> &[f64];
201 fn aligned_data(&self) -> &FdMatrix;
203 fn gammas(&self) -> &FdMatrix;
205 fn converged(&self) -> bool;
207 fn n_iter(&self) -> usize;
209}
210
211impl AlignmentOutput for KarcherMeanResult {
212 fn mean(&self) -> &[f64] {
213 &self.mean
214 }
215 fn mean_srsf(&self) -> &[f64] {
216 &self.mean_srsf
217 }
218 fn aligned_data(&self) -> &FdMatrix {
219 &self.aligned_data
220 }
221 fn gammas(&self) -> &FdMatrix {
222 &self.gammas
223 }
224 fn converged(&self) -> bool {
225 self.converged
226 }
227 fn n_iter(&self) -> usize {
228 self.n_iter
229 }
230}
231
232impl AlignmentOutput for RobustKarcherResult {
233 fn mean(&self) -> &[f64] {
234 &self.mean
235 }
236 fn mean_srsf(&self) -> &[f64] {
237 &self.mean_srsf
238 }
239 fn aligned_data(&self) -> &FdMatrix {
240 &self.aligned_data
241 }
242 fn gammas(&self) -> &FdMatrix {
243 &self.gammas
244 }
245 fn converged(&self) -> bool {
246 self.converged
247 }
248 fn n_iter(&self) -> usize {
249 self.n_iter
250 }
251}
252
253impl From<RobustKarcherResult> for KarcherMeanResult {
256 fn from(r: RobustKarcherResult) -> Self {
257 Self {
258 mean: r.mean,
259 mean_srsf: r.mean_srsf,
260 gammas: r.gammas,
261 aligned_data: r.aligned_data,
262 n_iter: r.n_iter,
263 converged: r.converged,
264 aligned_srsfs: None,
265 }
266 }
267}
268
269#[rustfmt::skip]
276const COPRIME_NBHD_7: [(usize, usize); 35] = [
277 (1,1),(1,2),(1,3),(1,4),(1,5),(1,6),(1,7),
278 (2,1), (2,3), (2,5), (2,7),
279 (3,1),(3,2), (3,4),(3,5), (3,7),
280 (4,1), (4,3), (4,5), (4,7),
281 (5,1),(5,2),(5,3),(5,4), (5,6),(5,7),
282 (6,1), (6,5), (6,7),
283 (7,1),(7,2),(7,3),(7,4),(7,5),(7,6),
284];
285
286#[inline]
294pub(super) fn dp_edge_weight(
295 q1: &[f64],
296 q2: &[f64],
297 argvals: &[f64],
298 sc: usize,
299 tc: usize,
300 sr: usize,
301 tr: usize,
302) -> f64 {
303 if tc == sc || tr == sr {
304 return f64::INFINITY;
305 }
306 let rslope = ((argvals[tr] - argvals[sr]) / (argvals[tc] - argvals[sc])).sqrt();
308 let span = argvals[tc] - argvals[sc];
309 dp_edge_weight_core(q1, q2, sc, tc, sr, tr, rslope, span)
310}
311
312#[inline]
325pub(super) fn dp_edge_weight_core(
326 q1: &[f64],
327 q2: &[f64],
328 sc: usize,
329 tc: usize,
330 sr: usize,
331 tr: usize,
332 rslope: f64,
333 span: f64,
334) -> f64 {
335 let n1 = tc - sc;
336 let n2 = tr - sr;
337 if n1 == 0 || n2 == 0 {
338 return f64::INFINITY;
339 }
340
341 let mut weight_scaled = 0.0;
342 let mut i1 = 0usize; let mut i2 = 0usize; while i1 < n1 && i2 < n2 {
346 let left = (i1 * n2).max(i2 * n1);
347 let right1 = (i1 + 1) * n2;
348 let right2 = (i2 + 1) * n1;
349 let right = right1.min(right2);
350 let dt = right - left;
351
352 if dt > 0 {
353 let diff = q1[sc + i1] - rslope * q2[sr + i2];
354 weight_scaled += diff * diff * dt as f64;
355 }
356
357 if right1 < right2 {
359 i1 += 1;
360 } else if right2 < right1 {
361 i2 += 1;
362 } else {
363 i1 += 1;
364 i2 += 1;
365 }
366 }
367
368 weight_scaled / (n1 * n2) as f64 * span
370}
371
372fn uniform_spacing(argvals: &[f64]) -> Option<f64> {
378 let m = argvals.len();
379 if m < 2 {
380 return None;
381 }
382 let h = (argvals[m - 1] - argvals[0]) / (m - 1) as f64;
383 if h.is_nan() || h <= 0.0 {
384 return None;
385 }
386 let tol = h * 1e-9;
387 for k in 1..m {
388 if (argvals[k] - argvals[k - 1] - h).abs() > tol {
389 return None;
390 }
391 }
392 Some(h)
393}
394
395#[inline]
397pub(super) fn dp_lambda_penalty(
398 argvals: &[f64],
399 sc: usize,
400 tc: usize,
401 sr: usize,
402 tr: usize,
403 lambda: f64,
404) -> f64 {
405 if lambda > 0.0 {
406 let dt = argvals[tc] - argvals[sc];
407 let slope = (argvals[tr] - argvals[sr]) / dt;
408 lambda * (slope - 1.0).powi(2) * dt
409 } else {
410 0.0
411 }
412}
413
414fn dp_traceback(parent: &[u32], nrows: usize, ncols: usize) -> Vec<(usize, usize)> {
418 let mut path = Vec::with_capacity(nrows + ncols);
419 let mut cur = (nrows - 1) * ncols + (ncols - 1);
420 loop {
421 path.push((cur / ncols, cur % ncols));
422 if cur == 0 || parent[cur] == u32::MAX {
423 break;
424 }
425 cur = parent[cur] as usize;
426 }
427 path.reverse();
428 path
429}
430
431#[inline]
433fn dp_relax_cell<F>(
434 e: &mut [f64],
435 parent: &mut [u32],
436 ncols: usize,
437 tr: usize,
438 tc: usize,
439 edge_cost: &F,
440) where
441 F: Fn(usize, usize, usize, usize) -> f64,
442{
443 let idx = tr * ncols + tc;
444 for &(dr, dc) in &COPRIME_NBHD_7 {
445 if dr > tr || dc > tc {
446 continue;
447 }
448 let sr = tr - dr;
449 let sc = tc - dc;
450 let src_idx = sr * ncols + sc;
451 if e[src_idx] == f64::INFINITY {
452 continue;
453 }
454 let cost = e[src_idx] + edge_cost(sr, sc, tr, tc);
455 if cost < e[idx] {
456 e[idx] = cost;
457 parent[idx] = src_idx as u32;
458 }
459 }
460}
461
462thread_local! {
463 static DP_SCRATCH: RefCell<(Vec<f64>, Vec<u32>)> = const { RefCell::new((Vec::new(), Vec::new())) };
472}
473
474pub(super) fn dp_grid_solve<F>(nrows: usize, ncols: usize, edge_cost: F) -> Vec<(usize, usize)>
480where
481 F: Fn(usize, usize, usize, usize) -> f64,
482{
483 dp_grid_solve_banded(nrows, ncols, None, edge_cost)
484}
485
486pub(super) fn dp_grid_solve_banded<F>(
496 nrows: usize,
497 ncols: usize,
498 band: Option<usize>,
499 edge_cost: F,
500) -> Vec<(usize, usize)>
501where
502 F: Fn(usize, usize, usize, usize) -> f64,
503{
504 DP_SCRATCH.with(|cell| {
505 let mut scratch = cell.borrow_mut();
506 let (e, parent) = &mut *scratch;
507 let size = nrows * ncols;
508 e.clear();
509 e.resize(size, f64::INFINITY);
510 parent.clear();
511 parent.resize(size, u32::MAX);
512 e[0] = 0.0;
513
514 for tr in 0..nrows {
515 for tc in 0..ncols {
516 if tr == 0 && tc == 0 {
517 continue;
518 }
519 if let Some(r) = band {
520 if tr.abs_diff(tc) > r {
521 continue;
522 }
523 }
524 dp_relax_cell(e, parent, ncols, tr, tc, &edge_cost);
525 }
526 }
527
528 dp_traceback(parent, nrows, ncols)
529 })
530}
531
532pub(super) fn band_radius(band_frac: f64, m: usize) -> Option<usize> {
540 if band_frac > 0.0 && band_frac < 1.0 {
541 Some(((band_frac * m as f64).ceil() as usize).max(1))
542 } else {
543 None
544 }
545}
546
547pub(super) fn dp_path_to_gamma(path: &[(usize, usize)], argvals: &[f64]) -> Vec<f64> {
549 let path_tc: Vec<f64> = path.iter().map(|&(_, c)| argvals[c]).collect();
550 let path_tr: Vec<f64> = path.iter().map(|&(r, _)| argvals[r]).collect();
551 let mut gamma: Vec<f64> = argvals
552 .iter()
553 .map(|&t| linear_interp(&path_tc, &path_tr, t))
554 .collect();
555 normalize_warp(&mut gamma, argvals);
556 gamma
557}
558
559pub(crate) fn dp_alignment_core(q1: &[f64], q2: &[f64], argvals: &[f64], lambda: f64) -> Vec<f64> {
565 dp_alignment_core_banded(q1, q2, argvals, lambda, None)
566}
567
568pub(crate) fn dp_alignment_core_banded(
574 q1: &[f64],
575 q2: &[f64],
576 argvals: &[f64],
577 lambda: f64,
578 band: Option<usize>,
579) -> Vec<f64> {
580 let m = argvals.len();
581 if m < 2 {
582 return argvals.to_vec();
583 }
584
585 let norm1 = q1.iter().map(|&v| v * v).sum::<f64>().sqrt().max(1e-10);
586 let norm2 = q2.iter().map(|&v| v * v).sum::<f64>().sqrt().max(1e-10);
587 let q1n: Vec<f64> = q1.iter().map(|&v| v / norm1).collect();
588 let q2n: Vec<f64> = q2.iter().map(|&v| v / norm2).collect();
589
590 let path = if let Some(h) = uniform_spacing(argvals) {
591 let mut rslope_tab = [[0.0f64; 8]; 8];
596 for (dr, row) in rslope_tab.iter_mut().enumerate().skip(1) {
597 for (dc, cell) in row.iter_mut().enumerate().skip(1) {
598 *cell = (dr as f64 / dc as f64).sqrt();
599 }
600 }
601 dp_grid_solve_banded(m, m, band, |sr, sc, tr, tc| {
602 let dr = tr - sr;
603 let dc = tc - sc;
604 dp_edge_weight_core(
605 &q1n,
606 &q2n,
607 sc,
608 tc,
609 sr,
610 tr,
611 rslope_tab[dr][dc],
612 dc as f64 * h,
613 ) + dp_lambda_penalty(argvals, sc, tc, sr, tr, lambda)
614 })
615 } else {
616 dp_grid_solve_banded(m, m, band, |sr, sc, tr, tc| {
617 dp_edge_weight(&q1n, &q2n, argvals, sc, tc, sr, tr)
618 + dp_lambda_penalty(argvals, sc, tc, sr, tr, lambda)
619 })
620 };
621
622 dp_path_to_gamma(&path, argvals)
623}
624
625#[cfg(test)]
627pub(super) fn gcd(a: usize, b: usize) -> usize {
628 if b == 0 {
629 a
630 } else {
631 gcd(b, a % b)
632 }
633}
634
635#[cfg(test)]
638pub(super) fn generate_coprime_nbhd(nbhd_dim: usize) -> Vec<(usize, usize)> {
639 let mut pairs = Vec::new();
640 for i in 1..=nbhd_dim {
641 for j in 1..=nbhd_dim {
642 if gcd(i, j) == 1 {
643 pairs.push((i, j));
644 }
645 }
646 }
647 pairs
648}