use faer::MatMut;
use rayon::prelude::*;
const KERNEL_FILL_PAR_MIN_ELEMENTS: usize = 1 << 16;
const SQRT_5: f64 = 2.236_067_977_499_79;
#[inline]
pub fn matern52(dist: f64, length_scale: f64) -> f64 {
let r = SQRT_5 * dist / length_scale;
(1.0 + r + r * r / 3.0) * (-r).exp()
}
pub fn fill_matern52_cross_1d(mut dst: MatMut<'_, f64>, a: &[f64], b: &[f64], length_scale: f64) {
assert_eq!(dst.nrows(), a.len(), "kernel fill: row count mismatch");
assert_eq!(dst.ncols(), b.len(), "kernel fill: column count mismatch");
for (j, &b_j) in b.iter().enumerate() {
for (i, &a_i) in a.iter().enumerate() {
dst[(i, j)] = matern52((a_i - b_j).abs(), length_scale);
}
}
}
pub fn fill_matern52_cross_1d_par(dst: MatMut<'_, f64>, a: &[f64], b: &[f64], length_scale: f64) {
assert_eq!(dst.nrows(), a.len(), "kernel fill: row count mismatch");
assert_eq!(dst.ncols(), b.len(), "kernel fill: column count mismatch");
if a.len() * b.len() < KERNEL_FILL_PAR_MIN_ELEMENTS {
return fill_matern52_cross_1d(dst, a, b, length_scale);
}
dst.par_col_iter_mut()
.zip(b.par_iter())
.for_each(|(mut col, &b_j)| {
for (i, &a_i) in a.iter().enumerate() {
col[i] = matern52((a_i - b_j).abs(), length_scale);
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use faer::Mat;
#[test]
fn test_matern52_unit_at_zero() {
for ls in [0.1, 1.0, 7.5] {
assert_relative_eq!(matern52(0.0, ls), 1.0, epsilon = 1e-15);
}
}
#[test]
fn test_matern52_matches_textbook_form() {
let ls = 1.3_f64;
for d in [0.0_f64, 0.25, 0.5, 1.0, 2.0, 5.0] {
let want =
(1.0 + SQRT_5 * d / ls + 5.0 * d * d / (3.0 * ls * ls)) * (-SQRT_5 * d / ls).exp();
assert_relative_eq!(matern52(d, ls), want, epsilon = 1e-14, max_relative = 1e-14);
}
}
#[test]
fn test_matern52_is_monotone_decreasing() {
let ls = 0.8_f64;
let mut prev = matern52(0.0, ls);
for step in 1..200 {
let cur = matern52(step as f64 * 0.05, ls);
assert!(cur < prev, "not decreasing at step {}", step);
prev = cur;
}
}
#[test]
fn test_matern52_scales_with_length_scale() {
for d in [0.1_f64, 0.5, 3.0] {
assert_relative_eq!(
matern52(d, 1.0),
matern52(2.0 * d, 2.0),
epsilon = 1e-14,
max_relative = 1e-14
);
}
}
#[test]
fn test_fill_cross_matches_scalar_and_is_symmetric_on_self() {
let a = vec![0.0_f64, 0.3, 0.9, 2.0];
let b = vec![-1.0_f64, 0.3, 4.0];
let ls = 1.1;
let mut dst = Mat::<f64>::zeros(a.len(), b.len());
fill_matern52_cross_1d(dst.as_mut(), &a, &b, ls);
for (i, &a_i) in a.iter().enumerate() {
for (j, &b_j) in b.iter().enumerate() {
assert_relative_eq!(
dst[(i, j)],
matern52((a_i - b_j).abs(), ls),
epsilon = 1e-15
);
}
}
let mut gram = Mat::<f64>::zeros(a.len(), a.len());
fill_matern52_cross_1d(gram.as_mut(), &a, &a, ls);
for i in 0..a.len() {
assert_relative_eq!(gram[(i, i)], 1.0, epsilon = 1e-15);
for j in 0..i {
assert_relative_eq!(gram[(i, j)], gram[(j, i)], epsilon = 1e-15);
}
}
}
}