use crate::error::SolveError;
use crate::linalg::matrix::Matrix;
use crate::monte_carlo::Rng;
#[derive(Debug, Clone, PartialEq)]
pub enum KernelFn {
Rbf { l: f64, s: f64 },
Matern32 { l: f64, s: f64 },
Matern52 { l: f64, s: f64 },
Periodic { l: f64, p: f64, s: f64 },
Linear { s: f64, c: f64 },
Sum(Box<KernelFn>, Box<KernelFn>),
Product(Box<KernelFn>, Box<KernelFn>),
}
fn distance(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum::<f64>().sqrt()
}
impl KernelFn {
pub fn eval(&self, a: &[f64], b: &[f64]) -> f64 {
match self {
KernelFn::Rbf { l, s } => {
let r = distance(a, b);
s * s * (-0.5 * r * r / (l * l)).exp()
}
KernelFn::Matern32 { l, s } => {
let z = 3.0f64.sqrt() * distance(a, b) / l;
s * s * (1.0 + z) * (-z).exp()
}
KernelFn::Matern52 { l, s } => {
let z = 5.0f64.sqrt() * distance(a, b) / l;
s * s * (1.0 + z + z * z / 3.0) * (-z).exp()
}
KernelFn::Periodic { l, p, s } => {
let r = distance(a, b);
let t = (std::f64::consts::PI * r / p).sin();
s * s * (-2.0 * t * t / (l * l)).exp()
}
KernelFn::Linear { s, c } => {
s * s * a.iter().zip(b).map(|(x, y)| x * y).sum::<f64>() + c
}
KernelFn::Sum(x, y) => x.eval(a, b) + y.eval(a, b),
KernelFn::Product(x, y) => x.eval(a, b) * y.eval(a, b),
}
}
pub fn is_valid(&self) -> bool {
match self {
KernelFn::Rbf { l, s } | KernelFn::Matern32 { l, s } | KernelFn::Matern52 { l, s } => {
l.is_finite() && *l > 0.0 && s.is_finite() && *s > 0.0
}
KernelFn::Periodic { l, p, s } => {
l.is_finite() && *l > 0.0 && p.is_finite() && *p > 0.0 && s.is_finite() && *s > 0.0
}
KernelFn::Linear { s, c } => s.is_finite() && *s > 0.0 && c.is_finite() && *c >= 0.0,
KernelFn::Sum(a, b) | KernelFn::Product(a, b) => a.is_valid() && b.is_valid(),
}
}
pub fn parameters(&self) -> Vec<f64> {
match self {
KernelFn::Rbf { l, s } | KernelFn::Matern32 { l, s } | KernelFn::Matern52 { l, s } => {
vec![*l, *s]
}
KernelFn::Periodic { l, p, s } => vec![*l, *p, *s],
KernelFn::Linear { s, c } => vec![*s, *c],
KernelFn::Sum(a, b) | KernelFn::Product(a, b) => {
let mut out = a.parameters();
out.extend(b.parameters());
out
}
}
}
fn take_parameters(&self, values: &[f64], at: &mut usize) -> KernelFn {
let next = |slot: &mut usize| {
let v = values[*slot];
*slot += 1;
v
};
match self {
KernelFn::Rbf { .. } => {
let l = next(at);
let s = next(at);
KernelFn::Rbf { l, s }
}
KernelFn::Matern32 { .. } => {
let l = next(at);
let s = next(at);
KernelFn::Matern32 { l, s }
}
KernelFn::Matern52 { .. } => {
let l = next(at);
let s = next(at);
KernelFn::Matern52 { l, s }
}
KernelFn::Periodic { .. } => {
let l = next(at);
let p = next(at);
let s = next(at);
KernelFn::Periodic { l, p, s }
}
KernelFn::Linear { .. } => {
let s = next(at);
let c = next(at);
KernelFn::Linear { s, c }
}
KernelFn::Sum(a, b) => {
let left = a.take_parameters(values, at);
let right = b.take_parameters(values, at);
KernelFn::Sum(Box::new(left), Box::new(right))
}
KernelFn::Product(a, b) => {
let left = a.take_parameters(values, at);
let right = b.take_parameters(values, at);
KernelFn::Product(Box::new(left), Box::new(right))
}
}
}
pub fn with_parameters(&self, values: &[f64]) -> Result<KernelFn, SolveError> {
let wanted = self.parameters().len();
if values.len() != wanted {
return Err(SolveError::DimensionMismatch { expected: wanted, got: values.len() });
}
let mut at = 0;
Ok(self.take_parameters(values, &mut at))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Gp {
pub kernel: KernelFn,
pub noise: f64,
x_train: Vec<Vec<f64>>,
y_train: Vec<f64>,
chol: Matrix,
alpha: Vec<f64>,
}
const JITTER: f64 = 1e-10;
impl Gp {
pub fn fit(kernel: KernelFn, noise: f64, x: &[Vec<f64>], y: &[f64]) -> Result<Self, SolveError> {
if !kernel.is_valid() {
return Err(SolveError::InvalidArgument("the kernel has invalid hyperparameters"));
}
if !noise.is_finite() || noise < 0.0 {
return Err(SolveError::InvalidArgument("the noise variance must be nonnegative"));
}
if x.is_empty() {
return Err(SolveError::InvalidArgument("the dataset is empty"));
}
if y.len() != x.len() {
return Err(SolveError::DimensionMismatch { expected: x.len(), got: y.len() });
}
let dim = x[0].len();
if dim == 0 || x.iter().any(|p| p.len() != dim) {
return Err(SolveError::InvalidArgument("the inputs are ragged or zero-dimensional"));
}
if x.iter().flatten().chain(y.iter()).any(|v| !v.is_finite()) {
return Err(SolveError::InvalidArgument("the data must be finite"));
}
let n = x.len();
let scale = kernel.eval(&x[0], &x[0]).abs().max(1.0);
let mut k = Matrix::zeros(n, n);
for i in 0..n {
for j in i..n {
let v = kernel.eval(&x[i], &x[j]);
k.set(i, j, v);
k.set(j, i, v);
}
k.set(i, i, k.get(i, i) + noise + JITTER * scale);
}
let chol = crate::linalg::cholesky::cholesky(&k)?;
let alpha = crate::linalg::cholesky::cholesky_solve(&chol, y)?;
Ok(Self { kernel, noise, x_train: x.to_vec(), y_train: y.to_vec(), chol, alpha })
}
pub fn len(&self) -> usize {
self.x_train.len()
}
pub fn is_empty(&self) -> bool {
self.x_train.is_empty()
}
pub fn condition_estimate(&self) -> f64 {
let n = self.chol.rows;
let mut lo = f64::INFINITY;
let mut hi: f64 = 0.0;
for i in 0..n {
let d = self.chol.get(i, i).abs();
lo = lo.min(d);
hi = hi.max(d);
}
if lo > 0.0 {
(hi / lo).powi(2)
} else {
f64::INFINITY
}
}
fn forward_substitute(&self, b: &[f64]) -> Vec<f64> {
let n = b.len();
let mut v = vec![0.0; n];
for i in 0..n {
let mut acc = b[i];
for j in 0..i {
acc -= self.chol.get(i, j) * v[j];
}
v[i] = acc / self.chol.get(i, i);
}
v
}
pub fn predict(&self, x_star: &[Vec<f64>]) -> Result<(Vec<f64>, Vec<f64>), SolveError> {
let dim = self.x_train[0].len();
let mut means = Vec::with_capacity(x_star.len());
let mut variances = Vec::with_capacity(x_star.len());
for q in x_star {
if q.len() != dim {
return Err(SolveError::DimensionMismatch { expected: dim, got: q.len() });
}
let ks: Vec<f64> = self.x_train.iter().map(|t| self.kernel.eval(t, q)).collect();
means.push(ks.iter().zip(self.alpha.iter()).map(|(a, b)| a * b).sum());
let v = self.forward_substitute(&ks);
let explained: f64 = v.iter().map(|a| a * a).sum();
variances.push((self.kernel.eval(q, q) - explained).max(0.0));
}
Ok((means, variances))
}
pub fn log_marginal_likelihood(&self) -> f64 {
let n = self.y_train.len();
let fit: f64 = self.y_train.iter().zip(self.alpha.iter()).map(|(a, b)| a * b).sum();
let log_det: f64 = (0..n).map(|i| self.chol.get(i, i).ln()).sum::<f64>() * 2.0;
-0.5 * fit - 0.5 * log_det - 0.5 * n as f64 * std::f64::consts::TAU.ln()
}
pub fn optimize_hyperparams(
&self,
restarts: usize,
rng: &mut Rng,
) -> Result<Gp, SolveError> {
let base = self.kernel.parameters();
let n = base.len();
let objective = |logs: &[f64]| -> f64 {
let values: Vec<f64> = logs.iter().map(|v| v.exp()).collect();
let Ok(kernel) = self.kernel.with_parameters(&values) else {
return f64::INFINITY;
};
if !kernel.is_valid() {
return f64::INFINITY;
}
match Gp::fit(kernel, self.noise, &self.x_train, &self.y_train) {
Ok(g) => {
let lml = g.log_marginal_likelihood();
if lml.is_finite() {
-lml
} else {
f64::INFINITY
}
}
Err(_) => f64::INFINITY,
}
};
let mut best: Option<(f64, Vec<f64>)> = None;
for attempt in 0..restarts.max(1) {
let start: Vec<f64> = (0..n)
.map(|k| {
let centre = base[k].max(1e-12).ln();
if attempt == 0 {
centre
} else {
centre + 2.0 * (rng.next_f64() - 0.5) * 2.0
}
})
.collect();
let found = crate::optimization::nelder_mead(&objective, &start, 0.5, 1e-10, 4000);
let value = objective(&found);
if value.is_finite() && best.as_ref().is_none_or(|(v, _)| value < *v) {
best = Some((value, found));
}
}
let (_, logs) = best.ok_or(SolveError::NoConvergence { iters: restarts, residual: f64::INFINITY })?;
let values: Vec<f64> = logs.iter().map(|v| v.exp()).collect();
let kernel = self.kernel.with_parameters(&values)?;
Gp::fit(kernel, self.noise, &self.x_train, &self.y_train)
}
pub fn sample_posterior(
&self,
x_star: &[Vec<f64>],
count: usize,
rng: &mut Rng,
) -> Result<Vec<Vec<f64>>, SolveError> {
let (mean, _) = self.predict(x_star)?;
let m = x_star.len();
let mut cov = Matrix::zeros(m, m);
let mut rows = Vec::with_capacity(m);
for q in x_star {
let ks: Vec<f64> = self.x_train.iter().map(|t| self.kernel.eval(t, q)).collect();
rows.push(self.forward_substitute(&ks));
}
let scale = self.kernel.eval(&x_star[0], &x_star[0]).abs().max(1.0);
for i in 0..m {
for j in 0..m {
let explained: f64 = rows[i].iter().zip(rows[j].iter()).map(|(a, b)| a * b).sum();
cov.set(i, j, self.kernel.eval(&x_star[i], &x_star[j]) - explained);
}
cov.set(i, i, cov.get(i, i) + JITTER * scale);
}
let symmetric = Matrix::from_fn(m, m, |i, j| 0.5 * (cov.get(i, j) + cov.get(j, i)));
let l = crate::linalg::cholesky::cholesky(&symmetric)?;
Ok((0..count)
.map(|_| {
let z: Vec<f64> = (0..m).map(|_| rng.next_gaussian()).collect();
(0..m)
.map(|i| mean[i] + (0..=i).map(|j| l.get(i, j) * z[j]).sum::<f64>())
.collect()
})
.collect())
}
}
pub fn sample_prior(
kernel: &KernelFn,
x: &[Vec<f64>],
count: usize,
rng: &mut Rng,
) -> Result<Vec<Vec<f64>>, SolveError> {
if !kernel.is_valid() {
return Err(SolveError::InvalidArgument("the kernel has invalid hyperparameters"));
}
if x.is_empty() {
return Err(SolveError::InvalidArgument("no points to sample at"));
}
let dim = x[0].len();
if dim == 0 || x.iter().any(|p| p.len() != dim) {
return Err(SolveError::InvalidArgument("the points are ragged or zero-dimensional"));
}
let n = x.len();
let scale = kernel.eval(&x[0], &x[0]).abs().max(1.0);
let mut k = Matrix::zeros(n, n);
for i in 0..n {
for j in i..n {
let v = kernel.eval(&x[i], &x[j]);
k.set(i, j, v);
k.set(j, i, v);
}
k.set(i, i, k.get(i, i) + JITTER * scale);
}
let l = crate::linalg::cholesky::cholesky(&k)?;
Ok((0..count)
.map(|_| {
let z: Vec<f64> = (0..n).map(|_| rng.next_gaussian()).collect();
(0..n).map(|i| (0..=i).map(|j| l.get(i, j) * z[j]).sum()).collect()
})
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
fn grid(n: usize, step: f64) -> Vec<Vec<f64>> {
(0..n).map(|i| vec![i as f64 * step]).collect()
}
#[test]
fn a_noiseless_process_interpolates_its_data_exactly() {
let x = grid(8, 0.4);
let y: Vec<f64> = x.iter().map(|p| p[0].sin()).collect();
let gp = Gp::fit(KernelFn::Rbf { l: 1.0, s: 1.0 }, 0.0, &x, &y).unwrap();
let (mean, var) = gp.predict(&x).unwrap();
for i in 0..x.len() {
assert!((mean[i] - y[i]).abs() < 1e-7, "point {i} was off by {}", mean[i] - y[i]);
assert!(var[i] < 1e-8, "point {i} had variance {}", var[i]);
}
assert_eq!(gp.len(), 8);
assert!(!gp.is_empty());
}
#[test]
fn the_posterior_variance_does_not_depend_on_the_observations() {
let x = grid(7, 0.5);
let a: Vec<f64> = x.iter().map(|p| p[0].sin()).collect();
let b: Vec<f64> = x.iter().map(|p| 4.0 * p[0] * p[0] - 3.0).collect();
let kernel = KernelFn::Matern52 { l: 0.8, s: 1.2 };
let ga = Gp::fit(kernel.clone(), 0.05, &x, &a).unwrap();
let gb = Gp::fit(kernel.clone(), 0.05, &x, &b).unwrap();
let q = grid(25, 0.15);
let (_, va) = ga.predict(&q).unwrap();
let (_, vb) = gb.predict(&q).unwrap();
for i in 0..q.len() {
assert_eq!(va[i], vb[i], "the variance moved with the data at {i}");
}
let scaled: Vec<f64> = a.iter().map(|v| 2.5 * v).collect();
let gs = Gp::fit(kernel, 0.05, &x, &scaled).unwrap();
let (ma, _) = ga.predict(&q).unwrap();
let (ms, _) = gs.predict(&q).unwrap();
for i in 0..q.len() {
assert!((ms[i] - 2.5 * ma[i]).abs() < 1e-10 * (1.0 + ma[i].abs()), "mean at {i}");
}
}
#[test]
fn far_from_the_data_the_posterior_is_the_prior() {
let x = grid(6, 0.3);
let y: Vec<f64> = x.iter().map(|p| p[0].cos()).collect();
let kernel = KernelFn::Rbf { l: 0.5, s: 1.4 };
let gp = Gp::fit(kernel.clone(), 0.0, &x, &y).unwrap();
let far = vec![vec![100.0]];
let (mean, var) = gp.predict(&far).unwrap();
assert!(mean[0].abs() < 1e-12, "the mean did not return to zero: {}", mean[0]);
let prior = kernel.eval(&far[0], &far[0]);
assert!((var[0] - prior).abs() < 1e-12, "the variance did not return to {prior}");
let q = grid(40, 0.1);
let (_, v) = gp.predict(&q).unwrap();
for (i, &value) in v.iter().enumerate() {
assert!(value <= prior + 1e-12, "point {i} had variance {value} above the prior");
}
}
#[test]
fn the_kernels_have_the_shapes_they_claim() {
let p = KernelFn::Periodic { l: 1.0, p: 2.5, s: 1.3 };
for x in [0.0, 0.7, -3.1] {
for m in [1.0, 2.0, 5.0] {
let shifted = x + m * 2.5;
assert_eq!(
p.eval(&[x], &[shifted]),
p.eval(&[x], &[x]),
"the period was not exact at {x} after {m} periods"
);
}
}
for kernel in [
KernelFn::Rbf { l: 0.9, s: 1.1 },
KernelFn::Matern32 { l: 0.9, s: 1.1 },
KernelFn::Matern52 { l: 0.9, s: 1.1 },
] {
let peak = kernel.eval(&[0.0], &[0.0]);
assert!((peak - 1.1 * 1.1).abs() < 1e-14, "the amplitude was wrong");
let mut previous = peak;
for k in 1..30 {
let r = k as f64 * 0.2;
let v = kernel.eval(&[0.0], &[r]);
assert!(v < previous, "the kernel rose at separation {r}");
assert!(v > 0.0, "the kernel went negative at {r}");
assert!((v - kernel.eval(&[7.3], &[7.3 + r])).abs() < 1e-14);
assert!((v - kernel.eval(&[0.0], &[-r])).abs() < 1e-14);
previous = v;
}
assert!(kernel.eval(&[0.0], &[50.0]) < 1e-12, "the kernel did not decay");
}
let a = KernelFn::Rbf { l: 1.0, s: 1.0 };
let b = KernelFn::Linear { s: 0.5, c: 0.25 };
let sum = KernelFn::Sum(Box::new(a.clone()), Box::new(b.clone()));
let product = KernelFn::Product(Box::new(a.clone()), Box::new(b.clone()));
let (u, v) = ([0.3], [1.1]);
assert!((sum.eval(&u, &v) - (a.eval(&u, &v) + b.eval(&u, &v))).abs() < 1e-15);
assert!((product.eval(&u, &v) - a.eval(&u, &v) * b.eval(&u, &v)).abs() < 1e-15);
assert!(sum.is_valid() && product.is_valid());
assert!(!KernelFn::Rbf { l: -1.0, s: 1.0 }.is_valid());
assert!(!KernelFn::Sum(
Box::new(KernelFn::Rbf { l: 1.0, s: 1.0 }),
Box::new(KernelFn::Rbf { l: 0.0, s: 1.0 })
)
.is_valid());
}
#[test]
fn the_marginal_likelihood_matches_its_own_definition() {
let x = grid(6, 0.45);
let y: Vec<f64> = x.iter().map(|p| (2.0 * p[0]).sin() + 0.3).collect();
let kernel = KernelFn::Matern32 { l: 0.7, s: 1.1 };
let noise = 0.02;
let gp = Gp::fit(kernel.clone(), noise, &x, &y).unwrap();
let n = x.len();
let mut k = Matrix::zeros(n, n);
for i in 0..n {
for j in 0..n {
k.set(i, j, kernel.eval(&x[i], &x[j]));
}
k.set(i, i, k.get(i, i) + noise + JITTER);
}
let lu = crate::linalg::lu::lu_decompose(&k).unwrap();
let solved = crate::linalg::lu::solve(&k, &y).unwrap();
let fit: f64 = y.iter().zip(solved.iter()).map(|(a, b)| a * b).sum();
let want = -0.5 * fit
- 0.5 * lu.determinant().ln()
- 0.5 * n as f64 * std::f64::consts::TAU.ln();
let got = gp.log_marginal_likelihood();
assert!((got - want).abs() < 1e-9 * want.abs().max(1.0), "{got} against {want}");
}
#[test]
fn noise_turns_interpolation_into_smoothing() {
let x = grid(9, 0.35);
let y: Vec<f64> = x.iter().map(|p| p[0].sin() + 0.4).collect();
let kernel = KernelFn::Rbf { l: 0.6, s: 1.0 };
let mut previous = -1.0;
for noise in [0.0, 1e-4, 1e-2, 1.0] {
let gp = Gp::fit(kernel.clone(), noise, &x, &y).unwrap();
let (mean, var) = gp.predict(&x).unwrap();
let residual = mean
.iter()
.zip(y.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0, f64::max);
assert!(residual > previous, "noise {noise} did not loosen the fit");
previous = residual;
assert!(var.iter().all(|&v| v >= 0.0));
}
let peak = y.iter().fold(0.0f64, |m, v| m.max(v.abs()));
let drowned = Gp::fit(kernel, 1e4, &x, &y).unwrap();
let (mean, _) = drowned.predict(&x).unwrap();
let left = mean.iter().fold(0.0f64, |m, v| m.max(v.abs()));
assert!(left < 0.01 * peak, "overwhelming noise left {left} of {peak}");
}
#[test]
fn tuning_the_hyperparameters_raises_the_marginal_likelihood() {
let mut rng = Rng::new(0x51ac_de07);
let x = grid(12, 0.3);
let y: Vec<f64> = x.iter().map(|p| (2.0 * p[0]).sin() + 0.05 * rng.next_gaussian()).collect();
let gp = Gp::fit(KernelFn::Rbf { l: 8.0, s: 0.15 }, 0.01, &x, &y).unwrap();
let before = gp.log_marginal_likelihood();
let tuned = gp.optimize_hyperparams(4, &mut rng).unwrap();
let after = tuned.log_marginal_likelihood();
assert!(after > before + 10.0, "the likelihood only moved from {before} to {after}");
let q = grid(30, 0.12);
let truth: Vec<f64> = q.iter().map(|p| (2.0 * p[0]).sin()).collect();
let error = |g: &Gp| {
let (m, _) = g.predict(&q).unwrap();
m.iter().zip(truth.iter()).map(|(a, b)| (a - b).abs()).fold(0.0, f64::max)
};
assert!(error(&tuned) < error(&gp), "tuning made the predictions worse");
assert!(tuned.kernel.parameters().iter().all(|v| *v > 0.0));
}
#[test]
fn prior_draws_have_the_covariance_they_were_asked_for() {
let mut rng = Rng::new(0x3d90_1b6e);
let kernel = KernelFn::Rbf { l: 1.0, s: 1.0 };
let points = grid(5, 0.5);
let draws = sample_prior(&kernel, &points, 20_000, &mut rng).unwrap();
assert_eq!(draws.len(), 20_000);
for i in 0..points.len() {
for j in 0..points.len() {
let empirical: f64 = draws.iter().map(|d| d[i] * d[j]).sum::<f64>()
/ draws.len() as f64;
let want = kernel.eval(&points[i], &points[j]);
assert!((empirical - want).abs() < 0.05, "({i},{j}): {empirical} vs {want}");
}
}
}
#[test]
fn posterior_draws_pass_through_noiseless_data() {
let mut rng = Rng::new(0x2b71_c045);
let x = grid(5, 0.6);
let y: Vec<f64> = x.iter().map(|p| p[0].cos()).collect();
let gp = Gp::fit(KernelFn::Rbf { l: 0.9, s: 1.0 }, 0.0, &x, &y).unwrap();
let draws = gp.sample_posterior(&x, 20, &mut rng).unwrap();
assert_eq!(draws.len(), 20);
for d in &draws {
for i in 0..x.len() {
assert!((d[i] - y[i]).abs() < 1e-4, "a draw missed point {i} by {}", d[i] - y[i]);
}
}
let q = vec![vec![10.0]];
let far = gp.sample_posterior(&q, 400, &mut rng).unwrap();
let spread = far.iter().map(|d| d[0] * d[0]).sum::<f64>() / far.len() as f64;
assert!(spread > 0.3, "the draws did not spread away from the data: {spread}");
}
#[test]
fn the_process_refuses_impossible_arguments() {
let x = grid(4, 0.5);
let y = vec![1.0, 2.0, 3.0, 4.0];
let good = KernelFn::Rbf { l: 1.0, s: 1.0 };
assert!(Gp::fit(KernelFn::Rbf { l: 0.0, s: 1.0 }, 0.0, &x, &y).is_err());
assert!(Gp::fit(good.clone(), -1.0, &x, &y).is_err());
assert!(Gp::fit(good.clone(), 0.0, &[], &[]).is_err());
assert!(Gp::fit(good.clone(), 0.0, &x, &y[..2]).is_err());
assert!(Gp::fit(good.clone(), 0.0, &[vec![1.0], vec![1.0, 2.0]], &[1.0, 2.0]).is_err());
assert!(Gp::fit(good.clone(), 0.0, &[vec![], vec![]], &[1.0, 2.0]).is_err());
assert!(Gp::fit(good.clone(), 0.0, &x, &[1.0, 2.0, 3.0, f64::NAN]).is_err());
let gp = Gp::fit(good.clone(), 0.1, &x, &y).unwrap();
assert!(gp.predict(&[vec![1.0, 2.0]]).is_err());
assert!(sample_prior(&KernelFn::Rbf { l: -1.0, s: 1.0 }, &x, 1, &mut Rng::new(1)).is_err());
assert!(sample_prior(&good, &[], 1, &mut Rng::new(1)).is_err());
assert!(sample_prior(&good, &[vec![]], 1, &mut Rng::new(1)).is_err());
let periodic = KernelFn::Periodic { l: 1.0, p: 2.0, s: 3.0 };
assert_eq!(periodic.parameters(), vec![1.0, 2.0, 3.0]);
assert_eq!(periodic.with_parameters(&[4.0, 5.0, 6.0]).unwrap().parameters(), vec![4.0, 5.0, 6.0]);
assert!(periodic.with_parameters(&[1.0]).is_err());
let compound = KernelFn::Sum(
Box::new(KernelFn::Rbf { l: 1.0, s: 2.0 }),
Box::new(KernelFn::Linear { s: 3.0, c: 4.0 }),
);
assert_eq!(compound.parameters(), vec![1.0, 2.0, 3.0, 4.0]);
let rebuilt = compound.with_parameters(&[5.0, 6.0, 7.0, 8.0]).unwrap();
assert_eq!(rebuilt.parameters(), vec![5.0, 6.0, 7.0, 8.0]);
}
}