use sprs::CsMat;
use sprs_ldl::{LdlNumeric};
use hdf5_metno::{File, Result as H5Result};
use ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis};
pub mod prng;
pub use crate::prng::PCG64Stream;
pub mod kernel;
pub use crate::kernel::{Wendland};
pub use crate::kernel::{WendlandKernel};
pub use crate::kernel::{Kernel};
pub mod spatial_hash;
pub use crate::spatial_hash::SpatialHash;
pub mod realization;
pub use crate::realization::GPRealization;
pub struct GP {
pub ndim: usize,
pub kernel: Kernel,
training_points: Array2<f64>, training_values: Array1<f64>, training_error: Array1<f64>, training_kernel: CsMat<f64>,
training_hash: SpatialHash,
alpha: Array1<f64>, #[allow(non_snake_case)]
lnL: f64,
ldl_numeric: Option<LdlNumeric<f64, usize>>,
}
impl GP {
pub fn train(
training_points: ArrayView2<f64>,
training_values: ArrayView1<f64>,
training_error: ArrayView1<f64>,
scale: ArrayView1<f64>,
whitenoise: f64,
msq_order: i32,
) -> GP {
let ndim = training_points.ncols();
let kernel = Kernel::new(ndim, msq_order, scale, whitenoise);
let scaled_training_points: Array2<f64> =
kernel.scale_arr(training_points);
let training_values = training_values.as_standard_layout().into_owned();
let training_error = training_error.as_standard_layout().into_owned();
let scaled_min: Array1<f64> = scaled_training_points.map_axis(Axis(0), |col| {
col.iter().copied().fold(f64::INFINITY, f64::min)
});
let scaled_max: Array1<f64> = scaled_training_points.map_axis(Axis(0), |col| {
col.iter().copied().fold(f64::NEG_INFINITY, f64::max)
});
let sparsity: f64 = (&scaled_max - &scaled_min).mean().unwrap();
let points_for_hash: Vec<Vec<f64>> = scaled_training_points
.axis_iter(Axis(0))
.map(|row| row.to_vec())
.collect();
let training_hash = SpatialHash::build(&points_for_hash, 1.0);
let training_kernel = match sparsity {
sparsity if sparsity > 2. => {
kernel.hashed_kernel_construction(
scaled_training_points.view(),
training_error.view(),
&training_hash,
)
}
_ => {
kernel.naive_kernel_construction(
scaled_training_points.view(),
training_error.view(),
)
}
};
let training_kernel = training_kernel.to_csr();
let numeric = LdlNumeric::new(training_kernel.view())
.expect("Numeric LDL analysis failed");
let alpha_vec = numeric.solve(training_values.as_slice().unwrap());
let alpha = Array1::from_vec(alpha_vec);
let mut gp = GP {
ndim,
kernel,
training_points: scaled_training_points,
training_values,
training_error,
training_kernel,
training_hash,
alpha,
lnL: 0.,
ldl_numeric: Some(numeric),
};
gp.log_marginal_likelihood();
gp
}
pub fn generate_ldl(&mut self) {
self.ldl_numeric = Some(LdlNumeric::new(self.training_kernel.view())
.expect("Numeric LDL analysis failed"));
}
pub fn retrain(
&mut self,
scale: ArrayView1<f64>,
whitenoise: f64,
msq_order: i32,
) {
let kernel = Kernel::new(self.ndim, msq_order, scale, whitenoise);
let training_points = &self.training_points * (&self.kernel.scale() / &scale);
let scaled_min: Array1<f64> = training_points.map_axis(Axis(0), |col| {
col.iter().copied().fold(f64::INFINITY, f64::min)
});
let scaled_max: Array1<f64> = training_points.map_axis(Axis(0), |col| {
col.iter().copied().fold(f64::NEG_INFINITY, f64::max)
});
let sparsity: f64 = (&scaled_max - &scaled_min).mean().unwrap();
let points_for_hash: Vec<Vec<f64>> = training_points
.axis_iter(Axis(0))
.map(|row| row.to_vec())
.collect();
let training_hash = SpatialHash::build(&points_for_hash, 1.0);
let training_kernel = match sparsity {
sparsity if sparsity > 2. => {
kernel.hashed_kernel_construction(
training_points.view(),
self.training_error.view(),
&training_hash,
)
}
_ => {
kernel.naive_kernel_construction(
training_points.view(),
self.training_error.view(),
)
}
};
let training_kernel = training_kernel.to_csr();
self.training_points = training_points;
self.training_hash = training_hash;
self.kernel = kernel;
self.training_kernel = training_kernel;
let numeric = LdlNumeric::new(self.training_kernel.view())
.expect("Numeric LDL analysis failed");
let alpha_vec = numeric.solve(self.training_values.as_slice().unwrap());
let alpha = Array1::from_vec(alpha_vec);
self.ldl_numeric = Some(numeric);
self.alpha = alpha;
self.log_marginal_likelihood();
}
pub fn for_each_kernel_entry<F: FnMut(usize, f64)>(
&self,
eval_point: ArrayView1<f64>,
mut f: F,
) {
debug_assert_eq!(eval_point.len(), self.ndim);
let eval_point_scaled = self.kernel.scale_sgl(eval_point);
self.training_hash.for_each_neighbor(
eval_point_scaled.as_slice().unwrap(), |i| {
let xi = self.training_points.row(i);
let r2: f64 = eval_point_scaled.iter()
.zip(xi.iter())
.map(|(ei, xi)| (ei-xi).powi(2))
.sum();
if r2 >= 1.0 { return; }
let r = r2.sqrt();
let kij = self.kernel.eval_sgl(r, r2);
f(i,kij);
}
);
}
fn fill_kernel_vec(&self, eval_point: ArrayView1<f64>, out: &mut [f64]) {
debug_assert_eq!(out.len(), self.training_points.nrows());
debug_assert_eq!(eval_point.len(), self.ndim);
out.fill(0.);
self.for_each_kernel_entry(eval_point,
|i, kij| {out[i] = kij;});
}
pub fn predict_mean_sgl(&self, eval_point: ArrayView1<f64>) -> f64 {
let eval_point_scaled = self.kernel.scale_sgl(eval_point);
let mut mean: f64 = 0.;
self.training_hash.for_each_neighbor(
eval_point_scaled.as_slice().unwrap(), |i| {
let xi = self.training_points.row(i);
let r2: f64 = eval_point_scaled.iter()
.zip(xi.iter())
.map(|(ei, xi)| (ei-xi).powi(2))
.sum();
if r2 >= 1.0 { return; }
let r = r2.sqrt();
let val = self.kernel.eval_sgl(r, r2);
mean += val * self.alpha[i];
}
);
mean
}
pub fn predict_mean(&self, eval_points: ArrayView2<f64>) -> Array1<f64> {
let scaled_points = self.kernel.scale_arr(eval_points);
let neval = eval_points.nrows();
let mut means = vec![0.; neval];
for (idx, scaled_row) in scaled_points.outer_iter().enumerate() {
let mut mean = 0.;
let scaled_slice = scaled_row.as_slice().unwrap();
self.training_hash.for_each_neighbor(scaled_slice, |i| {
let xi = self.training_points.row(i);
let mut r2 = 0.;
for (ei, xi) in scaled_slice.iter()
.zip(xi.iter()) {
let diff = ei - xi;
r2 += diff * diff;
}
if r2 < 1.0 {
let r = r2.sqrt();
mean += self.kernel.eval_sgl(r, r2) * self.alpha[i];
}
});
means[idx] = mean;
}
Array1::from_vec(means)
}
pub fn predict_var(&self, eval_points: ArrayView2<f64>) -> Array1<f64> {
let numeric = self.ldl_numeric
.as_ref()
.expect("GP not trained; call train method first!");
let ntrain = self.training_points.nrows();
let mut k_eval_pt = vec![0.;ntrain];
eval_points.axis_iter(Axis(0)).map(|x| {
self.fill_kernel_vec(x, &mut k_eval_pt);
let q = numeric.solve(&k_eval_pt);
let quad: f64 = k_eval_pt.iter()
.zip(q.iter())
.map(|(k, q)| k * q)
.sum();
let variance = 1.0 - quad;
variance.max(0.)
})
.collect()
}
pub fn predict_cov_sgl(
&self,
x1: ArrayView1<f64>,
x2: ArrayView1<f64>,
) -> f64 {
let numeric = self.ldl_numeric.as_ref()
.expect("GP not trained; call generate_ldl() first!");
let ntrain = self.training_points.nrows();
let mut k1 = vec![0.; ntrain];
let mut k2 = vec![0.; ntrain];
self.fill_kernel_vec(x1, &mut k1);
self.fill_kernel_vec(x2, &mut k2);
let q = numeric.solve(&k2);
let quad: f64 = k1.iter()
.zip(q.iter())
.map(|(k,q)| k * q)
.sum();
let x1s = self.kernel.scale_sgl(x1);
let x2s = self.kernel.scale_sgl(x2);
let r2: f64 = x1s.iter()
.zip(x2s.iter())
.map(|(k1, k2)| (k1 - k2).powi(2))
.sum();
let k12 = if r2 > 1.0 { 0. } else {
let r = r2.sqrt();
self.kernel.eval_sgl(r, r2)
};
let cov = k12 - quad;
if cov.abs() < 1e-14 { 0. } else { cov }
}
pub fn predict_var_sgl(&self, eval_point: ArrayView1<f64>) -> f64 {
let eval_points = eval_point.insert_axis(Axis(0));
self.predict_var(eval_points)[0]
}
pub fn save_to_hdf5(&self, filename: &str, group_name: &str) -> H5Result<()> {
let file = if std::path::Path::new(filename).exists() {
File::open_rw(filename)? } else {
File::create(filename)?
};
let group = if file.link_exists(group_name) {
panic!("Cannot save new GP! {group_name} already taken!");
} else {
file.create_group(group_name)?
};
group.new_attr::<f64>().create("whitenoise")?
.write_scalar(&self.kernel.whitenoise())?;
group.new_attr::<usize>().create("ndim")?
.write_scalar(&self.ndim)?;
group.new_attr::<i32>().create("msq_order")?
.write_scalar(&self.kernel.msq_order())?;
group.new_attr::<u32>().create("format_version")?
.write_scalar(&1)?;
group.new_attr::<f64>().create("lnL")?
.write_scalar(&self.lnL)?;
group.new_dataset_builder()
.with_data(self.kernel.scale().as_slice().unwrap())
.create("scale")?;
let pts_flat: Vec<f64> = self.training_points
.iter().copied().collect();
group.new_dataset_builder()
.with_data(&pts_flat)
.create("training_points")?;
group.new_dataset_builder()
.with_data(self.training_values.as_slice().unwrap())
.create("training_values")?;
group.new_dataset_builder()
.with_data(self.training_error.as_slice().unwrap())
.create("training_error")?;
group.new_dataset_builder()
.with_data(self.alpha.as_slice().unwrap())
.create("alpha")?;
let csr = &self.training_kernel;
group.new_dataset_builder()
.with_data(csr.indices())
.create("kernel_indices")?;
group.new_dataset_builder()
.with_data(csr.data())
.create("kernel_data")?;
group.new_dataset_builder()
.with_data(csr.indptr().raw_storage())
.create("kernel_indptr")?;
Ok(())
}
pub fn load_from_hdf5(filename: &str, group_name: &str) -> H5Result<GP> {
let file = File::open(filename)?;
let group = file.group(group_name)?;
let ndim: usize = group.attr("ndim")?.read_scalar()?;
let msq_order: i32 = group.attr("msq_order")?.read_scalar()?;
let whitenoise: f64 = group.attr("whitenoise")?.read_scalar()?;
#[allow(non_snake_case)]
let lnL: f64 = group.attr("lnL")?.read_scalar()?;
let scale_vec: Vec<f64> = group.dataset("scale")?
.read_raw()?
.to_vec();
let scale = Array1::from_vec(scale_vec);
let kernel = Kernel::new(ndim, msq_order, scale.view(), whitenoise);
let pts_flat: Vec<f64> = group.dataset("training_points")?
.read_raw()?
.to_vec();
let ntrain = pts_flat.len() / ndim;
let training_points = Array2::from_shape_vec((ntrain, ndim),pts_flat)
.expect("Failed to reshape training points");
let training_values_vec: Vec<f64> =
group.dataset("training_values")?
.read_raw()?
.to_vec();
let training_values = Array1::from_vec(training_values_vec);
let training_error_vec: Vec<f64> =
group.dataset("training_error")?
.read_raw()?
.to_vec();
let training_error = Array1::from_vec(training_error_vec);
let alpha_vec: Vec<f64> =
group.dataset("alpha")?
.read_raw()?
.to_vec();
let alpha = Array1::from_vec(alpha_vec);
let indptr: Vec<usize> = group.dataset("kernel_indptr")?
.read_raw()?
.to_vec();
let indices: Vec<usize> = group.dataset("kernel_indices")?
.read_raw()?
.to_vec();
let data: Vec<f64> = group.dataset("kernel_data")?
.read_raw()?
.to_vec();
let training_kernel = CsMat::new((ntrain,ntrain), indptr, indices, data);
let points_for_hash: Vec<Vec<f64>> = training_points
.axis_iter(Axis(0))
.map(|row| row.to_vec())
.collect();
let training_hash = SpatialHash::build(&points_for_hash, 1.0);
Ok(GP {
ndim,
kernel,
training_points,
training_values,
training_error,
training_kernel,
training_hash,
alpha,
lnL,
ldl_numeric: None,
})
}
pub fn log_marginal_likelihood(&mut self) -> f64 {
let ln2pi: f64 = (2. * std::f64::consts::PI).ln();
let numeric = self.ldl_numeric.as_ref().unwrap();
let quad: f64 = self.training_values.iter()
.zip(self.alpha.iter())
.map(|(y, a)| y * a)
.sum();
let d = numeric.d();
let logdet: f64 = d.iter().map(|&di| di.ln()).sum();
self.lnL = (-0.5 * quad) - (0.5 * logdet) -
(0.5 * (self.alpha.len() as f64) * ln2pi);
self.lnL
}
pub fn draw_realization<'a>(
&'a self,
rng: &mut PCG64Stream,
) -> GPRealization<'a> {
let numeric = self.ldl_numeric
.as_ref()
.expect("GP not trained");
let ntrain = self.training_points.nrows();
let mut z = vec![0.;ntrain];
rng.fill_standard_normal(&mut z);
let z0 = z.clone();
let d = numeric.d();
let v: Vec<f64> = z.iter()
.zip(d.iter())
.map(|(&zi, &di)| di.sqrt() * zi)
.collect();
let l = numeric.l();
let mut f0 = vec![0.;ntrain];
if l.is_csr() {
for (i, row) in l.outer_iterator().enumerate() {
let acc = v[i] + row.iter()
.filter_map(|(j,lij)| (j < i).then(|| lij * v[j]))
.sum::<f64>();
f0[i] = acc;
}
} else if l.is_csc() {
for (j, col) in l.outer_iterator().enumerate() {
let vj = v[j];
for (i, lij) in col.iter().filter(|&(i, _)| i > j) {
f0[i] += lij * vj;
}
}
}
let u = numeric.solve(&f0);
let k = &self.training_kernel;
let mut ku = vec![0.;ntrain];
if k.is_csr() {
for (i, row) in k.outer_iterator().enumerate() {
ku[i] = row.iter()
.map(|(j,kij)| kij * u[j])
.sum();
}
} else if k.is_csc() {
for (j, col) in k.outer_iterator().enumerate() {
let uj = u[j];
for (i, kij) in col.iter() {
ku[i] += kij * uj;
}
}
}
let f_train: Vec<f64> = self.training_values.iter()
.zip(f0.iter().zip(ku.iter()))
.map(|(y, (f, k))| y + f - k)
.collect();
let beta = numeric.solve(&f_train);
GPRealization::new(self, f_train, beta, z0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::arr1;
fn vec2d_to_array2(v: &[Vec<f64>]) -> Array2<f64> {
let nrows = v.len();
let ncols = v[0].len();
let flat: Vec<f64> = v.iter().flat_map(
|row| row.iter().copied()).collect();
Array2::from_shape_vec((nrows,ncols), flat).unwrap()
}
#[test]
fn gp_interpolates_training_points() {
let x_train = vec2d_to_array2(&[vec![0.], vec![0.5], vec![1.0]]);
let y_train = arr1(&[0., 1., 0.]);
let y_train_err = arr1(&[0., 0., 0.]);
let kernel_noise = 0.;
let scale = arr1(&[1.0]);
for order in 0..4 {
let gp = GP::train(
x_train.view(),
y_train.view(),
y_train_err.view(),
scale.view(),
kernel_noise,
order,
);
let prediction = gp.predict_mean(x_train.view());
for i in 0..x_train.nrows() {
assert!(
(prediction[i] - y_train[i]).abs() < 1e-6,
"prediction[i] = {}, y_train = {}", prediction[i], y_train[i],
);
}
}
}
#[test]
fn compact_support_gives_zero_mean_far_away() {
let x_train = vec2d_to_array2(&[vec![0.],vec![0.5]]);
let y_train = arr1(&[8., -6.]);
let y_train_err = arr1(&[0.04,0.08]);
let whitenoise = 1e-8;
let scale = arr1(&[1.]);
for order in 0..4 {
let gp = GP::train(
x_train.view(),
y_train.view(),
y_train_err.view(),
scale.view(),
whitenoise,
order,
);
let far = arr1(&[10.0]);
let prediction = gp.predict_mean_sgl(far.view());
assert!(prediction.abs() < 1e-10);
}
}
#[test]
fn test_interpolation() {
let x_train = vec2d_to_array2(&[vec![0.],vec![1.]]);
let y_train = arr1(&[0., 1.]);
let y_train_err = arr1(&[0.04,0.08]);
let whitenoise = 1e-8;
let scale = arr1(&[1.]);
for order in 0..4 {
let gp = GP::train(
x_train.view(),
y_train.view(),
y_train_err.view(),
scale.view(),
whitenoise,
order,
);
let between = arr1(&[0.5]);
let prediction = gp.predict_mean_sgl(between.view());
assert!(prediction.abs() > 0.);
assert!(prediction.abs() < 1.);
}
}
#[test]
fn disconnected_components_do_not_interact() {
let x_train = vec2d_to_array2(&[vec![0.],vec![0.2],vec![5.0],vec![5.2]]);
let y_train = arr1(&[1.,1.,-1.,-1.]);
let err = arr1(&[0.,0.,0.,0.,]);
let whitenoise = 1e-8;
let scale = arr1(&[1.0]);
for order in 0..4 {
let gp = GP::train(
x_train.view(),
y_train.view(),
err.view(),
scale.view(),
whitenoise,
order,
);
let near_left = arr1(&[0.1]);
let near_right = arr1(&[5.1]);
let prediction_left = gp.predict_mean_sgl(near_left.view());
let prediction_right = gp.predict_mean_sgl(near_right.view());
assert!(prediction_left > 0.5);
assert!(prediction_right < -0.5);
}
}
#[test]
fn variance_zero_at_training_points() {
let x_train = vec2d_to_array2(&[vec![0.],vec![0.5],vec![1.],]);
let y_train = arr1(&[0.,1.,0.]);
let y_err = arr1(&[0.;3]);
let whitenoise = 1e-10;
let scale = arr1(&[1.]);
for order in 0..4 {
let gp = GP::train(
x_train.view(), y_train.view(), y_err.view(),
scale.view(), whitenoise, order
);
let vars = gp.predict_var(x_train.view());
for (i, var) in vars.iter().enumerate() {
assert!(
*var < 1e-6,
"variance at training point {i} too large: {var}"
);
}
}
}
#[test]
fn variance_reverts_to_prior_far_away() {
let x_train = vec2d_to_array2(&[vec![0.],vec![0.3]]);
let y_train = arr1(&[1.,-1.]);
let y_err = arr1(&[0.,0.]);
let whitenoise = 1e-10;
let scale = arr1(&[1.]);
for order in 0..4 {
let gp = GP::train(
x_train.view(), y_train.view(), y_err.view(),
scale.view(), whitenoise, order
);
let far = arr1(&[10.]);
let var = gp.predict_var_sgl(far.view());
assert!((var - 1.).abs() < 1e-6, "far variance wrong: {var}");
}
}
#[test]
fn variance_reduced_inside_support() {
let x_train = vec2d_to_array2(&[vec![0.],vec![1.]]);
let y_train = arr1(&[0.,1.]);
let y_err = arr1(&[0.;2]);
let whitenoise = 1e-10;
let scale = arr1(&[1.]);
for order in 0..4 {
let gp = GP::train(
x_train.view(), y_train.view(), y_err.view(),
scale.view(), whitenoise, order
);
let mid = arr1(&[0.5]);
let var = gp.predict_var_sgl(mid.view());
assert!(var > 0., "variance should be positive");
assert!(var < 1., "variance should be reduced below prior");
}
}
#[test]
fn predictive_covariance_is_symmetric() {
let x_train = vec2d_to_array2(&[vec![0.],vec![0.5],vec![1.0]]);
let y_train = arr1(&[0., 1., 0.]);
let y_err = arr1(&[0.; 3]);
let whitenoise = 1e-10;
let scale = arr1(&[1.0]);
let gp = GP::train(
x_train.view(), y_train.view(), y_err.view(),
scale.view(), whitenoise, 2
);
let x1 = arr1(&[0.2]);
let x2 = arr1(&[0.7]);
let c12 = gp.predict_cov_sgl(x1.view(), x2.view());
let c21 = gp.predict_cov_sgl(x2.view(), x1.view());
assert!((c12-c21).abs() < 1e-10, "cov not symmetric: {c12} vs {c21}");
}
#[test]
fn predictive_covariance_reduces_to_variance() {
let x_train = vec2d_to_array2(&[vec![0.],vec![0.5],vec![1.0]]);
let y_train = arr1(&[0., 1., 0.]);
let y_err = arr1(&[0.,0.,0.]);
let whitenoise = 1e-10;
let scale = arr1(&[1.0]);
let gp = GP::train(
x_train.view(), y_train.view(), y_err.view(),
scale.view(), whitenoise, 2
);
let x = arr1(&[0.3]);
let var = gp.predict_var_sgl(x.view());
let cov = gp.predict_cov_sgl(x.view(), x.view());
assert!((var - cov).abs() <1e-8, "var {var} vs cov {cov}");
}
#[test]
fn predictive_covariance_zero_far_apart() {
let x_train = vec2d_to_array2(&[vec![0.],vec![0.3]]);
let y_train = arr1(&[-1.0, 1.0]);
let y_err = arr1(&[0.,0.]);
let whitenoise = 1e-10;
let scale = arr1(&[1.0]);
let gp = GP::train(
x_train.view(), y_train.view(), y_err.view(),
scale.view(), whitenoise, 2);
let x1 = arr1(&[0.1]);
let x2 = arr1(&[10.0]);
let c12 = gp.predict_cov_sgl(x1.view(), x2.view());
let c21 = gp.predict_cov_sgl(x2.view(), x1.view());
assert!(c12.abs() < 1e-10, "far cov (c12) not zero: {c12}");
assert!(c21.abs() < 1e-10, "far cov (c21) not zero: {c21}");
}
#[test]
fn log_marginal_likelihood_is_finite() {
let x_train = vec2d_to_array2(&[vec![0.],vec![0.5],vec![1.0]]);
let y_train = arr1(&[0., 1., 0.]);
let y_err = arr1(&[0.; 3]);
let whitenoise = 1e-6;
let scale = arr1(&[1.0]);
let gp = GP::train(
x_train.view(), y_train.view(), y_err.view(),
scale.view(), whitenoise, 2);
let ll = gp.lnL;
assert!(ll.is_finite(), "log likelihood not finite");
assert!(ll < 0., "log likelihood should not be negative, got {ll}")
}
#[test]
fn log_marginal_prefers_smooth_data() {
let x_train = vec2d_to_array2(
&[vec![0.],vec![0.25],vec![0.5],vec![0.75],vec![1.0]]
);
let y_train_smooth = arr1(&[0., 0.7, 1., 0.7, 0.]);
let y_train_noisy = arr1(&[0.3, -1.2, 0.4, 2.0, -0.7]);
let err = arr1(&[0.; 5]);
let whitenoise = 1e-6;
let scale = arr1(&[1.0]);
let gp_smooth = GP::train(
x_train.view(),
y_train_smooth.view(),
err.view(),
scale.view(),
whitenoise,
2,
);
let gp_noisy = GP::train(
x_train.view(),
y_train_noisy.view(),
err.view(),
scale.view(),
whitenoise,
2,
);
let ll_smooth = gp_smooth.lnL;
let ll_noisy = gp_noisy.lnL;
assert!(ll_smooth > ll_noisy,
"smooth ll {ll_smooth} not > noisy ll {ll_noisy}");
}
#[test]
fn predictive_covariance_psd_2x2() {
let x_train = vec2d_to_array2(&[vec![0.],vec![0.5],vec![1.0]]);
let y_train = arr1(&[0., 1., 0.]);
let err = arr1(&[0.; 3]);
let whitenoise = 1e-6;
let scale = arr1(&[1.0]);
let gp = GP::train(
x_train.view(), y_train.view(), err.view(),
scale.view(), whitenoise, 2);
let x1 = arr1(&[0.25]);
let x2 = arr1(&[0.75]);
let v1 = gp.predict_var_sgl(x1.view());
let v2 = gp.predict_var_sgl(x2.view());
let c12 = gp.predict_cov_sgl(x1.view(), x2.view());
let det = v1 * v2 - c12 * c12;
assert!(det >= -1e-10, "covariance not PSD, det={det}");
}
#[test]
fn posterior_variance_bounded_by_prior() {
let x_train = vec2d_to_array2(&[vec![0.],vec![0.4],vec![0.8]]);
let y_train = arr1(&[0.2,-0.5,0.1]);
let err = arr1(&[0.;3]);
let whitenoise = 1e-10;
let scale = arr1(&[1.0]);
let gp = GP::train(
x_train.view(), y_train.view(), err.view(),
scale.view(), whitenoise, 2);
for i in 0..20{
let x = arr1(&[i as f64 / 20.]);
let var = gp.predict_var_sgl(x.view());
assert!(var >= -1e-12, "negative variance {var}");
assert!(var <= 1. + 1e-12, "variance exceeds prior {var}");
}
}
#[test]
fn log_marginal_invariant_under_permutation() {
let x1 = vec2d_to_array2(&[vec![0.],vec![0.5],vec![1.0]]);
let y1 = arr1(&[0.,1.,0.]);
let x2 = vec2d_to_array2(&[vec![1.],vec![0.],vec![0.5]]);
let y2 = arr1(&[0.,0.,1.]);
let err = arr1(&[0.;3]);
let whitenoise = 1e-6;
let scale = arr1(&[1.0]);
let gp1 = GP::train(
x1.view(), y1.view(), err.view(),
scale.view(), whitenoise, 2);
let gp2 = GP::train(
x2.view(), y2.view(), err.view(),
scale.view(), whitenoise, 2);
let lnl_1 = gp1.lnL;
let lnl_2 = gp2.lnL;
assert!((lnl_1 - lnl_2).abs() < 1e-8,
"log likelihood not permutation invariant");
}
#[test]
fn more_noise_means_worse_likelihood() {
let x_train = vec2d_to_array2(&[vec![0.],vec![0.5],vec![1.0]]);
let y_train = arr1(&[0., 1., 0.]);
let err = arr1(&[0.; 3]);
let scale = arr1(&[1.0]);
let whitenoise_lo = 1e-8;
let whitenoise_hi = 1e-1;
let mut gp = GP::train(
x_train.view(), y_train.view(), err.view(),
scale.view(), whitenoise_lo, 2);
let lnl_lo = gp.lnL;
gp.retrain(scale.view(),whitenoise_hi,2);
let lnl_hi = gp.lnL;
assert!(lnl_lo > lnl_hi, "More noise, but better fit?");
}
#[test]
fn retrained_gp_has_correct_mean() {
let x_train = vec2d_to_array2(&[
vec![0.],vec![0.25],vec![0.5],vec![0.75],vec![1.0]
]);
let y_train = arr1(&[0., 1., 2., 3., 4.]);
let y_err = arr1(&[0.;5]);
let whitenoise = 1e-10;
let msq_order: i32 = 1;
let scale_lo = arr1(&[1.]);
let scale_hi = arr1(&[2.]);
let mut gp = GP::train(
x_train.view(),
y_train.view(),
y_err.view(),
scale_lo.view(),
whitenoise,
msq_order,
);
let x_sample = vec2d_to_array2(&[
vec![0.0],vec![0.1],vec![0.2],vec![0.3],vec![0.4],
vec![0.5],vec![0.6],vec![0.7],vec![0.8],vec![0.9],
]);
let y_lo = gp.predict_mean(x_sample.view());
gp.retrain(scale_hi.view(), whitenoise, msq_order);
let y_hi = gp.predict_mean(x_sample.view());
for (x_lo, x_hi) in y_lo.iter().zip(y_hi.iter()) {
assert![(x_hi - x_lo).abs() < 0.3];
}
}
#[test]
fn realizations_have_correct_mean() {
let x_train = vec2d_to_array2(&[vec![0.],vec![0.5],vec![1.0]]);
let y_train = arr1(&[0., 1., 0.]);
let err = arr1(&[0.; 3]);
let whitenoise = 1e-6;
let scale = arr1(&[1.0]);
let gp = GP::train(
x_train.view(), y_train.view(), err.view(),
scale.view(), whitenoise, 2);
let x = vec2d_to_array2(&[vec![0.1], vec![0.2], vec![0.3]]);
let mean = gp.predict_mean(x.view());
let mut rng = PCG64Stream::new(1234, 5678);
let mut acc = arr1(&vec![0.;x.nrows()]);
let n = 2000;
for _ in 0..n {
let r = gp.draw_realization(&mut rng);
for i in 0..x.nrows() {
acc[i] += r.value(x.row(i));
}
}
let est: Vec<f64> = acc.iter().map(
|a| a / n as f64
).collect();
for i in 0..acc.len() {
assert!((est[i] - mean[i]).abs() < 5e-2,
"realization mean mismatch: {} vs {}",
est[i], mean[i]);
}
}
#[test]
fn realization_deterministic_at_training_points() {
let x_train = vec2d_to_array2(&[vec![0.],vec![0.5],vec![1.0]]);
let y_train = arr1(&[0., 1., 0.]);
let err = arr1(&[0.; 3]);
let whitenoise = 1e-12;
let scale = arr1(&[1.0]);
let gp = GP::train(
x_train.view(), y_train.view(), err.view(),
scale.view(), whitenoise, 2);
let mut rng = PCG64Stream::new(42,99);
let mut acc = arr1(&vec![0.; x_train.nrows()]);
let n = 5000;
for _ in 0..n {
let real = gp.draw_realization(&mut rng);
for i in 0..x_train.nrows() {
acc[i] += real.value(x_train.row(i));
}
}
for i in 0..x_train.nrows() {
let est: f64 = acc[i] / n as f64;
assert!((est - y_train[i]).abs() < 5e-2);
}
}
#[test]
fn realization_training_point_variance_collapses() {
let x_train = vec2d_to_array2(&[vec![0.], vec![0.5], vec![1.0]]);
let y_train = arr1(&[0., 1., 0.]);
let err = arr1(&[0.; 3]);
let whitenoise = 1e-10;
let scale = arr1(&[1.0]);
let gp = GP::train(
x_train.view(), y_train.view(), err.view(),
scale.view(), whitenoise, 2);
let mut rng = PCG64Stream::new(7, 11);
let n = 4000;
let m = x_train.len();
let mut sum = arr1(&vec![0.; m]);
let mut sum2 = arr1(&vec![0.; m]);
for _ in 0..n {
let r = gp.draw_realization(&mut rng);
for i in 0..m {
let v = r.value(x_train.row(i));
sum[i] += v;
sum2[i] += v * v;
}
}
for i in 0..m {
let mean : f64 = sum[i] / n as f64;
let var : f64 = sum2[i] / n as f64 - mean * mean;
assert!(
(mean - y_train[i]).abs() < 5e-2,
"mean mismatch at training point {}: {} vs {}",
i, mean, y_train[i]
);
assert!(
var < 5e-3,
"variance did not collapse at training point {}: {}",
i, var
);
}
}
}