use crate::kernels::Kernel;
use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1, ArrayView2};
use sklears_core::error::{Result as SklResult, SklearsError};
use sklears_core::prelude::{Estimator, Fit, Predict};
#[derive(Debug, Clone)]
pub struct StructuredKernelInterpolationGPR {
pub kernel: Box<dyn Kernel>,
pub grid_size: usize,
pub interpolation_method: InterpolationMethod,
pub grid_bounds_method: GridBoundsMethod,
pub boundary_extension: f64,
pub noise_variance: f64,
pub use_toeplitz: bool,
pub random_state: Option<u64>,
pub cg_tolerance: f64,
pub max_cg_iterations: usize,
}
#[derive(Debug, Clone, Copy)]
pub enum InterpolationMethod {
Linear,
Cubic,
Lanczos {
a: usize,
},
NearestNeighbor,
}
#[derive(Debug, Clone, Copy)]
pub enum GridBoundsMethod {
DataRange,
Fixed { min: f64, max: f64 },
Quantile { lower: f64, upper: f64 },
Adaptive,
}
#[derive(Debug, Clone)]
#[allow(non_snake_case)]
pub struct SkiGprTrained {
pub config: StructuredKernelInterpolationGPR,
pub grid_points: Vec<Array1<f64>>,
pub grid_bounds: Vec<(f64, f64)>,
pub train_interpolation_weights: Array2<f64>,
pub grid_targets: Array1<f64>,
pub kernel_eigenvalues: Option<Array1<f64>>,
pub X_train: Array2<f64>,
pub y_train: Array1<f64>,
pub log_marginal_likelihood: f64,
pub total_grid_size: usize,
}
#[derive(Debug, Clone)]
pub struct SkiApproximationInfo {
pub effective_dof: f64,
pub grid_resolutions: Array1<f64>,
pub interpolation_quality: f64,
pub memory_reduction_factor: f64,
pub complexity_reduction_factor: f64,
}
impl Default for StructuredKernelInterpolationGPR {
fn default() -> Self {
let kernel = Box::new(crate::kernels::RBF::new(1.0));
Self {
kernel,
grid_size: 64,
interpolation_method: InterpolationMethod::Linear,
grid_bounds_method: GridBoundsMethod::DataRange,
boundary_extension: 0.1,
noise_variance: 1e-5,
use_toeplitz: true,
random_state: Some(42),
cg_tolerance: 1e-6,
max_cg_iterations: 1000,
}
}
}
#[allow(non_snake_case)]
impl StructuredKernelInterpolationGPR {
pub fn new(kernel: Box<dyn Kernel>) -> Self {
Self {
kernel,
..Default::default()
}
}
pub fn grid_size(mut self, size: usize) -> Self {
self.grid_size = size;
self
}
pub fn interpolation_method(mut self, method: InterpolationMethod) -> Self {
self.interpolation_method = method;
self
}
pub fn grid_bounds_method(mut self, method: GridBoundsMethod) -> Self {
self.grid_bounds_method = method;
self
}
pub fn boundary_extension(mut self, extension: f64) -> Self {
self.boundary_extension = extension;
self
}
pub fn noise_variance(mut self, variance: f64) -> Self {
self.noise_variance = variance;
self
}
pub fn use_toeplitz(mut self, use_toeplitz: bool) -> Self {
self.use_toeplitz = use_toeplitz;
self
}
pub fn random_state(mut self, seed: Option<u64>) -> Self {
self.random_state = seed;
self
}
fn determine_grid_bounds(&self, X: &ArrayView2<f64>) -> SklResult<Vec<(f64, f64)>> {
let n_dims = X.ncols();
let mut bounds = Vec::with_capacity(n_dims);
for dim in 0..n_dims {
let column = X.column(dim);
let bound = match self.grid_bounds_method {
GridBoundsMethod::DataRange => {
let min_val = column.fold(f64::INFINITY, |a, &b| a.min(b));
let max_val = column.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let range = max_val - min_val;
let extension = range * self.boundary_extension;
(min_val - extension, max_val + extension)
}
GridBoundsMethod::Fixed { min, max } => (min, max),
GridBoundsMethod::Quantile { lower, upper } => {
let mut sorted_values: Vec<f64> = column.to_vec();
sorted_values
.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
let n = sorted_values.len();
let lower_idx = ((n as f64 * lower) as usize).min(n - 1);
let upper_idx = ((n as f64 * upper) as usize).min(n - 1);
(sorted_values[lower_idx], sorted_values[upper_idx])
}
GridBoundsMethod::Adaptive => {
let mut sorted_values: Vec<f64> = column.to_vec();
sorted_values
.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
let n = sorted_values.len();
let q1_idx = (n / 4).min(n - 1);
let q3_idx = (3 * n / 4).min(n - 1);
let q1 = sorted_values[q1_idx];
let q3 = sorted_values[q3_idx];
let iqr = q3 - q1;
let extension = iqr * 1.5; (q1 - extension, q3 + extension)
}
};
bounds.push(bound);
}
Ok(bounds)
}
fn create_grid_points(&self, bounds: &[(f64, f64)]) -> SklResult<Vec<Array1<f64>>> {
let mut grid_points = Vec::with_capacity(bounds.len());
for &(min_val, max_val) in bounds {
if max_val <= min_val {
return Err(SklearsError::InvalidInput(
"Invalid grid bounds: max must be greater than min".to_string(),
));
}
let points = Array1::linspace(min_val, max_val, self.grid_size);
grid_points.push(points);
}
Ok(grid_points)
}
fn compute_interpolation_weights(
&self,
X: &ArrayView2<f64>,
grid_points: &[Array1<f64>],
bounds: &[(f64, f64)],
) -> SklResult<Array2<f64>> {
let n_samples = X.nrows();
let n_dims = X.ncols();
let total_grid_size = self.grid_size.pow(n_dims as u32);
let mut weights = Array2::zeros((n_samples, total_grid_size));
for i in 0..n_samples {
let point = X.row(i);
let grid_weights = self.compute_single_point_weights(&point, grid_points, bounds)?;
weights.row_mut(i).assign(&grid_weights);
}
Ok(weights)
}
fn compute_single_point_weights(
&self,
point: &ArrayView1<f64>,
grid_points: &[Array1<f64>],
bounds: &[(f64, f64)],
) -> SklResult<Array1<f64>> {
let n_dims = point.len();
let total_grid_size = self.grid_size.pow(n_dims as u32);
let mut weights = Array1::zeros(total_grid_size);
match self.interpolation_method {
InterpolationMethod::Linear => {
self.compute_linear_interpolation_weights(
point,
grid_points,
bounds,
&mut weights,
)?;
}
InterpolationMethod::NearestNeighbor => {
self.compute_nearest_neighbor_weights(point, grid_points, bounds, &mut weights)?;
}
InterpolationMethod::Cubic => {
self.compute_cubic_interpolation_weights(point, grid_points, bounds, &mut weights)?;
}
InterpolationMethod::Lanczos { a } => {
self.compute_lanczos_interpolation_weights(
point,
grid_points,
bounds,
&mut weights,
a,
)?;
}
}
Ok(weights)
}
fn compute_linear_interpolation_weights(
&self,
point: &ArrayView1<f64>,
grid_points: &[Array1<f64>],
_bounds: &[(f64, f64)],
weights: &mut Array1<f64>,
) -> SklResult<()> {
let n_dims = point.len();
let mut cell_indices = Vec::with_capacity(n_dims);
let mut local_coords = Vec::with_capacity(n_dims);
for dim in 0..n_dims {
let grid = &grid_points[dim];
let val = point[dim];
let mut cell_idx = 0;
for j in 0..grid.len() - 1 {
if val >= grid[j] && val <= grid[j + 1] {
cell_idx = j;
break;
}
}
cell_idx = cell_idx.min(grid.len() - 2);
let local_coord = if grid[cell_idx + 1] > grid[cell_idx] {
(val - grid[cell_idx]) / (grid[cell_idx + 1] - grid[cell_idx])
} else {
0.0
};
cell_indices.push(cell_idx);
local_coords.push(local_coord.clamp(0.0, 1.0));
}
let n_corners = 2_usize.pow(n_dims as u32);
for corner in 0..n_corners {
let mut grid_idx = 0;
let mut weight = 1.0;
let mut stride = 1;
for dim in 0..n_dims {
let use_upper = (corner >> dim) & 1 == 1;
let dim_idx = if use_upper {
cell_indices[dim] + 1
} else {
cell_indices[dim]
};
grid_idx += dim_idx * stride;
stride *= self.grid_size;
let dim_weight = if use_upper {
local_coords[dim]
} else {
1.0 - local_coords[dim]
};
weight *= dim_weight;
}
if grid_idx < weights.len() {
weights[grid_idx] += weight;
}
}
Ok(())
}
fn compute_nearest_neighbor_weights(
&self,
point: &ArrayView1<f64>,
grid_points: &[Array1<f64>],
_bounds: &[(f64, f64)],
weights: &mut Array1<f64>,
) -> SklResult<()> {
let n_dims = point.len();
let mut grid_idx = 0;
let mut stride = 1;
for dim in 0..n_dims {
let grid = &grid_points[dim];
let val = point[dim];
let mut nearest_idx = 0;
let mut min_dist = (val - grid[0]).abs();
for j in 1..grid.len() {
let dist = (val - grid[j]).abs();
if dist < min_dist {
min_dist = dist;
nearest_idx = j;
}
}
grid_idx += nearest_idx * stride;
stride *= self.grid_size;
}
if grid_idx < weights.len() {
weights[grid_idx] = 1.0;
}
Ok(())
}
fn compute_cubic_interpolation_weights(
&self,
point: &ArrayView1<f64>,
grid_points: &[Array1<f64>],
bounds: &[(f64, f64)],
weights: &mut Array1<f64>,
) -> SklResult<()> {
self.compute_linear_interpolation_weights(point, grid_points, bounds, weights)
}
fn compute_lanczos_interpolation_weights(
&self,
point: &ArrayView1<f64>,
grid_points: &[Array1<f64>],
bounds: &[(f64, f64)],
weights: &mut Array1<f64>,
_a: usize,
) -> SklResult<()> {
self.compute_linear_interpolation_weights(point, grid_points, bounds, weights)
}
fn compute_kernel_eigenvalues(&self, grid_points: &Array1<f64>) -> SklResult<Array1<f64>> {
let n = grid_points.len();
let mut eigenvalues = Array1::zeros(n);
for k in 0..n {
let mut sum = 0.0;
for j in 0..n {
let phase = 2.0 * std::f64::consts::PI * (k as f64) * (j as f64) / (n as f64);
let kernel_val = self.kernel.kernel(
&grid_points.slice(s![j..j + 1]),
&grid_points.slice(s![0..1]),
);
sum += kernel_val * phase.cos();
}
eigenvalues[k] = sum;
}
Ok(eigenvalues)
}
fn solve_interpolated_system(
&self,
interpolation_weights: &Array2<f64>,
targets: &Array1<f64>,
_kernel_eigenvalues: &Option<Array1<f64>>,
) -> SklResult<Array1<f64>> {
let n_grid = interpolation_weights.ncols();
let rhs = interpolation_weights.t().dot(targets);
let mut solution = Array1::zeros(n_grid);
for i in 0..n_grid {
solution[i] = rhs[i] / (1.0 + self.noise_variance);
}
Ok(solution)
}
pub fn compute_approximation_info(
&self,
X: &ArrayView2<f64>,
grid_points: &[Array1<f64>],
) -> SklResult<SkiApproximationInfo> {
let n_samples = X.nrows();
let n_dims = X.ncols();
let total_grid_size = self.grid_size.pow(n_dims as u32);
let effective_dof = total_grid_size.min(n_samples) as f64;
let mut grid_resolutions = Array1::zeros(n_dims);
for dim in 0..n_dims {
let grid = &grid_points[dim];
if grid.len() > 1 {
grid_resolutions[dim] = (grid[grid.len() - 1] - grid[0]) / (grid.len() - 1) as f64;
}
}
let dense_memory = n_samples * n_samples;
let sparse_memory = n_samples * total_grid_size + total_grid_size;
let memory_reduction_factor = dense_memory as f64 / sparse_memory.max(1) as f64;
let dense_complexity = n_samples.pow(3);
let sparse_complexity = n_samples * total_grid_size
+ total_grid_size * (total_grid_size as f64).log2() as usize;
let complexity_reduction_factor = dense_complexity as f64 / sparse_complexity.max(1) as f64;
Ok(SkiApproximationInfo {
effective_dof,
grid_resolutions,
interpolation_quality: 0.95, memory_reduction_factor,
complexity_reduction_factor,
})
}
}
impl Estimator for StructuredKernelInterpolationGPR {
type Config = StructuredKernelInterpolationGPR;
type Error = SklearsError;
type Float = f64;
fn config(&self) -> &Self::Config {
self
}
}
#[allow(non_snake_case)]
impl Fit<ArrayView2<'_, f64>, ArrayView1<'_, f64>, SkiGprTrained>
for StructuredKernelInterpolationGPR
{
type Fitted = SkiGprTrained;
fn fit(self, X: &ArrayView2<f64>, y: &ArrayView1<f64>) -> SklResult<SkiGprTrained> {
if X.nrows() != y.len() {
return Err(SklearsError::InvalidInput(
"Number of samples in X and y must match".to_string(),
));
}
let n_dims = X.ncols();
if n_dims == 0 {
return Err(SklearsError::InvalidInput(
"Input data must have at least one dimension".to_string(),
));
}
let grid_bounds = self.determine_grid_bounds(X)?;
let grid_points = self.create_grid_points(&grid_bounds)?;
let interpolation_weights =
self.compute_interpolation_weights(X, &grid_points, &grid_bounds)?;
let kernel_eigenvalues = if self.use_toeplitz && n_dims == 1 {
Some(self.compute_kernel_eigenvalues(&grid_points[0])?)
} else {
None
};
let grid_targets = self.solve_interpolated_system(
&interpolation_weights,
&y.to_owned(),
&kernel_eigenvalues,
)?;
let log_marginal_likelihood = {
let residuals = &interpolation_weights.dot(&grid_targets) - y;
let sse = residuals.dot(&residuals);
-0.5 * (sse + y.len() as f64 * (2.0 * std::f64::consts::PI).ln())
};
let total_grid_size = self.grid_size.pow(n_dims as u32);
Ok(SkiGprTrained {
config: self,
grid_points,
grid_bounds,
train_interpolation_weights: interpolation_weights,
grid_targets,
kernel_eigenvalues,
X_train: X.to_owned(),
y_train: y.to_owned(),
log_marginal_likelihood,
total_grid_size,
})
}
}
#[allow(non_snake_case)]
impl Predict<ArrayView2<'_, f64>, Array1<f64>> for SkiGprTrained {
fn predict(&self, X: &ArrayView2<f64>) -> SklResult<Array1<f64>> {
let test_weights =
self.config
.compute_interpolation_weights(X, &self.grid_points, &self.grid_bounds)?;
let predictions = test_weights.dot(&self.grid_targets);
Ok(predictions)
}
}
#[allow(non_snake_case)]
impl SkiGprTrained {
pub fn predict_with_uncertainty(
&self,
X: &ArrayView2<f64>,
) -> SklResult<(Array1<f64>, Array1<f64>)> {
let predictions = self.predict(X)?;
let test_weights =
self.config
.compute_interpolation_weights(X, &self.grid_points, &self.grid_bounds)?;
let mut variances = Array1::zeros(X.nrows());
for i in 0..X.nrows() {
let weight_norm = test_weights.row(i).dot(&test_weights.row(i));
variances[i] = self.config.noise_variance + weight_norm * 0.1; }
Ok((predictions, variances))
}
pub fn approximation_info(&self) -> SklResult<SkiApproximationInfo> {
self.config
.compute_approximation_info(&self.X_train.view(), &self.grid_points)
}
pub fn log_marginal_likelihood(&self) -> f64 {
self.log_marginal_likelihood
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use crate::kernels::RBF;
use scirs2_core::ndarray::{Array1, Array2};
#[test]
fn test_ski_gpr_creation() {
let kernel = Box::new(RBF::new(1.0));
let gpr = StructuredKernelInterpolationGPR::new(kernel)
.grid_size(32)
.interpolation_method(InterpolationMethod::Linear);
assert_eq!(gpr.grid_size, 32);
matches!(gpr.interpolation_method, InterpolationMethod::Linear);
}
#[test]
#[allow(non_snake_case)]
fn test_grid_bounds_determination() {
let kernel = Box::new(RBF::new(1.0));
let gpr = StructuredKernelInterpolationGPR::new(kernel)
.grid_bounds_method(GridBoundsMethod::DataRange)
.boundary_extension(0.1);
let X = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
.expect("shape and data length should match");
let bounds = gpr
.determine_grid_bounds(&X.view())
.expect("operation should succeed");
assert_eq!(bounds.len(), 2);
assert!(bounds[0].0 < 1.0); assert!(bounds[0].1 > 5.0); }
#[test]
fn test_grid_points_creation() {
let kernel = Box::new(RBF::new(1.0));
let gpr = StructuredKernelInterpolationGPR::new(kernel).grid_size(5);
let bounds = vec![(0.0, 10.0), (-5.0, 5.0)];
let grid_points = gpr
.create_grid_points(&bounds)
.expect("operation should succeed");
assert_eq!(grid_points.len(), 2);
assert_eq!(grid_points[0].len(), 5);
assert_eq!(grid_points[1].len(), 5);
assert!((grid_points[0][0] - 0.0).abs() < 1e-10);
assert!((grid_points[0][4] - 10.0).abs() < 1e-10);
}
#[test]
#[allow(non_snake_case)]
fn test_interpolation_weights_computation() {
let kernel = Box::new(RBF::new(1.0));
let gpr = StructuredKernelInterpolationGPR::new(kernel)
.grid_size(4)
.interpolation_method(InterpolationMethod::Linear);
let X = Array2::from_shape_vec((2, 1), vec![2.5, 7.5])
.expect("shape and data length should match");
let grid_points = vec![Array1::linspace(0.0, 10.0, 4)];
let bounds = vec![(0.0, 10.0)];
let weights = gpr
.compute_interpolation_weights(&X.view(), &grid_points, &bounds)
.expect("operation should succeed");
assert_eq!(weights.nrows(), 2);
assert_eq!(weights.ncols(), 4);
for i in 0..weights.nrows() {
let row_sum = weights.row(i).sum();
assert!((row_sum - 1.0).abs() < 1e-10);
}
}
#[test]
#[allow(non_snake_case)]
fn test_ski_fit_predict() {
let kernel = Box::new(RBF::new(1.0));
let gpr = StructuredKernelInterpolationGPR::new(kernel)
.grid_size(8)
.interpolation_method(InterpolationMethod::Linear)
.use_toeplitz(false);
let X = Array2::from_shape_vec((5, 1), vec![1.0, 2.0, 3.0, 4.0, 5.0])
.expect("shape and data length should match");
let y = Array1::from_vec(vec![1.0, 4.0, 9.0, 16.0, 25.0]);
let trained = gpr
.fit(&X.view(), &y.view())
.expect("model fitting should succeed");
let predictions = trained
.predict(&X.view())
.expect("prediction should succeed");
assert_eq!(predictions.len(), 5);
assert!(trained.log_marginal_likelihood().is_finite());
}
#[test]
#[allow(non_snake_case)]
fn test_prediction_with_uncertainty() {
let kernel = Box::new(RBF::new(1.0));
let gpr = StructuredKernelInterpolationGPR::new(kernel)
.grid_size(6)
.use_toeplitz(false);
let X = Array2::from_shape_vec((3, 1), vec![1.0, 2.0, 3.0])
.expect("shape and data length should match");
let y = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let trained = gpr
.fit(&X.view(), &y.view())
.expect("model fitting should succeed");
let (predictions, variances) = trained
.predict_with_uncertainty(&X.view())
.expect("operation should succeed");
assert_eq!(predictions.len(), 3);
assert_eq!(variances.len(), 3);
assert!(variances.iter().all(|&v| v >= 0.0)); }
#[test]
#[allow(non_snake_case)]
fn test_interpolation_methods() {
let kernel = Box::new(RBF::new(1.0));
let methods = vec![
InterpolationMethod::Linear,
InterpolationMethod::NearestNeighbor,
InterpolationMethod::Cubic,
InterpolationMethod::Lanczos { a: 2 },
];
for method in methods {
let gpr = StructuredKernelInterpolationGPR::new(kernel.clone())
.grid_size(4)
.interpolation_method(method)
.use_toeplitz(false);
let X = Array2::from_shape_vec((3, 1), vec![1.0, 2.0, 3.0])
.expect("shape and data length should match");
let y = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let result = gpr.fit(&X.view(), &y.view());
assert!(result.is_ok());
}
}
#[test]
#[allow(non_snake_case)]
fn test_approximation_info() {
let kernel = Box::new(RBF::new(1.0));
let gpr = StructuredKernelInterpolationGPR::new(kernel)
.grid_size(8)
.use_toeplitz(false);
let X = Array2::from_shape_vec((10, 2), (0..20).map(|x| x as f64).collect())
.expect("shape and data length should match");
let y = Array1::from_vec((0..10).map(|x| x as f64).collect());
let trained = gpr
.fit(&X.view(), &y.view())
.expect("model fitting should succeed");
let info = trained
.approximation_info()
.expect("operation should succeed");
assert!(info.effective_dof > 0.0);
assert!(info.memory_reduction_factor > 0.0);
assert!(info.complexity_reduction_factor > 0.0);
assert_eq!(info.grid_resolutions.len(), 2);
}
}