1use rand::Rng;
4
5use super::karcher::karcher_mean;
6use super::pairwise::elastic_align_pair;
7use crate::error::FdarError;
8use crate::iter_maybe_parallel;
9use crate::matrix::FdMatrix;
10#[cfg(feature = "parallel")]
11use rayon::iter::ParallelIterator;
12
13#[non_exhaustive]
19#[derive(Debug, Clone, PartialEq)]
20pub struct ShapeCiConfig {
21 pub n_bootstrap: usize,
23 pub confidence_level: f64,
25 pub lambda: f64,
27 pub max_iter: usize,
29 pub tol: f64,
31 pub seed: u64,
33}
34
35impl Default for ShapeCiConfig {
36 fn default() -> Self {
37 Self {
38 n_bootstrap: 200,
39 confidence_level: 0.95,
40 lambda: 0.0,
41 max_iter: 15,
42 tol: 1e-3,
43 seed: 42,
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq)]
50#[non_exhaustive]
51pub struct ShapeCiResult {
52 pub mean: Vec<f64>,
54 pub lower_band: Vec<f64>,
56 pub upper_band: Vec<f64>,
58 pub bootstrap_means: FdMatrix,
60}
61
62#[must_use = "expensive computation whose result should not be discarded"]
81pub fn shape_confidence_interval(
82 data: &FdMatrix,
83 argvals: &[f64],
84 config: &ShapeCiConfig,
85) -> Result<ShapeCiResult, FdarError> {
86 let (n, m) = data.shape();
87
88 if argvals.len() != m {
90 return Err(FdarError::InvalidDimension {
91 parameter: "argvals",
92 expected: format!("{m}"),
93 actual: format!("{}", argvals.len()),
94 });
95 }
96 if n < 3 {
97 return Err(FdarError::InvalidDimension {
98 parameter: "data",
99 expected: "at least 3 rows".to_string(),
100 actual: format!("{n} rows"),
101 });
102 }
103 if config.confidence_level <= 0.0 || config.confidence_level >= 1.0 {
104 return Err(FdarError::InvalidParameter {
105 parameter: "confidence_level",
106 message: format!("must be in (0, 1), got {}", config.confidence_level),
107 });
108 }
109 if config.n_bootstrap < 1 {
110 return Err(FdarError::InvalidParameter {
111 parameter: "n_bootstrap",
112 message: format!("must be >= 1, got {}", config.n_bootstrap),
113 });
114 }
115
116 let full_karcher = karcher_mean(data, argvals, config.max_iter, config.tol, config.lambda);
118
119 let boot_means: Vec<Vec<f64>> = iter_maybe_parallel!(0..config.n_bootstrap)
121 .map(|b| {
122 let mut rng = crate::helpers::seed_for_thread(config.seed, b);
123
124 let indices: Vec<usize> = (0..n).map(|_| rng.gen_range(0..n)).collect();
126
127 let mut boot_data = FdMatrix::zeros(n, m);
129 for (row, &idx) in indices.iter().enumerate() {
130 for j in 0..m {
131 boot_data[(row, j)] = data[(idx, j)];
132 }
133 }
134
135 let boot_karcher = karcher_mean(
137 &boot_data,
138 argvals,
139 config.max_iter,
140 config.tol,
141 config.lambda,
142 );
143
144 let aligned = elastic_align_pair(
146 &full_karcher.mean,
147 &boot_karcher.mean,
148 argvals,
149 config.lambda,
150 );
151
152 aligned.f_aligned
153 })
154 .collect();
155
156 let mut bootstrap_means = FdMatrix::zeros(config.n_bootstrap, m);
158 for (b, bm) in boot_means.iter().enumerate() {
159 for j in 0..m {
160 bootstrap_means[(b, j)] = bm[j];
161 }
162 }
163
164 let alpha = 1.0 - config.confidence_level;
166 let mut lower_band = vec![0.0; m];
167 let mut upper_band = vec![0.0; m];
168
169 for j in 0..m {
170 let mut col_vals: Vec<f64> = (0..config.n_bootstrap)
171 .map(|b| bootstrap_means[(b, j)])
172 .collect();
173 col_vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
174
175 lower_band[j] = quantile_sorted(&col_vals, alpha / 2.0);
176 upper_band[j] = quantile_sorted(&col_vals, 1.0 - alpha / 2.0);
177 }
178
179 Ok(ShapeCiResult {
180 mean: full_karcher.mean,
181 lower_band,
182 upper_band,
183 bootstrap_means,
184 })
185}
186
187use crate::helpers::quantile_sorted;
188
189#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::simulation::{sim_fundata, EFunType, EValType};
195 use crate::test_helpers::uniform_grid;
196
197 fn make_data(n: usize, m: usize) -> (FdMatrix, Vec<f64>) {
198 let t = uniform_grid(m);
199 let data = sim_fundata(n, &t, 3, EFunType::Fourier, EValType::Exponential, Some(99));
200 (data, t)
201 }
202
203 #[test]
204 fn shape_ci_band_contains_mean() {
205 let (data, t) = make_data(8, 20);
206 let config = ShapeCiConfig {
207 n_bootstrap: 30,
208 confidence_level: 0.95,
209 max_iter: 5,
210 tol: 1e-2,
211 ..Default::default()
212 };
213 let result = shape_confidence_interval(&data, &t, &config).unwrap();
214 let m = t.len();
215 for j in 0..m {
216 assert!(
217 result.lower_band[j] <= result.mean[j] + 1e-6
218 && result.mean[j] <= result.upper_band[j] + 1e-6,
219 "mean[{j}]={} not in [{}, {}]",
220 result.mean[j],
221 result.lower_band[j],
222 result.upper_band[j],
223 );
224 }
225 }
226
227 #[test]
228 fn shape_ci_band_width_positive() {
229 let (data, t) = make_data(8, 20);
230 let config = ShapeCiConfig {
231 n_bootstrap: 30,
232 confidence_level: 0.95,
233 max_iter: 5,
234 tol: 1e-2,
235 ..Default::default()
236 };
237 let result = shape_confidence_interval(&data, &t, &config).unwrap();
238 let m = t.len();
239 let n_positive = (0..m)
240 .filter(|&j| result.upper_band[j] > result.lower_band[j] + 1e-12)
241 .count();
242 assert!(
243 n_positive > m / 2,
244 "upper > lower for only {n_positive}/{m} points, expected > {}/{}",
245 m / 2,
246 m
247 );
248 }
249
250 #[test]
251 fn shape_ci_bootstrap_means_shape() {
252 let (data, t) = make_data(6, 20);
253 let n_boot = 15;
254 let config = ShapeCiConfig {
255 n_bootstrap: n_boot,
256 confidence_level: 0.90,
257 max_iter: 3,
258 tol: 1e-2,
259 ..Default::default()
260 };
261 let result = shape_confidence_interval(&data, &t, &config).unwrap();
262 assert_eq!(result.bootstrap_means.shape(), (n_boot, t.len()));
263 }
264
265 #[test]
266 fn shape_ci_rejects_too_few_curves() {
267 let t = uniform_grid(20);
268 let data = FdMatrix::zeros(2, 20);
269 let config = ShapeCiConfig::default();
270 assert!(shape_confidence_interval(&data, &t, &config).is_err());
271 }
272}