use super::*;
pub fn build_duchon_collocation_operator_matrices(
centers: ArrayView2<'_, f64>,
collocationweights: Option<ArrayView1<'_, f64>>,
length_scale: Option<f64>,
power: f64,
nullspace_order: DuchonNullspaceOrder,
aniso_log_scales: Option<&[f64]>,
identifiability_transform: Option<ArrayView2<'_, f64>>,
max_operator_derivative_order: usize,
) -> Result<CollocationOperatorMatrices, BasisError> {
let mut workspace = BasisWorkspace::default();
build_duchon_collocation_operator_matriceswithworkspace(
centers,
centers,
collocationweights,
length_scale,
power,
nullspace_order,
aniso_log_scales,
identifiability_transform,
max_operator_derivative_order,
&mut workspace,
)
}
pub fn build_duchon_operator_penalty_matrices(
centers: ArrayView2<'_, f64>,
collocationweights: Option<ArrayView1<'_, f64>>,
length_scale: Option<f64>,
power: f64,
nullspace_order: DuchonNullspaceOrder,
aniso_log_scales: Option<&[f64]>,
identifiability_transform: Option<ArrayView2<'_, f64>>,
) -> Result<DuchonOperatorPenaltyMatrices, BasisError> {
let ops = build_duchon_collocation_operator_matrices(
centers,
collocationweights,
length_scale,
power,
nullspace_order,
aniso_log_scales,
identifiability_transform,
2,
)?;
let (mass, _) = normalize_penalty(&symmetrize(&fast_ata(&ops.d0)));
let (tension, _) = normalize_penalty(&symmetrize(&fast_ata(&ops.d1)));
let (stiffness, _) = normalize_penalty(&symmetrize(&fast_ata(&ops.d2)));
Ok(DuchonOperatorPenaltyMatrices {
mass,
tension,
stiffness,
})
}
pub fn build_thin_plate_penalty_matrix(
centers: ArrayView2<'_, f64>,
length_scale: f64,
) -> Result<ThinPlatePenaltyMatrix, BasisError> {
let mut workspace = BasisWorkspace::default();
let kernel_transform = thin_plate_kernel_constraint_nullspace(centers, &mut workspace.cache)?;
let (penalty, _) =
build_thin_plate_penalty_matrices(centers, length_scale, &kernel_transform, false)?;
let (penalty, _) = normalize_penalty(&penalty);
Ok(ThinPlatePenaltyMatrix { penalty })
}
pub fn build_duchon_collocation_operator_matriceswithworkspace(
centers: ArrayView2<'_, f64>,
collocation_points: ArrayView2<'_, f64>,
collocationweights: Option<ArrayView1<'_, f64>>,
length_scale: Option<f64>,
power: f64,
nullspace_order: DuchonNullspaceOrder,
aniso_log_scales: Option<&[f64]>,
identifiability_transform: Option<ArrayView2<'_, f64>>,
max_operator_derivative_order: usize,
workspace: &mut BasisWorkspace,
) -> Result<CollocationOperatorMatrices, BasisError> {
let nullspace_order = duchon_effective_nullspace_order(centers, nullspace_order);
let p_order = duchon_p_from_nullspace_order(nullspace_order);
let s_order: f64 = power;
let p_colloc = collocation_points.nrows();
let n_basis = centers.nrows();
let dim = centers.ncols();
if collocation_points.ncols() != dim {
crate::bail_dim_basis!(
"collocation points dim {} != centers dim {dim}",
collocation_points.ncols()
);
}
validate_duchon_collocation_orders(
length_scale,
p_order,
s_order,
dim,
max_operator_derivative_order,
)?;
if let Some(eta) = aniso_log_scales
&& eta.len() != dim
{
crate::bail_dim_basis!(
"Duchon anisotropy dimension mismatch: got {}, expected {dim}",
eta.len()
);
}
let coeffs = length_scale.map(|scale| {
let s_int = duchon_power_to_usize(s_order);
duchon_partial_fraction_coeffs(p_order, s_int, 1.0 / scale.max(1e-300))
});
let metric_weights: Option<Vec<f64>> = aniso_log_scales.map(centered_aniso_metric_weights);
let row_scales = if let Some(w) = collocationweights {
if w.len() != p_colloc {
crate::bail_dim_basis!(
"collocation weight length mismatch: got {}, expected {p_colloc}",
w.len()
);
}
let mut out = Vec::with_capacity(p_colloc);
for &wk in w {
if !wk.is_finite() || wk < 0.0 {
crate::bail_invalid_basis!(
"collocation weights must be finite and non-negative; got {wk}"
);
}
out.push(wk.sqrt());
}
out
} else {
vec![1.0; p_colloc]
};
let z = kernel_constraint_nullspace(centers, nullspace_order, &mut workspace.cache)?;
let build_d1 = max_operator_derivative_order >= 1;
let build_d2 = max_operator_derivative_order >= 2;
let mut d0_raw = Array2::<f64>::zeros((p_colloc, n_basis));
let mut d1_raw = Array2::<f64>::zeros((if build_d1 { p_colloc * dim } else { 0 }, n_basis));
let mut d2_raw =
Array2::<f64>::zeros((if build_d2 { p_colloc * dim * dim } else { 0 }, n_basis));
const R_EPS: f64 = 1e-10;
for i in 0..p_colloc {
let scale_i = row_scales[i];
for j in 0..n_basis {
let r = if let Some(eta) = aniso_log_scales {
let row_i: Vec<f64> = (0..dim).map(|a| collocation_points[[i, a]]).collect();
let row_j: Vec<f64> = (0..dim).map(|a| centers[[j, a]]).collect();
aniso_distance(&row_i, &row_j, eta)
} else {
stable_euclidean_norm(
(0..dim).map(|axis| collocation_points[[i, axis]] - centers[[j, axis]]),
)
};
let r = r.max(R_EPS);
let (phi, q, t) = if let (Some(length_scale), Some(coeffs)) =
(length_scale, coeffs.as_ref())
{
let jets =
duchon_radial_jets(r, length_scale, p_order, s_order as usize, dim, coeffs)?;
(jets.phi, jets.q, jets.t)
} else {
let (phi, phi_r, phi_rr) = duchon_kernel_radial_triplet(
r,
length_scale,
p_order,
s_order,
dim,
coeffs.as_ref(),
)?;
let q = if r > R_EPS { phi_r / r } else { phi_rr };
let t = if r > R_EPS {
(phi_rr - q) / (r * r)
} else {
0.0
};
(phi, q, t)
};
if !phi.is_finite() || !q.is_finite() || !t.is_finite() {
crate::bail_invalid_basis!(
"non-finite Duchon collocation operator derivative at (colloc {i}, center {j}), r={r}"
);
}
d0_raw[[i, j]] = scale_i * phi;
if build_d2 {
for axis_a in 0..dim {
let h_a = collocation_points[[i, axis_a]] - centers[[j, axis_a]];
let w_a = metric_weights
.as_ref()
.map(|weights| weights[axis_a])
.unwrap_or(1.0);
for axis_b in 0..dim {
let h_b = collocation_points[[i, axis_b]] - centers[[j, axis_b]];
let w_b = metric_weights
.as_ref()
.map(|weights| weights[axis_b])
.unwrap_or(1.0);
let diagonal = if axis_a == axis_b { q * w_a } else { 0.0 };
let mixed = if r > R_EPS {
t * w_a * h_a * w_b * h_b
} else {
0.0
};
let value = diagonal + mixed;
let row_i = (i * dim + axis_a) * dim + axis_b;
d2_raw[[row_i, j]] = scale_i * value;
}
}
}
if build_d1 && r > R_EPS {
for axis in 0..dim {
let delta = collocation_points[[i, axis]] - centers[[j, axis]];
let axis_scale = metric_weights
.as_ref()
.map(|weights| weights[axis])
.unwrap_or(1.0);
d1_raw[[i * dim + axis, j]] = scale_i * q * axis_scale * delta;
}
}
}
}
let d0_kernel = fast_ab(&d0_raw, &z);
let poly = polynomial_block_from_order(centers, nullspace_order);
let kernel_cols = d0_kernel.ncols();
let poly_cols = poly.ncols();
let total_cols = kernel_cols + poly_cols;
let mut d0 = Array2::<f64>::zeros((p_colloc, total_cols));
d0.slice_mut(s![.., 0..kernel_cols]).assign(&d0_kernel);
let mut d1 = Array2::<f64>::zeros((if build_d1 { p_colloc * dim } else { 0 }, total_cols));
if build_d1 {
d1.slice_mut(s![.., 0..kernel_cols])
.assign(&fast_ab(&d1_raw, &z));
}
let mut d2 =
Array2::<f64>::zeros((if build_d2 { p_colloc * dim * dim } else { 0 }, total_cols));
if build_d2 {
d2.slice_mut(s![.., 0..kernel_cols])
.assign(&fast_ab(&d2_raw, &z));
}
if let Some(z) = identifiability_transform {
let z = z.to_owned();
d0 = fast_ab(&d0, &z);
d1 = fast_ab(&d1, &z);
d2 = fast_ab(&d2, &z);
}
Ok(CollocationOperatorMatrices {
d0,
d1,
d2,
collocation_points: collocation_points.to_owned(),
kernel_nullspace_transform: Some(z),
polynomial_block_cols: poly_cols,
})
}
#[inline(always)]
pub(crate) fn bessel_k0_stable(x: f64) -> f64 {
let x_pos = x.max(1e-300);
if x_pos <= 2.0 {
return bessel_k0_small_series(x_pos);
}
let y = 2.0 / x_pos;
(-x_pos).exp() / x_pos.sqrt()
* (1.253_314_14
+ y * (-0.078_323_58
+ y * (0.021_895_68
+ y * (-0.010_624_46
+ y * (0.005_878_72 + y * (-0.002_515_40 + y * 0.000_532_08))))))
}
#[inline(always)]
pub(crate) fn bessel_k1_stable(x: f64) -> f64 {
let x_pos = x.max(1e-300);
if x_pos <= 2.0 {
return bessel_k1_small_series(x_pos);
}
let y = 2.0 / x_pos;
(-x_pos).exp() / x_pos.sqrt()
* (1.253_314_14
+ y * (0.234_986_19
+ y * (-0.036_556_20
+ y * (0.015_042_68
+ y * (-0.007_803_53 + y * (0.003_256_14 + y * -0.000_682_45))))))
}
#[inline(always)]
pub(crate) fn bessel_k0_k1_small_series(x: f64) -> (f64, f64) {
const EULER_GAMMA: f64 = 0.577_215_664_901_532_9;
let y = 0.25 * x * x;
let log_half_plus_gamma = 0.5 * y.ln() + EULER_GAMMA;
let mut i0 = 1.0;
let mut i1 = 0.5 * x;
let mut harmonic = 0.0;
let mut y_power_over_fact_sq = 1.0;
let mut k0_series = 0.0;
let mut k0_series_y_derivative_times_y = 0.0;
for k in 1..=256 {
let kf = k as f64;
harmonic += 1.0 / kf;
y_power_over_fact_sq *= y / (kf * kf);
let k0_term = harmonic * y_power_over_fact_sq;
k0_series += k0_term;
k0_series_y_derivative_times_y += kf * k0_term;
i0 += y_power_over_fact_sq;
i1 += 0.5 * x * y_power_over_fact_sq / (kf + 1.0);
if k0_term.abs() <= f64::EPSILON * i0.abs().max(k0_series.abs()).max(1.0) {
break;
}
}
let k0 = -log_half_plus_gamma * i0 + k0_series;
let k1 = i0 / x + log_half_plus_gamma * i1 - (2.0 / x) * k0_series_y_derivative_times_y;
(k0, k1)
}
#[inline(always)]
pub(crate) fn bessel_k0_small_series(x: f64) -> f64 {
bessel_k0_k1_small_series(x).0
}
#[inline(always)]
pub(crate) fn bessel_k1_small_series(x: f64) -> f64 {
bessel_k0_k1_small_series(x).1
}
pub(crate) const DUCHON_DERIVATIVE_R_FLOOR_REL: f64 = 1e-5;
pub(crate) const DUCHON_COLLISION_TAYLOR_REL: f64 = 1e-4;
pub(crate) const RADIAL_PROFILE_MIN_PAIRS: usize = 16_384;
#[inline(always)]
pub(crate) fn duchon_p_from_nullspace_order(order: DuchonNullspaceOrder) -> usize {
match order {
DuchonNullspaceOrder::Zero => 1,
DuchonNullspaceOrder::Linear => 2,
DuchonNullspaceOrder::Degree(degree) => degree + 1,
}
}
pub(crate) fn duchon_effective_nullspace_order(
centers: ArrayView2<'_, f64>,
order: DuchonNullspaceOrder,
) -> DuchonNullspaceOrder {
if order == DuchonNullspaceOrder::Zero {
return order;
}
let mut effective = order;
while effective != DuchonNullspaceOrder::Zero
&& centers.nrows() <= polynomial_block_from_order(centers, effective).ncols()
{
effective = duchon_previous_nullspace_order(effective);
}
if effective != order {
static SEEN: std::sync::OnceLock<
std::sync::Mutex<std::collections::HashSet<(usize, usize, DuchonNullspaceOrder)>>,
> = std::sync::OnceLock::new();
let seen = SEEN.get_or_init(|| std::sync::Mutex::new(std::collections::HashSet::new()));
let key = (centers.nrows(), centers.ncols(), order);
let fresh = seen.lock().map(|mut s| s.insert(key)).unwrap_or(true);
if fresh {
let requested_cols = polynomial_block_from_order(centers, order).ncols();
let effective_cols = polynomial_block_from_order(centers, effective).ncols();
log::warn!(
"Duchon nullspace order={:?} in dim={} with {} centers leaves no radial kernel columns (polynomial_cols={}); degrading to {:?} (polynomial_cols={})",
order,
centers.ncols(),
centers.nrows(),
requested_cols,
effective,
effective_cols
);
}
}
effective
}
#[inline(always)]
pub(crate) fn gamma_lanczos(x: f64) -> f64 {
const G: f64 = 7.0;
const P: [f64; 9] = [
0.999_999_999_999_809_9,
676.520_368_121_885_1,
-1_259.139_216_722_402_8,
771.323_428_777_653_1,
-176.615_029_162_140_6,
12.507_343_278_686_905,
-0.138_571_095_265_720_12,
9.984_369_578_019_571e-6,
1.505_632_735_149_311_6e-7,
];
if x < 0.5 {
let pix = std::f64::consts::PI * x;
return std::f64::consts::PI / (pix.sin() * gamma_lanczos(1.0 - x));
}
let z = x - 1.0;
let mut a = P[0];
for (i, coeff) in P.iter().enumerate().skip(1) {
a += coeff / (z + i as f64);
}
let t = z + G + 0.5;
(2.0 * std::f64::consts::PI).sqrt() * t.powf(z + 0.5) * (-t).exp() * a
}
#[inline(always)]
pub(crate) fn bessel_k_integer_order(n: usize, z: f64) -> f64 {
let zz = z.max(1e-300);
if n == 0 {
return bessel_k0_stable(zz);
}
if n == 1 {
return bessel_k1_stable(zz);
}
let mut km1 = bessel_k0_stable(zz);
let mut k = bessel_k1_stable(zz);
for m in 1..n {
let kp1 = km1 + 2.0 * (m as f64) * k / zz;
km1 = k;
k = kp1;
}
k
}
#[inline(always)]
pub(crate) fn bessel_k_half_integer_order(l: usize, z: f64) -> f64 {
let zz = z.max(1e-300);
let k_half = (std::f64::consts::PI / (2.0 * zz)).sqrt() * (-zz).exp();
if l == 0 {
return k_half;
}
let mut km1 = k_half;
let mut k = k_half * (1.0 + 1.0 / zz);
for m in 1..l {
let nu = m as f64 + 0.5;
let kp1 = km1 + 2.0 * nu * k / zz;
km1 = k;
k = kp1;
}
k
}
#[inline(always)]
pub(crate) fn bessel_k_real_half_integer_or_integer(
nu_abs: f64,
z: f64,
) -> Result<f64, BasisError> {
let two_nu = (2.0 * nu_abs).round();
if (two_nu - 2.0 * nu_abs).abs() > 1e-12 {
crate::bail_invalid_basis!(
"unsupported Bessel-K order ν={nu_abs}; only integer/half-integer orders are supported"
);
}
let two_nu_i = two_nu as i64;
if two_nu_i % 2 == 0 {
let n = (two_nu_i / 2).max(0) as usize;
Ok(bessel_k_integer_order(n, z))
} else {
let l = ((two_nu_i - 1) / 2).max(0) as usize;
Ok(bessel_k_half_integer_order(l, z))
}
}
#[derive(Clone, Copy)]
pub(crate) struct PolyharmonicBlockCoeff {
pub(crate) c: f64,
pub(crate) power: f64,
pub(crate) is_log_case: bool,
}
impl PolyharmonicBlockCoeff {
pub(crate) fn new(m: f64, k_dim: usize) -> Self {
assert!(
m.is_finite() && m > 0.0,
"PolyharmonicBlockCoeff::new: m must be finite and > 0, got {m}"
);
let k_half = 0.5 * k_dim as f64;
let power = 2.0 * m - k_dim as f64;
const LOG_EPS: f64 = 1e-12;
let two_m = 2.0 * m;
let is_log_case = k_dim.is_multiple_of(2) && {
let n_f = (power / 2.0).round();
n_f >= 0.0 && (n_f * 2.0 - power).abs() < LOG_EPS
};
if is_log_case {
let m_int = m.round() as i64;
let m_minus_half_d_plus_one = (m - k_half + 1.0).round() as i64;
let c = polyharmonic_log_sign(m_int as usize, k_dim)
/ (2.0_f64.powi((two_m.round() as i32) - 1)
* std::f64::consts::PI.powf(k_half)
* gamma_lanczos(m)
* gamma_lanczos(m_minus_half_d_plus_one as f64));
Self {
c,
power,
is_log_case: true,
}
} else {
let c = gamma_lanczos(k_half - m)
/ (4.0_f64.powf(m) * std::f64::consts::PI.powf(k_half) * gamma_lanczos(m));
Self {
c,
power,
is_log_case: false,
}
}
}
#[inline(always)]
pub(crate) fn eval(&self, r: f64) -> f64 {
if r <= 0.0 {
return self.origin_limit();
}
if self.is_log_case {
self.c * r.powf(self.power) * r.max(1e-300).ln()
} else {
self.c * r.powf(self.power)
}
}
#[inline(always)]
pub(crate) fn origin_limit(&self) -> f64 {
if self.is_log_case {
log_power_origin_limit(self.c, self.power, 1.0, 0.0)
} else {
log_power_origin_limit(self.c, self.power, 0.0, 1.0)
}
}
}
pub(crate) fn polyharmonic_kernel(r: f64, m: f64, k_dim: usize) -> f64 {
PolyharmonicBlockCoeff::new(m, k_dim).eval(r)
}
#[inline(always)]
pub(crate) fn signed_infinity(sign: f64) -> f64 {
if sign.is_sign_negative() {
f64::NEG_INFINITY
} else {
f64::INFINITY
}
}
#[inline(always)]
pub(crate) fn log_power_origin_limit(
coeff: f64,
exponent: f64,
log_coeff: f64,
pure_coeff: f64,
) -> f64 {
if log_coeff == 0.0 && pure_coeff == 0.0 {
return 0.0;
}
if exponent > 0.0 {
return 0.0;
}
if exponent == 0.0 {
if log_coeff != 0.0 {
signed_infinity(-coeff * log_coeff)
} else {
coeff * pure_coeff
}
} else if log_coeff != 0.0 {
signed_infinity(-coeff * log_coeff)
} else {
signed_infinity(coeff * pure_coeff)
}
}
#[inline(always)]
pub(crate) fn polyharmonic_log_sign(m: usize, k_dim: usize) -> f64 {
assert!(
k_dim.is_multiple_of(2),
"polyharmonic_log_sign requires even kernel dimension: k_dim={k_dim}, m={m}"
);
(-1.0_f64).powi(m as i32 - (k_dim as i32 / 2) + 1)
}
#[inline(always)]
pub(crate) fn duchon_matern_block(
r: f64,
kappa: f64,
n_order: usize,
k_dim: usize,
) -> Result<f64, BasisError> {
let n = n_order as f64;
let k_half = 0.5 * k_dim as f64;
let nu = n - k_half;
let nu_abs = nu.abs();
let c = kappa.powf(k_half - n)
/ ((2.0 * std::f64::consts::PI).powf(k_half) * 2.0_f64.powf(n - 1.0) * gamma_lanczos(n));
if r <= 0.0 {
if nu > 0.0 {
return Ok(c * 2.0_f64.powf(nu - 1.0) * gamma_lanczos(nu) * kappa.powf(-nu));
}
crate::bail_invalid_basis!(
"Duchon Matérn block at r=0 with ν={nu} ≤ 0 is divergent; \
evaluate the hybrid kernel diagonal via the collision routine"
);
}
let z = (kappa * r).max(1e-300);
let k_nu = bessel_k_real_half_integer_or_integer(nu_abs, z)?;
Ok(c * r.powf(nu) * k_nu)
}
#[inline(always)]
pub(crate) fn polyharmonic_kernel_triplet(
r: f64,
m: f64,
k_dim: usize,
) -> Result<(f64, f64, f64), BasisError> {
let (value, first, second, _, _) = polyharmonic_block_jet4(r, m, k_dim)?;
Ok((value, first, second))
}
#[inline(always)]
pub(crate) fn falling_factorial(alpha: f64, order: usize) -> f64 {
(0..order).fold(1.0, |acc, idx| acc * (alpha - idx as f64))
}
#[inline(always)]
pub(crate) fn falling_factorial_derivative(alpha: f64, order: usize) -> f64 {
if order == 0 {
return 0.0;
}
let mut total = 0.0;
for omit in 0..order {
let mut term = 1.0;
for idx in 0..order {
if idx != omit {
term *= alpha - idx as f64;
}
}
total += term;
}
total
}
pub(crate) fn polyharmonic_block_jet4(
r: f64,
m: f64,
k_dim: usize,
) -> Result<(f64, f64, f64, f64, f64), BasisError> {
if !r.is_finite() || r < 0.0 {
crate::bail_invalid_basis!("polyharmonic distance must be finite and non-negative");
}
assert!(
m.is_finite() && m > 0.0,
"polyharmonic_block_jet4: m must be finite and > 0, got {m}"
);
let k_half = 0.5 * k_dim as f64;
let alpha = 2.0 * m - k_dim as f64;
const LOG_EPS: f64 = 1e-12;
let is_log_case = k_dim.is_multiple_of(2) && {
let n_f = (alpha / 2.0).round();
n_f >= 0.0 && (n_f * 2.0 - alpha).abs() < LOG_EPS
};
if is_log_case {
let m_int = m.round() as usize;
let c = polyharmonic_log_sign(m_int, k_dim)
/ (2.0_f64.powi((2 * m_int - 1) as i32)
* std::f64::consts::PI.powf(k_half)
* gamma_lanczos(m)
* gamma_lanczos((m_int - k_dim / 2 + 1) as f64));
let mut out = [0.0; 5];
for d in 0..5 {
let e = alpha - d as f64;
let ff = falling_factorial(alpha, d);
let ff_d = falling_factorial_derivative(alpha, d);
out[d] = if r <= 0.0 {
log_power_origin_limit(c, e, ff, ff_d)
} else {
c * r.powf(e) * (ff * r.ln() + ff_d)
};
}
return Ok((out[0], out[1], out[2], out[3], out[4]));
}
let c = gamma_lanczos(k_half - m)
/ (4.0_f64.powf(m) * std::f64::consts::PI.powf(k_half) * gamma_lanczos(m));
let mut out = [0.0; 5];
for d in 0..5 {
let e = alpha - d as f64;
let ff = falling_factorial(alpha, d);
out[d] = if r <= 0.0 {
log_power_origin_limit(c, e, 0.0, ff)
} else {
c * ff * r.powf(e)
};
}
Ok((out[0], out[1], out[2], out[3], out[4]))
}
#[inline(always)]
pub(crate) fn log_power_family_derivative(
exponent: f64,
log_coeff: f64,
pure_coeff: f64,
) -> (f64, f64, f64) {
(
exponent - 1.0,
exponent * log_coeff,
exponent * pure_coeff + log_coeff,
)
}
#[inline(always)]
pub(crate) fn log_power_family_value(
r: f64,
coeff: f64,
exponent: f64,
log_coeff: f64,
pure_coeff: f64,
) -> f64 {
if r <= 0.0 {
log_power_origin_limit(coeff, exponent, log_coeff, pure_coeff)
} else {
coeff * r.powf(exponent) * (log_coeff * r.ln() + pure_coeff)
}
}
#[inline(always)]
pub(crate) fn duchon_polyharmonic_operator_block_jets(
r: f64,
m: f64,
k_dim: usize,
) -> Result<(f64, f64, f64, f64), BasisError> {
if !r.is_finite() || r < 0.0 {
crate::bail_invalid_basis!("polyharmonic distance must be finite and non-negative");
}
assert!(
m.is_finite() && m > 0.0,
"duchon_polyharmonic_operator_block_jets: m must be finite and > 0, got {m}"
);
let k_half = 0.5 * k_dim as f64;
let alpha = 2.0 * m - k_dim as f64;
const LOG_EPS: f64 = 1e-12;
let is_log_case = k_dim.is_multiple_of(2) && {
let n_f = (alpha / 2.0).round();
n_f >= 0.0 && (n_f * 2.0 - alpha).abs() < LOG_EPS
};
let (c, phi_log_coeff, phi_pure_coeff) = if is_log_case {
let m_int = m.round() as usize;
(
polyharmonic_log_sign(m_int, k_dim)
/ (2.0_f64.powi((2 * m_int - 1) as i32)
* std::f64::consts::PI.powf(k_half)
* gamma_lanczos(m)
* gamma_lanczos((m_int - k_dim / 2 + 1) as f64)),
1.0,
0.0,
)
} else {
(
gamma_lanczos(k_half - m)
/ (4.0_f64.powf(m) * std::f64::consts::PI.powf(k_half) * gamma_lanczos(m)),
0.0,
1.0,
)
};
let (phi_r_exp, phi_r_log, phi_r_pure) =
log_power_family_derivative(alpha, phi_log_coeff, phi_pure_coeff);
let q_exp = phi_r_exp - 1.0;
let q = log_power_family_value(r, c, q_exp, phi_r_log, phi_r_pure);
let (q_r_exp_raw, q_r_log, q_r_pure) =
log_power_family_derivative(q_exp, phi_r_log, phi_r_pure);
let t_exp = q_r_exp_raw - 1.0;
let t = log_power_family_value(r, c, t_exp, q_r_log, q_r_pure);
let (t_r_exp, t_r_log, t_r_pure) = log_power_family_derivative(t_exp, q_r_log, q_r_pure);
let t_r = log_power_family_value(r, c, t_r_exp, t_r_log, t_r_pure);
let (t_rr_exp, t_rr_log, t_rr_pure) = log_power_family_derivative(t_r_exp, t_r_log, t_r_pure);
let t_rr = log_power_family_value(r, c, t_rr_exp, t_rr_log, t_rr_pure);
Ok((q, t, t_r, t_rr))
}
pub(crate) struct BesselKLadder {
pub(crate) values: SmallVec<[f64; 16]>,
pub(crate) half_integer: bool,
}
impl BesselKLadder {
pub(crate) fn build(z: f64, half_integer: bool, max_order_steps: usize) -> Self {
let zz = z.max(1e-300);
let mut values: SmallVec<[f64; 16]> = SmallVec::with_capacity(max_order_steps + 2);
if half_integer {
let k_half = (std::f64::consts::PI / (2.0 * zz)).sqrt() * (-zz).exp();
values.push(k_half);
values.push(k_half * (1.0 + 1.0 / zz));
} else {
values.push(bessel_k0_stable(zz));
values.push(bessel_k1_stable(zz));
}
let base = if half_integer { 0.5 } else { 0.0 };
for i in 1..max_order_steps {
let nu = base + i as f64;
let next = values[i - 1] + 2.0 * nu * values[i] / zz;
values.push(next);
}
Self {
values,
half_integer,
}
}
#[inline]
pub(crate) fn k_abs(&self, order_abs: f64) -> f64 {
let base = if self.half_integer { 0.5 } else { 0.0 };
let idx = (order_abs - base).round() as usize;
self.values[idx]
}
}
pub(crate) fn duchon_matern_family_jets_with_ladder(
r: f64,
kappa: f64,
coeff: f64,
mu: f64,
max_j: usize,
ladder: &BesselKLadder,
out: &mut [f64],
) -> Result<(), BasisError> {
if max_j > 4 || out.len() <= max_j {
crate::bail_invalid_basis!(
"Duchon Matérn-family ladder jets support derivative orders 0..=4 with an output slot per order"
);
}
if r <= 0.0 {
out[..=max_j].fill(0.0);
if mu > 0.0 {
out[0] = coeff * 2.0_f64.powf(mu - 1.0) * gamma_lanczos(mu) * kappa.powf(-mu);
}
return Ok(());
}
let mut terms: SmallVec<[DuchonMaternDerivativeTerm; 16]> =
smallvec![DuchonMaternDerivativeTerm {
coeff,
kappa_power: 0,
r_power: mu,
bessel_order: mu,
}];
for (j, slot) in out.iter_mut().enumerate().take(max_j + 1) {
if j > 0 {
let mut next: SmallVec<[DuchonMaternDerivativeTerm; 16]> =
SmallVec::with_capacity(terms.len() * 2);
for term in &terms {
let stay_coeff = term.coeff * (term.r_power - term.bessel_order);
if stay_coeff != 0.0 {
next.push(DuchonMaternDerivativeTerm {
coeff: stay_coeff,
kappa_power: term.kappa_power,
r_power: term.r_power - 1.0,
bessel_order: term.bessel_order,
});
}
next.push(DuchonMaternDerivativeTerm {
coeff: -term.coeff,
kappa_power: term.kappa_power + 1,
r_power: term.r_power,
bessel_order: term.bessel_order - 1.0,
});
}
terms = next;
}
let mut value = KahanSum::default();
for term in &terms {
if term.coeff == 0.0 {
continue;
}
value.add(
term.coeff
* kappa.powi(term.kappa_power as i32)
* r.powf(term.r_power)
* ladder.k_abs(term.bessel_order.abs()),
);
}
*slot = value.sum();
}
Ok(())
}
pub(crate) fn duchon_matern_block_max_ladder_steps(n_order: usize, k_dim: usize) -> usize {
let nu = n_order as f64 - 0.5 * k_dim as f64;
let candidates = [
(nu - 1.0).abs(),
(nu - 2.0).abs(),
(nu - 3.0).abs(),
(nu - 4.0).abs(),
];
let max_abs = candidates.iter().copied().fold(0.0_f64, f64::max);
max_abs.floor() as usize + 1
}
pub(crate) fn duchon_matern_operator_block_jets_with_ladder(
r: f64,
kappa: f64,
n_order: usize,
k_dim: usize,
ladder: &BesselKLadder,
) -> Result<(f64, f64, f64, f64), BasisError> {
if r <= 0.0 {
return Ok((0.0, 0.0, 0.0, 0.0));
}
let n = n_order as f64;
let k_half = 0.5 * k_dim as f64;
let nu = n - k_half;
let c = kappa.powf(k_half - n)
/ ((2.0 * std::f64::consts::PI).powf(k_half) * 2.0_f64.powf(n - 1.0) * gamma_lanczos(n));
let mut q_out = [0.0_f64; 1];
duchon_matern_family_jets_with_ladder(r, kappa, -c * kappa, nu - 1.0, 0, ladder, &mut q_out)?;
let mut t_out = [0.0_f64; 3];
duchon_matern_family_jets_with_ladder(
r,
kappa,
c * kappa * kappa,
nu - 2.0,
2,
ladder,
&mut t_out,
)?;
Ok((q_out[0], t_out[0], t_out[1], t_out[2]))
}
#[inline(always)]
pub(crate) fn pure_duchon_block_order(p_order: usize, s_order: f64) -> f64 {
p_order as f64 + s_order
}
pub(crate) fn validate_duchon_kernel_orders(
length_scale: Option<f64>,
p_order: usize,
s_order: f64,
k_dim: usize,
) -> Result<(), BasisError> {
if k_dim == 0 {
crate::bail_invalid_basis!("Duchon basis requires at least one covariate dimension");
}
if let Some(scale) = length_scale
&& (!scale.is_finite() || scale <= 0.0)
{
crate::bail_invalid_basis!("Duchon hybrid length_scale must be finite and positive");
}
if !s_order.is_finite() || s_order < 0.0 {
crate::bail_invalid_basis!("Duchon spectral power must be finite and ≥ 0; got s={s_order}");
}
if length_scale.is_none() && p_order < 2 && 2.0 * s_order >= k_dim as f64 {
crate::bail_invalid_basis!(
"pure Duchon requires power < dimension/2 for nullspace degree < {p_order}; got power={s_order}, dimension={k_dim}"
);
}
let spectral_order = 2.0 * (p_order as f64 + s_order);
if spectral_order <= k_dim as f64 {
crate::bail_invalid_basis!(
"Duchon pointwise kernel values require 2*(p+s) > dimension; got 2*(p+s)={spectral_order}, dimension={k_dim}, p={p_order}, s={s_order}"
);
}
Ok(())
}
pub(crate) fn validate_duchon_collocation_orders(
length_scale: Option<f64>,
p_order: usize,
s_order: f64,
k_dim: usize,
max_operator_derivative_order: usize,
) -> Result<(), BasisError> {
validate_duchon_kernel_orders(length_scale, p_order, s_order, k_dim)?;
let spectral_order = 2.0 * (p_order as f64 + s_order);
if max_operator_derivative_order >= 1 && spectral_order <= k_dim as f64 + 1.0 {
crate::bail_invalid_basis!(
"Duchon D1 collocation requires 2*(p+s) > dimension+1; got 2*(p+s)={spectral_order}, dimension={k_dim}, p={p_order}, s={s_order}"
);
}
if max_operator_derivative_order >= 2 && spectral_order <= k_dim as f64 + 2.0 {
crate::bail_invalid_basis!(
"Duchon D2 collocation requires 2*(p+s) > dimension+2; got 2*(p+s)={spectral_order}, dimension={k_dim}, p={p_order}, s={s_order}"
);
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct DuchonPartialFractionCoeffs {
pub(crate) a: Vec<f64>,
pub(crate) b: Vec<f64>,
}
#[inline(always)]
pub(crate) fn duchon_partial_fraction_coeffs(
p_order: usize,
s_order: usize,
kappa: f64,
) -> DuchonPartialFractionCoeffs {
let mut a = vec![0.0_f64; p_order + 1]; let mut b = vec![0.0_f64; s_order + 1]; if s_order == 0 {
if p_order > 0 {
a[p_order] = 1.0;
}
return DuchonPartialFractionCoeffs { a, b };
}
for m in 1..=p_order {
let sign = if (p_order - m).is_multiple_of(2) {
1.0
} else {
-1.0
};
let expo = -2.0 * (s_order + p_order - m) as f64;
let comb = binomial_f64(s_order + p_order - m - 1, p_order - m);
a[m] = sign * kappa.powf(expo) * comb;
}
for n in 1..=s_order {
let sign = if p_order.is_multiple_of(2) { 1.0 } else { -1.0 };
let expo = -2.0 * (p_order + s_order - n) as f64;
let comb = if p_order == 0 && n == s_order {
1.0
} else {
let top = p_order + s_order - n - 1;
binomial_f64(top, s_order - n)
};
b[n] = sign * kappa.powf(expo) * comb;
}
DuchonPartialFractionCoeffs { a, b }
}
fn gauss_legendre_01_64() -> &'static [(f64, f64)] {
use std::sync::OnceLock;
static NODES: OnceLock<Vec<(f64, f64)>> = OnceLock::new();
NODES.get_or_init(|| {
const N: usize = 64;
let nf = N as f64;
let mut nodes: Vec<(f64, f64)> = Vec::with_capacity(N);
let half = N.div_ceil(2);
for i in 0..half {
let mut x = (std::f64::consts::PI * (i as f64 + 0.75) / (nf + 0.5)).cos();
let mut dp = 0.0_f64;
for _ in 0..100 {
let mut p0 = 1.0_f64;
let mut p1 = x;
for k in 2..=N {
let kf = k as f64;
let p2 = ((2.0 * kf - 1.0) * x * p1 - (kf - 1.0) * p0) / kf;
p0 = p1;
p1 = p2;
}
dp = nf * (x * p1 - p0) / (x * x - 1.0);
let dx = p1 / dp;
x -= dx;
if dx.abs() <= 1e-16 * x.abs().max(1.0) {
break;
}
}
let w = 2.0 / ((1.0 - x * x) * dp * dp);
nodes.push((x, w));
if x.abs() > 1e-300 {
nodes.push((-x, w));
}
}
nodes.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
nodes
.into_iter()
.map(|(x, w)| (0.5 * (x + 1.0), 0.5 * w))
.collect()
})
}
pub(crate) fn duchon_hybrid_kernel_stable_integral(
r: f64,
kappa: f64,
p_order: usize,
s_order: usize,
k_dim: usize,
) -> Result<f64, BasisError> {
assert!(
duchon_hybrid_stable_integral_applies(p_order, s_order, k_dim),
"duchon_hybrid_kernel_stable_integral precondition violated: 2(p+s) > d and 2p < d required (p={p_order}, s={s_order}, d={k_dim})"
);
let p = p_order as f64;
let s = s_order as f64;
let half_d = 0.5 * k_dim as f64;
let b = p + s - half_d;
let pref = (4.0 * std::f64::consts::PI).powf(-half_d) / (gamma_lanczos(p) * gamma_lanczos(s));
if r == 0.0 {
let beta = gamma_lanczos(s - b) * gamma_lanczos(p) / gamma_lanczos(s - b + p);
let value = pref * gamma_lanczos(b) * kappa.powf(-2.0 * b) * beta;
if !value.is_finite() {
crate::bail_invalid_basis!(
"non-finite Duchon hybrid diagonal (stable form) for p={p_order}, s={s_order}, d={k_dim}"
);
}
return Ok(value);
}
let mut acc = KahanSum::default();
for &(w, weight) in gauss_legendre_01_64() {
let sqrt_w = w.sqrt();
let z = (kappa * r * sqrt_w).max(1e-300);
let k_b = bessel_k_real_half_integer_or_integer(b.abs(), z)?;
let smooth = 2.0 * (r / (2.0 * kappa * sqrt_w)).powf(b) * k_b;
let factor = (1.0 - w).powf(p - 1.0) * w.powf(s - 1.0) * smooth;
acc.add(weight * factor);
}
let value = pref * acc.sum();
if !value.is_finite() {
crate::bail_invalid_basis!(
"non-finite Duchon hybrid value (stable form) at r={r}, p={p_order}, s={s_order}, d={k_dim}"
);
}
Ok(value)
}
pub(crate) fn duchon_hybrid_operator_stable_integral(
r: f64,
kappa: f64,
p_order: usize,
s_order: usize,
k_dim: usize,
) -> Result<DuchonRegularizedOperatorCore, BasisError> {
assert!(
duchon_hybrid_stable_integral_applies(p_order, s_order, k_dim),
"duchon_hybrid_operator_stable_integral precondition violated: 2(p+s) > d and 2p < d required (p={p_order}, s={s_order}, d={k_dim})"
);
assert!(
r > 0.0 && r.is_finite(),
"duchon_hybrid_operator_stable_integral requires r > 0, got r={r}"
);
let p = p_order as f64;
let s = s_order as f64;
let half_d = 0.5 * k_dim as f64;
let b = p + s - half_d;
let pref = (4.0 * std::f64::consts::PI).powf(-half_d) / (gamma_lanczos(p) * gamma_lanczos(s));
let mut d1 = KahanSum::default();
let mut d2 = KahanSum::default();
let mut d3 = KahanSum::default();
let mut d4 = KahanSum::default();
for &(w, weight) in gauss_legendre_01_64() {
let sqrt_w = w.sqrt();
let c = (kappa * sqrt_w).max(1e-300);
let z = (c * r).max(1e-300);
let a0 = 2.0 * (2.0 * c).powf(-b);
let mut terms: Vec<(f64, f64, i32)> = vec![(a0, b, 0)];
let bessel = |j: i32| -> Result<f64, BasisError> {
bessel_k_real_half_integer_or_integer((b + j as f64).abs(), z)
};
let evaluate = |terms: &Vec<(f64, f64, i32)>| -> Result<f64, BasisError> {
let mut acc = KahanSum::default();
for &(c0, a, j) in terms {
if c0 == 0.0 {
continue;
}
acc.add(c0 * r.powf(a) * bessel(j)?);
}
Ok(acc.sum())
};
let mut slice_derivs = [0.0_f64; 4];
for slot in slice_derivs.iter_mut() {
let mut next: Vec<(f64, f64, i32)> = Vec::with_capacity(terms.len() * 3);
for &(c0, a, j) in &terms {
if c0 == 0.0 {
continue;
}
if a != 0.0 {
next.push((c0 * a, a - 1.0, j));
}
let half = -c0 * c * 0.5;
next.push((half, a, j - 1));
next.push((half, a, j + 1));
}
terms = next;
*slot = evaluate(&terms)?;
}
d1.add(weight * (1.0 - w).powf(p - 1.0) * w.powf(s - 1.0) * slice_derivs[0]);
d2.add(weight * (1.0 - w).powf(p - 1.0) * w.powf(s - 1.0) * slice_derivs[1]);
d3.add(weight * (1.0 - w).powf(p - 1.0) * w.powf(s - 1.0) * slice_derivs[2]);
d4.add(weight * (1.0 - w).powf(p - 1.0) * w.powf(s - 1.0) * slice_derivs[3]);
}
let phi1 = pref * d1.sum();
let phi2 = pref * d2.sum();
let phi3 = pref * d3.sum();
let phi4 = pref * d4.sum();
if !(phi1.is_finite() && phi2.is_finite() && phi3.is_finite() && phi4.is_finite()) {
crate::bail_invalid_basis!(
"non-finite Duchon hybrid operator (stable form) at r={r}, p={p_order}, s={s_order}, d={k_dim}"
);
}
let inv_r = 1.0 / r;
let q = phi1 * inv_r;
let q_r = phi2 * inv_r - phi1 * inv_r * inv_r;
let q_rr = phi3 * inv_r - 2.0 * phi2 * inv_r * inv_r + 2.0 * phi1 * inv_r * inv_r * inv_r;
let q_rrr = phi4 * inv_r - 3.0 * phi3 * inv_r * inv_r + 6.0 * phi2 * inv_r * inv_r * inv_r
- 6.0 * phi1 * inv_r * inv_r * inv_r * inv_r;
let t = q_r * inv_r;
let t_r = q_rr * inv_r - q_r * inv_r * inv_r;
let t_rr = q_rrr * inv_r - 2.0 * q_rr * inv_r * inv_r + 2.0 * q_r * inv_r * inv_r * inv_r;
Ok(DuchonRegularizedOperatorCore {
q,
t,
t_r,
t_rr,
})
}
#[inline]
pub(crate) fn duchon_hybrid_stable_integral_applies(
p_order: usize,
s_order: usize,
k_dim: usize,
) -> bool {
s_order >= 1 && 2 * p_order < k_dim
}
pub(crate) fn duchon_matern_kernel_general_from_distance(
r: f64,
length_scale: Option<f64>,
p_order: usize,
s_order: usize,
k_dim: usize,
coeffs: Option<&DuchonPartialFractionCoeffs>,
) -> Result<f64, BasisError> {
if !r.is_finite() || r < 0.0 {
crate::bail_invalid_basis!("Duchon kernel distance must be finite and non-negative");
}
let Some(length_scale) = length_scale else {
return Ok(polyharmonic_kernel(
r,
pure_duchon_block_order(p_order, s_order as f64),
k_dim,
));
};
if !length_scale.is_finite() || length_scale <= 0.0 {
crate::bail_invalid_basis!("Duchon hybrid length_scale must be finite and positive");
}
let kappa = 1.0 / length_scale;
if duchon_hybrid_stable_integral_applies(p_order, s_order, k_dim) {
return duchon_hybrid_kernel_stable_integral(r, kappa, p_order, s_order, k_dim);
}
let coeffs_local;
let coeffs_ref = if let Some(c) = coeffs {
c
} else {
coeffs_local = duchon_partial_fraction_coeffs(p_order, s_order, kappa);
&coeffs_local
};
let collision_taylor_radius = DUCHON_COLLISION_TAYLOR_REL * length_scale.max(1e-8);
let kernel_finite_at_origin = 2 * (p_order + s_order) > k_dim;
if r <= collision_taylor_radius && kernel_finite_at_origin {
return duchon_hybrid_kernel_near_collision_value(
r,
length_scale,
p_order,
s_order,
k_dim,
coeffs_ref,
);
}
let mut val = KahanSum::default();
for (m, coeff) in coeffs_ref.a.iter().enumerate().skip(1) {
if *coeff == 0.0 {
continue;
}
val.add(coeff * polyharmonic_kernel(r, (m) as f64, k_dim));
}
for (n, coeff) in coeffs_ref.b.iter().enumerate().skip(1) {
if *coeff == 0.0 {
continue;
}
val.add(coeff * duchon_matern_block(r, kappa, n, k_dim)?);
}
Ok(val.sum())
}
pub(crate) fn duchon_hybrid_kernel_collision_value(
length_scale: f64,
p_order: usize,
s_order: usize,
k_dim: usize,
coeffs: &DuchonPartialFractionCoeffs,
) -> Result<f64, BasisError> {
let spectral_order = 2 * (p_order + s_order);
if spectral_order <= k_dim {
crate::bail_invalid_basis!(
"Duchon hybrid diagonal is not finite when 2*(p+s) <= dimension; got 2*(p+s)={spectral_order}, dimension={k_dim}, p={p_order}, s={s_order}"
);
}
let kappa = 1.0 / length_scale.max(1e-300);
let mut pure = KahanSum::default();
let mut log_part = KahanSum::default();
for (m, &a_m) in coeffs.a.iter().enumerate().skip(1) {
if a_m == 0.0 {
continue;
}
let (block_pure, block_log) = duchon_polyharmonic_block_taylor_r2j(m, k_dim, 0);
pure.add(a_m * block_pure);
log_part.add(a_m * block_log);
}
for (n, &b_n) in coeffs.b.iter().enumerate().skip(1) {
if b_n == 0.0 {
continue;
}
let (block_pure, block_log) = duchon_matern_block_taylor_r2j(kappa, n, k_dim, 0);
pure.add(b_n * block_pure);
log_part.add(b_n * block_log);
}
let value = pure.sum();
let log_value = log_part.sum();
if log_value.abs() > 1e-8 * value.abs().max(1e-30) {
crate::bail_invalid_basis!(
"Duchon hybrid diagonal log terms did not cancel: log={log_value:.6e}, value={value:.6e}; p={p_order}, s={s_order}, d={k_dim}"
);
}
if !value.is_finite() {
crate::bail_invalid_basis!(
"non-finite Duchon hybrid diagonal value for p={p_order}, s={s_order}, d={k_dim}"
);
}
Ok(value)
}
pub(crate) fn duchon_hybrid_kernel_near_collision_value(
r: f64,
length_scale: f64,
p_order: usize,
s_order: usize,
k_dim: usize,
coeffs: &DuchonPartialFractionCoeffs,
) -> Result<f64, BasisError> {
let mut value =
duchon_hybrid_kernel_collision_value(length_scale, p_order, s_order, k_dim, coeffs)?;
if r == 0.0 {
return Ok(value);
}
let smoothness_order = 2 * (p_order + s_order);
let r2 = r * r;
if smoothness_order > k_dim + 2 {
let (phi_rr, _, _) =
duchonphi_rr_collision_psi_triplet(length_scale, p_order, s_order, k_dim, coeffs)?;
value += 0.5 * phi_rr * r2;
}
if smoothness_order > k_dim + 4 {
let phi_rrrr = duchon_phi_rrrr_collision(length_scale, p_order, s_order, k_dim, coeffs)?;
value += (1.0 / 24.0) * phi_rrrr * r2 * r2;
}
if smoothness_order > k_dim + 6 {
let phi_rrrrrr =
duchon_phi_rrrrrr_collision(length_scale, p_order, s_order, k_dim, coeffs)?;
value += (1.0 / 720.0) * phi_rrrrrr * r2 * r2 * r2;
}
if !value.is_finite() {
crate::bail_invalid_basis!(
"non-finite Duchon hybrid near-collision value at r={r}, p={p_order}, s={s_order}, d={k_dim}"
);
}
Ok(value)
}
#[inline(always)]
pub(crate) fn stable_euclidean_norm<I>(components: I) -> f64
where
I: IntoIterator<Item = f64>,
{
let mut scale = 0.0_f64;
let mut sumsq = 1.0_f64;
let mut has_nonzero = false;
for component in components {
let abs = component.abs();
if abs == 0.0 {
continue;
}
if !abs.is_finite() {
return f64::INFINITY;
}
if !has_nonzero {
scale = abs;
has_nonzero = true;
continue;
}
if scale < abs {
let ratio = scale / abs;
sumsq = 1.0 + sumsq * ratio * ratio;
scale = abs;
} else {
let ratio = abs / scale;
sumsq += ratio * ratio;
}
}
if has_nonzero {
scale * sumsq.sqrt()
} else {
0.0
}
}
#[inline]
pub(crate) fn centered_aniso_log_scale_mean(eta: &[f64]) -> f64 {
if eta.len() <= 1 {
0.0
} else {
eta.iter().sum::<f64>() / eta.len() as f64
}
}
#[inline]
pub(crate) fn centered_aniso_log_scale(value: f64, mean: f64) -> f64 {
let centered = value - mean;
if centered.is_finite() {
centered.clamp(-50.0, 50.0)
} else if centered > 0.0 {
50.0
} else {
-50.0
}
}
#[inline]
pub(crate) fn aniso_axis_scale(value: f64, mean: f64) -> f64 {
centered_aniso_log_scale(value, mean).exp()
}
#[inline]
pub(crate) fn aniso_metric_weight(value: f64, mean: f64) -> f64 {
(2.0 * centered_aniso_log_scale(value, mean)).exp()
}
pub(crate) fn centered_aniso_metric_weights(eta: &[f64]) -> Vec<f64> {
let mean = centered_aniso_log_scale_mean(eta);
eta.iter()
.map(|&value| aniso_metric_weight(value, mean))
.collect()
}
#[inline]
pub(crate) fn aniso_distance_and_components(
data_row: &[f64],
center: &[f64],
eta: &[f64],
) -> (f64, Vec<f64>) {
assert_eq!(data_row.len(), center.len());
assert_eq!(data_row.len(), eta.len());
let d = data_row.len();
let eta_mean = centered_aniso_log_scale_mean(eta);
let mut s_vec = Vec::with_capacity(d);
let mut scaled_components = Vec::with_capacity(d);
for a in 0..d {
let h_a = data_row[a] - center[a];
let scale_a = aniso_axis_scale(eta[a], eta_mean);
let scaled_h_a = scale_a * h_a;
let s_a = scaled_h_a * scaled_h_a;
scaled_components.push(scaled_h_a);
s_vec.push(s_a);
}
(stable_euclidean_norm(scaled_components), s_vec)
}
#[inline]
pub(crate) fn aniso_distance(data_row: &[f64], center: &[f64], eta: &[f64]) -> f64 {
assert_eq!(data_row.len(), center.len());
assert_eq!(data_row.len(), eta.len());
let eta_mean = centered_aniso_log_scale_mean(eta);
stable_euclidean_norm(
(0..data_row.len()).map(|a| aniso_axis_scale(eta[a], eta_mean) * (data_row[a] - center[a])),
)
}
#[inline(always)]
pub(crate) fn euclidean_distance_rows(
lhs: ArrayView2<'_, f64>,
lhs_row: usize,
rhs: ArrayView2<'_, f64>,
rhs_row: usize,
) -> f64 {
assert_eq!(lhs.ncols(), rhs.ncols());
stable_euclidean_norm((0..lhs.ncols()).map(|axis| lhs[[lhs_row, axis]] - rhs[[rhs_row, axis]]))
}
#[inline(always)]
pub(crate) fn aniso_axis_scales(eta: &[f64]) -> Vec<f64> {
let eta_mean = centered_aniso_log_scale_mean(eta);
eta.iter()
.map(|&value| aniso_axis_scale(value, eta_mean))
.collect()
}
#[inline(always)]
pub(crate) fn aniso_distance_rows_with_scales(
lhs: ArrayView2<'_, f64>,
lhs_row: usize,
rhs: ArrayView2<'_, f64>,
rhs_row: usize,
axis_scales: &[f64],
) -> f64 {
assert_eq!(lhs.ncols(), rhs.ncols());
assert_eq!(lhs.ncols(), axis_scales.len());
stable_euclidean_norm(
(0..lhs.ncols())
.map(|axis| axis_scales[axis] * (lhs[[lhs_row, axis]] - rhs[[rhs_row, axis]])),
)
}
pub(crate) fn fill_symmetric_from_row_kernel<F>(
matrix: &mut Array2<f64>,
kernel: F,
) -> Result<(), BasisError>
where
F: Fn(usize, usize) -> Result<f64, BasisError> + Sync,
{
assert_eq!(matrix.nrows(), matrix.ncols());
let k = matrix.nrows();
matrix
.axis_iter_mut(Axis(0))
.into_par_iter()
.enumerate()
.try_for_each(|(i, mut row)| {
for j in i..k {
row[j] = kernel(i, j)?;
}
Ok::<(), BasisError>(())
})?;
for i in 1..k {
for j in 0..i {
matrix[[i, j]] = matrix[[j, i]];
}
}
Ok(())
}
pub(crate) fn points_in_aniso_y_space(points: ArrayView2<'_, f64>, eta: &[f64]) -> Array2<f64> {
assert_eq!(points.ncols(), eta.len());
let mut y = points.to_owned();
let eta_mean = centered_aniso_log_scale_mean(eta);
let weights: Vec<f64> = eta.iter().map(|&e| aniso_axis_scale(e, eta_mean)).collect();
for a in 0..eta.len() {
let w_a = weights[a];
y.column_mut(a).mapv_inplace(|v| v * w_a);
}
y
}
pub fn knot_cloud_axis_scales(centers: ArrayView2<'_, f64>) -> Vec<f64> {
let k = centers.nrows();
let d = centers.ncols();
if k < 2 || d == 0 {
return vec![1.0; d];
}
let n = k as f64;
let mut scales = Vec::with_capacity(d);
for a in 0..d {
let col = centers.column(a);
let mean = col.sum() / n;
let var = col.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
let sigma = var.sqrt();
let sigma = if sigma < 1e-12 { 1.0 } else { sigma };
scales.push(sigma.clamp(1e-6, 1e6));
}
scales
}
pub fn initial_aniso_contrasts(centers: ArrayView2<'_, f64>) -> Vec<f64> {
let d = centers.ncols();
if d <= 1 {
return Vec::new();
}
let scales = knot_cloud_axis_scales(centers);
let mean_neg_log: f64 = scales.iter().map(|&s| -s.ln()).sum::<f64>() / d as f64;
scales
.iter()
.map(|&scale| -scale.ln() - mean_neg_log)
.collect()
}
pub(crate) fn centered_aniso_contrasts(aniso: Option<&[f64]>) -> Option<Vec<f64>> {
use crate::terms::smooth::center_aniso_log_scales as center;
match aniso {
Some(v) if v.len() > 1 => Some(center(v)),
Some(v) => Some(v.to_vec()),
None => None,
}
}
pub(crate) fn auto_seed_aniso_contrasts(
centers: ArrayView2<'_, f64>,
aniso: Option<&[f64]>,
) -> Option<Vec<f64>> {
use crate::terms::smooth::center_aniso_log_scales as center;
let eta = match aniso {
Some(v) if v.len() > 1 => v,
Some(v) => return Some(v.to_vec()),
None => return None,
};
let all_zero = eta.iter().all(|&e| e == 0.0);
if !all_zero {
return Some(center(eta));
}
let contrasts = initial_aniso_contrasts(centers);
if contrasts.is_empty() {
Some(center(eta))
} else {
Some(center(&contrasts))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AnisoSeedMode {
AutoSeedFromGeometry,
Literal,
}
pub(crate) fn resolve_matern_forward_aniso(
mode: AnisoSeedMode,
centers: ArrayView2<'_, f64>,
aniso: Option<&[f64]>,
) -> Option<Vec<f64>> {
match mode {
AnisoSeedMode::Literal => centered_aniso_contrasts(aniso),
AnisoSeedMode::AutoSeedFromGeometry => auto_seed_aniso_contrasts(centers, aniso),
}
}
pub(crate) fn pairwise_distance_bounds(points: ArrayView2<'_, f64>) -> Option<(f64, f64)> {
let n = points.nrows();
let d = points.ncols();
if n < 2 || d == 0 {
return None;
}
let mut r_min = f64::INFINITY;
let mut r_max = 0.0_f64;
for i in 0..n {
for j in (i + 1)..n {
let r = stable_euclidean_norm((0..d).map(|c| points[[i, c]] - points[[j, c]]));
if r.is_finite() && r > 0.0 {
r_min = r_min.min(r);
r_max = r_max.max(r);
}
}
}
if r_min.is_finite() && r_max.is_finite() && r_min > 0.0 && r_max > 0.0 {
Some((r_min, r_max))
} else {
None
}
}
pub(crate) fn pairwise_distance_bounds_sampled(points: ArrayView2<'_, f64>) -> Option<(f64, f64)> {
const K_CAP: usize = 1024;
let n = points.nrows();
let d = points.ncols();
if n < 2 || d == 0 {
return None;
}
if n <= K_CAP {
return pairwise_distance_bounds(points);
}
let stride = n / K_CAP;
let k = K_CAP; let mut r_min = f64::INFINITY;
let mut r_max = 0.0_f64;
for i_idx in 0..k {
let i = i_idx * stride;
for j_idx in (i_idx + 1)..k {
let j = j_idx * stride;
let r = stable_euclidean_norm((0..d).map(|c| points[[i, c]] - points[[j, c]]));
if r.is_finite() && r > 0.0 {
r_min = r_min.min(r);
r_max = r_max.max(r);
}
}
}
if r_min.is_finite() && r_max.is_finite() && r_min > 0.0 && r_max > 0.0 {
Some((r_min, r_max))
} else {
None
}
}
#[cfg(test)]
mod duchon_hybrid_psd_tests {
use super::*;
use crate::linalg::faer_ndarray::FaerEigh;
use faer::Side;
fn fixture_centers(d: usize, n: usize) -> Array2<f64> {
const BASES: [u64; 24] = [
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83,
89,
];
let mut centers = Array2::<f64>::zeros((n, d));
for i in 0..n {
for axis in 0..d {
let base = BASES[axis % BASES.len()];
let mut f = 1.0_f64;
let mut idx = (i + 1) as u64;
let mut value = 0.0_f64;
while idx > 0 {
f /= base as f64;
value += f * (idx % base) as f64;
idx /= base;
}
centers[[i, axis]] = 2.0 * value - 1.0;
}
}
centers
}
fn lambda_min(matrix: &Array2<f64>) -> f64 {
let sym = symmetrize_penalty(matrix);
let (evals, _) = FaerEigh::eigh(&sym, Side::Lower).expect("symmetric eigendecomposition");
evals.iter().copied().fold(f64::INFINITY, f64::min)
}
#[test]
fn high_dim_hybrid_penalty_is_numerically_psd_1424() {
let d = 16usize;
let (nullspace_order, default_power) = duchon_cubic_default(d);
assert!(matches!(nullspace_order, DuchonNullspaceOrder::Linear));
assert!((default_power - 7.5).abs() < 1e-12, "cubic-default power for d=16 is 7.5");
let power = 7.0_f64;
assert_eq!(duchon_power_to_usize(power), 7);
assert!(duchon_hybrid_stable_integral_applies(
duchon_p_from_nullspace_order(nullspace_order),
duchon_power_to_usize(power),
d,
));
let length_scale = Some(1.0_f64);
let centers = fixture_centers(d, 4 * d);
let mut cache = BasisCacheContext::default();
let z = kernel_constraint_nullspace(centers.view(), nullspace_order, &mut cache)
.expect("constraint null space");
let omega = duchon_constrained_bending_penalty(
centers.view(),
length_scale,
power,
nullspace_order,
None,
&z,
)
.expect("constrained bending penalty assembles for the hybrid fixture");
let (penalty, _scale) = normalize_penalty(&omega);
let lam_min = lambda_min(&penalty);
assert!(
lam_min >= -1e-10,
"gam#1424: (d=16, m=2, s=7) hybrid penalty is not numerically PSD: \
λ_min={lam_min:.6e} (was ≈ −0.26442 with the cancellation-prone \
partial-fraction kernel)"
);
}
#[test]
fn low_dim_hybrid_kernel_values_unchanged_1424() {
let d = 2usize;
let p_order = 2usize; let s_order = 2usize;
let kappa = 1.0_f64;
let length_scale = Some(1.0_f64);
assert!(!duchon_hybrid_stable_integral_applies(p_order, s_order, d));
let coeffs = duchon_partial_fraction_coeffs(p_order, s_order, kappa);
for &r in &[0.25_f64, 0.75, 1.5] {
let mut reference = 0.0_f64;
for (m, &coeff) in coeffs.a.iter().enumerate().skip(1) {
if coeff != 0.0 {
reference += coeff * polyharmonic_kernel(r, m as f64, d);
}
}
for (n, &coeff) in coeffs.b.iter().enumerate().skip(1) {
if coeff != 0.0 {
reference += coeff
* duchon_matern_block(r, kappa, n, d).expect("matern block");
}
}
let got = duchon_matern_kernel_general_from_distance(
r,
length_scale,
p_order,
s_order,
d,
Some(&coeffs),
)
.expect("low-d hybrid kernel value");
assert!(
(got - reference).abs() <= 1e-10,
"low-d hybrid kernel value regressed at r={r}: got {got:.15e}, reference {reference:.15e}"
);
}
}
}