1use super::srsf::{reparameterize_curve, srsf_inverse};
4use super::KarcherMeanResult;
5use crate::elastic_fpca::{horiz_fpca, sphere_karcher_mean, vert_fpca, warps_to_normalized_psi};
6use crate::error::FdarError;
7use crate::matrix::FdMatrix;
8use crate::warping::{exp_map_sphere, normalize_warp, psi_to_gam};
9
10use rand::prelude::*;
11use rand_distr::StandardNormal;
12
13#[derive(Debug, Clone, PartialEq)]
17#[non_exhaustive]
18pub struct GenerativeModelResult {
19 pub samples: FdMatrix,
21 pub warps: FdMatrix,
23 pub scores: FdMatrix,
25}
26
27#[must_use = "expensive computation whose result should not be discarded"]
46pub fn gauss_model(
47 karcher: &KarcherMeanResult,
48 argvals: &[f64],
49 ncomp: usize,
50 n_samples: usize,
51 seed: u64,
52) -> Result<GenerativeModelResult, FdarError> {
53 let (n, m) = karcher.aligned_data.shape();
54 if argvals.len() != m {
55 return Err(FdarError::InvalidDimension {
56 parameter: "argvals",
57 expected: format!("length {m}"),
58 actual: format!("length {}", argvals.len()),
59 });
60 }
61 if n < 2 || m < 2 {
62 return Err(FdarError::InvalidDimension {
63 parameter: "aligned_data",
64 expected: "n >= 2, m >= 2".to_string(),
65 actual: format!("n={n}, m={m}"),
66 });
67 }
68 if ncomp < 1 {
69 return Err(FdarError::InvalidParameter {
70 parameter: "ncomp",
71 message: "ncomp must be >= 1".to_string(),
72 });
73 }
74 if n_samples < 1 {
75 return Err(FdarError::InvalidParameter {
76 parameter: "n_samples",
77 message: "n_samples must be >= 1".to_string(),
78 });
79 }
80
81 let vert = vert_fpca(karcher, argvals, ncomp)?;
83 let vert_ncomp = vert.eigenvalues.len();
84 let m_aug = m + 1;
85
86 let horiz = horiz_fpca(karcher, argvals, ncomp)?;
88 let horiz_ncomp = horiz.eigenvalues.len();
89
90 let t0 = argvals[0];
91 let t1 = argvals[m - 1];
92 let domain = t1 - t0;
93 let time: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
94
95 let psis = warps_to_normalized_psi(&karcher.gammas, argvals);
97 let mu_psi = sphere_karcher_mean(&psis, &time, 50);
98
99 let mean_q = &vert.mean_q;
101
102 let total_ncomp = vert_ncomp + horiz_ncomp;
103 let mut samples = FdMatrix::zeros(n_samples, m);
104 let mut warps = FdMatrix::zeros(n_samples, m);
105 let mut scores = FdMatrix::zeros(n_samples, total_ncomp);
106
107 for i in 0..n_samples {
108 let mut rng = StdRng::seed_from_u64(seed + i as u64);
109
110 let mut q_new = vec![0.0; m_aug];
112 q_new[..m_aug].copy_from_slice(&mean_q[..m_aug]);
113 for k in 0..vert_ncomp {
114 let std_dev = vert.eigenvalues[k].max(0.0).sqrt();
115 let z: f64 = rng.sample(StandardNormal);
116 let score_k = z * std_dev;
117 scores[(i, k)] = score_k;
118 for j in 0..m_aug {
119 q_new[j] += score_k * vert.eigenfunctions_q[(k, j)];
120 }
121 }
122
123 let aug_val = q_new[m];
129 let f_mid = aug_val.signum() * aug_val * aug_val;
130 let mut f_new = srsf_inverse(&q_new[..m], argvals, 0.0);
131 let shift = f_mid - f_new[m / 2];
132 for val in f_new.iter_mut() {
133 *val += shift;
134 }
135
136 let mut v = vec![0.0; m];
138 for k in 0..horiz_ncomp {
139 let std_dev = horiz.eigenvalues[k].max(0.0).sqrt();
140 let z: f64 = rng.sample(StandardNormal);
141 let score_k = z * std_dev;
142 scores[(i, vert_ncomp + k)] = score_k;
143 for j in 0..m {
144 v[j] += score_k * horiz.eigenfunctions_psi[(k, j)];
145 }
146 }
147
148 let psi_new = exp_map_sphere(&mu_psi, &v, &time);
150 let gam_01 = psi_to_gam(&psi_new, &time);
151
152 let mut gamma: Vec<f64> = gam_01.iter().map(|&g| t0 + g * domain).collect();
154 normalize_warp(&mut gamma, argvals);
155
156 let sample = reparameterize_curve(&f_new, argvals, &gamma);
158
159 for j in 0..m {
160 samples[(i, j)] = sample[j];
161 warps[(i, j)] = gamma[j];
162 }
163 }
164
165 Ok(GenerativeModelResult {
166 samples,
167 warps,
168 scores,
169 })
170}
171
172#[must_use = "expensive computation whose result should not be discarded"]
191pub fn joint_gauss_model(
192 karcher: &KarcherMeanResult,
193 argvals: &[f64],
194 ncomp: usize,
195 n_samples: usize,
196 balance_c: f64,
197 seed: u64,
198) -> Result<GenerativeModelResult, FdarError> {
199 let (_n, m) = karcher.aligned_data.shape();
200 if argvals.len() != m {
201 return Err(FdarError::InvalidDimension {
202 parameter: "argvals",
203 expected: format!("length {m}"),
204 actual: format!("length {}", argvals.len()),
205 });
206 }
207 if ncomp < 1 {
208 return Err(FdarError::InvalidParameter {
209 parameter: "ncomp",
210 message: "ncomp must be >= 1".to_string(),
211 });
212 }
213 if n_samples < 1 {
214 return Err(FdarError::InvalidParameter {
215 parameter: "n_samples",
216 message: "n_samples must be >= 1".to_string(),
217 });
218 }
219
220 let vert = vert_fpca(karcher, argvals, ncomp)?;
222 let vert_ncomp = vert.eigenvalues.len();
223 let m_aug = m + 1;
224
225 let horiz = horiz_fpca(karcher, argvals, ncomp)?;
227 let horiz_ncomp = horiz.eigenvalues.len();
228
229 let total_ncomp = vert_ncomp + horiz_ncomp;
230 let n = karcher.aligned_data.nrows();
231
232 let mut joint_scores = FdMatrix::zeros(n, total_ncomp);
234 for i in 0..n {
235 for k in 0..vert_ncomp {
236 joint_scores[(i, k)] = vert.scores[(i, k)];
237 }
238 for k in 0..horiz_ncomp {
239 joint_scores[(i, vert_ncomp + k)] = balance_c * horiz.scores[(i, k)];
240 }
241 }
242
243 let mut joint_mean = vec![0.0; total_ncomp];
245 for k in 0..total_ncomp {
246 for i in 0..n {
247 joint_mean[k] += joint_scores[(i, k)];
248 }
249 joint_mean[k] /= n as f64;
250 }
251
252 let mut joint_var = vec![0.0; total_ncomp];
253 for k in 0..total_ncomp {
254 for i in 0..n {
255 let diff = joint_scores[(i, k)] - joint_mean[k];
256 joint_var[k] += diff * diff;
257 }
258 joint_var[k] /= (n - 1).max(1) as f64;
259 }
260
261 let t0 = argvals[0];
263 let t1 = argvals[m - 1];
264 let domain = t1 - t0;
265 let time: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
266
267 let psis = warps_to_normalized_psi(&karcher.gammas, argvals);
268 let mu_psi = sphere_karcher_mean(&psis, &time, 50);
269 let mean_q = &vert.mean_q;
270
271 let mut samples = FdMatrix::zeros(n_samples, m);
272 let mut warps_out = FdMatrix::zeros(n_samples, m);
273 let mut scores_out = FdMatrix::zeros(n_samples, total_ncomp);
274
275 for i in 0..n_samples {
276 let mut rng = StdRng::seed_from_u64(seed + i as u64);
277
278 let mut joint_z = vec![0.0; total_ncomp];
280 for k in 0..total_ncomp {
281 let z: f64 = rng.sample(StandardNormal);
282 joint_z[k] = joint_mean[k] + z * joint_var[k].max(0.0).sqrt();
283 scores_out[(i, k)] = joint_z[k];
284 }
285
286 let mut q_new = vec![0.0; m_aug];
288 q_new[..m_aug].copy_from_slice(&mean_q[..m_aug]);
289 for k in 0..vert_ncomp {
290 let score_k = joint_z[k];
291 for j in 0..m_aug {
292 q_new[j] += score_k * vert.eigenfunctions_q[(k, j)];
293 }
294 }
295 let aug_val = q_new[m];
299 let f_mid = aug_val.signum() * aug_val * aug_val;
300 let mut f_new = srsf_inverse(&q_new[..m], argvals, 0.0);
301 let shift = f_mid - f_new[m / 2];
302 for val in f_new.iter_mut() {
303 *val += shift;
304 }
305
306 let mut v = vec![0.0; m];
308 for k in 0..horiz_ncomp {
309 let score_k = if balance_c.abs() > 1e-15 {
311 joint_z[vert_ncomp + k] / balance_c
312 } else {
313 0.0
314 };
315 for j in 0..m {
316 v[j] += score_k * horiz.eigenfunctions_psi[(k, j)];
317 }
318 }
319
320 let psi_new = exp_map_sphere(&mu_psi, &v, &time);
321 let gam_01 = psi_to_gam(&psi_new, &time);
322 let mut gamma: Vec<f64> = gam_01.iter().map(|&g| t0 + g * domain).collect();
323 normalize_warp(&mut gamma, argvals);
324
325 let sample = reparameterize_curve(&f_new, argvals, &gamma);
326 for j in 0..m {
327 samples[(i, j)] = sample[j];
328 warps_out[(i, j)] = gamma[j];
329 }
330 }
331
332 Ok(GenerativeModelResult {
333 samples,
334 warps: warps_out,
335 scores: scores_out,
336 })
337}
338
339#[cfg(test)]
342mod tests {
343 use super::*;
344 use crate::alignment::karcher_mean;
345 use std::f64::consts::PI;
346
347 fn make_test_karcher(n: usize, m: usize) -> (KarcherMeanResult, Vec<f64>) {
348 let t: Vec<f64> = (0..m).map(|j| j as f64 / (m - 1) as f64).collect();
349 let mut data = FdMatrix::zeros(n, m);
350 for i in 0..n {
351 let shift = 0.1 * (i as f64 - n as f64 / 2.0);
352 let scale = 1.0 + 0.2 * (i as f64 / n as f64);
353 for j in 0..m {
354 data[(i, j)] = scale * (2.0 * PI * (t[j] + shift)).sin();
355 }
356 }
357 let km = karcher_mean(&data, &t, 10, 1e-4, 0.0);
358 (km, t)
359 }
360
361 #[test]
362 fn gauss_model_correct_shapes() {
363 let (km, t) = make_test_karcher(15, 51);
364 let ncomp = 3;
365 let n_samples = 10;
366 let result = gauss_model(&km, &t, ncomp, n_samples, 42).unwrap();
367
368 assert_eq!(result.samples.shape(), (n_samples, 51));
369 assert_eq!(result.warps.shape(), (n_samples, 51));
370 let (_, score_cols) = result.scores.shape();
372 assert!(
373 score_cols >= ncomp,
374 "scores should have at least ncomp columns, got {score_cols}"
375 );
376 assert_eq!(result.scores.nrows(), n_samples);
377 }
378
379 #[test]
380 fn gauss_model_reproducible() {
381 let (km, t) = make_test_karcher(15, 51);
382 let r1 = gauss_model(&km, &t, 3, 5, 42).unwrap();
383 let r2 = gauss_model(&km, &t, 3, 5, 42).unwrap();
384
385 assert_eq!(r1.samples, r2.samples);
386 assert_eq!(r1.warps, r2.warps);
387 assert_eq!(r1.scores, r2.scores);
388 }
389
390 #[test]
391 fn gauss_model_warps_valid() {
392 let (km, t) = make_test_karcher(15, 51);
393 let result = gauss_model(&km, &t, 3, 10, 99).unwrap();
394 let m = t.len();
395
396 for i in 0..result.warps.nrows() {
397 let warp = result.warps.row(i);
398
399 for j in 1..m {
401 assert!(
402 warp[j] >= warp[j - 1] - 1e-12,
403 "warp {i} not monotone at j={j}: {} < {}",
404 warp[j],
405 warp[j - 1]
406 );
407 }
408
409 assert!(
411 (warp[0] - t[0]).abs() < 1e-10,
412 "warp {i} start: {} != {}",
413 warp[0],
414 t[0]
415 );
416 assert!(
417 (warp[m - 1] - t[m - 1]).abs() < 1e-10,
418 "warp {i} end: {} != {}",
419 warp[m - 1],
420 t[m - 1]
421 );
422 }
423 }
424
425 #[test]
430 fn gauss_model_sample_mean_no_constant_offset() {
431 let m = 60;
433 let t: Vec<f64> = (0..m).map(|j| j as f64 / (m - 1) as f64).collect();
434 let n = 20;
435 let mut data = FdMatrix::zeros(n, m);
436 for i in 0..n {
437 let amp = 0.8 + 0.02 * i as f64;
438 for j in 0..m {
439 let x = t[j] - 0.5;
440 data[(i, j)] = amp * (-(x * x) / 0.02).exp();
441 }
442 }
443 let km = karcher_mean(&data, &t, 15, 1e-4, 0.0);
444 let result = gauss_model(&km, &t, 3, 300, 42).unwrap();
445 let samples = &result.samples;
446 let ns = samples.nrows();
447
448 let mut offset = 0.0;
451 for j in 0..m {
452 let dm = (0..n).map(|i| data[(i, j)]).sum::<f64>() / n as f64;
453 let sm = (0..ns).map(|i| samples[(i, j)]).sum::<f64>() / ns as f64;
454 offset += sm - dm;
455 }
456 offset /= m as f64;
457 assert!(
458 offset.abs() < 0.3,
459 "sample mean has a constant offset of {offset:.3} from the data mean (GH #34)"
460 );
461 }
462
463 #[test]
464 fn joint_gauss_model_smoke() {
465 let (km, t) = make_test_karcher(15, 51);
466 let ncomp = 3;
467 let n_samples = 8;
468 let result = joint_gauss_model(&km, &t, ncomp, n_samples, 1.0, 42).unwrap();
469
470 assert_eq!(result.samples.shape(), (n_samples, 51));
471 assert_eq!(result.warps.shape(), (n_samples, 51));
472 assert_eq!(result.scores.nrows(), n_samples);
473
474 for i in 0..n_samples {
476 for j in 0..51 {
477 assert!(
478 result.samples[(i, j)].is_finite(),
479 "sample ({i},{j}) is not finite"
480 );
481 }
482 }
483 }
484}