use gam_linalg::utils::KahanSum;
use gam_math::special::binomial_coefficient_f64 as binomial_f64;
use statrs::function::gamma::{gamma as gamma_fn, ln_gamma};
use std::sync::OnceLock;
pub(crate) fn compute_gauss_legendre(n: usize) -> (Vec<f64>, Vec<f64>) {
let mut tmp: Vec<(f64, f64)> = Vec::with_capacity(n);
let half = n.div_ceil(2);
for i in 0..half {
let mut z = (std::f64::consts::PI * (i as f64 + 0.75) / (n as f64 + 0.5)).cos();
let mut pp = 0.0_f64;
for _ in 0..200 {
let mut p1 = 1.0_f64;
let mut p2 = 0.0_f64;
for j in 0..n {
let p3 = p2;
p2 = p1;
p1 = ((2.0 * j as f64 + 1.0) * z * p2 - j as f64 * p3) / (j as f64 + 1.0);
}
pp = n as f64 * (z * p1 - p2) / (z * z - 1.0);
let z_prev = z;
z = z_prev - p1 / pp;
if (z - z_prev).abs() < 1e-15 {
break;
}
}
let w = 2.0 / ((1.0 - z * z) * pp * pp);
if !n.is_multiple_of(2) && i == half - 1 {
tmp.push((0.0, w));
} else {
tmp.push((-z.abs(), w));
tmp.push((z.abs(), w));
}
}
tmp.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut nodes = Vec::with_capacity(n);
let mut weights = Vec::with_capacity(n);
for (z, w) in tmp.into_iter().take(n) {
nodes.push(z);
weights.push(w);
}
(nodes, weights)
}
pub(crate) fn gauss_legendre_64() -> &'static (Vec<f64>, Vec<f64>) {
static CACHE: OnceLock<(Vec<f64>, Vec<f64>)> = OnceLock::new();
CACHE.get_or_init(|| compute_gauss_legendre(64))
}
#[inline]
pub(crate) fn schwinger_radial_is_convergent(d: usize, m: usize) -> bool {
d > 4 * m
}
pub(crate) fn stable_hybrid_duchon_radial(
d: usize,
m: usize,
s: usize,
kappa: f64,
r: f64,
max_order: usize,
) -> Vec<f64> {
assert!(m >= 1, "stable_hybrid_duchon_radial: m ≥ 1");
assert!(s >= 1, "stable_hybrid_duchon_radial: s ≥ 1");
assert!(kappa > 0.0, "stable_hybrid_duchon_radial: κ > 0");
assert!(r > 0.0, "stable_hybrid_duchon_radial: r > 0");
assert!(
max_order <= 6,
"stable_hybrid_duchon_radial requires max_order <= 6: max_order={max_order}"
);
assert!(
schwinger_radial_is_convergent(d, m),
"stable_hybrid_duchon_radial: requires d > 4m"
);
let (nodes, weights) = gauss_legendre_64();
let p_eff = 2 * m;
let q_eff = 2 * s;
let matern_order = p_eff + q_eff;
let log_beta =
ln_gamma(p_eff as f64) + ln_gamma(q_eff as f64) - ln_gamma((p_eff + q_eff) as f64);
let inv_beta = (-log_beta).exp();
let mut accum = vec![KahanSum::default(); max_order + 1];
for (xi, wi) in nodes.iter().zip(weights.iter()) {
let u = 0.5 * (1.0 + xi);
if u <= 0.0 || u >= 1.0 {
continue;
}
let kappa_u = u * kappa; let one_minus_u2 = 1.0 - u * u;
let one_minus_u2_pow = one_minus_u2.powi((p_eff - 1) as i32);
let u_pow = u.powi((2 * q_eff - 1) as i32);
let weight = wi * one_minus_u2_pow * u_pow;
let matern_derivs = matern_block_radial_derivatives(d, matern_order, kappa_u, r, max_order);
for (k, v) in matern_derivs.iter().enumerate() {
accum[k].add(weight * v);
}
}
accum.iter().map(|acc| inv_beta * acc.sum()).collect()
}
pub(crate) fn factorial_f64(n: usize) -> f64 {
let mut acc = 1.0_f64;
for k in 2..=n {
acc *= k as f64;
}
acc
}
pub fn bessel_k(nu: f64, x: f64) -> f64 {
assert!(x > 0.0 && x.is_finite(), "bessel_k requires finite x > 0");
assert!(nu.is_finite(), "bessel_k requires finite ν");
let nu_abs = nu.abs();
let two_nu = 2.0 * nu_abs;
let n_round = two_nu.round();
if (two_nu - n_round).abs() < 1e-12 && (n_round as i64) % 2 == 1 {
let n = ((n_round as i64 - 1) / 2) as usize; return bessel_k_half_integer(n, x);
}
bessel_k_bessik(nu_abs, x)
}
pub(crate) const BESSEL_K_EPS: f64 = 1.0e-15;
pub(crate) const BESSEL_K_MAX_ITER: usize = 10_000;
pub(crate) const BESSEL_K_CHEB_C1: [f64; 7] = [
-1.142_022_680_371_168,
6.516_511_267_073_7e-3,
3.087_090_173_086e-4,
-3.470_626_964_9e-6,
6.943_766_4e-9,
3.677_95e-11,
-1.356e-13,
];
pub(crate) const BESSEL_K_CHEB_C2: [f64; 8] = [
1.843_740_587_300_905,
-7.685_284_084_478_67e-2,
1.271_927_136_654_6e-3,
-4.971_736_704_2e-6,
-3.312_611_98e-8,
2.423_096e-10,
-1.702e-13,
-1.49e-15,
];
pub(crate) fn bessel_k_bessik(nu: f64, x: f64) -> f64 {
let nl = (nu + 0.5).floor() as usize;
let mu = nu - nl as f64;
let (mut rkmu, mut rk1) = if x <= 2.0 {
bessel_k_temme(mu, x)
} else {
bessel_k_steed_cf2(mu, x)
};
for j in 1..=nl {
let order = mu + j as f64;
let rk_next = rk1 * (2.0 * order) / x + rkmu;
rkmu = rk1;
rk1 = rk_next;
}
rkmu
}
pub(crate) fn bessel_k_half_integer(n: usize, x: f64) -> f64 {
let pref = (std::f64::consts::PI / (2.0 * x)).sqrt() * (-x).exp();
let mut sum = 0.0_f64;
let two_x = 2.0 * x;
for k in 0..=n {
let num = factorial_f64(n + k);
let den = factorial_f64(k) * factorial_f64(n - k);
sum += num / den / two_x.powi(k as i32);
}
pref * sum
}
pub(crate) fn bessel_k_temme(mu: f64, x: f64) -> (f64, f64) {
let half_x = 0.5 * x;
let mu2 = mu * mu;
let pimu = std::f64::consts::PI * mu;
let fact = if pimu.abs() < BESSEL_K_EPS {
1.0
} else {
pimu / pimu.sin()
};
let dlog = -half_x.ln();
let sigma = mu * dlog;
let fact2 = if sigma.abs() < BESSEL_K_EPS {
1.0
} else {
sigma.sinh() / sigma
};
let (gam1, gam2, gampl, gammi) = bessel_k_beschb(mu);
let mut ff = fact * (gam1 * sigma.cosh() + gam2 * fact2 * dlog);
let mut sum = ff;
let exp_sigma = sigma.exp();
let mut p = 0.5 * exp_sigma / gampl;
let mut q = 0.5 / (exp_sigma * gammi);
let mut c = 1.0_f64;
let d = half_x * half_x;
let mut sum1 = p;
for i in 1..=BESSEL_K_MAX_ITER {
let i_f = i as f64;
ff = (i_f * ff + p + q) / (i_f * i_f - mu2);
c *= d / i_f;
p /= i_f - mu;
q /= i_f + mu;
let del = c * ff;
sum += del;
let del1 = c * (p - i_f * ff);
sum1 += del1;
if del.abs() < BESSEL_K_EPS * sum.abs() {
return (sum, sum1 * 2.0 / x);
}
}
panic!("bessel_k Temme series failed to converge for mu={mu} x={x}");
}
pub(crate) fn bessel_k_steed_cf2(mu: f64, x: f64) -> (f64, f64) {
let mut b = 2.0 * (1.0 + x);
let mut d = 1.0 / b;
let mut delh = d;
let mut h = delh;
let mut q1 = 0.0_f64;
let mut q2 = 1.0_f64;
let a1 = 0.25 - mu * mu;
let mut q = a1;
let mut c = a1;
let mut a = -a1;
let mut s = 1.0 + q * delh;
for i in 2..=BESSEL_K_MAX_ITER {
let i_f = i as f64;
a -= 2.0 * (i_f - 1.0);
c = -a * c / i_f;
let qnew = (q1 - b * q2) / a;
q1 = q2;
q2 = qnew;
q += c * qnew;
b += 2.0;
d = 1.0 / (b + a * d);
delh *= b * d - 1.0;
h += delh;
let dels = q * delh;
s += dels;
if dels.abs() < BESSEL_K_EPS * s.abs() {
h *= a1;
let rkmu = (std::f64::consts::PI / (2.0 * x)).sqrt() * (-x).exp() / s;
let rk1 = rkmu * (mu + x + 0.5 - h) / x;
return (rkmu, rk1);
}
}
panic!("bessel_k Steed CF2 failed to converge for mu={mu} x={x}");
}
pub(crate) fn bessel_k_beschb(mu: f64) -> (f64, f64, f64, f64) {
let xx = 8.0 * mu * mu - 1.0;
let gam1 = chebyshev_eval_minus1_to_1(&BESSEL_K_CHEB_C1, xx);
let gam2 = chebyshev_eval_minus1_to_1(&BESSEL_K_CHEB_C2, xx);
let gampl = gam2 - mu * gam1;
let gammi = gam2 + mu * gam1;
(gam1, gam2, gampl, gammi)
}
pub(crate) fn chebyshev_eval_minus1_to_1(coeffs: &[f64], x: f64) -> f64 {
let mut d = 0.0_f64;
let mut dd = 0.0_f64;
let y2 = 2.0 * x;
for j in (1..coeffs.len()).rev() {
let previous_d = d;
d = y2 * d - dd + coeffs[j];
dd = previous_d;
}
x * d - dd + 0.5 * coeffs[0]
}
pub fn riesz_kernel_value(d: usize, j: f64, r: f64) -> f64 {
assert!(d >= 1, "riesz_kernel_value: d must be ≥ 1");
assert!(
j.is_finite() && j >= 1.0,
"riesz_kernel_value: j must be ≥ 1, got {j}"
);
assert!(r > 0.0, "riesz_kernel_value: r must be > 0");
let two_j = 2.0 * j;
const LOG_EPS: f64 = 1e-12;
let offset = two_j - d as f64;
if offset >= -LOG_EPS && (offset.round() - offset).abs() < LOG_EPS {
let n_f64 = (offset / 2.0).round();
if n_f64 >= 0.0 && (n_f64 * 2.0 - offset).abs() < LOG_EPS {
let n = n_f64 as usize;
let two_j_i = (two_j.round()) as i32;
let sign = if n.is_multiple_of(2) { -1.0 } else { 1.0 }; let denom = 2.0_f64.powi(two_j_i - 1)
* std::f64::consts::PI.powf(d as f64 / 2.0)
* gamma_fn(j)
* factorial_f64(n);
return sign / denom
* r.powi((2 * n) as i32)
* (r.ln() + log_riesz_finite_part_shift(d, n));
}
}
let half_d = d as f64 / 2.0;
let num = gamma_fn(half_d - j);
let denom = 4.0_f64.powf(j) * std::f64::consts::PI.powf(half_d) * gamma_fn(j);
num / denom * r.powf(2.0 * j - d as f64)
}
pub(crate) fn log_riesz_finite_part_shift(d: usize, n: usize) -> f64 {
let half_d = 0.5 * d as f64;
let mut shift = 0.0_f64;
for t in 1..=n {
let tf = t as f64;
shift -= (4.0 * tf + d as f64 - 2.0) / (4.0 * tf * (tf + half_d - 1.0));
}
shift
}
pub fn matern_kernel_value(d: usize, ell: usize, kappa: f64, r: f64) -> f64 {
assert!(d >= 1, "matern_kernel_value: d must be ≥ 1");
assert!(ell >= 1, "matern_kernel_value: ell must be ≥ 1");
if !(kappa > 0.0) {
return f64::NAN;
}
assert!(r >= 0.0, "matern_kernel_value: r must be ≥ 0");
let nu = ell as f64 - d as f64 / 2.0;
let ln_pref = (d as f64 / 2.0 - ell as f64) * kappa.ln()
- (d as f64 / 2.0) * (2.0 * std::f64::consts::PI).ln()
- (ell as f64 - 1.0) * std::f64::consts::LN_2
- ln_gamma(ell as f64);
let pref = ln_pref.exp();
if r == 0.0 {
if nu > 0.0 {
let lim = 0.5 * gamma_fn(nu) * (0.5 * kappa).powf(-nu);
return pref * lim;
} else {
return f64::INFINITY;
}
}
let kr = kappa * r;
let kv = bessel_k(nu, kr);
pref * r.powf(nu) * kv
}
pub(crate) const DUCHON_SMALL_CHI_SERIES_MAX: f64 = 0.125;
const DUCHON_SMALL_CHI_SERIES_MAX_TERMS: usize = 96;
pub(crate) const DUCHON_SMALL_CHI_SERIES_REL_TOL: f64 = 4.0e-16;
#[inline]
pub(crate) fn use_duchon_small_chi_riesz_series(kappa: f64, r: f64) -> bool {
kappa > 0.0
&& kappa.is_finite()
&& r > 0.0
&& r.is_finite()
&& (kappa * r).abs() <= DUCHON_SMALL_CHI_SERIES_MAX
}
pub(crate) fn duchon_small_chi_riesz_series_radial_derivatives(
d: usize,
a: usize,
b: usize,
kappa: f64,
r: f64,
max_order: usize,
kappa_derivative_order: usize,
) -> Vec<f64> {
assert!(b >= 1);
assert!(
kappa > 0.0,
"matern kernel derivative requires kappa > 0: kappa={kappa}"
);
assert!(r > 0.0, "matern kernel derivative requires r > 0: r={r}");
assert!(
kappa_derivative_order <= 2,
"matern kernel derivative supports kappa_derivative_order <= 2: order={kappa_derivative_order}"
);
let mut total = vec![KahanSum::default(); max_order + 1];
let mut coeff = 1.0_f64;
let kappa_sq = kappa * kappa;
let base = a + b;
let mut prev_term_norm = f64::INFINITY;
let mut saw_nonzero_term = false;
for n in 0..DUCHON_SMALL_CHI_SERIES_MAX_TERMS {
let kappa_factor = if kappa_derivative_order == 0 {
1.0
} else if n == 0 {
0.0
} else {
let p = 2.0 * n as f64;
let mut numerator = 1.0_f64;
for j in 0..kappa_derivative_order {
numerator *= p - j as f64;
}
let denom = kappa.powi(kappa_derivative_order as i32);
numerator / denom
};
let scale = coeff * kappa_factor;
let block = if kappa_factor == 0.0 {
None
} else {
Some(riesz_block_radial_derivatives(
d,
(base + n) as f64,
r,
max_order,
))
};
let term_norm = block
.as_ref()
.map(|values| {
values
.iter()
.map(|&value| (scale * value).abs())
.fold(0.0_f64, f64::max)
})
.unwrap_or(0.0);
if term_norm > 0.0 {
if saw_nonzero_term && term_norm > prev_term_norm {
break;
}
saw_nonzero_term = true;
prev_term_norm = term_norm;
}
if let Some(block) = block {
for (order, value) in block.into_iter().enumerate() {
total[order].add(scale * value);
}
}
let total_norm = total
.iter()
.map(|acc| acc.sum().abs())
.fold(0.0_f64, f64::max);
if n >= 4 && term_norm <= DUCHON_SMALL_CHI_SERIES_REL_TOL * total_norm.max(1.0) {
break;
}
coeff *= -((b + n) as f64) * kappa_sq / ((n + 1) as f64);
}
total.iter().map(|acc| acc.sum()).collect()
}
pub(crate) fn duchon_small_chi_riesz_series_value(
d: usize,
a: usize,
b: usize,
kappa: f64,
r: f64,
) -> f64 {
duchon_small_chi_riesz_series_radial_derivatives(d, a, b, kappa, r, 0, 0)[0]
}
pub fn isotropic_duchon_penalty(q: usize, d: usize, m: usize, s: f64, kappa: f64, r: f64) -> f64 {
assert!(2 * m >= q + 1, "isotropic_duchon_penalty: need 2m - q ≥ 1");
assert!(
s.is_finite() && s >= 0.0,
"isotropic_duchon_penalty: s must be finite and ≥ 0, got {s}"
);
let a = 2 * m - q;
if s == 0.0 {
return riesz_kernel_value(d, a as f64, r);
}
if kappa == 0.0 {
return riesz_kernel_value(d, a as f64 + 2.0 * s, r);
}
assert!(
s.fract() == 0.0,
"isotropic_duchon_penalty: hybrid Matérn (κ > 0) requires integer s, got {s}"
);
let s_int = s as usize;
let b = 2 * s_int;
if use_duchon_small_chi_riesz_series(kappa, r) {
return duchon_small_chi_riesz_series_value(d, a, b, kappa, r);
}
let kappa_sq = kappa * kappa;
let mut sum = KahanSum::default();
for j in 1..=a {
let sign = if (a - j).is_multiple_of(2) { 1.0 } else { -1.0 };
let binom = binomial_f64(a + b - j - 1, a - j);
let coeff = sign * binom * kappa_sq.powi(-((a + b - j) as i32));
let term = coeff * riesz_kernel_value(d, j as f64, r);
sum.add(term);
}
let sign_a = if a.is_multiple_of(2) { 1.0 } else { -1.0 };
for ell in 1..=b {
let binom = binomial_f64(a + b - ell - 1, b - ell);
let coeff = sign_a * binom * kappa_sq.powi(-((a + b - ell) as i32));
let term = coeff * matern_kernel_value(d, ell, kappa, r);
sum.add(term);
}
sum.sum()
}
pub fn anisotropic_duchon_penalty(
q: usize,
m: usize,
s: f64,
kappa: f64,
eta: &[f64],
r: &[f64],
) -> f64 {
assert_eq!(
eta.len(),
r.len(),
"anisotropic_duchon_penalty: eta and r dimension mismatch"
);
assert!(!r.is_empty(), "anisotropic_duchon_penalty: empty input");
assert!(q <= 2, "anisotropic_duchon_penalty: q must be in {{0,1,2}}");
anisotropic_duchon_penalty_radial(q, m, s, kappa, eta, r)
}
#[derive(Debug, Clone)]
pub struct PairBlockBundle {
pub value: f64,
pub d_eta: Vec<f64>,
pub d_kappa: f64,
pub d2_eta: Vec<Vec<f64>>,
pub d2_eta_kappa: Vec<f64>,
pub d2_kappa: f64,
}
#[derive(Debug, Clone)]
pub(crate) struct AnisoMetricPowers {
pub(crate) b: Vec<f64>,
pub(crate) b2: Vec<f64>,
pub(crate) b3: Vec<f64>,
}
impl AnisoMetricPowers {
pub(crate) fn new(eta: &[f64]) -> Self {
let mut b = Vec::with_capacity(eta.len());
let mut b2 = Vec::with_capacity(eta.len());
let mut b3 = Vec::with_capacity(eta.len());
for &e in eta {
let v = (-2.0 * e).exp();
let v2 = v * v;
b.push(v);
b2.push(v2);
b3.push(v2 * v);
}
Self { b, b2, b3 }
}
#[inline(always)]
pub(crate) fn assert_dim(&self, dim: usize) {
assert_eq!(self.b.len(), dim);
assert_eq!(self.b2.len(), dim);
assert_eq!(self.b3.len(), dim);
}
}
pub(crate) fn schoenberg_self_pair_bundle(
q: usize,
m: usize,
s: usize,
kappa: f64,
eta: &[f64],
) -> Option<PairBlockBundle> {
let d = eta.len();
if q > 2 || s == 0 || !(kappa > 0.0) || !kappa.is_finite() {
return None;
}
let half_d = 0.5 * d as f64;
let order = 2 * (m + s);
let s_total = 2 * s;
let lambda = order as f64 - half_d - q as f64;
let mu = s_total as f64 - lambda;
if !(lambda > 0.0 && mu > 0.0) {
return None;
}
let mut s1 = 0.0_f64;
let mut s2 = 0.0_f64;
let mut b = vec![0.0_f64; d];
for (axis, &e) in eta.iter().enumerate() {
let bb = (-2.0 * e).exp();
b[axis] = bb;
s1 += bb;
s2 += bb * bb;
}
let (c_eta, c_eta_grad, c_eta_hess) = match q {
0 => (1.0, vec![0.0_f64; d], vec![vec![0.0_f64; d]; d]),
1 => {
let mut grad = vec![0.0_f64; d];
let mut hess = vec![vec![0.0_f64; d]; d];
for k in 0..d {
grad[k] = -b[k];
hess[k][k] = 2.0 * b[k];
}
(0.5 * s1, grad, hess)
}
2 => {
let mut grad = vec![0.0_f64; d];
let mut hess = vec![vec![0.0_f64; d]; d];
for k in 0..d {
grad[k] = -s1 * b[k] - 2.0 * b[k] * b[k];
for l in 0..d {
hess[k][l] = if k == l {
2.0 * s1 * b[k] + 10.0 * b[k] * b[k]
} else {
2.0 * b[k] * b[l]
};
}
}
(0.25 * (s1 * s1 + 2.0 * s2), grad, hess)
}
_ => return None,
};
if c_eta == 0.0 || !c_eta.is_finite() {
return None;
}
let log_base = -half_d * (4.0 * std::f64::consts::PI).ln() + ln_gamma(lambda) + ln_gamma(mu)
- ln_gamma(s_total as f64)
- ln_gamma(half_d + q as f64)
- 2.0 * lambda * kappa.ln();
let base = log_base.exp();
if !base.is_finite() {
return None;
}
let exponent = -2.0 * lambda;
let g = base * c_eta;
let g_kappa = exponent * g / kappa;
let g_kappa2 = exponent * (exponent - 1.0) * g / (kappa * kappa);
let mut g_eta = vec![0.0_f64; d];
let mut g_eta2 = vec![vec![0.0_f64; d]; d];
let mut g_eta_kappa = vec![0.0_f64; d];
for k in 0..d {
g_eta[k] = base * c_eta_grad[k];
g_eta_kappa[k] = exponent * g_eta[k] / kappa;
for l in 0..d {
g_eta2[k][l] = base * c_eta_hess[k][l];
}
}
let big_j = eta.iter().sum::<f64>().exp();
let value = big_j * g;
let d_eta = (0..d).map(|k| big_j * (g + g_eta[k])).collect();
let d_kappa = big_j * g_kappa;
let d2_eta = (0..d)
.map(|k| {
(0..d)
.map(|l| big_j * (g + g_eta[k] + g_eta[l] + g_eta2[k][l]))
.collect::<Vec<f64>>()
})
.collect();
let d2_eta_kappa = (0..d).map(|k| big_j * (g_kappa + g_eta_kappa[k])).collect();
let d2_kappa = big_j * g_kappa2;
Some(PairBlockBundle {
value,
d_eta,
d_kappa,
d2_eta,
d2_eta_kappa,
d2_kappa,
})
}
pub(crate) fn hybrid_self_pair_radial_derivative_with_kappa_derivs_odd_d(
q: usize,
m: usize,
s: usize,
d: usize,
kappa: f64,
) -> Option<(f64, f64, f64)> {
if d % 2 != 1 || q > 2 || !(kappa > 0.0) || !kappa.is_finite() {
return None;
}
let smoothness_order = 2 * (m + s);
let required = d + 2 * q;
if smoothness_order <= required {
return None;
}
let length_scale = 1.0 / kappa;
let coeffs = super::duchon_partial_fraction_coeffs(m, s, kappa);
let f = super::duchon_phi_even_derivative_collision(length_scale, m, s, d, &coeffs, q).ok()?;
if !f.is_finite() {
return None;
}
let exponent = d as f64 + 2.0 * q as f64 - 2.0 * (m + s) as f64;
let f_kappa = exponent * f / kappa;
let f_kappa2 = exponent * (exponent - 1.0) * f / (kappa * kappa);
Some((f, f_kappa, f_kappa2))
}
pub(crate) fn hybrid_self_pair_bundle_odd_d(
q: usize,
m: usize,
s: usize,
kappa: f64,
eta: &[f64],
) -> Option<PairBlockBundle> {
let d = eta.len();
let (f, f_kappa, f_kappa2) =
hybrid_self_pair_radial_derivative_with_kappa_derivs_odd_d(q, m, s, d, kappa)?;
let mut s1 = 0.0_f64;
let mut s2 = 0.0_f64;
let mut b = vec![0.0_f64; d];
for (axis, &e) in eta.iter().enumerate() {
let bb = (-2.0 * e).exp();
b[axis] = bb;
s1 += bb;
s2 += bb * bb;
}
let (g, g_kappa, g_kappa2, g_eta, g_eta2, g_eta_kappa) = match q {
0 => (
f,
f_kappa,
f_kappa2,
vec![0.0_f64; d],
vec![vec![0.0_f64; d]; d],
vec![0.0_f64; d],
),
1 => {
let mut g_eta = vec![0.0_f64; d];
let mut g_eta2 = vec![vec![0.0_f64; d]; d];
let mut g_eta_kappa = vec![0.0_f64; d];
for k in 0..d {
g_eta[k] = 2.0 * b[k] * f;
g_eta2[k][k] = -4.0 * b[k] * f;
g_eta_kappa[k] = 2.0 * b[k] * f_kappa;
}
(
-s1 * f,
-s1 * f_kappa,
-s1 * f_kappa2,
g_eta,
g_eta2,
g_eta_kappa,
)
}
2 => {
let a = s1 * s1 + 2.0 * s2;
let mut g_eta = vec![0.0_f64; d];
let mut g_eta2 = vec![vec![0.0_f64; d]; d];
let mut g_eta_kappa = vec![0.0_f64; d];
for k in 0..d {
let a_k = -4.0 * s1 * b[k] - 8.0 * b[k] * b[k];
g_eta[k] = f * a_k / 3.0;
g_eta_kappa[k] = f_kappa * a_k / 3.0;
for l in 0..d {
let a_kl = if k == l {
8.0 * s1 * b[k] + 40.0 * b[k] * b[k]
} else {
8.0 * b[k] * b[l]
};
g_eta2[k][l] = f * a_kl / 3.0;
}
}
(
f * a / 3.0,
f_kappa * a / 3.0,
f_kappa2 * a / 3.0,
g_eta,
g_eta2,
g_eta_kappa,
)
}
_ => return None,
};
let big_j = eta.iter().sum::<f64>().exp();
let value = big_j * g;
let d_eta = (0..d).map(|k| big_j * (g + g_eta[k])).collect();
let d_kappa = big_j * g_kappa;
let d2_eta = (0..d)
.map(|k| {
(0..d)
.map(|l| big_j * (g + g_eta[k] + g_eta[l] + g_eta2[k][l]))
.collect::<Vec<f64>>()
})
.collect();
let d2_eta_kappa = (0..d).map(|k| big_j * (g_kappa + g_eta_kappa[k])).collect();
let d2_kappa = big_j * g_kappa2;
Some(PairBlockBundle {
value,
d_eta,
d_kappa,
d2_eta,
d2_eta_kappa,
d2_kappa,
})
}
pub(crate) fn analytic_self_pair_bundle(
q: usize,
m: usize,
s: usize,
kappa: f64,
eta: &[f64],
) -> Option<PairBlockBundle> {
schoenberg_self_pair_bundle(q, m, s, kappa, eta)
.or_else(|| hybrid_self_pair_bundle_odd_d(q, m, s, kappa, eta))
}
pub(crate) const BESSEL_TABLE_HALF_WIDTH: i32 = 6;
pub(crate) fn bessel_k_table_around(nu: f64, x: f64) -> Vec<f64> {
let h = BESSEL_TABLE_HALF_WIDTH;
let len = (2 * h + 1) as usize;
let mut out = vec![0.0_f64; len];
for i in -h..=h {
let order = nu + i as f64;
out[(i + h) as usize] = bessel_k(order, x);
}
out
}
pub(crate) fn matern_block_radial_derivatives(
d: usize,
ell: usize,
kappa: f64,
r: f64,
max_order: usize,
) -> Vec<f64> {
assert!(d >= 1, "matern block requires dimension >= 1: d={d}");
assert!(ell >= 1, "matern block requires ell >= 1: ell={ell}");
assert!(
kappa > 0.0,
"matern block requires kappa > 0: kappa={kappa}"
);
assert!(r > 0.0, "matern block requires r > 0: r={r}");
assert!(
max_order <= 6,
"matern_block_radial_derivatives: max_order ≤ 6"
);
let nu = ell as f64 - d as f64 / 2.0;
let ln_pref = (d as f64 / 2.0 - ell as f64) * kappa.ln()
- (d as f64 / 2.0) * (2.0 * std::f64::consts::PI).ln()
- (ell as f64 - 1.0) * std::f64::consts::LN_2
- ln_gamma(ell as f64);
let pref = ln_pref.exp();
let mut terms: Vec<(f64, f64, i32)> = vec![(pref, nu, 0)];
let mut out = Vec::with_capacity(max_order + 1);
let kr = kappa * r;
let bessel_table = bessel_k_table_around(nu, kr);
let half_width = BESSEL_TABLE_HALF_WIDTH;
let bessel = |b_off: i32| -> f64 {
let idx = (b_off + half_width) as usize;
bessel_table[idx]
};
let a_is_integer = d.is_multiple_of(2);
let evaluate = |terms: &Vec<(f64, f64, i32)>| -> f64 {
let mut sum = 0.0_f64;
if a_is_integer {
for &(c, a, b) in terms {
if c == 0.0 {
continue;
}
assert!(
a.fract() == 0.0,
"matern_block_radial_derivatives: expected integer exponent for even d"
);
sum += c * r.powi(a as i32) * bessel(b);
}
} else {
for &(c, a, b) in terms {
if c == 0.0 {
continue;
}
sum += c * r.powf(a) * bessel(b);
}
}
sum
};
out.push(evaluate(&terms));
for _ in 0..max_order {
let mut next: Vec<(f64, f64, i32)> = Vec::with_capacity(terms.len() * 3);
for &(c, a, b) in &terms {
if c == 0.0 {
continue;
}
if a != 0.0 {
next.push((c * a, a - 1.0, b));
}
let coef = -c * kappa * 0.5;
next.push((coef, a, b - 1));
next.push((coef, a, b + 1));
}
terms = compress_terms(next);
out.push(evaluate(&terms));
}
out
}
pub(crate) fn compress_terms(mut terms: Vec<(f64, f64, i32)>) -> Vec<(f64, f64, i32)> {
terms.sort_by(|x, y| {
x.2.cmp(&y.2)
.then_with(|| x.1.partial_cmp(&y.1).unwrap_or(std::cmp::Ordering::Equal))
});
let mut out: Vec<(f64, f64, i32)> = Vec::with_capacity(terms.len());
for (c, a, b) in terms {
if let Some(last) = out.last_mut()
&& last.2 == b
&& (last.1 - a).abs() < 1e-15
{
last.0 += c;
continue;
}
out.push((c, a, b));
}
out
}
pub(crate) fn riesz_block_radial_derivatives(
d: usize,
j: f64,
r: f64,
max_order: usize,
) -> Vec<f64> {
assert!(d >= 1, "riesz block requires dimension >= 1: d={d}");
assert!(
j.is_finite() && j >= 1.0,
"riesz_block: need j ≥ 1, got {j}"
);
assert!(r > 0.0, "riesz block requires r > 0: r={r}");
let two_j = 2.0 * j;
let half_d = d as f64 / 2.0;
let mut out = Vec::with_capacity(max_order + 1);
const LOG_EPS: f64 = 1e-12;
let offset = two_j - d as f64;
let log_case = offset >= -LOG_EPS && {
let n_f = (offset / 2.0).round();
n_f >= 0.0 && (n_f * 2.0 - offset).abs() < LOG_EPS
};
if log_case {
let n_f = (offset / 2.0).round();
let n = n_f as usize;
let two_j_i = two_j.round() as i32;
let sign = if n.is_multiple_of(2) { -1.0 } else { 1.0 };
let denom = 2.0_f64.powi(two_j_i - 1)
* std::f64::consts::PI.powf(d as f64 / 2.0)
* gamma_fn(j)
* factorial_f64(n);
let c = sign / denom;
let two_n = 2 * n;
let shift = log_riesz_finite_part_shift(d, n);
for k in 0..=max_order {
let mut sum = 0.0_f64;
for i in 0..=k {
if i > two_n {
continue;
}
let binom = binomial_f64(k, i);
let mut ff = 1.0_f64;
for q in 0..i {
ff *= (two_n - q) as f64;
}
let r_pow = r.powi((two_n - i) as i32);
let log_part = if k - i == 0 {
r.ln() + shift
} else {
let m = (k - i) as i32;
let sign2 = if (m as usize) % 2 == 1 { 1.0 } else { -1.0 };
sign2 * factorial_f64((k - i) - 1) * r.powi(-m)
};
sum += binom * ff * r_pow * log_part;
}
out.push(c * sum);
}
return out;
}
let c =
gamma_fn(half_d - j) / (4.0_f64.powf(j) * std::f64::consts::PI.powf(half_d) * gamma_fn(j));
let mut coef = c;
let mut exp = 2.0 * j - d as f64;
out.push(coef * r.powf(exp));
for _ in 0..max_order {
coef *= exp;
exp -= 1.0;
out.push(coef * r.powf(exp));
}
out
}
pub fn radial_derivatives_of_isotropic_duchon(
d: usize,
m: usize,
s: f64,
kappa: f64,
r: f64,
max_order: usize,
) -> Vec<f64> {
assert!(
r > 0.0,
"radial_derivatives_of_isotropic_duchon: r must be > 0"
);
assert!(
2 * m >= 1,
"radial_derivatives_of_isotropic_duchon: need m ≥ 1"
);
assert!(
max_order <= 6,
"radial_derivatives_of_isotropic_duchon: max_order ≤ 6"
);
assert!(
s.is_finite() && s >= 0.0,
"radial_derivatives_of_isotropic_duchon: s must be finite and ≥ 0, got {s}"
);
let a = 2 * m;
if s == 0.0 {
return riesz_block_radial_derivatives(d, a as f64, r, max_order);
}
if kappa == 0.0 {
return riesz_block_radial_derivatives(d, a as f64 + 2.0 * s, r, max_order);
}
assert!(
s.fract() == 0.0,
"radial_derivatives_of_isotropic_duchon: hybrid Matérn (κ > 0) requires integer s, got {s}"
);
let s = s as usize;
let b = 2 * s;
if use_duchon_small_chi_riesz_series(kappa, r) {
return duchon_small_chi_riesz_series_radial_derivatives(d, a, b, kappa, r, max_order, 0);
}
if schwinger_radial_is_convergent(d, m) {
return stable_hybrid_duchon_radial(d, m, s, kappa, r, max_order);
}
let kappa_sq = kappa * kappa;
let mut total_acc = vec![KahanSum::default(); max_order + 1];
for j in 1..=a {
let sign = if (a - j).is_multiple_of(2) { 1.0 } else { -1.0 };
let binom = binomial_f64(a + b - j - 1, a - j);
let coeff = sign * binom * kappa_sq.powi(-((a + b - j) as i32));
let block = riesz_block_radial_derivatives(d, (j) as f64, r, max_order);
for (k, v) in block.into_iter().enumerate() {
let term = coeff * v;
total_acc[k].add(term);
}
}
let sign_a = if a.is_multiple_of(2) { 1.0 } else { -1.0 };
for ell in 1..=b {
let binom = binomial_f64(a + b - ell - 1, b - ell);
let coeff = sign_a * binom * kappa_sq.powi(-((a + b - ell) as i32));
let block = matern_block_radial_derivatives(d, ell, kappa, r, max_order);
for (k, v) in block.into_iter().enumerate() {
let term = coeff * v;
total_acc[k].add(term);
}
}
total_acc.iter().map(|acc| acc.sum()).collect()
}
pub fn pure_duchon_self_pair_value(
q: usize,
d: usize,
m: usize,
s: usize,
eta: &[f64],
) -> Option<f64> {
if q != 1 && q != 2 {
return None;
}
if eta.len() != d {
return None;
}
let mm = 2 * (m + s); let two_mm = 2 * mm;
if two_mm >= d && (two_mm - d).is_multiple_of(2) {
return None; }
let p_int = two_mm as isize - d as isize;
let two_q = 2 * q as isize;
if p_int < two_q {
return None; }
let mut s_1 = 0.0_f64;
let mut s_2 = 0.0_f64;
for &e in eta {
let bb = (-2.0 * e).exp();
s_1 += bb;
s_2 += bb * bb;
}
let f_2q_0 = if p_int > two_q {
0.0
} else {
let c = riesz_kernel_coefficient_nonlog(d, mm);
c * factorial_f64(two_q as usize)
};
let value = match q {
1 => -s_1 * f_2q_0,
2 => (f_2q_0 / 3.0) * (s_1 * s_1 + 2.0 * s_2),
_ => return None,
};
Some(value)
}
pub(crate) fn riesz_kernel_coefficient_nonlog(d: usize, j: usize) -> f64 {
let half_d = d as f64 / 2.0;
let num = gamma_fn(half_d - j as f64);
let denom = 4.0_f64.powi(j as i32) * std::f64::consts::PI.powf(half_d) * gamma_fn(j as f64);
num / denom
}
pub fn anisotropic_duchon_penalty_radial(
q: usize,
m: usize,
s: f64,
kappa: f64,
eta: &[f64],
r: &[f64],
) -> f64 {
assert_eq!(
eta.len(),
r.len(),
"anisotropic_duchon_penalty_radial: eta and r dimension mismatch"
);
assert!(
!r.is_empty(),
"anisotropic_duchon_penalty_radial: empty input"
);
assert!(
q <= 2,
"anisotropic_duchon_penalty_radial: q must be in {{0,1,2}}"
);
let powers = AnisoMetricPowers::new(eta);
anisotropic_duchon_penalty_radial_with_powers(q, m, s, kappa, eta, &powers, r)
}
pub(crate) fn anisotropic_duchon_penalty_radial_with_powers(
q: usize,
m: usize,
s: f64,
kappa: f64,
eta: &[f64],
powers: &AnisoMetricPowers,
r: &[f64],
) -> f64 {
assert_eq!(
eta.len(),
r.len(),
"anisotropic_duchon_penalty_radial_with_powers: eta and r dimension mismatch"
);
assert!(
!r.is_empty(),
"anisotropic_duchon_penalty_radial_with_powers: empty input"
);
assert!(
q <= 2,
"anisotropic_duchon_penalty_radial_with_powers: q must be in {{0,1,2}}"
);
assert!(
s.is_finite() && s >= 0.0,
"anisotropic_duchon_penalty_radial_with_powers: s must be finite and ≥ 0, got {s}"
);
powers.assert_dim(r.len());
let d = r.len();
let s_int = if s.fract() == 0.0 {
Some(s as usize)
} else {
None
};
if is_zero_lag(r) {
if let Some(si) = s_int
&& let Some(bundle) = analytic_self_pair_bundle(q, m, si, kappa, eta)
{
return bundle.value / eta.iter().sum::<f64>().exp();
}
panic!(
"anisotropic_duchon_penalty_radial: zero lag has no finite analytic self-pair for q={q} d={d} m={m} s={s}"
);
}
if let Some(common_eta) = uniform_eta_value(eta) {
let euclidean_r2 = squared_norm(r);
if let Some(value) =
uniform_metric_radial_duchon_penalty(q, m, s, kappa, d, common_eta, euclidean_r2)
{
return value;
}
}
let (big_r, s1, s2, u1, u2) = aniso_invariants_with_powers(powers, r);
let max_order = if q == 0 { 0 } else { (2 * q + 2).min(6) };
let fr = radial_derivatives_of_isotropic_duchon(d, m, s, kappa, big_r, max_order);
if q == 0 {
fr[0]
} else if q == 1 {
-anisotropic_laplacian_of_radial_first(big_r, s1, u1, &fr)
} else {
anisotropic_laplacian_of_radial_second(big_r, s1, s2, u1, u2, &fr)
}
}
pub(crate) fn uniform_metric_radial_duchon_penalty(
q: usize,
m: usize,
s: f64,
kappa: f64,
d: usize,
common_eta: f64,
euclidean_r2: f64,
) -> Option<f64> {
let b = (-2.0 * common_eta).exp();
if !(b.is_finite() && b > 0.0) {
return None;
}
if euclidean_r2 == 0.0 {
return None;
}
let big_r = (b * euclidean_r2).sqrt();
let max_order = if q == 0 { 0 } else { 2 * q };
let fr = radial_derivatives_of_isotropic_duchon(d, m, s, kappa, big_r, max_order);
let big_r2 = big_r * big_r;
let s1 = (d as f64) * b;
let s2 = (d as f64) * b * b;
let u1 = b * big_r2;
let u2 = b * b * big_r2;
let value = if q == 0 {
fr[0]
} else if q == 1 {
-anisotropic_laplacian_of_radial_first(big_r, s1, u1, &fr)
} else if q == 2 {
anisotropic_laplacian_of_radial_second(big_r, s1, s2, u1, u2, &fr)
} else {
return None;
};
Some(value)
}
pub(crate) fn uniform_eta_value(eta: &[f64]) -> Option<f64> {
let (&first, rest) = eta.split_first()?;
(first.is_finite() && rest.iter().all(|&value| value == first)).then_some(first)
}
pub(crate) fn squared_norm(x: &[f64]) -> f64 {
x.iter().map(|&value| value * value).sum()
}
pub(crate) fn is_zero_lag(r: &[f64]) -> bool {
r.iter().all(|&value| value == 0.0)
}
pub(crate) fn anisotropic_laplacian_of_radial_first(
big_r: f64,
s1: f64,
u1: f64,
fr: &[f64],
) -> f64 {
let r2 = big_r * big_r;
let r3 = r2 * big_r;
fr[2] * u1 / r2 + fr[1] * (s1 / big_r - u1 / r3)
}
pub(crate) fn anisotropic_laplacian_of_radial_second(
big_r: f64,
s1: f64,
s2: f64,
u1: f64,
u2: f64,
fr: &[f64],
) -> f64 {
let r = big_r;
let r2 = r * r;
let r3 = r2 * r;
let r4 = r2 * r2;
let r5 = r4 * r;
let r6 = r4 * r2;
let r7 = r6 * r;
let part_u1sq =
u1 * u1 * (fr[4] / r4 - 6.0 * fr[3] / r5 + 15.0 * fr[2] / r6 - 15.0 * fr[1] / r7);
let part_s1u1 = s1 * u1 * (2.0 * fr[3] / r3 - 6.0 * fr[2] / r4 + 6.0 * fr[1] / r5);
let part_s1sq = s1 * s1 * (fr[2] / r2 - fr[1] / r3);
let part_u2 = u2 * (4.0 * fr[3] / r3 - 12.0 * fr[2] / r4 + 12.0 * fr[1] / r5);
let part_s2 = s2 * (2.0 * fr[2] / r2 - 2.0 * fr[1] / r3);
part_u1sq + part_s1u1 + part_s1sq + part_u2 + part_s2
}
pub(crate) fn aniso_invariants(eta: &[f64], r: &[f64]) -> (f64, f64, f64, f64, f64) {
let powers = AnisoMetricPowers::new(eta);
aniso_invariants_with_powers(&powers, r)
}
pub(crate) fn aniso_invariants_with_powers(
powers: &AnisoMetricPowers,
r: &[f64],
) -> (f64, f64, f64, f64, f64) {
use wide::f64x4;
let d = r.len();
powers.assert_dim(d);
let mut s1_v = f64x4::ZERO;
let mut s2_v = f64x4::ZERO;
let mut r2_v = f64x4::ZERO;
let mut u1_v = f64x4::ZERO;
let mut u2_v = f64x4::ZERO;
let chunks = d / 4;
let tail = d % 4;
for c in 0..chunks {
let base = c * 4;
let b_arr = [
powers.b[base],
powers.b[base + 1],
powers.b[base + 2],
powers.b[base + 3],
];
let b2_arr = [
powers.b2[base],
powers.b2[base + 1],
powers.b2[base + 2],
powers.b2[base + 3],
];
let b3_arr = [
powers.b3[base],
powers.b3[base + 1],
powers.b3[base + 2],
powers.b3[base + 3],
];
let rk_arr = [r[base], r[base + 1], r[base + 2], r[base + 3]];
let b = f64x4::from(b_arr);
let b2 = f64x4::from(b2_arr);
let b3 = f64x4::from(b3_arr);
let rk = f64x4::from(rk_arr);
let rk2 = rk * rk;
s1_v += b;
s2_v += b2;
r2_v += b * rk2;
u1_v += b2 * rk2;
u2_v += b3 * rk2;
}
let s1_a = s1_v.to_array();
let s2_a = s2_v.to_array();
let r2_a = r2_v.to_array();
let u1_a = u1_v.to_array();
let u2_a = u2_v.to_array();
let mut s1 = s1_a[0] + s1_a[1] + s1_a[2] + s1_a[3];
let mut s2 = s2_a[0] + s2_a[1] + s2_a[2] + s2_a[3];
let mut r2 = r2_a[0] + r2_a[1] + r2_a[2] + r2_a[3];
let mut u1 = u1_a[0] + u1_a[1] + u1_a[2] + u1_a[3];
let mut u2 = u2_a[0] + u2_a[1] + u2_a[2] + u2_a[3];
let tail_start = chunks * 4;
for k in 0..tail {
let idx = tail_start + k;
let rk2 = r[idx] * r[idx];
let b = powers.b[idx];
let b2 = powers.b2[idx];
s1 += b;
s2 += b2;
r2 += b * rk2;
u1 += b2 * rk2;
u2 += powers.b3[idx] * rk2;
}
(r2.sqrt(), s1, s2, u1, u2)
}
pub fn aniso_invariants_scalar(eta: &[f64], r: &[f64]) -> (f64, f64, f64, f64, f64) {
assert_eq!(eta.len(), r.len());
let mut s1 = 0.0_f64;
let mut s2 = 0.0_f64;
let mut r2 = 0.0_f64;
let mut u1 = 0.0_f64;
let mut u2 = 0.0_f64;
for k in 0..eta.len() {
let b = (-2.0 * eta[k]).exp();
let b2 = b * b;
let rk2 = r[k] * r[k];
s1 += b;
s2 += b2;
r2 += b * rk2;
u1 += b2 * rk2;
u2 += b2 * b * rk2;
}
(r2.sqrt(), s1, s2, u1, u2)
}
pub fn aniso_invariants_simd(eta: &[f64], r: &[f64]) -> (f64, f64, f64, f64, f64) {
aniso_invariants(eta, r)
}
pub fn radial_derivatives_of_isotropic_duchon_kappa_partial(
d: usize,
m: usize,
s: usize,
kappa: f64,
r: f64,
max_order: usize,
) -> Vec<f64> {
assert!(
r > 0.0,
"Duchon kappa partial requires positive radius: r={r}, d={d}, m={m}, s={s}, kappa={kappa}"
);
assert!(
max_order <= 6,
"Duchon kappa partial supports max_order <= 6: max_order={max_order}, d={d}, m={m}, s={s}"
);
if s == 0 || kappa == 0.0 {
return vec![0.0_f64; max_order + 1];
}
let a = 2 * m;
let b = 2 * s;
if use_duchon_small_chi_riesz_series(kappa, r) {
return duchon_small_chi_riesz_series_radial_derivatives(d, a, b, kappa, r, max_order, 1);
}
let kappa_sq = kappa * kappa;
let mut total = vec![KahanSum::default(); max_order + 1];
for j in 1..=a {
let n_j = a + b - j;
let sign = if (a - j).is_multiple_of(2) { 1.0 } else { -1.0 };
let binom = binomial_f64(a + b - j - 1, a - j);
let a_j = sign * binom * kappa_sq.powi(-(n_j as i32));
let a_j_prime = -(2.0 * n_j as f64 / kappa) * a_j;
let block = riesz_block_radial_derivatives(d, (j) as f64, r, max_order);
for (k, v) in block.into_iter().enumerate() {
total[k].add(a_j_prime * v);
}
}
let sign_a = if a.is_multiple_of(2) { 1.0 } else { -1.0 };
for ell in 1..=b {
let n_ell = a + b - ell;
let binom = binomial_f64(a + b - ell - 1, b - ell);
let b_ell = sign_a * binom * kappa_sq.powi(-(n_ell as i32));
let b_ell_prime = -(2.0 * n_ell as f64 / kappa) * b_ell;
let m_ell = matern_block_radial_derivatives(d, ell, kappa, r, max_order);
let m_ell_p1 = matern_block_radial_derivatives(d, ell + 1, kappa, r, max_order);
let kappa_factor = -2.0 * ell as f64 * kappa;
for k in 0..=max_order {
total[k].add(b_ell_prime * m_ell[k] + b_ell * kappa_factor * m_ell_p1[k]);
}
}
total.iter().map(|acc| acc.sum()).collect()
}
pub fn radial_derivatives_of_isotropic_duchon_kappa_partial2(
d: usize,
m: usize,
s: usize,
kappa: f64,
r: f64,
max_order: usize,
) -> Vec<f64> {
assert!(
r > 0.0,
"Duchon second kappa partial requires positive radius: r={r}, d={d}, m={m}, s={s}, kappa={kappa}"
);
assert!(
max_order <= 6,
"Duchon second kappa partial supports max_order <= 6: max_order={max_order}, d={d}, m={m}, s={s}"
);
if s == 0 || kappa == 0.0 {
return vec![0.0_f64; max_order + 1];
}
let a = 2 * m;
let b = 2 * s;
if use_duchon_small_chi_riesz_series(kappa, r) {
return duchon_small_chi_riesz_series_radial_derivatives(d, a, b, kappa, r, max_order, 2);
}
let kappa_sq = kappa * kappa;
let mut total = vec![KahanSum::default(); max_order + 1];
for j in 1..=a {
let n_j = a + b - j;
let sign = if (a - j).is_multiple_of(2) { 1.0 } else { -1.0 };
let binom = binomial_f64(a + b - j - 1, a - j);
let a_j = sign * binom * kappa_sq.powi(-(n_j as i32));
let nj_f = n_j as f64;
let a_j_dd = (2.0 * nj_f * (2.0 * nj_f + 1.0) / kappa_sq) * a_j;
let block = riesz_block_radial_derivatives(d, (j) as f64, r, max_order);
for (k, v) in block.into_iter().enumerate() {
total[k].add(a_j_dd * v);
}
}
let sign_a = if a.is_multiple_of(2) { 1.0 } else { -1.0 };
for ell in 1..=b {
let n_ell = a + b - ell;
let binom = binomial_f64(a + b - ell - 1, b - ell);
let b_ell = sign_a * binom * kappa_sq.powi(-(n_ell as i32));
let n_ell_f = n_ell as f64;
let b_ell_prime = -(2.0 * n_ell_f / kappa) * b_ell;
let b_ell_dd = (2.0 * n_ell_f * (2.0 * n_ell_f + 1.0) / kappa_sq) * b_ell;
let ell_f = ell as f64;
let m_ell = matern_block_radial_derivatives(d, ell, kappa, r, max_order);
let m_ell_p1 = matern_block_radial_derivatives(d, ell + 1, kappa, r, max_order);
let m_ell_p2 = matern_block_radial_derivatives(d, ell + 2, kappa, r, max_order);
let cross_factor = -4.0 * ell_f * kappa; let m_dd_a = -2.0 * ell_f; let m_dd_b = 4.0 * ell_f * (ell_f + 1.0) * kappa_sq; for k in 0..=max_order {
total[k].add(
b_ell_dd * m_ell[k]
+ b_ell_prime * cross_factor * m_ell_p1[k]
+ b_ell * (m_dd_a * m_ell_p1[k] + m_dd_b * m_ell_p2[k]),
);
}
}
total.iter().map(|acc| acc.sum()).collect()
}
pub(crate) fn radial_g_q_partials(
q: usize,
big_r: f64,
s1: f64,
s2: f64,
u1: f64,
u2: f64,
fr: &[f64],
) -> (f64, f64, f64, f64, f64, f64) {
let r = big_r;
let r2 = r * r;
let r3 = r2 * r;
let r4 = r2 * r2;
let r5 = r4 * r;
let r6 = r4 * r2;
let r7 = r6 * r;
let r8 = r4 * r4;
match q {
0 => {
let g = fr[0];
let g_r = fr[1];
(g, g_r, 0.0, 0.0, 0.0, 0.0)
}
1 => {
let g = -(fr[2] * u1 / r2 + fr[1] * (s1 / r - u1 / r3));
let g_r = -fr[3] * u1 / r2 + 2.0 * fr[2] * u1 / r3 - fr[2] * s1 / r
+ fr[1] * s1 / r2
+ fr[2] * u1 / r3
- 3.0 * fr[1] * u1 / r4;
let g_s1 = -fr[1] / r;
let g_u1 = -fr[2] / r2 + fr[1] / r3;
(g, g_r, g_s1, 0.0, g_u1, 0.0)
}
2 => {
let f1 = fr[4] / r4 - 6.0 * fr[3] / r5 + 15.0 * fr[2] / r6 - 15.0 * fr[1] / r7;
let f2 = 2.0 * fr[3] / r3 - 6.0 * fr[2] / r4 + 6.0 * fr[1] / r5;
let f3 = fr[2] / r2 - fr[1] / r3;
let f4 = 4.0 * fr[3] / r3 - 12.0 * fr[2] / r4 + 12.0 * fr[1] / r5;
let f5 = 2.0 * fr[2] / r2 - 2.0 * fr[1] / r3;
let g = u1 * u1 * f1 + s1 * u1 * f2 + s1 * s1 * f3 + u2 * f4 + s2 * f5;
let df1 = fr[5] / r4 - 10.0 * fr[4] / r5 + 45.0 * fr[3] / r6 - 105.0 * fr[2] / r7
+ 105.0 * fr[1] / r8;
let df2 = 2.0 * fr[4] / r3 - 12.0 * fr[3] / r4 + 30.0 * fr[2] / r5 - 30.0 * fr[1] / r6;
let df3 = fr[3] / r2 - 3.0 * fr[2] / r3 + 3.0 * fr[1] / r4;
let df4 = 4.0 * fr[4] / r3 - 24.0 * fr[3] / r4 + 60.0 * fr[2] / r5 - 60.0 * fr[1] / r6;
let df5 = 2.0 * fr[3] / r2 - 6.0 * fr[2] / r3 + 6.0 * fr[1] / r4;
let g_r = u1 * u1 * df1 + s1 * u1 * df2 + s1 * s1 * df3 + u2 * df4 + s2 * df5;
let g_s1 = u1 * f2 + 2.0 * s1 * f3;
let g_s2 = f5;
let g_u1 = 2.0 * u1 * f1 + s1 * f2;
let g_u2 = f4;
(g, g_r, g_s1, g_s2, g_u1, g_u2)
}
_ => panic!("radial_g_q_partials requires q in {{0, 1, 2}}: q={q}"),
}
}
pub(crate) fn radial_g_q_hessian(
q: usize,
big_r: f64,
s1: f64,
s2: f64,
u1: f64,
u2: f64,
fr: &[f64],
) -> (
f64, // g_RR
f64, // g_R_s1
f64, // g_R_s2
f64, // g_R_u1
f64, // g_R_u2
f64, // g_s1s1
f64, // g_s1s2
f64, // g_s1u1
f64, // g_s1u2
f64, // g_s2s2
f64, // g_s2u1
f64, // g_s2u2
f64, // g_u1u1
f64, // g_u1u2
f64, // g_u2u2
) {
let r = big_r;
let r2 = r * r;
let r3 = r2 * r;
let r4 = r2 * r2;
let r5 = r4 * r;
let r6 = r4 * r2;
let r7 = r6 * r;
let r8 = r4 * r4;
let r9 = r8 * r;
match q {
0 => {
let g_rr = fr[2];
(
g_rr, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
)
}
1 => {
let g_rr =
-fr[4] * u1 / r2 + 5.0 * fr[3] * u1 / r3 - 12.0 * fr[2] * u1 / r4 - fr[3] * s1 / r
+ 2.0 * fr[2] * s1 / r2
- 2.0 * fr[1] * s1 / r3
+ 12.0 * fr[1] * u1 / r5;
let g_r_s1 = -fr[2] / r + fr[1] / r2;
let g_r_u1 = -fr[3] / r2 + 3.0 * fr[2] / r3 - 3.0 * fr[1] / r4;
(
g_rr, g_r_s1, 0.0, g_r_u1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
)
}
2 => {
let f1 = fr[4] / r4 - 6.0 * fr[3] / r5 + 15.0 * fr[2] / r6 - 15.0 * fr[1] / r7;
let f2 = 2.0 * fr[3] / r3 - 6.0 * fr[2] / r4 + 6.0 * fr[1] / r5;
let f3 = fr[2] / r2 - fr[1] / r3;
let df1 = fr[5] / r4 - 10.0 * fr[4] / r5 + 45.0 * fr[3] / r6 - 105.0 * fr[2] / r7
+ 105.0 * fr[1] / r8;
let df2 = 2.0 * fr[4] / r3 - 12.0 * fr[3] / r4 + 30.0 * fr[2] / r5 - 30.0 * fr[1] / r6;
let df3 = fr[3] / r2 - 3.0 * fr[2] / r3 + 3.0 * fr[1] / r4;
let df4 = 4.0 * fr[4] / r3 - 24.0 * fr[3] / r4 + 60.0 * fr[2] / r5 - 60.0 * fr[1] / r6;
let df5 = 2.0 * fr[3] / r2 - 6.0 * fr[2] / r3 + 6.0 * fr[1] / r4;
let d2f1 = fr[6] / r4 - 14.0 * fr[5] / r5 + 95.0 * fr[4] / r6 - 375.0 * fr[3] / r7
+ 840.0 * fr[2] / r8
- 840.0 * fr[1] / r9;
let d2f2 = 2.0 * fr[5] / r3 - 18.0 * fr[4] / r4 + 78.0 * fr[3] / r5
- 180.0 * fr[2] / r6
+ 180.0 * fr[1] / r7;
let d2f3 = fr[4] / r2 - 5.0 * fr[3] / r3 + 12.0 * fr[2] / r4 - 12.0 * fr[1] / r5;
let d2f4 = 4.0 * fr[5] / r3 - 36.0 * fr[4] / r4 + 156.0 * fr[3] / r5
- 360.0 * fr[2] / r6
+ 360.0 * fr[1] / r7;
let d2f5 = 2.0 * fr[4] / r2 - 10.0 * fr[3] / r3 + 24.0 * fr[2] / r4 - 24.0 * fr[1] / r5;
let g_rr = u1 * u1 * d2f1 + s1 * u1 * d2f2 + s1 * s1 * d2f3 + u2 * d2f4 + s2 * d2f5;
let g_r_s1 = u1 * df2 + 2.0 * s1 * df3;
let g_r_s2 = df5;
let g_r_u1 = 2.0 * u1 * df1 + s1 * df2;
let g_r_u2 = df4;
let g_s1s1 = 2.0 * f3;
let g_s1u1 = f2;
let g_u1u1 = 2.0 * f1;
(
g_rr, g_r_s1, g_r_s2, g_r_u1, g_r_u2, g_s1s1, 0.0, g_s1u1, 0.0, 0.0, 0.0, 0.0,
g_u1u1, 0.0, 0.0,
)
}
_ => panic!("radial_g_q_hessian requires q in {{0, 1, 2}}: q={q}"),
}
}
pub(crate) fn aniso_invariants_eta_jacobian_with_powers(
eta: &[f64],
r: &[f64],
powers: &AnisoMetricPowers,
) -> (
f64,
f64,
f64,
f64,
f64,
Vec<f64>,
Vec<f64>,
Vec<f64>,
Vec<f64>,
Vec<f64>,
) {
let d = eta.len();
powers.assert_dim(d);
let (big_r, s1, s2, u1, u2) = aniso_invariants_with_powers(powers, r);
let mut dr_de = vec![0.0_f64; d];
let mut ds1_de = vec![0.0_f64; d];
let mut ds2_de = vec![0.0_f64; d];
let mut du1_de = vec![0.0_f64; d];
let mut du2_de = vec![0.0_f64; d];
for l in 0..d {
let b_l = powers.b[l];
let b_l_sq = powers.b2[l];
let b_l_cu = powers.b3[l];
let r_l_sq = r[l] * r[l];
ds1_de[l] = -2.0 * b_l;
ds2_de[l] = -4.0 * b_l_sq;
dr_de[l] = if big_r > 0.0 {
-b_l * r_l_sq / big_r
} else {
0.0
};
du1_de[l] = -4.0 * b_l_sq * r_l_sq;
du2_de[l] = -6.0 * b_l_cu * r_l_sq;
}
(big_r, s1, s2, u1, u2, dr_de, ds1_de, ds2_de, du1_de, du2_de)
}
pub fn pair_block_radial_with_j_second_derivatives(
q: usize,
m: usize,
s: usize,
kappa: f64,
eta: &[f64],
r: &[f64],
) -> PairBlockBundle {
let powers = AnisoMetricPowers::new(eta);
pair_block_radial_with_j_second_derivatives_with_powers(q, m, s, kappa, eta, &powers, r)
}
pub(crate) fn pair_block_radial_with_j_second_derivatives_with_powers(
q: usize,
m: usize,
s: usize,
kappa: f64,
eta: &[f64],
powers: &AnisoMetricPowers,
r: &[f64],
) -> PairBlockBundle {
assert_eq!(
eta.len(),
r.len(),
"pair_block_radial_with_j_second_derivatives_with_powers: eta and r dimension mismatch"
);
assert!(
!r.is_empty(),
"pair_block_radial_with_j_second_derivatives_with_powers: empty input"
);
assert!(
q <= 2,
"pair_block_radial_with_j_second_derivatives_with_powers: q must be in {{0,1,2}}"
);
let d = eta.len();
powers.assert_dim(d);
let (big_r_check, _, _, _, _) = aniso_invariants_with_powers(powers, r);
let analytic_first_ok = big_r_check > 0.0;
if big_r_check == 0.0
&& let Some(bundle) = analytic_self_pair_bundle(q, m, s, kappa, eta)
{
return bundle;
}
assert!(
analytic_first_ok,
"pair_block_radial_with_j_second_derivatives: zero lag has no finite analytic self-pair for q={q} d={d} m={m} s={s}"
);
let mut d_eta = vec![0.0_f64; d];
let mut d2_eta = vec![vec![0.0_f64; d]; d];
let mut d2_eta_kappa = vec![0.0_f64; d];
let max_order_h = (2 * q + 2).min(6);
let (big_r, s1, s2, u1, u2, dr_de, ds1_de, ds2_de, du1_de, du2_de) =
aniso_invariants_eta_jacobian_with_powers(eta, r, powers);
let fr = radial_derivatives_of_isotropic_duchon(d, m, (s) as f64, kappa, big_r, max_order_h);
let (g, g_r, g_s1, g_s2, g_u1, g_u2) = radial_g_q_partials(q, big_r, s1, s2, u1, u2, &fr);
let big_j = eta.iter().sum::<f64>().exp();
let value = big_j * g;
for l in 0..d {
let bare_d_eta_g = g_r * dr_de[l]
+ g_s1 * ds1_de[l]
+ g_s2 * ds2_de[l]
+ g_u1 * du1_de[l]
+ g_u2 * du2_de[l];
d_eta[l] = big_j * (g + bare_d_eta_g);
}
let dfr = if s != 0 && kappa != 0.0 {
let max_order = 2 * q + 1;
Some(radial_derivatives_of_isotropic_duchon_kappa_partial(
d, m, s, kappa, big_r, max_order,
))
} else {
None
};
let d_kappa = if let Some(dfr) = &dfr {
let (dg, _, _, _, _, _) = radial_g_q_partials(q, big_r, s1, s2, u1, u2, dfr);
big_j * dg
} else {
0.0
};
let d2_kappa = if s != 0 && kappa != 0.0 {
let max_order = 2 * q + 1;
let ddfr =
radial_derivatives_of_isotropic_duchon_kappa_partial2(d, m, s, kappa, big_r, max_order);
let (ddg, _, _, _, _, _) = radial_g_q_partials(q, big_r, s1, s2, u1, u2, &ddfr);
big_j * ddg
} else {
0.0
};
let (
g_rr,
g_r_s1,
g_r_s2,
g_r_u1,
g_r_u2,
g_s1s1,
_g_s1s2,
g_s1u1,
_g_s1u2,
_g_s2s2,
_g_s2u1,
_g_s2u2,
g_u1u1,
_g_u1u2,
_g_u2u2,
) = radial_g_q_hessian(q, big_r, s1, s2, u1, u2, &fr);
let bare_d_eta_g: Vec<f64> = (0..d)
.map(|l| {
g_r * dr_de[l]
+ g_s1 * ds1_de[l]
+ g_s2 * ds2_de[l]
+ g_u1 * du1_de[l]
+ g_u2 * du2_de[l]
})
.collect();
for k in 0..d {
for l in 0..d {
let dr_k = dr_de[k];
let dr_l = dr_de[l];
let ds1_k = ds1_de[k];
let ds1_l = ds1_de[l];
let ds2_k = ds2_de[k];
let ds2_l = ds2_de[l];
let du1_k = du1_de[k];
let du1_l = du1_de[l];
let du2_k = du2_de[k];
let du2_l = du2_de[l];
let mut hess_term = g_rr * dr_k * dr_l
+ g_r_s1 * (dr_k * ds1_l + ds1_k * dr_l)
+ g_r_s2 * (dr_k * ds2_l + ds2_k * dr_l)
+ g_r_u1 * (dr_k * du1_l + du1_k * dr_l)
+ g_r_u2 * (dr_k * du2_l + du2_k * dr_l);
hess_term += g_s1s1 * ds1_k * ds1_l
+ g_s1u1 * (ds1_k * du1_l + du1_k * ds1_l)
+ g_u1u1 * du1_k * du1_l;
let kron = if k == l { 1.0 } else { 0.0 };
let b_l_v = powers.b[l];
let b_l_sq_v = powers.b2[l];
let b_l_cu_v = powers.b3[l];
let r_l_sq_v = r[l] * r[l];
let b_k_v = powers.b[k];
let r_k_sq_v = r[k] * r[k];
let d2s1 = if k == l { 4.0 * b_l_v } else { 0.0 };
let d2s2 = if k == l { 16.0 * b_l_sq_v } else { 0.0 };
let d2u1 = if k == l {
16.0 * b_l_sq_v * r_l_sq_v
} else {
0.0
};
let d2u2 = if k == l {
36.0 * b_l_cu_v * r_l_sq_v
} else {
0.0
};
let d2r = {
let r_inv = if big_r > 0.0 { 1.0 / big_r } else { 0.0 };
let r_inv_cu = r_inv * r_inv * r_inv;
kron * 2.0 * b_l_v * r_l_sq_v * r_inv
- b_k_v * b_l_v * r_k_sq_v * r_l_sq_v * r_inv_cu
};
let inv_term = g_r * d2r + g_s1 * d2s1 + g_s2 * d2s2 + g_u1 * d2u1 + g_u2 * d2u2;
let bare_d2 = hess_term + inv_term;
d2_eta[k][l] = big_j * (g + bare_d_eta_g[k] + bare_d_eta_g[l] + bare_d2);
}
}
if let Some(dfr) = &dfr {
let (dg, dg_r, dg_s1, dg_s2, dg_u1, dg_u2) =
radial_g_q_partials(q, big_r, s1, s2, u1, u2, dfr);
for l in 0..d {
let bare_cross = dg_r * dr_de[l]
+ dg_s1 * ds1_de[l]
+ dg_s2 * ds2_de[l]
+ dg_u1 * du1_de[l]
+ dg_u2 * du2_de[l];
d2_eta_kappa[l] = big_j * (dg + bare_cross);
}
}
PairBlockBundle {
value,
d_eta,
d_kappa,
d2_eta,
d2_eta_kappa,
d2_kappa,
}
}
pub fn psi_first_derivative(
q: usize,
m: usize,
s: usize,
kappa: f64,
eta: &[f64],
r: &[f64],
k: usize,
) -> f64 {
assert!(
k < eta.len(),
"psi_first_derivative: axis index out of range"
);
let bundle = pair_block_radial_with_j_second_derivatives(q, m, s, kappa, eta, r);
let big_j = eta.iter().sum::<f64>().exp();
(bundle.d_eta[k] - bundle.value) / big_j
}
pub fn psi_second_derivative(
q: usize,
m: usize,
s: usize,
kappa: f64,
eta: &[f64],
r: &[f64],
k: usize,
l: usize,
) -> f64 {
assert!(
k < eta.len() && l < eta.len(),
"psi_second_derivative: axis index out of range"
);
let bundle = pair_block_radial_with_j_second_derivatives(q, m, s, kappa, eta, r);
let big_j = eta.iter().sum::<f64>().exp();
(bundle.d2_eta[k][l] - bundle.d_eta[k] - bundle.d_eta[l] + bundle.value) / big_j
}
pub fn kappa_first_derivative(
q: usize,
m: usize,
s: usize,
kappa: f64,
eta: &[f64],
r: &[f64],
) -> f64 {
let bundle = pair_block_radial_with_j_second_derivatives(q, m, s, kappa, eta, r);
bundle.d_kappa / eta.iter().sum::<f64>().exp()
}
pub fn kappa_second_derivative(
q: usize,
m: usize,
s: usize,
kappa: f64,
eta: &[f64],
r: &[f64],
) -> f64 {
let bundle = pair_block_radial_with_j_second_derivatives(q, m, s, kappa, eta, r);
bundle.d2_kappa / eta.iter().sum::<f64>().exp()
}
pub fn psi_kappa_mixed_derivative(
q: usize,
m: usize,
s: usize,
kappa: f64,
eta: &[f64],
r: &[f64],
k: usize,
) -> f64 {
assert!(
k < eta.len(),
"psi_kappa_mixed_derivative: axis index out of range"
);
let bundle = pair_block_radial_with_j_second_derivatives(q, m, s, kappa, eta, r);
let big_j = eta.iter().sum::<f64>().exp();
(bundle.d2_eta_kappa[k] - bundle.d_kappa) / big_j
}
#[cfg(test)]
mod tests {
use super::{bessel_k, bessel_k_half_integer, gamma_fn};
pub(crate) fn bessel_i_series(nu: f64, x: f64) -> f64 {
let half_x = 0.5 * x;
let half_x_sq = half_x * half_x;
let prefix = (nu * half_x.ln()).exp();
let mut term = 1.0 / gamma_fn(nu + 1.0);
let mut sum = term;
for k in 1..200 {
term *= half_x_sq / (k as f64 * (nu + k as f64));
sum += term;
if term.abs() < 1e-18 * sum.abs() {
break;
}
}
prefix * sum
}
pub(crate) fn assert_relative_close(actual: f64, expected: f64, rel_tol: f64) {
let scale = expected.abs();
let diff = (actual - expected).abs();
assert!(
diff <= rel_tol * scale,
"actual={actual} expected={expected} diff={diff} rel_tol={rel_tol}"
);
}
#[test]
pub(crate) fn bessel_k_matches_classic_k0_k1_values() {
assert_relative_close(bessel_k(0.0, 1.0), 0.421_024_438_240_708_34, 1e-12);
assert_relative_close(bessel_k(1.0, 1.0), 0.601_907_230_197_234_6, 1e-12);
}
#[test]
pub(crate) fn bessel_k_large_order_at_moderate_x_matches_reference() {
assert_relative_close(bessel_k(10.0, 3.0), 2_459.6, 1e-3);
}
#[test]
pub(crate) fn bessel_k_half_integer_formula_and_nearby_orders_are_continuous() {
for x in [0.5, 1.5, 3.0, 10.0] {
let closed_form = bessel_k_half_integer(2, x);
assert_relative_close(bessel_k(2.5, x), closed_form, 1e-14);
assert_relative_close(bessel_k(2.5 - 1e-9, x), closed_form, 1e-6);
assert_relative_close(bessel_k(2.5 + 1e-9, x), closed_form, 1e-6);
}
}
#[test]
pub(crate) fn bessel_k_satisfies_order_recurrence() {
for nu in [0.3, 1.7, 6.2, 11.4] {
for x in [0.5, 1.9, 2.1, 3.0, 8.0, 25.0] {
let k_next = bessel_k(nu + 1.0, x);
let residual = k_next - bessel_k(nu - 1.0, x) - (2.0 * nu / x) * bessel_k(nu, x);
assert!(
residual.abs() <= 1e-10 * k_next.abs(),
"nu={nu} x={x} residual={residual} k_next={k_next}"
);
}
}
}
#[test]
pub(crate) fn bessel_i_k_wronskian_holds_for_small_x() {
for nu in [0.0, 0.3, 1.7, 6.2] {
for x in [0.5, 1.0, 1.9, 2.0] {
let lhs = bessel_i_series(nu, x) * bessel_k(nu + 1.0, x)
+ bessel_i_series(nu + 1.0, x) * bessel_k(nu, x);
let expected = 1.0 / x;
assert_relative_close(lhs, expected, 1e-10);
}
}
}
#[test]
pub(crate) fn bessel_k_is_continuous_across_x_equals_two_dispatch() {
for nu in [0.3, 4.6] {
let left = bessel_k(nu, 2.0 - 1e-9);
let right = bessel_k(nu, 2.0 + 1e-9);
let diff = (left - right).abs();
let scale = left.abs().max(right.abs()).max(1.0);
assert!(
diff <= 1e-8 * scale,
"nu={nu} left={left} right={right} diff={diff}"
);
}
}
}