#![allow(clippy::needless_range_loop)]
use super::cholesky::{cholesky, cholesky_solve, forward_sub};
use super::kernel::Kernel;
use super::GpError;
use crate::core::scalar::ControlScalar;
pub struct SparseGp<S, K, const D: usize, const M: usize, const N: usize>
where
S: ControlScalar,
K: Kernel<S, D>,
{
kernel: K,
noise_var: S,
inducing: [[S; D]; M],
trained: bool,
mu: [S; M],
l_post: [[S; M]; M],
kmm_chol: [[S; M]; M],
}
impl<S, K, const D: usize, const M: usize, const N: usize> SparseGp<S, K, D, M, N>
where
S: ControlScalar,
K: Kernel<S, D>,
{
pub fn new(kernel: K, noise_var: S, inducing: [[S; D]; M]) -> Self {
Self {
kernel,
noise_var,
inducing,
trained: false,
mu: [S::ZERO; M],
l_post: [[S::ZERO; M]; M],
kmm_chol: [[S::ZERO; M]; M],
}
}
#[inline]
fn dot_m(a: &[S; M], b: &[S; M]) -> S {
let mut acc = S::ZERO;
for i in 0..M {
acc += a[i] * b[i];
}
acc
}
pub fn fit(&mut self, x_train: [[S; D]; N], y_train: [S; N]) -> Result<(), GpError> {
let jitter = S::from_f64(1e-8);
let mut kmm = [[S::ZERO; M]; M];
for i in 0..M {
for j in 0..=i {
let kij = self.kernel.eval(&self.inducing[i], &self.inducing[j]);
kmm[i][j] = kij;
kmm[j][i] = kij;
}
kmm[i][i] += jitter;
}
self.kmm_chol = cholesky(&kmm)?;
let mut kmn = [[S::ZERO; N]; M];
for i in 0..M {
for j in 0..N {
kmn[i][j] = self.kernel.eval(&self.inducing[i], &x_train[j]);
}
}
let min_lambda = S::from_f64(1e-8);
let mut lambda = [S::ZERO; N];
for j in 0..N {
let mut col_j = [S::ZERO; M];
for i in 0..M {
col_j[i] = kmn[i][j];
}
let v_j = forward_sub(&self.kmm_chol, &col_j)?;
let qnn_j = Self::dot_m(&v_j, &v_j);
let knn_j = self.kernel.eval(&x_train[j], &x_train[j]);
let raw = knn_j - qnn_j + self.noise_var;
lambda[j] = if raw < min_lambda { min_lambda } else { raw };
}
let mut q_post = [[S::ZERO; M]; M];
for i in 0..M {
for j in 0..M {
q_post[i][j] = kmm[i][j];
}
}
for i in 0..M {
for j in 0..M {
let mut acc = S::ZERO;
for k in 0..N {
acc += kmn[i][k] * kmn[j][k] / lambda[k];
}
q_post[i][j] += acc;
}
q_post[i][i] += jitter;
}
self.l_post = cholesky(&q_post)?;
let mut rhs = [S::ZERO; M];
for i in 0..M {
let mut acc = S::ZERO;
for k in 0..N {
acc += kmn[i][k] * y_train[k] / lambda[k];
}
rhs[i] = acc;
}
self.mu = cholesky_solve(&q_post, &rhs)?;
self.trained = true;
Ok(())
}
pub fn predict(&self, x_star: &[S; D]) -> Result<(S, S), GpError> {
if !self.trained {
return Err(GpError::NotTrained);
}
let mut k_m = [S::ZERO; M];
for i in 0..M {
k_m[i] = self.kernel.eval(x_star, &self.inducing[i]);
}
let mean = Self::dot_m(&k_m, &self.mu);
let prior_kss = self.kernel.eval(x_star, x_star) + self.noise_var;
let v1 = forward_sub(&self.kmm_chol, &k_m)?;
let prior_reduction = Self::dot_m(&v1, &v1);
let v2 = forward_sub(&self.l_post, &k_m)?;
let post_boost = Self::dot_m(&v2, &v2);
let var_raw = prior_kss - prior_reduction + post_boost;
let var = if var_raw < S::ZERO { S::ZERO } else { var_raw };
Ok((mean, var))
}
pub fn is_trained(&self) -> bool {
self.trained
}
pub fn mu(&self) -> &[S; M] {
&self.mu
}
pub fn noise_var(&self) -> S {
self.noise_var
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gp::kernel::{Matern52Kernel, RbfKernel};
fn make_rbf() -> RbfKernel<f64> {
RbfKernel {
variance: 1.0,
length_scale: 1.0,
}
}
#[test]
fn fit_n5_m2_succeeds() {
let inducing = [[0.5_f64], [1.5_f64]];
let mut sgp: SparseGp<f64, _, 1, 2, 5> = SparseGp::new(make_rbf(), 0.1, inducing);
let x = [[0.0_f64], [0.5], [1.0], [1.5], [2.0]];
let y = [0.0_f64, 0.5, 1.0, 0.5, 0.0];
assert!(sgp.fit(x, y).is_ok(), "fit must succeed");
assert!(sgp.is_trained());
}
#[test]
fn predict_variance_positive() {
let inducing = [[0.5_f64], [1.5_f64]];
let mut sgp: SparseGp<f64, _, 1, 2, 5> = SparseGp::new(make_rbf(), 0.1, inducing);
let x = [[0.0_f64], [0.5], [1.0], [1.5], [2.0]];
let y = [0.0_f64, 0.5, 1.0, 0.5, 0.0];
sgp.fit(x, y).expect("fit must succeed");
let (_m, var) = sgp.predict(&[1.0_f64]).expect("predict must succeed");
assert!(var >= 0.0, "variance {var} must be non-negative");
}
#[test]
fn m1_degenerate_fits() {
let inducing = [[1.0_f64]];
let mut sgp: SparseGp<f64, _, 1, 1, 3> = SparseGp::new(make_rbf(), 0.1, inducing);
let x = [[0.0_f64], [1.0], [2.0]];
let y = [0.0_f64, 1.0, 0.0];
assert!(sgp.fit(x, y).is_ok(), "M=1 fit must succeed");
}
#[test]
fn variance_always_nonneg() {
let inducing = [[0.5_f64], [1.5_f64]];
let mut sgp: SparseGp<f64, _, 1, 2, 5> = SparseGp::new(make_rbf(), 0.01, inducing);
let x = [[0.0_f64], [0.5], [1.0], [1.5], [2.0]];
let y = [0.0_f64, 0.5, 1.0, 0.5, 0.0];
sgp.fit(x, y).expect("fit must succeed");
for xstar in [-10.0_f64, 0.0, 0.5, 1.0, 1.5, 2.0, 10.0, 100.0] {
let (_m, var) = sgp.predict(&[xstar]).expect("predict failed");
assert!(var >= 0.0, "variance {var} < 0 at x={xstar}");
}
}
#[test]
fn mu_updated_after_fit() {
let inducing = [[0.5_f64], [1.5_f64]];
let mut sgp: SparseGp<f64, _, 1, 2, 5> = SparseGp::new(make_rbf(), 0.1, inducing);
let x = [[0.0_f64], [0.5], [1.0], [1.5], [2.0]];
let y = [0.0_f64, 5.0, 10.0, 5.0, 0.0];
sgp.fit(x, y).expect("fit must succeed");
let mu_norm: f64 = sgp.mu().iter().map(|&v| v * v).sum::<f64>().sqrt();
assert!(
mu_norm > 1e-6,
"mu should be non-trivial after fit with large y"
);
}
#[test]
fn predict_before_fit_returns_not_trained() {
let inducing = [[0.5_f64], [1.5_f64]];
let sgp: SparseGp<f64, _, 1, 2, 5> = SparseGp::new(make_rbf(), 0.1, inducing);
let result = sgp.predict(&[1.0_f64]);
assert_eq!(result, Err(GpError::NotTrained));
}
#[test]
fn sparse_gp_matern52_fits() {
let kernel = Matern52Kernel::<f64> {
variance: 1.0,
length_scale: 1.0,
};
let inducing = [[0.5_f64], [1.5_f64], [2.5_f64]];
let mut sgp: SparseGp<f64, _, 1, 3, 6> = SparseGp::new(kernel, 0.1, inducing);
let x = [[0.0_f64], [0.5], [1.0], [1.5], [2.0], [3.0]];
let y = [0.0_f64, 1.0, 2.0, 1.5, 1.0, 0.5];
assert!(sgp.fit(x, y).is_ok());
let (_m, var) = sgp.predict(&[1.5_f64]).expect("predict failed");
assert!(var >= 0.0, "variance must be non-negative");
}
#[test]
fn sparse_gp_2d_input() {
let inducing = [[0.5_f64, 0.5], [1.5_f64, 1.5]];
let mut sgp: SparseGp<f64, _, 2, 2, 4> = SparseGp::new(make_rbf(), 0.1, inducing);
let x = [[0.0_f64, 0.0], [1.0, 0.0], [0.0, 1.0], [2.0, 2.0]];
let y = [0.0_f64, 1.0, 1.0, 4.0];
assert!(sgp.fit(x, y).is_ok());
let (mean, var) = sgp.predict(&[1.0_f64, 1.0]).expect("predict failed");
assert!(var >= 0.0, "variance {var} must be non-negative");
assert!(mean.is_finite(), "mean {mean} must be finite");
}
}