Skip to main content

scirs2_interpolate/
statistical_advanced.rs

1//! Advanced statistical interpolation methods
2//!
3//! This module implements sophisticated statistical interpolation techniques including:
4//! - Variational Sparse Gaussian Processes
5//! - Statistical Spline Inference with confidence bands
6//! - Advanced Bootstrap methods (block, residual, wild)
7//! - Model diagnostics and goodness-of-fit testing
8//! - Multi-output Gaussian Processes
9
10use crate::error::{InterpolateError, InterpolateResult};
11use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis};
12use scirs2_core::numeric::{Float, FromPrimitive};
13use scirs2_core::random::{rngs::StdRng, Rng, RngExt, SeedableRng};
14use std::fmt::{Debug, Display};
15use std::marker::PhantomData;
16
17/// Variational Sparse Gaussian Process using inducing points
18///
19/// Implements Titsias' variational sparse GP with ELBO optimization
20/// for scalable Gaussian process regression on large datasets.
21#[derive(Debug, Clone)]
22pub struct VariationalSparseGP<F: Float> {
23    /// Inducing point locations
24    pub inducing_points: Array2<F>,
25    /// Variational mean parameters
26    pub variational_mean: Array1<F>,
27    /// Variational covariance parameters (Cholesky factor)
28    pub variational_cov_chol: Array2<F>,
29    /// Kernel hyperparameters
30    pub kernel_params: KernelParameters<F>,
31    /// Noise variance
32    pub noise_variance: F,
33    /// Evidence lower bound (ELBO)
34    pub elbo: F,
35}
36
37/// Kernel parameters for Gaussian processes
38#[derive(Debug, Clone)]
39pub struct KernelParameters<F: Float> {
40    /// Signal variance (amplitude squared)
41    pub signal_variance: F,
42    /// Length scales for each dimension
43    pub length_scales: Array1<F>,
44    /// Kernel type
45    pub kernel_type: KernelType,
46}
47
48/// Types of kernels supported
49#[derive(Debug, Clone, Copy, PartialEq)]
50pub enum KernelType {
51    /// Radial Basis Function (Gaussian) kernel
52    RBF,
53    /// Matérn 3/2 kernel
54    Matern32,
55    /// Matérn 5/2 kernel
56    Matern52,
57    /// Rational Quadratic kernel
58    RationalQuadratic,
59}
60
61impl<F> VariationalSparseGP<F>
62where
63    F: Float
64        + FromPrimitive
65        + Debug
66        + Display
67        + std::iter::Sum
68        + 'static
69        + std::ops::AddAssign
70        + scirs2_core::ndarray::ScalarOperand
71        + std::ops::SubAssign
72        + std::ops::DivAssign,
73{
74    /// Create a new variational sparse GP
75    pub fn new(
76        inducing_points: Array2<F>,
77        kernel_params: KernelParameters<F>,
78        noise_variance: F,
79    ) -> Self {
80        let n_inducing = inducing_points.nrows();
81        let variational_mean = Array1::zeros(n_inducing);
82        let variational_cov_chol = Array2::eye(n_inducing);
83
84        Self {
85            inducing_points,
86            variational_mean,
87            variational_cov_chol,
88            kernel_params,
89            noise_variance,
90            elbo: F::neg_infinity(),
91        }
92    }
93
94    /// Fit the model using variational inference
95    pub fn fit(
96        &mut self,
97        x_train: &ArrayView2<F>,
98        y_train: &ArrayView1<F>,
99        max_iter: usize,
100        _learning_rate: F,
101        tolerance: F,
102    ) -> InterpolateResult<()> {
103        let _n_data = x_train.nrows();
104        let n_inducing = self.inducing_points.nrows();
105
106        for _iter in 0..max_iter {
107            // Compute kernel matrices
108            let k_uu = self.compute_kernel_matrix(
109                &self.inducing_points.view(),
110                &self.inducing_points.view(),
111            )?;
112            let k_fu = self.compute_kernel_matrix(x_train, &self.inducing_points.view())?;
113
114            // Add jitter for numerical stability
115            let mut k_uu_jitter = k_uu.clone();
116            let jitter = F::from_f64(1e-6).unwrap_or_else(|| F::epsilon());
117            for i in 0..n_inducing {
118                k_uu_jitter[[i, i]] += jitter;
119            }
120
121            // Cholesky decomposition of K_uu
122            let l_uu = self.cholesky_decomposition(&k_uu_jitter)?;
123
124            // Solve for A = K_fu @ inv(K_uu)
125            let a_matrix = self.solve_triangular_system(&l_uu, &k_fu.t().to_owned())?;
126
127            // Compute variational parameters
128            let sigma_inv = F::one() / self.noise_variance;
129            let lambda = Array2::eye(n_inducing) + &(a_matrix.dot(&a_matrix.t()) * sigma_inv);
130
131            // Update variational mean
132            let y_centered = y_train.to_owned();
133            let ata_y = a_matrix.dot(&y_centered);
134            self.variational_mean = self.solve_system(&lambda, &ata_y)? * sigma_inv;
135
136            // Update variational covariance (Cholesky factor)
137            self.variational_cov_chol = self.cholesky_decomposition(&lambda)?;
138
139            // Compute ELBO
140            let new_elbo = self.compute_elbo(x_train, y_train, &k_uu, &k_fu, &a_matrix)?;
141
142            // Check convergence
143            if _iter > 0 && (new_elbo - self.elbo).abs() < tolerance {
144                break;
145            }
146
147            self.elbo = new_elbo;
148
149            // Simple gradient-based updates for hyperparameters could be added here
150            // For now, we keep them fixed during optimization
151        }
152
153        Ok(())
154    }
155
156    /// Make predictions at new points
157    pub fn predict(&self, xtest: &ArrayView2<F>) -> InterpolateResult<(Array1<F>, Array1<F>)> {
158        let n_test = xtest.nrows();
159
160        // Compute kernel matrices
161        let k_uu =
162            self.compute_kernel_matrix(&self.inducing_points.view(), &self.inducing_points.view())?;
163        let k_su = self.compute_kernel_matrix(xtest, &self.inducing_points.view())?;
164        let k_ss_diag = self.compute_kernel_diagonal(xtest)?;
165
166        // Add jitter
167        let mut k_uu_jitter = k_uu.clone();
168        let jitter = F::from_f64(1e-6).unwrap_or_else(|| F::epsilon());
169        for i in 0..k_uu_jitter.nrows() {
170            k_uu_jitter[[i, i]] += jitter;
171        }
172
173        // Solve K_uu^{-1} * K_su^T
174        let l_uu = self.cholesky_decomposition(&k_uu_jitter)?;
175        let alpha = self.solve_triangular_system(&l_uu, &k_su.t().to_owned())?;
176
177        // Predictive mean
178        let mean = k_su.dot(&self.variational_mean);
179
180        // Predictive variance
181        let mut variance = Array1::zeros(n_test);
182        for i in 0..n_test {
183            let _k_s = k_su.row(i);
184            let alpha_i = alpha.column(i);
185
186            // Diagonal element of predictive covariance
187            let var_term1 = k_ss_diag[i];
188            let var_term2 = alpha_i.dot(&alpha_i);
189            let var_term3 = self.compute_trace_correction(&alpha_i)?;
190
191            variance[i] = var_term1 - var_term2 + var_term3 + self.noise_variance;
192        }
193
194        Ok((mean, variance))
195    }
196
197    /// Compute kernel matrix between two sets of points
198    fn compute_kernel_matrix(
199        &self,
200        x1: &ArrayView2<F>,
201        x2: &ArrayView2<F>,
202    ) -> InterpolateResult<Array2<F>> {
203        let n1 = x1.nrows();
204        let n2 = x2.nrows();
205        let mut k = Array2::zeros((n1, n2));
206
207        for i in 0..n1 {
208            for j in 0..n2 {
209                let distsq = self.squared_distance(&x1.row(i), &x2.row(j))?;
210                k[[i, j]] = self.kernel_function(distsq);
211            }
212        }
213
214        Ok(k)
215    }
216
217    /// Compute diagonal of kernel matrix for efficiency
218    fn compute_kernel_diagonal(&self, x: &ArrayView2<F>) -> InterpolateResult<Array1<F>> {
219        let n = x.nrows();
220        let mut diag = Array1::zeros(n);
221
222        for i in 0..n {
223            diag[i] = self.kernel_params.signal_variance;
224        }
225
226        Ok(diag)
227    }
228
229    /// Compute squared distance between two points with anisotropic scaling
230    fn squared_distance(&self, x1: &ArrayView1<F>, x2: &ArrayView1<F>) -> InterpolateResult<F> {
231        if x1.len() != x2.len() || x1.len() != self.kernel_params.length_scales.len() {
232            return Err(InterpolateError::DimensionMismatch(
233                "Point dimensions must match length scales".to_string(),
234            ));
235        }
236
237        let mut distsq = F::zero();
238        for i in 0..x1.len() {
239            let diff = x1[i] - x2[i];
240            let scaled_diff = diff / self.kernel_params.length_scales[i];
241            distsq += scaled_diff * scaled_diff;
242        }
243
244        Ok(distsq)
245    }
246
247    /// Evaluate kernel function
248    fn kernel_function(&self, distsq: F) -> F {
249        match self.kernel_params.kernel_type {
250            KernelType::RBF => {
251                self.kernel_params.signal_variance
252                    * (-F::from_f64(0.5).expect("Failed to convert f64 to target float type")
253                        * distsq)
254                        .exp()
255            }
256            KernelType::Matern32 => {
257                let dist = distsq.sqrt();
258                let sqrt3 = F::from_f64(3.0_f64.sqrt())
259                    .expect("Failed to convert f64 to target float type");
260                let term = sqrt3 * dist;
261                self.kernel_params.signal_variance * (F::one() + term) * (-term).exp()
262            }
263            KernelType::Matern52 => {
264                let dist = distsq.sqrt();
265                let sqrt5 = F::from_f64(5.0_f64.sqrt())
266                    .expect("Failed to convert f64 to target float type");
267                let term = sqrt5 * dist;
268                let term2 = F::from_f64(5.0).expect("Failed to convert f64 to target float type")
269                    * distsq
270                    / F::from_f64(3.0).expect("Failed to convert f64 to target float type");
271                self.kernel_params.signal_variance * (F::one() + term + term2) * (-term).exp()
272            }
273            KernelType::RationalQuadratic => {
274                let alpha = F::from_f64(1.0).expect("Failed to convert f64 to target float type"); // Scale mixture parameter
275                self.kernel_params.signal_variance
276                    * (F::one()
277                        + distsq
278                            / (F::from_f64(2.0)
279                                .expect("Failed to convert f64 to target float type")
280                                * alpha))
281                        .powf(-alpha)
282            }
283        }
284    }
285
286    /// Cholesky decomposition
287    fn cholesky_decomposition(&self, matrix: &Array2<F>) -> InterpolateResult<Array2<F>> {
288        let n = matrix.nrows();
289        let mut l = Array2::zeros((n, n));
290
291        for i in 0..n {
292            for j in 0..=i {
293                if i == j {
294                    // Diagonal elements
295                    let mut sum = F::zero();
296                    for k in 0..j {
297                        sum += l[[j, k]] * l[[j, k]];
298                    }
299                    let diag_val = matrix[[j, j]] - sum;
300                    if diag_val <= F::zero() {
301                        return Err(InterpolateError::ComputationError(
302                            "Matrix is not positive definite".to_string(),
303                        ));
304                    }
305                    l[[j, j]] = diag_val.sqrt();
306                } else {
307                    // Lower triangular elements
308                    let mut sum = F::zero();
309                    for k in 0..j {
310                        sum += l[[i, k]] * l[[j, k]];
311                    }
312                    l[[i, j]] = (matrix[[i, j]] - sum) / l[[j, j]];
313                }
314            }
315        }
316
317        Ok(l)
318    }
319
320    /// Solve triangular system L * X = B
321    fn solve_triangular_system(
322        &self,
323        l: &Array2<F>,
324        b: &Array2<F>,
325    ) -> InterpolateResult<Array2<F>> {
326        let n = l.nrows();
327        let m = b.ncols();
328        let mut x = Array2::zeros((n, m));
329
330        for col in 0..m {
331            for i in 0..n {
332                let mut sum = F::zero();
333                for j in 0..i {
334                    sum += l[[i, j]] * x[[j, col]];
335                }
336                x[[i, col]] = (b[[i, col]] - sum) / l[[i, i]];
337            }
338        }
339
340        Ok(x)
341    }
342
343    /// Solve linear system using forward/backward substitution
344    fn solve_system(&self, a: &Array2<F>, b: &Array1<F>) -> InterpolateResult<Array1<F>> {
345        // Simple implementation - in practice would use more sophisticated solver
346        let n = a.nrows();
347        let mut x = Array1::zeros(n);
348
349        // Very basic Gaussian elimination (not optimal, but functional)
350        let mut aug = Array2::zeros((n, n + 1));
351        for i in 0..n {
352            for j in 0..n {
353                aug[[i, j]] = a[[i, j]];
354            }
355            aug[[i, n]] = b[i];
356        }
357
358        // Forward elimination with partial pivoting
359        for i in 0..n {
360            // Find pivot
361            let mut max_row = i;
362            for k in i + 1..n {
363                if aug[[k, i]].abs() > aug[[max_row, i]].abs() {
364                    max_row = k;
365                }
366            }
367
368            // Swap rows
369            for j in 0..=n {
370                let temp = aug[[max_row, j]];
371                aug[[max_row, j]] = aug[[i, j]];
372                aug[[i, j]] = temp;
373            }
374
375            // Make all rows below this one 0 in current column
376            for k in i + 1..n {
377                if aug[[i, i]].abs() < F::epsilon() {
378                    return Err(InterpolateError::ComputationError(
379                        "Matrix is singular".to_string(),
380                    ));
381                }
382                let c = aug[[k, i]] / aug[[i, i]];
383                for j in i..=n {
384                    let aug_i_j = aug[[i, j]];
385                    aug[[k, j]] -= c * aug_i_j;
386                }
387            }
388        }
389
390        // Back substitution
391        for i in (0..n).rev() {
392            let mut sum = aug[[i, n]];
393            for j in i + 1..n {
394                sum -= aug[[i, j]] * x[j];
395            }
396            x[i] = sum / aug[[i, i]];
397        }
398
399        Ok(x)
400    }
401
402    /// Compute trace correction term for predictive variance
403    fn compute_trace_correction(&self, alpha: &ArrayView1<F>) -> InterpolateResult<F> {
404        // Simplified trace computation - in practice would be more sophisticated
405        let trace_term = alpha.dot(alpha)
406            * F::from_f64(0.1).expect("Failed to convert f64 to target float type");
407        Ok(trace_term)
408    }
409
410    /// Compute Evidence Lower Bound (ELBO)
411    fn compute_elbo(
412        &self,
413        x_train: &ArrayView2<F>,
414        y_train: &ArrayView1<F>,
415        _k_uu: &Array2<F>,
416        k_fu: &Array2<F>,
417        _a_matrix: &Array2<F>,
418    ) -> InterpolateResult<F> {
419        let n_data = x_train.nrows();
420        let n_inducing = self.inducing_points.nrows();
421
422        // Data fit term
423        let y_pred = k_fu.dot(&self.variational_mean);
424        let residuals = y_train - &y_pred;
425        let data_fit = -F::from_f64(0.5).expect("Failed to convert f64 to target float type")
426            * residuals.dot(&residuals)
427            / self.noise_variance;
428
429        // Complexity penalty (KL divergence)
430        let log_det_term = F::from_f64(n_inducing as f64 * (2.0 * std::f64::consts::PI).ln())
431            .expect("Operation failed");
432        let trace_term = self.variational_cov_chol.diag().mapv(|x| x.ln()).sum();
433        let kl_penalty = -F::from_f64(0.5).expect("Failed to convert f64 to target float type")
434            * (log_det_term
435                + F::from_f64(2.0).expect("Failed to convert f64 to target float type")
436                    * trace_term);
437
438        // Noise term
439        let noise_term = -F::from_f64(0.5 * n_data as f64)
440            .expect("Failed to convert f64 to target float type")
441            * self.noise_variance.ln();
442
443        Ok(data_fit + kl_penalty + noise_term)
444    }
445}
446
447/// Statistical spline with confidence bands
448///
449/// Extends standard spline interpolation with statistical inference,
450/// including confidence and prediction bands.
451#[derive(Debug, Clone)]
452pub struct StatisticalSpline<F: Float> {
453    /// Spline coefficients
454    pub coefficients: Array1<F>,
455    /// Knot locations
456    pub knots: Array1<F>,
457    /// Covariance matrix of coefficients
458    pub coef_covariance: Array2<F>,
459    /// Residual standard error
460    pub residual_std_error: F,
461    /// Degrees of freedom
462    pub degrees_of_freedom: usize,
463}
464
465impl<F> StatisticalSpline<F>
466where
467    F: Float
468        + FromPrimitive
469        + Debug
470        + Display
471        + scirs2_core::ndarray::ScalarOperand
472        + std::ops::AddAssign
473        + std::ops::SubAssign
474        + std::ops::MulAssign
475        + std::ops::DivAssign,
476{
477    /// Fit a statistical spline with inference
478    pub fn fit(
479        x: &ArrayView1<F>,
480        y: &ArrayView1<F>,
481        n_knots: usize,
482        smoothing_parameter: F,
483    ) -> InterpolateResult<Self> {
484        let n = x.len();
485        if n != y.len() {
486            return Err(InterpolateError::DimensionMismatch(
487                "x and y must have same length".to_string(),
488            ));
489        }
490
491        // Create knot vector (simplified)
492        let x_min = x.fold(F::infinity(), |a, &b| a.min(b));
493        let x_max = x.fold(F::neg_infinity(), |a, &b| a.max(b));
494        let mut knots = Array1::zeros(n_knots);
495        for i in 0..n_knots {
496            let t = F::from_usize(i).expect("Failed to convert usize to float")
497                / F::from_usize(n_knots - 1).expect("Failed to convert usize to float");
498            knots[i] = x_min + t * (x_max - x_min);
499        }
500
501        // Build design matrix (B-spline basis - simplified)
502        let design_matrix = Self::build_bspline_matrix(x, &knots)?;
503
504        // Add smoothing penalty matrix
505        let penalty_matrix = Self::build_penalty_matrix(n_knots)?;
506        let penalized_matrix =
507            design_matrix.t().dot(&design_matrix) + &(penalty_matrix.clone() * smoothing_parameter);
508
509        // Solve penalized least squares
510        let rhs = design_matrix.t().dot(y);
511        let coefficients = Self::solve_penalized_system(&penalized_matrix, &rhs)?;
512
513        // Compute residuals and standard error
514        let fitted_values = design_matrix.dot(&coefficients);
515        let residuals = y - &fitted_values;
516        let rss = residuals.dot(&residuals);
517        let dof = n - Self::effective_degrees_of_freedom(&design_matrix, smoothing_parameter)?;
518        let residual_std_error =
519            (rss / F::from_usize(dof).expect("Failed to convert usize to float")).sqrt();
520
521        // Compute coefficient covariance matrix
522        let coef_covariance = Self::compute_coefficient_covariance(
523            &design_matrix,
524            &penalty_matrix,
525            smoothing_parameter,
526            residual_std_error,
527        )?;
528
529        Ok(Self {
530            coefficients,
531            knots,
532            coef_covariance,
533            residual_std_error,
534            degrees_of_freedom: dof,
535        })
536    }
537
538    /// Predict with confidence and prediction bands
539    pub fn predict_with_bands(
540        &self,
541        x_new: &ArrayView1<F>,
542        confidence_level: F,
543    ) -> InterpolateResult<(Array1<F>, Array1<F>, Array1<F>, Array1<F>, Array1<F>)> {
544        let design_new = Self::build_bspline_matrix(x_new, &self.knots)?;
545
546        // Point predictions
547        let predictions = design_new.dot(&self.coefficients);
548
549        // Standard errors
550        let mut std_errors = Array1::zeros(x_new.len());
551        let mut prediction_std_errors = Array1::zeros(x_new.len());
552
553        for i in 0..x_new.len() {
554            let x_row = design_new.row(i);
555            let variance = x_row.dot(&self.coef_covariance.dot(&x_row));
556            std_errors[i] = variance.sqrt();
557            prediction_std_errors[i] =
558                (variance + self.residual_std_error * self.residual_std_error).sqrt();
559        }
560
561        // Critical value for confidence _level
562        let _alpha = F::one() - confidence_level;
563        let t_crit = F::from_f64(1.96).expect("Failed to convert f64 to target float type"); // Simplified - should use proper t-distribution
564
565        // Confidence bands (for the mean function)
566        let conf_lower = &predictions - &(std_errors.clone() * t_crit);
567        let conf_upper = &predictions + &(std_errors * t_crit);
568
569        // Prediction bands (for _new observations)
570        let pred_lower = &predictions - &(prediction_std_errors.clone() * t_crit);
571        let pred_upper = &predictions + &(prediction_std_errors * t_crit);
572
573        Ok((predictions, conf_lower, conf_upper, pred_lower, pred_upper))
574    }
575
576    /// Build B-spline design matrix (simplified implementation)
577    fn build_bspline_matrix(x: &ArrayView1<F>, knots: &Array1<F>) -> InterpolateResult<Array2<F>> {
578        let n = x.len();
579        let m = knots.len();
580        let mut matrix = Array2::zeros((n, m));
581
582        // Simplified B-spline basis functions (linear for now)
583        for i in 0..n {
584            for j in 0..m {
585                if j == 0 {
586                    matrix[[i, j]] = F::one();
587                } else {
588                    matrix[[i, j]] =
589                        x[i].powf(F::from_usize(j).expect("Failed to convert usize to float"));
590                }
591            }
592        }
593
594        Ok(matrix)
595    }
596
597    /// Build penalty matrix for smoothing
598    fn build_penalty_matrix(_nknots: usize) -> InterpolateResult<Array2<F>> {
599        let mut penalty = Array2::zeros((_nknots, _nknots));
600
601        // Second-order difference penalty (simplified)
602        for i in 2.._nknots {
603            penalty[[i - 2, i - 2]] += F::one();
604            penalty[[i - 2, i - 1]] -=
605                F::from_f64(2.0).expect("Failed to convert f64 to target float type");
606            penalty[[i - 2, i]] += F::one();
607            penalty[[i - 1, i - 2]] -=
608                F::from_f64(2.0).expect("Failed to convert f64 to target float type");
609            penalty[[i - 1, i - 1]] +=
610                F::from_f64(4.0).expect("Failed to convert f64 to target float type");
611            penalty[[i - 1, i]] -=
612                F::from_f64(2.0).expect("Failed to convert f64 to target float type");
613            penalty[[i, i - 2]] += F::one();
614            penalty[[i, i - 1]] -=
615                F::from_f64(2.0).expect("Failed to convert f64 to target float type");
616            penalty[[i, i]] += F::one();
617        }
618
619        Ok(penalty)
620    }
621
622    /// Solve penalized least squares system
623    fn solve_penalized_system(a: &Array2<F>, b: &Array1<F>) -> InterpolateResult<Array1<F>> {
624        // Simplified solver - in practice would use Cholesky or QR
625        let n = a.nrows();
626        let mut x = Array1::zeros(n);
627
628        // Simple iterative solver (Gauss-Seidel)
629        for _iter in 0..100 {
630            let mut max_change = F::zero();
631            for i in 0..n {
632                let mut sum = b[i];
633                for j in 0..n {
634                    if i != j {
635                        sum -= a[[i, j]] * x[j];
636                    }
637                }
638                let new_val = sum / a[[i, i]];
639                let change = (new_val - x[i]).abs();
640                if change > max_change {
641                    max_change = change;
642                }
643                x[i] = new_val;
644            }
645
646            if max_change < F::from_f64(1e-8).expect("Failed to convert f64 to target float type") {
647                break;
648            }
649        }
650
651        Ok(x)
652    }
653
654    /// Compute effective degrees of freedom
655    fn effective_degrees_of_freedom(
656        design: &Array2<F>,
657        smoothing_parameter: F,
658    ) -> InterpolateResult<usize> {
659        // Simplified computation
660        let base_dof = design.ncols();
661        let penalty_reduction = (smoothing_parameter.ln()
662            * F::from_f64(0.1).expect("Failed to convert f64 to target float type"))
663        .exp();
664        let effective_dof = F::from_usize(base_dof).expect("Failed to convert usize to float")
665            * (F::one() - penalty_reduction);
666        Ok(effective_dof.to_usize().unwrap_or(base_dof))
667    }
668
669    /// Compute coefficient covariance matrix
670    fn compute_coefficient_covariance(
671        design: &Array2<F>,
672        penalty: &Array2<F>,
673        smoothing_parameter: F,
674        residual_std_error: F,
675    ) -> InterpolateResult<Array2<F>> {
676        let penalized_matrix = design.t().dot(design) + &(penalty * smoothing_parameter);
677
678        // Simplified inverse computation (in practice would use proper matrix inversion)
679        let n = penalized_matrix.nrows();
680        let mut inv_matrix = Array2::eye(n);
681
682        // Very simplified - would use proper matrix inverse
683        for i in 0..n {
684            inv_matrix[[i, i]] = F::one() / penalized_matrix[[i, i]];
685        }
686
687        Ok(inv_matrix * residual_std_error * residual_std_error)
688    }
689}
690
691/// Advanced bootstrap methods for interpolation
692#[derive(Debug, Clone)]
693pub struct AdvancedBootstrap<F: Float> {
694    /// Block size for block bootstrap
695    pub block_size: usize,
696    /// Bootstrap method type
697    pub method: BootstrapMethod,
698    /// Number of bootstrap samples
699    pub n_samples: usize,
700    /// Random seed for reproducibility
701    pub seed: Option<u64>,
702    /// Phantom data to use type parameter F
703    pub _phantom: PhantomData<F>,
704}
705
706/// Types of bootstrap methods
707#[derive(Debug, Clone, Copy, PartialEq)]
708pub enum BootstrapMethod {
709    /// Standard bootstrap (iid resampling)
710    Standard,
711    /// Block bootstrap for time series
712    Block,
713    /// Residual bootstrap for regression
714    Residual,
715    /// Wild bootstrap for heteroskedasticity
716    Wild,
717}
718
719impl<F> AdvancedBootstrap<F>
720where
721    F: Float + FromPrimitive + Debug + Display + std::iter::Sum,
722{
723    /// Create new advanced bootstrap
724    pub fn new(method: BootstrapMethod, n_samples: usize, blocksize: usize) -> Self {
725        Self {
726            block_size: blocksize,
727            method,
728            n_samples,
729            seed: None,
730            _phantom: PhantomData,
731        }
732    }
733
734    /// Perform bootstrap interpolation with specified method
735    pub fn bootstrap_interpolate<InterpolatorFn>(
736        &self,
737        x: &ArrayView1<F>,
738        y: &ArrayView1<F>,
739        x_new: &ArrayView1<F>,
740        interpolator_factory: InterpolatorFn,
741    ) -> InterpolateResult<(Array1<F>, Array1<F>, Array1<F>)>
742    where
743        InterpolatorFn:
744            Fn(&ArrayView1<F>, &ArrayView1<F>, &ArrayView1<F>) -> InterpolateResult<Array1<F>>,
745    {
746        let _n = x.len();
747        let m = x_new.len();
748        let mut rng = match self.seed {
749            Some(seed) => StdRng::seed_from_u64(seed),
750            None => StdRng::seed_from_u64(42),
751        };
752
753        let mut bootstrap_results = Array2::zeros((self.n_samples, m));
754
755        for sample in 0..self.n_samples {
756            let (x_boot, y_boot) = match self.method {
757                BootstrapMethod::Standard => self.standard_bootstrap(x, y, &mut rng)?,
758                BootstrapMethod::Block => self.block_bootstrap(x, y, &mut rng)?,
759                BootstrapMethod::Residual => {
760                    self.residual_bootstrap(x, y, &mut rng, &interpolator_factory)?
761                }
762                BootstrapMethod::Wild => {
763                    self.wild_bootstrap(x, y, &mut rng, &interpolator_factory)?
764                }
765            };
766
767            let y_pred = interpolator_factory(&x_boot.view(), &y_boot.view(), x_new)?;
768            bootstrap_results.row_mut(sample).assign(&y_pred);
769        }
770
771        // Compute statistics
772        let mean = bootstrap_results
773            .mean_axis(Axis(0))
774            .expect("Failed to compute mean along axis");
775        let _std_dev = bootstrap_results.std_axis(Axis(0), F::zero());
776
777        // Compute percentiles for confidence intervals
778        let mut conf_lower = Array1::zeros(m);
779        let mut conf_upper = Array1::zeros(m);
780
781        for i in 0..m {
782            let mut column: Vec<F> = bootstrap_results.column(i).to_vec();
783            column.sort_by(|a, b| a.partial_cmp(b).expect("Float comparison failed"));
784
785            let lower_idx = ((F::from_f64(0.025)
786                .expect("Failed to convert f64 to target float type")
787                * F::from_usize(self.n_samples).expect("Failed to convert usize to float"))
788            .to_usize()
789            .expect("Failed to convert to usize"))
790            .min(self.n_samples - 1);
791            let upper_idx = ((F::from_f64(0.975)
792                .expect("Failed to convert f64 to target float type")
793                * F::from_usize(self.n_samples).expect("Failed to convert usize to float"))
794            .to_usize()
795            .expect("Failed to convert to usize"))
796            .min(self.n_samples - 1);
797
798            conf_lower[i] = column[lower_idx];
799            conf_upper[i] = column[upper_idx];
800        }
801
802        Ok((mean, conf_lower, conf_upper))
803    }
804
805    /// Standard bootstrap resampling
806    fn standard_bootstrap(
807        &self,
808        x: &ArrayView1<F>,
809        y: &ArrayView1<F>,
810        rng: &mut StdRng,
811    ) -> InterpolateResult<(Array1<F>, Array1<F>)> {
812        let n = x.len();
813        let mut indices = Vec::with_capacity(n);
814
815        for _ in 0..n {
816            indices.push(rng.random_range(0..n));
817        }
818
819        let x_boot = Array1::from_iter(indices.iter().map(|&i| x[i]));
820        let y_boot = Array1::from_iter(indices.iter().map(|&i| y[i]));
821
822        Ok((x_boot, y_boot))
823    }
824
825    /// Block bootstrap for time series data
826    fn block_bootstrap(
827        &self,
828        x: &ArrayView1<F>,
829        y: &ArrayView1<F>,
830        rng: &mut StdRng,
831    ) -> InterpolateResult<(Array1<F>, Array1<F>)> {
832        let n = x.len();
833        let n_blocks = n.div_ceil(self.block_size);
834
835        let mut x_boot = Vec::new();
836        let mut y_boot = Vec::new();
837
838        for _ in 0..n_blocks {
839            let start_idx = rng.random_range(0..=(n.saturating_sub(self.block_size)));
840            let end_idx = (start_idx + self.block_size).min(n);
841
842            for i in start_idx..end_idx {
843                x_boot.push(x[i]);
844                y_boot.push(y[i]);
845                if x_boot.len() >= n {
846                    break;
847                }
848            }
849            if x_boot.len() >= n {
850                break;
851            }
852        }
853
854        x_boot.truncate(n);
855        y_boot.truncate(n);
856
857        Ok((Array1::from(x_boot), Array1::from(y_boot)))
858    }
859
860    /// Residual bootstrap for regression models
861    fn residual_bootstrap<InterpolatorFn>(
862        &self,
863        x: &ArrayView1<F>,
864        y: &ArrayView1<F>,
865        rng: &mut StdRng,
866        interpolator_factory: &InterpolatorFn,
867    ) -> InterpolateResult<(Array1<F>, Array1<F>)>
868    where
869        InterpolatorFn:
870            Fn(&ArrayView1<F>, &ArrayView1<F>, &ArrayView1<F>) -> InterpolateResult<Array1<F>>,
871    {
872        let n = x.len();
873
874        // Fit original model to get residuals
875        let y_fitted = interpolator_factory(x, y, x)?;
876        let residuals = y - &y_fitted;
877
878        // Resample residuals
879        let mut resampled_residuals = Array1::zeros(n);
880        for i in 0..n {
881            let idx = rng.random_range(0..n);
882            resampled_residuals[i] = residuals[idx];
883        }
884
885        // Create new bootstrap sample
886        let y_boot = y_fitted + resampled_residuals;
887
888        Ok((x.to_owned(), y_boot))
889    }
890
891    /// Wild bootstrap for heteroskedastic errors
892    fn wild_bootstrap<InterpolatorFn>(
893        &self,
894        x: &ArrayView1<F>,
895        y: &ArrayView1<F>,
896        rng: &mut StdRng,
897        interpolator_factory: &InterpolatorFn,
898    ) -> InterpolateResult<(Array1<F>, Array1<F>)>
899    where
900        InterpolatorFn:
901            Fn(&ArrayView1<F>, &ArrayView1<F>, &ArrayView1<F>) -> InterpolateResult<Array1<F>>,
902    {
903        let n = x.len();
904
905        // Fit original model to get residuals
906        let y_fitted = interpolator_factory(x, y, x)?;
907        let residuals = y - &y_fitted;
908
909        // Generate wild bootstrap multipliers (Rademacher distribution)
910        let mut multipliers = Array1::zeros(n);
911        for i in 0..n {
912            multipliers[i] = if rng.random::<f64>() < 0.5 {
913                F::from_f64(-1.0).expect("Failed to convert f64 to target float type")
914            } else {
915                F::one()
916            };
917        }
918
919        // Create new bootstrap sample
920        let y_boot = y_fitted + &residuals * &multipliers;
921
922        Ok((x.to_owned(), y_boot))
923    }
924}
925
926#[cfg(test)]
927mod tests {
928    use super::*;
929    // use approx::assert_abs_diff_eq;
930
931    #[test]
932    fn test_variational_sparse_gp() {
933        // Generate simple test data
934        let x_train = Array2::from_shape_vec((5, 1), vec![0.0, 1.0, 2.0, 3.0, 4.0])
935            .expect("Operation failed");
936        let y_train = Array1::from(vec![0.0, 1.0, 4.0, 9.0, 16.0]); // y = x^2
937
938        // Create kernel parameters
939        let kernel_params = KernelParameters {
940            signal_variance: 1.0,
941            length_scales: Array1::from(vec![1.0]),
942            kernel_type: KernelType::RBF,
943        };
944
945        // Create sparse GP with 3 inducing points
946        let inducing_points =
947            Array2::from_shape_vec((3, 1), vec![0.0, 2.0, 4.0]).expect("Operation failed");
948        let mut sparse_gp = VariationalSparseGP::new(
949            inducing_points,
950            kernel_params,
951            0.1, // noise variance
952        );
953
954        // Fit the model
955        let result = sparse_gp.fit(&x_train.view(), &y_train.view(), 10, 0.01, 1e-6);
956        assert!(result.is_ok());
957
958        // Make predictions
959        let xtest = Array2::from_shape_vec((3, 1), vec![0.5, 1.5, 2.5]).expect("Operation failed");
960        let (mean, variance) = sparse_gp.predict(&xtest.view()).expect("Operation failed");
961
962        // Check that predictions are reasonable
963        assert_eq!(mean.len(), 3);
964        assert_eq!(variance.len(), 3);
965        assert!(variance.iter().all(|&v| v > 0.0));
966    }
967
968    #[test]
969    fn test_statistical_spline() {
970        // Generate test data
971        let x = Array1::from(vec![0.0, 1.0, 2.0, 3.0, 4.0]);
972        let y = Array1::from(vec![0.0, 1.0, 4.0, 9.0, 16.0]);
973
974        // Fit statistical spline
975        let spline =
976            StatisticalSpline::fit(&x.view(), &y.view(), 5, 0.1).expect("Operation failed");
977
978        // Test prediction with confidence bands
979        let x_new = Array1::from(vec![0.5, 1.5, 2.5, 3.5]);
980        let (pred, conf_lower, conf_upper, pred_lower, pred_upper) = spline
981            .predict_with_bands(&x_new.view(), 0.95)
982            .expect("Operation failed");
983
984        // Check that predictions are reasonable
985        assert_eq!(pred.len(), 4);
986        assert!(conf_lower
987            .iter()
988            .zip(conf_upper.iter())
989            .all(|(&l, &u)| l < u));
990        assert!(pred_lower
991            .iter()
992            .zip(pred_upper.iter())
993            .all(|(&l, &u)| l < u));
994        assert!(conf_lower
995            .iter()
996            .zip(pred_lower.iter())
997            .all(|(&c, &p)| c >= p));
998        assert!(conf_upper
999            .iter()
1000            .zip(pred_upper.iter())
1001            .all(|(&c, &p)| c <= p));
1002    }
1003
1004    #[test]
1005    fn test_advanced_bootstrap() {
1006        let x = Array1::from(vec![0.0, 1.0, 2.0, 3.0, 4.0]);
1007        let y = Array1::from(vec![0.0, 1.0, 4.0, 9.0, 16.0]);
1008        let x_new = Array1::from(vec![0.5, 1.5, 2.5]);
1009
1010        let bootstrap = AdvancedBootstrap::new(BootstrapMethod::Standard, 100, 2);
1011
1012        // Simple linear interpolation factory
1013        let interpolator =
1014            |x_data: &ArrayView1<f64>, y_data: &ArrayView1<f64>, x_pred: &ArrayView1<f64>| {
1015                // Very simple linear interpolation for testing
1016                let mut result = Array1::zeros(x_pred.len());
1017                for (i, &x_val) in x_pred.iter().enumerate() {
1018                    // Find nearest neighbors and interpolate
1019                    if x_val <= x_data[0] {
1020                        result[i] = y_data[0];
1021                    } else if x_val >= x_data[x_data.len() - 1] {
1022                        result[i] = y_data[y_data.len() - 1];
1023                    } else {
1024                        // Find bracketing points
1025                        for j in 0..x_data.len() - 1 {
1026                            if x_val >= x_data[j] && x_val <= x_data[j + 1] {
1027                                let t = (x_val - x_data[j]) / (x_data[j + 1] - x_data[j]);
1028                                result[i] = y_data[j] + t * (y_data[j + 1] - y_data[j]);
1029                                break;
1030                            }
1031                        }
1032                    }
1033                }
1034                Ok(result)
1035            };
1036
1037        let (mean, lower, upper) = bootstrap
1038            .bootstrap_interpolate(&x.view(), &y.view(), &x_new.view(), interpolator)
1039            .expect("Bootstrap interpolation failed");
1040
1041        assert_eq!(mean.len(), 3);
1042        assert!(lower.iter().zip(upper.iter()).all(|(&l, &u)| l <= u));
1043    }
1044}
1045
1046/// Savitzky-Golay filter for statistical smoothing and interpolation
1047///
1048/// Implements the Savitzky-Golay filter which fits local polynomials to data points
1049/// using least squares regression. This provides both smoothing and the ability to
1050/// compute derivatives of the smoothed signal.
1051#[derive(Debug, Clone)]
1052pub struct SavitzkyGolayFilter<F: Float> {
1053    /// Window length (must be odd)
1054    pub window_length: usize,
1055    /// Polynomial order
1056    pub polynomial_order: usize,
1057    /// Derivative order (0 for smoothing, 1 for first derivative, etc.)
1058    pub derivative_order: usize,
1059    /// Phantom data to use type parameter F
1060    pub _phantom: PhantomData<F>,
1061}
1062
1063impl<F> SavitzkyGolayFilter<F>
1064where
1065    F: Float + FromPrimitive + Debug + std::iter::Sum + 'static,
1066{
1067    /// Create a new Savitzky-Golay filter
1068    pub fn new(
1069        window_length: usize,
1070        polynomial_order: usize,
1071        derivative_order: usize,
1072    ) -> InterpolateResult<Self> {
1073        if window_length.is_multiple_of(2) {
1074            return Err(InterpolateError::InvalidValue(
1075                "Window _length must be odd".to_string(),
1076            ));
1077        }
1078
1079        if polynomial_order >= window_length {
1080            return Err(InterpolateError::InvalidValue(
1081                "Polynomial _order must be less than window _length".to_string(),
1082            ));
1083        }
1084
1085        if derivative_order > polynomial_order {
1086            return Err(InterpolateError::InvalidValue(
1087                "Derivative _order cannot exceed polynomial _order".to_string(),
1088            ));
1089        }
1090
1091        Ok(Self {
1092            window_length,
1093            polynomial_order,
1094            derivative_order: 0,
1095            _phantom: PhantomData,
1096        })
1097    }
1098
1099    /// Apply the Savitzky-Golay filter to input data
1100    pub fn filter(&self, y: &ArrayView1<F>) -> InterpolateResult<Array1<F>> {
1101        let n = y.len();
1102        if n < self.window_length {
1103            return Err(InterpolateError::InvalidValue(
1104                "Data length must be at least window length".to_string(),
1105            ));
1106        }
1107
1108        let mut result = Array1::zeros(n);
1109        let half_window = self.window_length / 2;
1110
1111        // Compute Savitzky-Golay coefficients
1112        let coeffs = self.compute_coefficients()?;
1113
1114        // Apply filter to each point
1115        for i in 0..n {
1116            let mut sum = F::zero();
1117
1118            for j in 0..self.window_length {
1119                let data_idx = if i < half_window {
1120                    // Handle left boundary
1121                    j.min(n - 1)
1122                } else if i >= n - half_window {
1123                    // Handle right boundary
1124                    (n - self.window_length + j).max(0).min(n - 1)
1125                } else {
1126                    // Normal case
1127                    i - half_window + j
1128                };
1129
1130                sum = sum + coeffs[j] * y[data_idx];
1131            }
1132
1133            result[i] = sum;
1134        }
1135
1136        Ok(result)
1137    }
1138
1139    /// Compute Savitzky-Golay filter coefficients
1140    fn compute_coefficients(&self) -> InterpolateResult<Array1<F>> {
1141        let m = self.window_length;
1142        let n = self.polynomial_order + 1;
1143        let half_window = (m - 1) / 2;
1144
1145        // Create design matrix (Vandermonde matrix)
1146        let mut design = Array2::<F>::zeros((m, n));
1147        for i in 0..m {
1148            let x = F::from_isize(i as isize - half_window as isize)
1149                .expect("Failed to convert isize to float");
1150            for j in 0..n {
1151                design[[i, j]] = x.powi(j as i32);
1152            }
1153        }
1154
1155        // Solve normal equations: (X^T X) c = X^T e_d
1156        // where e_d is the unit vector for the derivative order
1157        let xtx = design.t().dot(&design);
1158        let mut rhs = Array1::<F>::zeros(n);
1159
1160        // Factorial for derivative scaling
1161        let mut factorial = F::one();
1162        for i in 1..=self.derivative_order {
1163            factorial = factorial * F::from_usize(i).expect("Failed to convert usize to float");
1164        }
1165        rhs[self.derivative_order] = factorial;
1166
1167        // Solve the system (simplified - would use proper linear solver in practice)
1168        let coeffs_polynomial = self.solve_linear_system(&xtx, &rhs)?;
1169
1170        // Transform to filter coefficients
1171        let filter_coeffs = design.dot(&coeffs_polynomial);
1172
1173        Ok(filter_coeffs)
1174    }
1175
1176    /// Simplified linear system solver
1177    fn solve_linear_system(&self, a: &Array2<F>, b: &Array1<F>) -> InterpolateResult<Array1<F>> {
1178        let n = a.nrows();
1179        let mut x = Array1::<F>::zeros(n);
1180
1181        // Very simplified diagonal solver (would use proper LU decomposition in practice)
1182        for i in 0..n {
1183            if a[[i, i]].abs()
1184                < F::from_f64(1e-12).expect("Failed to convert f64 to target float type")
1185            {
1186                return Err(InterpolateError::ComputationError(
1187                    "Singular matrix in Savitzky-Golay computation".to_string(),
1188                ));
1189            }
1190            x[i] = b[i] / a[[i, i]];
1191        }
1192
1193        Ok(x)
1194    }
1195}
1196
1197/// Bias-Corrected and Accelerated (BCa) Bootstrap
1198///
1199/// Provides more accurate confidence intervals than basic percentile bootstrap
1200/// by correcting for bias and skewness in the bootstrap distribution.
1201#[derive(Debug, Clone)]
1202pub struct BcaBootstrap<F: Float> {
1203    /// Number of bootstrap samples
1204    pub n_bootstrap: usize,
1205    /// Confidence level (e.g., 0.95)
1206    pub confidence_level: F,
1207    /// Random seed for reproducibility
1208    pub seed: Option<u64>,
1209}
1210
1211impl<F> BcaBootstrap<F>
1212where
1213    F: Float + FromPrimitive + Debug + std::iter::Sum,
1214{
1215    /// Create a new BCa bootstrap
1216    pub fn new(n_bootstrap: usize, confidence_level: F, seed: Option<u64>) -> Self {
1217        Self {
1218            n_bootstrap,
1219            confidence_level,
1220            seed,
1221        }
1222    }
1223
1224    /// Compute BCa confidence intervals
1225    pub fn confidence_intervals<G>(
1226        &self,
1227        x: &ArrayView1<F>,
1228        y: &ArrayView1<F>,
1229        x_new: &ArrayView1<F>,
1230        interpolator: G,
1231    ) -> InterpolateResult<(Array1<F>, Array1<F>, Array1<F>)>
1232    where
1233        G: Fn(&ArrayView1<F>, &ArrayView1<F>, &ArrayView1<F>) -> InterpolateResult<Array1<F>>,
1234    {
1235        let n_data = x.len();
1236        let n_pred = x_new.len();
1237
1238        // Generate bootstrap samples
1239        let mut rng = match self.seed {
1240            Some(seed) => StdRng::seed_from_u64(seed),
1241            None => {
1242                let mut rng = scirs2_core::random::rng();
1243                StdRng::from_rng(&mut rng)
1244            }
1245        };
1246
1247        let mut bootstrap_results = Array2::<F>::zeros((self.n_bootstrap, n_pred));
1248
1249        // Bootstrap resampling
1250        for b in 0..self.n_bootstrap {
1251            let mut x_boot = Array1::<F>::zeros(n_data);
1252            let mut y_boot = Array1::<F>::zeros(n_data);
1253
1254            for i in 0..n_data {
1255                let idx = rng.random_range(0..n_data);
1256                x_boot[i] = x[idx];
1257                y_boot[i] = y[idx];
1258            }
1259
1260            let pred = interpolator(&x_boot.view(), &y_boot.view(), x_new)?;
1261            bootstrap_results.row_mut(b).assign(&pred);
1262        }
1263
1264        // Compute original estimate
1265        let original_pred = interpolator(x, y, x_new)?;
1266
1267        // Compute bias correction
1268        let bias_correction = self.compute_bias_correction(&bootstrap_results, &original_pred)?;
1269
1270        // Compute acceleration
1271        let acceleration = self.compute_acceleration(x, y, x_new, &interpolator)?;
1272
1273        // Compute BCa intervals
1274        let alpha = (F::one() - self.confidence_level)
1275            / F::from_f64(2.0).expect("Failed to convert f64 to target float type");
1276        let z_alpha = self.inverse_normal_cdf(alpha)?;
1277        let z_1_alpha = self.inverse_normal_cdf(F::one() - alpha)?;
1278
1279        let mut lower = Array1::<F>::zeros(n_pred);
1280        let mut upper = Array1::<F>::zeros(n_pred);
1281
1282        for i in 0..n_pred {
1283            let bc = bias_correction[i];
1284            let acc = acceleration[i];
1285
1286            // Adjusted percentiles
1287            let alpha1 =
1288                self.normal_cdf(bc + (bc + z_alpha) / (F::one() - acc * (bc + z_alpha)))?;
1289            let alpha2 =
1290                self.normal_cdf(bc + (bc + z_1_alpha) / (F::one() - acc * (bc + z_1_alpha)))?;
1291
1292            // Extract percentiles from bootstrap distribution
1293            let mut column: Vec<F> = bootstrap_results.column(i).to_vec();
1294            column.sort_by(|a, b| a.partial_cmp(b).expect("Float comparison failed"));
1295
1296            let idx1 = ((alpha1
1297                * F::from_usize(self.n_bootstrap).expect("Failed to convert usize to float"))
1298            .floor()
1299            .to_usize()
1300            .expect("Failed to convert to usize"))
1301            .min(self.n_bootstrap - 1);
1302            let idx2 = ((alpha2
1303                * F::from_usize(self.n_bootstrap).expect("Failed to convert usize to float"))
1304            .floor()
1305            .to_usize()
1306            .expect("Failed to convert to usize"))
1307            .min(self.n_bootstrap - 1);
1308
1309            lower[i] = column[idx1];
1310            upper[i] = column[idx2];
1311        }
1312
1313        Ok((original_pred, lower, upper))
1314    }
1315
1316    /// Compute bias correction parameter
1317    fn compute_bias_correction(
1318        &self,
1319        bootstrap_results: &Array2<F>,
1320        original_pred: &Array1<F>,
1321    ) -> InterpolateResult<Array1<F>> {
1322        let n_pred = original_pred.len();
1323        let mut bias_correction = Array1::<F>::zeros(n_pred);
1324
1325        for i in 0..n_pred {
1326            let column = bootstrap_results.column(i);
1327            let count_less = column.iter().filter(|&&val| val < original_pred[i]).count();
1328
1329            let proportion = F::from_usize(count_less).expect("Failed to convert usize to float")
1330                / F::from_usize(self.n_bootstrap).expect("Failed to convert usize to float");
1331            bias_correction[i] = self.inverse_normal_cdf(proportion)?;
1332        }
1333
1334        Ok(bias_correction)
1335    }
1336
1337    /// Compute acceleration parameter using jackknife
1338    fn compute_acceleration<G>(
1339        &self,
1340        x: &ArrayView1<F>,
1341        y: &ArrayView1<F>,
1342        x_new: &ArrayView1<F>,
1343        interpolator: &G,
1344    ) -> InterpolateResult<Array1<F>>
1345    where
1346        G: Fn(&ArrayView1<F>, &ArrayView1<F>, &ArrayView1<F>) -> InterpolateResult<Array1<F>>,
1347    {
1348        let n_data = x.len();
1349        let n_pred = x_new.len();
1350
1351        // Jackknife estimates
1352        let mut jackknife_results = Array2::<F>::zeros((n_data, n_pred));
1353
1354        for i in 0..n_data {
1355            // Leave-one-out sample
1356            let mut x_jack = Array1::<F>::zeros(n_data - 1);
1357            let mut y_jack = Array1::<F>::zeros(n_data - 1);
1358
1359            let mut idx = 0;
1360            for j in 0..n_data {
1361                if j != i {
1362                    x_jack[idx] = x[j];
1363                    y_jack[idx] = y[j];
1364                    idx += 1;
1365                }
1366            }
1367
1368            let pred = interpolator(&x_jack.view(), &y_jack.view(), x_new)?;
1369            jackknife_results.row_mut(i).assign(&pred);
1370        }
1371
1372        // Compute jackknife mean
1373        let jack_mean = jackknife_results
1374            .mean_axis(Axis(0))
1375            .expect("Failed to compute mean along axis");
1376
1377        // Compute acceleration
1378        let mut acceleration = Array1::<F>::zeros(n_pred);
1379        for i in 0..n_pred {
1380            let mut sum_cubed = F::zero();
1381            let mut sum_squared = F::zero();
1382
1383            for j in 0..n_data {
1384                let diff = jack_mean[i] - jackknife_results[[j, i]];
1385                sum_cubed = sum_cubed + diff * diff * diff;
1386                sum_squared = sum_squared + diff * diff;
1387            }
1388
1389            if sum_squared > F::zero() {
1390                acceleration[i] = sum_cubed
1391                    / (F::from_f64(6.0).expect("Failed to convert f64 to target float type")
1392                        * sum_squared.powf(
1393                            F::from_f64(1.5).expect("Failed to convert f64 to target float type"),
1394                        ));
1395            }
1396        }
1397
1398        Ok(acceleration)
1399    }
1400
1401    /// Simplified normal CDF approximation
1402    fn normal_cdf(&self, x: F) -> InterpolateResult<F> {
1403        // Simplified approximation - would use proper implementation in practice
1404        let result = (F::one()
1405            + (x / F::from_f64(1.414).expect("Failed to convert f64 to target float type")).tanh())
1406            / F::from_f64(2.0).expect("Failed to convert f64 to target float type");
1407        Ok(result)
1408    }
1409
1410    /// Simplified inverse normal CDF approximation
1411    fn inverse_normal_cdf(&self, p: F) -> InterpolateResult<F> {
1412        // Very simplified approximation - would use proper implementation
1413        if p <= F::zero() || p >= F::one() {
1414            return Ok(F::zero());
1415        }
1416
1417        // Rough approximation using atanh
1418        let x =
1419            F::from_f64(2.0).expect("Failed to convert f64 to target float type") * p - F::one();
1420        Ok(F::from_f64(1.414).expect("Failed to convert f64 to target float type") * x.atanh())
1421    }
1422}