1use super::karcher::karcher_mean;
4use super::pairwise::{elastic_align_pair, elastic_distance};
5use super::srsf::{compose_warps, reparameterize_curve};
6use crate::error::FdarError;
7use crate::iter_maybe_parallel;
8use crate::matrix::FdMatrix;
9#[cfg(feature = "parallel")]
10use rayon::iter::ParallelIterator;
11
12#[non_exhaustive]
18#[derive(Debug, Clone, PartialEq)]
19pub struct TransferAlignConfig {
20 pub lambda: f64,
22 pub max_iter: usize,
24 pub tol: f64,
26}
27
28impl Default for TransferAlignConfig {
29 fn default() -> Self {
30 Self {
31 lambda: 0.0,
32 max_iter: 15,
33 tol: 1e-3,
34 }
35 }
36}
37
38#[derive(Debug, Clone, PartialEq)]
40#[non_exhaustive]
41pub struct TransferAlignResult {
42 pub source_mean: Vec<f64>,
44 pub aligned_data: FdMatrix,
46 pub gammas: FdMatrix,
48 pub bridging_gamma: Vec<f64>,
50 pub distances: Vec<f64>,
52}
53
54#[must_use = "expensive computation whose result should not be discarded"]
73pub fn transfer_alignment(
74 source_data: &FdMatrix,
75 target_data: &FdMatrix,
76 argvals: &[f64],
77 config: &TransferAlignConfig,
78) -> Result<TransferAlignResult, FdarError> {
79 let (n_source, m_source) = source_data.shape();
80 let (n_target, m_target) = target_data.shape();
81
82 if m_source != m_target {
84 return Err(FdarError::InvalidDimension {
85 parameter: "target_data",
86 expected: format!("{m_source} columns (matching source_data)"),
87 actual: format!("{m_target} columns"),
88 });
89 }
90 let m = m_source;
91 if argvals.len() != m {
92 return Err(FdarError::InvalidDimension {
93 parameter: "argvals",
94 expected: format!("{m}"),
95 actual: format!("{}", argvals.len()),
96 });
97 }
98 if n_source < 1 {
99 return Err(FdarError::InvalidDimension {
100 parameter: "source_data",
101 expected: "at least 1 row".to_string(),
102 actual: format!("{n_source} rows"),
103 });
104 }
105 if n_target < 1 {
106 return Err(FdarError::InvalidDimension {
107 parameter: "target_data",
108 expected: "at least 1 row".to_string(),
109 actual: format!("{n_target} rows"),
110 });
111 }
112
113 let source_karcher = karcher_mean(
115 source_data,
116 argvals,
117 config.max_iter,
118 config.tol,
119 config.lambda,
120 );
121
122 let target_karcher = karcher_mean(
124 target_data,
125 argvals,
126 config.max_iter,
127 config.tol,
128 config.lambda,
129 );
130
131 let bridge_result = elastic_align_pair(
133 &source_karcher.mean,
134 &target_karcher.mean,
135 argvals,
136 config.lambda,
137 );
138
139 let results: Vec<(Vec<f64>, Vec<f64>, f64)> = iter_maybe_parallel!(0..n_target)
143 .map(|i| {
144 let within_gamma = target_karcher.gammas.row(i);
146
147 let gamma_total = compose_warps(&bridge_result.gamma, &within_gamma, argvals);
149
150 let aligned_i = reparameterize_curve(&target_data.row(i), argvals, &gamma_total);
152
153 let dist_i = elastic_distance(&source_karcher.mean, &aligned_i, argvals, config.lambda);
155
156 (gamma_total, aligned_i, dist_i)
157 })
158 .collect();
159
160 let mut gammas = FdMatrix::zeros(n_target, m);
162 let mut aligned_data = FdMatrix::zeros(n_target, m);
163 let mut distances = Vec::with_capacity(n_target);
164
165 for (i, (gamma, aligned, dist)) in results.into_iter().enumerate() {
166 for j in 0..m {
167 gammas[(i, j)] = gamma[j];
168 aligned_data[(i, j)] = aligned[j];
169 }
170 distances.push(dist);
171 }
172
173 Ok(TransferAlignResult {
174 source_mean: source_karcher.mean,
175 aligned_data,
176 gammas,
177 bridging_gamma: bridge_result.gamma,
178 distances,
179 })
180}
181
182#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::simulation::{sim_fundata, EFunType, EValType};
188 use crate::test_helpers::uniform_grid;
189
190 fn make_data(n: usize, m: usize, seed: u64) -> (FdMatrix, Vec<f64>) {
191 let t = uniform_grid(m);
192 let data = sim_fundata(
193 n,
194 &t,
195 3,
196 EFunType::Fourier,
197 EValType::Exponential,
198 Some(seed),
199 );
200 (data, t)
201 }
202
203 #[test]
204 fn transfer_same_population() {
205 let (data, t) = make_data(8, 20, 42);
206 let config = TransferAlignConfig {
207 max_iter: 5,
208 tol: 1e-2,
209 ..Default::default()
210 };
211 let result = transfer_alignment(&data, &data, &t, &config).unwrap();
212
213 let max_dev: f64 = result
215 .bridging_gamma
216 .iter()
217 .zip(t.iter())
218 .map(|(&g, &ti)| (g - ti).abs())
219 .fold(0.0_f64, f64::max);
220 assert!(
221 max_dev < 0.3,
222 "bridging warp should be near identity for same population, max_dev={max_dev}"
223 );
224
225 for (i, &d) in result.distances.iter().enumerate() {
227 assert!(
228 d < 5.0,
229 "distance[{i}]={d} should be small for same-population transfer"
230 );
231 }
232 }
233
234 #[test]
235 fn transfer_shifted_population() {
236 let (source, t) = make_data(8, 20, 42);
237 let m = t.len();
238 let n = source.nrows();
239
240 let mut target = FdMatrix::zeros(n, m);
242 for i in 0..n {
243 for j in 0..m {
244 target[(i, j)] = source[(i, j)] + 2.0;
245 }
246 }
247
248 let config = TransferAlignConfig {
249 max_iter: 5,
250 tol: 1e-2,
251 ..Default::default()
252 };
253 let result = transfer_alignment(&source, &target, &t, &config).unwrap();
254
255 let source_mean = &result.source_mean;
258 let raw_mean_dist: f64 = (0..m)
259 .map(|j| {
260 let diff = target[(0, j)] - source_mean[j];
261 diff * diff
262 })
263 .sum::<f64>()
264 .sqrt();
265
266 let aligned_mean_dist: f64 = (0..m)
267 .map(|j| {
268 let diff = result.aligned_data[(0, j)] - source_mean[j];
269 diff * diff
270 })
271 .sum::<f64>()
272 .sqrt();
273
274 assert!(
277 aligned_mean_dist < raw_mean_dist + 1.0,
278 "aligned dist ({aligned_mean_dist:.2}) should not be much worse than raw dist ({raw_mean_dist:.2})"
279 );
280 }
281
282 #[test]
283 fn transfer_output_dimensions() {
284 let (source, t) = make_data(6, 20, 42);
285 let (target, _) = make_data(10, 20, 99);
286 let config = TransferAlignConfig {
287 max_iter: 3,
288 tol: 1e-2,
289 ..Default::default()
290 };
291 let result = transfer_alignment(&source, &target, &t, &config).unwrap();
292
293 assert_eq!(result.aligned_data.shape(), (10, 20));
294 assert_eq!(result.gammas.shape(), (10, 20));
295 assert_eq!(result.distances.len(), 10);
296 assert_eq!(result.source_mean.len(), 20);
297 assert_eq!(result.bridging_gamma.len(), 20);
298 }
299
300 #[test]
301 fn transfer_config_default() {
302 let config = TransferAlignConfig::default();
303 assert!((config.lambda - 0.0).abs() < f64::EPSILON);
304 assert_eq!(config.max_iter, 15);
305 assert!((config.tol - 1e-3).abs() < f64::EPSILON);
306 }
307}