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, elastic_hierarchical, elastic_kmeans, ElasticClusterConfig,
53 ElasticClusterMethod, ElasticClusterResult, ElasticDendrogram,
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;
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_penalized, elastic_cross_distance_matrix, elastic_distance,
78 elastic_self_distance_matrix, phase_distance_pair, phase_self_distance_matrix, WarpPenaltyType,
79};
80pub use partial_match::{elastic_partial_match, PartialMatchConfig, PartialMatchResult};
81pub use persistence::{peak_persistence, PersistenceDiagramResult};
82pub use phase_boxplot::{phase_boxplot, PhaseBoxplot};
83pub use quality::{
84 alignment_quality, pairwise_consistency, warp_complexity, warp_smoothness, AlignmentQuality,
85};
86pub use robust_karcher::{
87 karcher_median, robust_karcher_mean, RobustKarcherConfig, RobustKarcherResult,
88};
89pub use set::{align_to_target, elastic_decomposition, DecompositionResult};
90pub use shape::{
91 orbit_representative, shape_distance, shape_mean, shape_self_distance_matrix,
92 OrbitRepresentative, ShapeDistanceResult, ShapeMeanResult, ShapeQuotient,
93};
94pub use shape_ci::{shape_confidence_interval, ShapeCiConfig, ShapeCiResult};
95pub use srsf::{
96 compose_warps, invert_warp, reparameterize_curve, srsf_inverse, srsf_transform,
97 warp_inverse_error,
98};
99pub use transfer::{transfer_alignment, TransferAlignConfig, TransferAlignResult};
100pub use tsrvf::{
101 tsrvf_from_alignment, tsrvf_from_alignment_with_method, tsrvf_inverse, tsrvf_transform,
102 tsrvf_transform_with_method, TransportMethod, TsrvfResult,
103};
104pub use warp_stats::{warp_statistics, WarpStatistics};
105
106pub(crate) use karcher::sqrt_mean_inverse;
108
109use crate::helpers::linear_interp;
110use crate::matrix::FdMatrix;
111use crate::warping::normalize_warp;
112
113#[derive(Debug, Clone, PartialEq)]
117#[non_exhaustive]
118pub struct AlignmentResult {
119 pub gamma: Vec<f64>,
121 pub f_aligned: Vec<f64>,
123 pub distance: f64,
125}
126
127#[derive(Debug, Clone, PartialEq)]
129#[non_exhaustive]
130pub struct AlignmentSetResult {
131 pub gammas: FdMatrix,
133 pub aligned_data: FdMatrix,
135 pub distances: Vec<f64>,
137}
138
139#[derive(Debug, Clone, PartialEq)]
141#[non_exhaustive]
142pub struct KarcherMeanResult {
143 pub mean: Vec<f64>,
145 pub mean_srsf: Vec<f64>,
147 pub gammas: FdMatrix,
149 pub aligned_data: FdMatrix,
151 pub n_iter: usize,
153 pub converged: bool,
155 pub aligned_srsfs: Option<FdMatrix>,
158}
159
160impl KarcherMeanResult {
161 pub fn new(
163 mean: Vec<f64>,
164 mean_srsf: Vec<f64>,
165 gammas: FdMatrix,
166 aligned_data: FdMatrix,
167 n_iter: usize,
168 converged: bool,
169 aligned_srsfs: Option<FdMatrix>,
170 ) -> Self {
171 Self {
172 mean,
173 mean_srsf,
174 gammas,
175 aligned_data,
176 n_iter,
177 converged,
178 aligned_srsfs,
179 }
180 }
181}
182
183#[rustfmt::skip]
190const COPRIME_NBHD_7: [(usize, usize); 35] = [
191 (1,1),(1,2),(1,3),(1,4),(1,5),(1,6),(1,7),
192 (2,1), (2,3), (2,5), (2,7),
193 (3,1),(3,2), (3,4),(3,5), (3,7),
194 (4,1), (4,3), (4,5), (4,7),
195 (5,1),(5,2),(5,3),(5,4), (5,6),(5,7),
196 (6,1), (6,5), (6,7),
197 (7,1),(7,2),(7,3),(7,4),(7,5),(7,6),
198];
199
200#[inline]
208pub(super) fn dp_edge_weight(
209 q1: &[f64],
210 q2: &[f64],
211 argvals: &[f64],
212 sc: usize,
213 tc: usize,
214 sr: usize,
215 tr: usize,
216) -> f64 {
217 let n1 = tc - sc;
218 let n2 = tr - sr;
219 if n1 == 0 || n2 == 0 {
220 return f64::INFINITY;
221 }
222
223 let slope = (argvals[tr] - argvals[sr]) / (argvals[tc] - argvals[sc]);
224 let rslope = slope.sqrt();
225
226 let mut weight = 0.0;
228 let mut i1 = 0usize; let mut i2 = 0usize; while i1 < n1 && i2 < n2 {
232 let left1 = i1 as f64 / n1 as f64;
234 let right1 = (i1 + 1) as f64 / n1 as f64;
235 let left2 = i2 as f64 / n2 as f64;
236 let right2 = (i2 + 1) as f64 / n2 as f64;
237
238 let left = left1.max(left2);
239 let right = right1.min(right2);
240 let dt = right - left;
241
242 if dt > 0.0 {
243 let diff = q1[sc + i1] - rslope * q2[sr + i2];
244 weight += diff * diff * dt;
245 }
246
247 if right1 < right2 {
249 i1 += 1;
250 } else if right2 < right1 {
251 i2 += 1;
252 } else {
253 i1 += 1;
254 i2 += 1;
255 }
256 }
257
258 weight * (argvals[tc] - argvals[sc])
260}
261
262#[inline]
264pub(super) fn dp_lambda_penalty(
265 argvals: &[f64],
266 sc: usize,
267 tc: usize,
268 sr: usize,
269 tr: usize,
270 lambda: f64,
271) -> f64 {
272 if lambda > 0.0 {
273 let dt = argvals[tc] - argvals[sc];
274 let slope = (argvals[tr] - argvals[sr]) / dt;
275 lambda * (slope - 1.0).powi(2) * dt
276 } else {
277 0.0
278 }
279}
280
281fn dp_traceback(parent: &[u32], nrows: usize, ncols: usize) -> Vec<(usize, usize)> {
285 let mut path = Vec::with_capacity(nrows + ncols);
286 let mut cur = (nrows - 1) * ncols + (ncols - 1);
287 loop {
288 path.push((cur / ncols, cur % ncols));
289 if cur == 0 || parent[cur] == u32::MAX {
290 break;
291 }
292 cur = parent[cur] as usize;
293 }
294 path.reverse();
295 path
296}
297
298#[inline]
300fn dp_relax_cell<F>(
301 e: &mut [f64],
302 parent: &mut [u32],
303 ncols: usize,
304 tr: usize,
305 tc: usize,
306 edge_cost: &F,
307) where
308 F: Fn(usize, usize, usize, usize) -> f64,
309{
310 let idx = tr * ncols + tc;
311 for &(dr, dc) in &COPRIME_NBHD_7 {
312 if dr > tr || dc > tc {
313 continue;
314 }
315 let sr = tr - dr;
316 let sc = tc - dc;
317 let src_idx = sr * ncols + sc;
318 if e[src_idx] == f64::INFINITY {
319 continue;
320 }
321 let cost = e[src_idx] + edge_cost(sr, sc, tr, tc);
322 if cost < e[idx] {
323 e[idx] = cost;
324 parent[idx] = src_idx as u32;
325 }
326 }
327}
328
329pub(super) fn dp_grid_solve<F>(nrows: usize, ncols: usize, edge_cost: F) -> Vec<(usize, usize)>
335where
336 F: Fn(usize, usize, usize, usize) -> f64,
337{
338 let mut e = vec![f64::INFINITY; nrows * ncols];
339 let mut parent = vec![u32::MAX; nrows * ncols];
340 e[0] = 0.0;
341
342 for tr in 0..nrows {
343 for tc in 0..ncols {
344 if tr == 0 && tc == 0 {
345 continue;
346 }
347 dp_relax_cell(&mut e, &mut parent, ncols, tr, tc, &edge_cost);
348 }
349 }
350
351 dp_traceback(&parent, nrows, ncols)
352}
353
354pub(super) fn dp_path_to_gamma(path: &[(usize, usize)], argvals: &[f64]) -> Vec<f64> {
356 let path_tc: Vec<f64> = path.iter().map(|&(_, c)| argvals[c]).collect();
357 let path_tr: Vec<f64> = path.iter().map(|&(r, _)| argvals[r]).collect();
358 let mut gamma: Vec<f64> = argvals
359 .iter()
360 .map(|&t| linear_interp(&path_tc, &path_tr, t))
361 .collect();
362 normalize_warp(&mut gamma, argvals);
363 gamma
364}
365
366pub(crate) fn dp_alignment_core(q1: &[f64], q2: &[f64], argvals: &[f64], lambda: f64) -> Vec<f64> {
372 let m = argvals.len();
373 if m < 2 {
374 return argvals.to_vec();
375 }
376
377 let norm1 = q1.iter().map(|&v| v * v).sum::<f64>().sqrt().max(1e-10);
378 let norm2 = q2.iter().map(|&v| v * v).sum::<f64>().sqrt().max(1e-10);
379 let q1n: Vec<f64> = q1.iter().map(|&v| v / norm1).collect();
380 let q2n: Vec<f64> = q2.iter().map(|&v| v / norm2).collect();
381
382 let path = dp_grid_solve(m, m, |sr, sc, tr, tc| {
383 dp_edge_weight(&q1n, &q2n, argvals, sc, tc, sr, tr)
384 + dp_lambda_penalty(argvals, sc, tc, sr, tr, lambda)
385 });
386
387 dp_path_to_gamma(&path, argvals)
388}
389
390#[cfg(test)]
392pub(super) fn gcd(a: usize, b: usize) -> usize {
393 if b == 0 {
394 a
395 } else {
396 gcd(b, a % b)
397 }
398}
399
400#[cfg(test)]
403pub(super) fn generate_coprime_nbhd(nbhd_dim: usize) -> Vec<(usize, usize)> {
404 let mut pairs = Vec::new();
405 for i in 1..=nbhd_dim {
406 for j in 1..=nbhd_dim {
407 if gcd(i, j) == 1 {
408 pairs.push((i, j));
409 }
410 }
411 }
412 pairs
413}