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