use super::fpca::fpca_tolerance_band;
use super::helpers::valid_band_params;
use super::{
BandType, ElasticToleranceBandResult, ElasticToleranceConfig, PhaseToleranceBand, ToleranceBand,
};
use crate::alignment::KarcherMeanResult;
use crate::elastic_fpca::{
shooting_vectors_from_psis, sphere_karcher_mean, warps_to_normalized_psi,
};
use crate::error::FdarError;
use crate::matrix::FdMatrix;
use crate::warping::{exp_map_sphere, normalize_warp, psi_to_gam};
fn validate_elastic_inputs(
data: &FdMatrix,
argvals: &[f64],
ncomp: usize,
nb: usize,
coverage: f64,
max_iter: usize,
) -> Result<(usize, usize), FdarError> {
let (n, m) = data.shape();
if n < 3 || m == 0 {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: "at least 3 rows and 1 column".to_string(),
actual: format!("{n} x {m}"),
});
}
if argvals.len() != m {
return Err(FdarError::InvalidDimension {
parameter: "argvals",
expected: format!("length {m} (matching data columns)"),
actual: format!("length {}", argvals.len()),
});
}
if !valid_band_params(n, m, ncomp, nb, coverage) {
return Err(FdarError::InvalidParameter {
parameter: "ncomp/nb/coverage",
message: "ncomp and nb must be >= 1, coverage must be in (0, 1)".to_string(),
});
}
if max_iter == 0 {
return Err(FdarError::InvalidParameter {
parameter: "max_iter",
message: "must be >= 1".to_string(),
});
}
Ok((n, m))
}
#[must_use = "expensive computation whose result should not be discarded"]
pub fn elastic_tolerance_band(
data: &FdMatrix,
argvals: &[f64],
ncomp: usize,
nb: usize,
coverage: f64,
band_type: BandType,
max_iter: usize,
seed: u64,
) -> Result<ToleranceBand, FdarError> {
validate_elastic_inputs(data, argvals, ncomp, nb, coverage, max_iter)?;
let karcher = crate::alignment::karcher_mean(data, argvals, max_iter, 1e-4, 0.0);
fpca_tolerance_band(&karcher.aligned_data, ncomp, nb, coverage, band_type, seed)
}
fn phase_band_from_karcher(
karcher: &KarcherMeanResult,
argvals: &[f64],
ncomp: usize,
nb: usize,
coverage: f64,
band_type: BandType,
seed: u64,
) -> Result<PhaseToleranceBand, FdarError> {
let m = argvals.len();
let time: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
let psis = warps_to_normalized_psi(&karcher.gammas, argvals);
let mu_psi = sphere_karcher_mean(&psis, &time, 20);
let shooting = shooting_vectors_from_psis(&psis, &mu_psi, &time);
let tangent_band = fpca_tolerance_band(&shooting, ncomp, nb, coverage, band_type, seed)?;
let tangent_lower: Vec<f64> = tangent_band
.center
.iter()
.zip(tangent_band.half_width.iter())
.map(|(&c, &h)| c - h)
.collect();
let tangent_upper: Vec<f64> = tangent_band
.center
.iter()
.zip(tangent_band.half_width.iter())
.map(|(&c, &h)| c + h)
.collect();
let psi_lower = exp_map_sphere(&mu_psi, &tangent_lower, &time);
let psi_upper = exp_map_sphere(&mu_psi, &tangent_upper, &time);
let mut gamma_lower = psi_to_gam(&psi_lower, &time);
let mut gamma_upper = psi_to_gam(&psi_upper, &time);
let t0 = argvals[0];
let domain = argvals[m - 1] - t0;
for j in 0..m {
gamma_lower[j] = t0 + gamma_lower[j] * domain;
gamma_upper[j] = t0 + gamma_upper[j] * domain;
}
normalize_warp(&mut gamma_lower, argvals);
normalize_warp(&mut gamma_upper, argvals);
let gamma_center = argvals.to_vec();
Ok(PhaseToleranceBand {
gamma_lower,
gamma_upper,
gamma_center,
tangent_band,
})
}
#[must_use = "expensive computation whose result should not be discarded"]
pub fn phase_tolerance_band(
data: &FdMatrix,
argvals: &[f64],
ncomp: usize,
nb: usize,
coverage: f64,
band_type: BandType,
max_iter: usize,
seed: u64,
) -> Result<PhaseToleranceBand, FdarError> {
validate_elastic_inputs(data, argvals, ncomp, nb, coverage, max_iter)?;
let karcher = crate::alignment::karcher_mean(data, argvals, max_iter, 1e-4, 0.0);
phase_band_from_karcher(&karcher, argvals, ncomp, nb, coverage, band_type, seed)
}
#[must_use = "expensive computation whose result should not be discarded"]
pub fn elastic_tolerance_band_with_config(
data: &FdMatrix,
argvals: &[f64],
config: &ElasticToleranceConfig,
) -> Result<ElasticToleranceBandResult, FdarError> {
validate_elastic_inputs(
data,
argvals,
config.ncomp_amplitude,
config.nb,
config.coverage,
config.max_iter,
)?;
if config.ncomp_phase == 0 {
return Err(FdarError::InvalidParameter {
parameter: "ncomp_phase",
message: "must be >= 1".to_string(),
});
}
let karcher = crate::alignment::karcher_mean(data, argvals, config.max_iter, config.tol, 0.0);
let amplitude = fpca_tolerance_band(
&karcher.aligned_data,
config.ncomp_amplitude,
config.nb,
config.coverage,
config.band_type,
config.seed,
)?;
let phase = phase_band_from_karcher(
&karcher,
argvals,
config.ncomp_phase,
config.nb,
config.coverage,
config.band_type,
config.seed,
)?;
Ok(ElasticToleranceBandResult { amplitude, phase })
}