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