use super::*;
pub(crate) fn duchon_coeff_exponents(p_order: usize, s_order: usize, m_or_n: usize) -> f64 {
-2.0 * (p_order + s_order - m_or_n) as f64
}
#[inline(always)]
pub(crate) fn duchon_scaling_exponent(p_order: usize, s_order: usize, k_dim: usize) -> f64 {
k_dim as f64 - 2.0 * (p_order + s_order) as f64
}
#[derive(Clone, Copy)]
pub(crate) struct DuchonMaternDerivativeTerm {
pub(crate) coeff: f64,
pub(crate) kappa_power: usize,
pub(crate) r_power: f64,
pub(crate) bessel_order: f64,
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct PsiTriplet {
pub(crate) value: f64,
pub(crate) psi: f64,
pub(crate) psi_psi: f64,
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct DuchonRadialCore {
pub(crate) phi: PsiTriplet,
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct DuchonRadialJets {
pub(crate) phi: f64,
pub(crate) phi_r: f64,
pub(crate) phi_rr: f64,
pub(crate) phi_rrr: f64,
pub(crate) q: f64,
pub(crate) q_r: f64,
pub(crate) q_rr: f64,
pub(crate) lap: f64,
pub(crate) lap_r: f64,
pub(crate) lap_rr: f64,
pub(crate) t: f64,
pub(crate) t_r: f64,
pub(crate) t_rr: f64,
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct DuchonRegularizedOperatorCore {
pub(crate) q: f64,
pub(crate) t: f64,
pub(crate) t_r: f64,
pub(crate) t_rr: f64,
}
#[inline(always)]
pub(crate) fn duchon_operator_jets_from_primary_core(
core: DuchonRegularizedOperatorCore,
r: f64,
d: f64,
) -> DuchonRadialJets {
let r2 = r * r;
let mut out = DuchonRadialJets {
q: core.q,
t: core.t,
t_r: core.t_r,
t_rr: core.t_rr,
..DuchonRadialJets::default()
};
out.q_r = r * out.t;
out.q_rr = out.t + r * out.t_r;
out.lap = d * out.q + r2 * out.t;
out.lap_r = (d + 2.0) * r * out.t + r2 * out.t_r;
out.lap_rr = (d + 2.0) * out.t + (d + 4.0) * r * out.t_r + r2 * out.t_rr;
out.phi_r = r * out.q;
out.phi_rr = out.q + r2 * out.t;
out.phi_rrr = 3.0 * r * out.t + r2 * out.t_r;
assert!(
((out.phi_rr - (out.q + r * out.q_r)).abs()) <= 1e-10 * out.phi_rr.abs().max(1.0),
"radial scalar identity failed: phi_rr != q + r*q_r, phi_rr={}, q={}, r={}, q_r={}",
out.phi_rr,
out.q,
r,
out.q_r
);
assert!(
((out.phi_rr - (out.q + r2 * out.t)).abs()) <= 1e-10 * out.phi_rr.abs().max(1.0),
"radial scalar identity failed: phi_rr != q + r2*t, phi_rr={}, q={}, r2={}, t={}",
out.phi_rr,
out.q,
r2,
out.t
);
assert!(
((out.lap - (d * out.q + r2 * out.t)).abs()) <= 1e-10 * out.lap.abs().max(1.0),
"radial scalar identity failed: lap != d*q + r2*t, lap={}, d={}, q={}, r2={}, t={}",
out.lap,
d,
out.q,
r2,
out.t
);
out
}
#[inline(always)]
pub(crate) fn scaled_log_kappa_derivatives(
value: f64,
radial_first: f64,
radialsecond: f64,
exponent: f64,
r: f64,
) -> (f64, f64) {
let first = exponent * value + r * radial_first;
let second = exponent * exponent * value
+ (2.0 * exponent + 1.0) * r * radial_first
+ r * r * radialsecond;
(first, second)
}
#[inline(always)]
pub(crate) fn duchon_q_psi_triplet_from_jets(
jets: &DuchonRadialJets,
p_order: usize,
s_order: usize,
k_dim: usize,
r: f64,
) -> (f64, f64) {
scaled_log_kappa_derivatives(
jets.q,
jets.q_r,
jets.q_rr,
duchon_operator_scaling_exponent(p_order, s_order, k_dim),
r,
)
}
#[inline(always)]
pub(crate) fn duchon_operator_scaling_exponent(
p_order: usize,
s_order: usize,
k_dim: usize,
) -> f64 {
duchon_scaling_exponent(p_order, s_order, k_dim) + 2.0
}
pub(crate) fn duchon_regularized_operator_core(
r_eval: f64,
kappa: f64,
k_dim: usize,
coeffs: &DuchonPartialFractionCoeffs,
) -> Result<DuchonRegularizedOperatorCore, BasisError> {
let mut q_sum = KahanSum::default();
let mut t_sum = KahanSum::default();
let mut t_r_sum = KahanSum::default();
let mut t_rr_sum = KahanSum::default();
for (m, coeff) in coeffs.a.iter().enumerate().skip(1) {
if *coeff == 0.0 {
continue;
}
let (q, t, t_r, t_rr) = duchon_polyharmonic_operator_block_jets(r_eval, m as f64, k_dim)?;
q_sum.add(coeff * q);
t_sum.add(coeff * t);
t_r_sum.add(coeff * t_r);
t_rr_sum.add(coeff * t_rr);
}
let max_ladder_steps = coeffs
.b
.iter()
.enumerate()
.skip(1)
.filter(|(_, coeff)| **coeff != 0.0)
.map(|(n, _)| duchon_matern_block_max_ladder_steps(n, k_dim))
.max();
if let Some(max_ladder_steps) = max_ladder_steps {
let ladder =
BesselKLadder::build(kappa * r_eval, !k_dim.is_multiple_of(2), max_ladder_steps);
for (n, coeff) in coeffs.b.iter().enumerate().skip(1) {
if *coeff == 0.0 {
continue;
}
let (q, t, t_r, t_rr) =
duchon_matern_operator_block_jets_with_ladder(r_eval, kappa, n, k_dim, &ladder)?;
q_sum.add(coeff * q);
t_sum.add(coeff * t);
t_r_sum.add(coeff * t_r);
t_rr_sum.add(coeff * t_rr);
}
}
Ok(DuchonRegularizedOperatorCore {
q: q_sum.sum(),
t: t_sum.sum(),
t_r: t_r_sum.sum(),
t_rr: t_rr_sum.sum(),
})
}
#[inline(always)]
pub(crate) fn duchon_collision_taylor_operator_core(
r: f64,
phi_rr_collision: f64,
t_collision: f64,
t_rr_collision: f64,
) -> DuchonRegularizedOperatorCore {
let r2 = r * r;
let r4 = r2 * r2;
DuchonRegularizedOperatorCore {
q: phi_rr_collision + 0.5 * t_collision * r2 + 0.125 * t_rr_collision * r4,
t: t_collision + 0.5 * t_rr_collision * r2,
t_r: t_rr_collision * r,
t_rr: t_rr_collision,
}
}
pub(crate) fn duchon_radial_jets(
r: f64,
length_scale: f64,
p_order: usize,
s_order: usize,
k_dim: usize,
coeffs: &DuchonPartialFractionCoeffs,
) -> Result<DuchonRadialJets, BasisError> {
let kappa = 1.0 / length_scale.max(1e-300);
let r_floor = DUCHON_DERIVATIVE_R_FLOOR_REL * length_scale.max(1e-8);
let collision_taylor_radius = DUCHON_COLLISION_TAYLOR_REL * length_scale.max(1e-8);
let r_eval = r.max(r_floor);
let d = k_dim as f64;
let phi = duchon_matern_kernel_general_from_distance(
r,
Some(length_scale),
p_order,
s_order,
k_dim,
Some(coeffs),
)?;
if !phi.is_finite() {
crate::bail_invalid_basis!(
"non-finite Duchon radial kernel value at r={r}, length_scale={length_scale}, p={p_order}, s={s_order}, dim={k_dim}"
);
}
let operator_core = if duchon_hybrid_stable_integral_applies(p_order, s_order, k_dim) {
duchon_hybrid_operator_stable_integral(r_eval, kappa, p_order, s_order, k_dim)?
} else {
duchon_regularized_operator_core(r_eval, kappa, k_dim, coeffs)?
};
let generic_jets = duchon_operator_jets_from_primary_core(operator_core, r_eval, d);
let mut out = DuchonRadialJets {
phi,
..generic_jets
};
let smoothness_order = 2 * (p_order + s_order);
let collision_q_exists = smoothness_order > k_dim + 2;
let collision_t_exists = smoothness_order > k_dim + 4;
let collision_t_rr_exists = smoothness_order > k_dim + 6;
if r <= collision_taylor_radius.max(r_floor) && collision_t_exists {
let (analytic_phi_rr, _, _) =
duchonphi_rr_collision_psi_triplet(length_scale, p_order, s_order, k_dim, coeffs)?;
let analytic_t_collision =
duchon_phi_rrrr_collision(length_scale, p_order, s_order, k_dim, coeffs)? / 3.0;
let analytic_t_rr_collision = if collision_t_rr_exists {
duchon_phi_rrrrrr_collision(length_scale, p_order, s_order, k_dim, coeffs)? / 15.0
} else {
0.0
};
let collision_jets = duchon_operator_jets_from_primary_core(
duchon_collision_taylor_operator_core(
r,
analytic_phi_rr,
analytic_t_collision,
analytic_t_rr_collision,
),
r,
d,
);
out = DuchonRadialJets {
phi: out.phi,
..collision_jets
};
} else if r < r_floor && collision_q_exists {
let (analytic_phi_rr, _, _) =
duchonphi_rr_collision_psi_triplet(length_scale, p_order, s_order, k_dim, coeffs)?;
out.phi_r = analytic_phi_rr * r;
out.phi_rr = analytic_phi_rr;
out.q = analytic_phi_rr;
out.q_r = 0.0;
out.lap = d * analytic_phi_rr;
out.lap_r = 0.0;
}
if !out.phi_r.is_finite()
|| !out.phi_rr.is_finite()
|| !out.phi_rrr.is_finite()
|| !out.q.is_finite()
|| !out.q_r.is_finite()
|| !out.q_rr.is_finite()
|| !out.lap.is_finite()
|| !out.lap_r.is_finite()
|| !out.lap_rr.is_finite()
|| !out.t.is_finite()
|| !out.t_r.is_finite()
|| !out.t_rr.is_finite()
{
crate::bail_invalid_basis!(
"non-finite Duchon radial jets at r={r}, length_scale={length_scale}, p={p_order}, s={s_order}, dim={k_dim}"
);
}
Ok(out)
}
pub(crate) fn duchon_radial_core_psi_triplet(
r: f64,
length_scale: f64,
p_order: usize,
s_order: usize,
k_dim: usize,
coeffs: &DuchonPartialFractionCoeffs,
) -> Result<DuchonRadialCore, BasisError> {
let delta = duchon_scaling_exponent(p_order, s_order, k_dim);
let jets = duchon_radial_jets(r, length_scale, p_order, s_order, k_dim, coeffs)?;
let phi = jets.phi;
let (phi_psi, phi_psi_psi) =
scaled_log_kappa_derivatives(phi, jets.phi_r, jets.phi_rr, delta, r);
if r > 1e-10 {
assert!(
((delta * phi + r * jets.phi_r) - phi_psi).abs() < 1e-7_f64.max(1e-7_f64 * phi.abs())
);
return Ok(DuchonRadialCore {
phi: PsiTriplet {
value: phi,
psi: phi_psi,
psi_psi: phi_psi_psi,
},
});
}
Ok(DuchonRadialCore {
phi: PsiTriplet {
value: phi,
psi: phi_psi,
psi_psi: phi_psi_psi,
},
})
}
pub(crate) fn duchonphi_rr_collision_psi_triplet(
length_scale: f64,
p_order: usize,
s_order: usize,
k_dim: usize,
coeffs: &DuchonPartialFractionCoeffs,
) -> Result<(f64, f64, f64), BasisError> {
duchon_phi_even_derivative_collision_psi_triplet(
length_scale,
p_order,
s_order,
k_dim,
coeffs,
1,
)
}
pub(crate) const EULER_MASCHERONI: f64 = 0.577_215_664_901_532_9;
#[inline(always)]
pub(crate) fn digamma_pos_int(n: usize) -> f64 {
assert!(n >= 1, "digamma_pos_int requires n >= 1: n={n}");
let mut h = 0.0_f64;
for j in 1..n {
h += 1.0 / j as f64;
}
-EULER_MASCHERONI + h
}
pub(crate) fn duchon_matern_block_taylor_r2j(
kappa: f64,
n_order: usize,
k_dim: usize,
j: usize,
) -> (f64, f64) {
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));
if k_dim.is_multiple_of(2) {
let nu_int = n_order as i64 - (k_dim as i64) / 2;
duchon_matern_block_taylor_r2j_integer_nu(kappa, c, nu_int, j)
} else {
duchon_matern_block_taylor_r2j_half_integer_nu(kappa, c, nu, j)
}
}
#[inline(always)]
pub(crate) fn psi_power_triplet(value: f64, exponent: f64) -> (f64, f64, f64) {
(value, exponent * value, exponent * exponent * value)
}
#[inline(always)]
pub(crate) fn psi_power_log_triplet(
base: f64,
exponent: f64,
log_kappa_half: f64,
) -> (f64, f64, f64) {
(
base * log_kappa_half,
base * (exponent * log_kappa_half + 1.0),
base * (exponent * exponent * log_kappa_half + 2.0 * exponent),
)
}
#[inline(always)]
pub(crate) fn add_triplet(dst: &mut (f64, f64, f64), inc: (f64, f64, f64)) {
dst.0 += inc.0;
dst.1 += inc.1;
dst.2 += inc.2;
}
pub(crate) fn duchon_matern_block_taylor_r2j_triplet(
kappa: f64,
n_order: usize,
k_dim: usize,
j: usize,
) -> ((f64, f64, f64), (f64, f64, f64)) {
let n = n_order as f64;
let k_half = 0.5 * k_dim as f64;
let nu = n - k_half;
let c_const = 1.0
/ ((2.0 * std::f64::consts::PI).powf(k_half) * 2.0_f64.powf(n - 1.0) * gamma_lanczos(n));
let c_exp = k_half - n;
let mut pure = (0.0, 0.0, 0.0);
let mut log_part = (0.0, 0.0, 0.0);
let log_kappa_half = (0.5 * kappa).ln();
if k_dim.is_multiple_of(2) {
let nu_int = n_order as i64 - (k_dim as i64) / 2;
let mu = nu_int.unsigned_abs() as usize;
let sign_mu = if mu.is_multiple_of(2) { 1.0 } else { -1.0 };
if nu_int >= 0 {
let nu_usize = nu_int as usize;
if j < nu_usize {
let sign = if j.is_multiple_of(2) { 1.0 } else { -1.0 };
let power = 2 * j as i32 - nu_usize as i32;
let coeff = 0.5 * sign * gamma_lanczos((nu_usize - j) as f64)
/ gamma_lanczos((j + 1) as f64)
* 2.0_f64.powi(-power);
let exponent = c_exp + power as f64;
let value = c_const * coeff * kappa.powf(exponent);
add_triplet(&mut pure, psi_power_triplet(value, exponent));
}
if j >= nu_usize {
let k = j - nu_usize;
let inv_fac = 1.0
/ (gamma_lanczos((k + 1) as f64) * gamma_lanczos((nu_usize + k + 1) as f64));
let power = (2 * k + nu_usize) as i32;
let exponent = c_exp + power as f64;
let kp_base = c_const * kappa.powf(exponent) * 2.0_f64.powi(-power);
let log_base = -sign_mu * kp_base * inv_fac;
add_triplet(&mut log_part, psi_power_triplet(log_base, exponent));
add_triplet(
&mut pure,
psi_power_log_triplet(log_base, exponent, log_kappa_half),
);
let psi_sum = digamma_pos_int(k + 1) + digamma_pos_int(nu_usize + k + 1);
let digamma_base = sign_mu * 0.5 * kp_base * inv_fac * psi_sum;
add_triplet(&mut pure, psi_power_triplet(digamma_base, exponent));
}
} else {
let k = j;
let inv_fac =
1.0 / (gamma_lanczos((k + 1) as f64) * gamma_lanczos((mu + k + 1) as f64));
let power = (mu + 2 * k) as i32;
let exponent = c_exp + power as f64;
let kp_base = c_const * kappa.powf(exponent) * 2.0_f64.powi(-power);
let log_base = -sign_mu * kp_base * inv_fac;
add_triplet(&mut log_part, psi_power_triplet(log_base, exponent));
add_triplet(
&mut pure,
psi_power_log_triplet(log_base, exponent, log_kappa_half),
);
let psi_sum = digamma_pos_int(k + 1) + digamma_pos_int(mu + k + 1);
let digamma_base = sign_mu * 0.5 * kp_base * inv_fac * psi_sum;
add_triplet(&mut pure, psi_power_triplet(digamma_base, exponent));
}
} else {
let nu_abs = nu.abs();
let l = (2.0 * nu_abs - 1.0).round().max(0.0) as usize;
let prefactor_const = (std::f64::consts::PI / 2.0).sqrt();
let prefactor_exp = -0.5;
let target = 2 * j;
for i in 0..=l {
let c_i = gamma_lanczos((l + i + 1) as f64)
/ (gamma_lanczos((i + 1) as f64) * gamma_lanczos((l - i + 1) as f64));
let p_f64 = nu - 0.5 - i as f64;
let p_round = p_f64.round() as i64;
if (p_f64 - p_round as f64).abs() > 1e-12 {
continue;
}
let q_needed = target as i64 - p_round;
if q_needed < 0 {
continue;
}
let q = q_needed as usize;
let sign = if q.is_multiple_of(2) { 1.0 } else { -1.0 };
let exponent = c_exp + prefactor_exp - i as f64 + q as f64;
let value = c_const * prefactor_const * c_i * 2.0_f64.powi(-(i as i32)) * sign
/ gamma_lanczos((q + 1) as f64)
* kappa.powf(exponent);
add_triplet(&mut pure, psi_power_triplet(value, exponent));
}
}
(pure, log_part)
}
pub(crate) fn duchon_matern_block_taylor_r2j_integer_nu(
kappa: f64,
c: f64,
nu_int: i64,
j: usize,
) -> (f64, f64) {
let mu = nu_int.unsigned_abs() as usize;
let kappa_half = 0.5 * kappa;
if nu_int >= 0 {
let nu = nu_int as usize;
let mut pure = 0.0;
let mut log_part = 0.0;
if j < nu {
let sign = if j.is_multiple_of(2) { 1.0 } else { -1.0 };
let coeff = sign * gamma_lanczos((nu - j) as f64) / gamma_lanczos((j + 1) as f64)
* kappa_half.powi(2 * j as i32 - nu as i32)
* 0.5;
pure += coeff;
}
if j >= nu {
let k = j - nu;
let inv_fac =
1.0 / (gamma_lanczos((k + 1) as f64) * gamma_lanczos((nu + k + 1) as f64));
let kp = kappa_half.powi(2 * k as i32 + nu as i32);
let sign_mu = if mu.is_multiple_of(2) { 1.0 } else { -1.0 };
log_part += -sign_mu * kp * inv_fac;
let psi_sum = digamma_pos_int(k + 1) + digamma_pos_int(nu + k + 1);
pure += -sign_mu * kp * inv_fac * kappa_half.ln();
pure += sign_mu * 0.5 * kp * inv_fac * psi_sum;
}
(c * pure, c * log_part)
} else {
let k = j;
let inv_fac = 1.0 / (gamma_lanczos((k + 1) as f64) * gamma_lanczos((mu + k + 1) as f64));
let kp = kappa_half.powi(mu as i32 + 2 * k as i32);
let sign_mu = if mu.is_multiple_of(2) { 1.0 } else { -1.0 };
let log_part = -sign_mu * kp * inv_fac;
let psi_sum = digamma_pos_int(k + 1) + digamma_pos_int(mu + k + 1);
let pure =
-sign_mu * kp * inv_fac * kappa_half.ln() + sign_mu * 0.5 * kp * inv_fac * psi_sum;
(c * pure, c * log_part)
}
}
pub(crate) fn duchon_matern_block_taylor_r2j_half_integer_nu(
kappa: f64,
c: f64,
nu: f64,
j: usize,
) -> (f64, f64) {
let nu_abs = nu.abs();
let l = (2.0 * nu_abs - 1.0).round().max(0.0) as usize;
let prefactor = (std::f64::consts::PI / (2.0 * kappa)).sqrt();
let target = 2 * j;
let mut pure = 0.0;
for i in 0..=l {
let c_i = gamma_lanczos((l + i + 1) as f64)
/ (gamma_lanczos((i + 1) as f64) * gamma_lanczos((l - i + 1) as f64));
let inv_2kappa_i = (2.0 * kappa).powi(-(i as i32));
let p_f64 = nu - 0.5 - i as f64;
let p_round = p_f64.round() as i64;
if (p_f64 - p_round as f64).abs() > 1e-12 {
continue;
}
let q_needed = target as i64 - p_round;
if q_needed < 0 {
continue;
}
let q = q_needed as usize;
let exp_coeff = (-kappa).powi(q as i32) / gamma_lanczos((q + 1) as f64);
pure += c_i * inv_2kappa_i * exp_coeff;
}
(c * prefactor * pure, 0.0) }
pub(crate) fn duchon_polyharmonic_block_taylor_r2j(m: usize, k_dim: usize, j: usize) -> (f64, f64) {
let k_half = 0.5 * k_dim as f64;
let alpha = 2 * m as i64 - k_dim as i64;
if alpha != 2 * j as i64 {
return (0.0, 0.0);
}
if k_dim.is_multiple_of(2) && m >= k_dim / 2 {
let c = polyharmonic_log_sign(m, k_dim)
/ (2.0_f64.powi((2 * m - 1) as i32)
* std::f64::consts::PI.powf(k_half)
* gamma_lanczos(m as f64)
* gamma_lanczos((m - k_dim / 2 + 1) as f64));
(0.0, c)
} else {
let c = gamma_lanczos(k_half - m as f64)
/ (4.0_f64.powi(m as i32)
* std::f64::consts::PI.powf(k_half)
* gamma_lanczos(m as f64));
(c, 0.0)
}
}
pub(crate) fn duchon_phi_even_derivative_collision(
length_scale: f64,
p_order: usize,
s_order: usize,
k_dim: usize,
coeffs: &DuchonPartialFractionCoeffs,
j: usize,
) -> Result<f64, BasisError> {
let smoothness_order = 2 * (p_order + s_order);
let required = k_dim + 2 * j;
if smoothness_order <= required {
let min_power = (required / 2 + 1).saturating_sub(p_order);
crate::bail_invalid_basis!(
"Duchon collision derivative phi^({}) requires 2*(p+s) > dimension+{}; got 2*(p+s)={}, dimension={}, p={}, s={}. \
This path needs the {}-order radial-kernel derivative at the origin, which is finite only for a smoother spline: raise power to >= {} (or reduce the joint smooth's dimension).",
2 * j,
2 * j,
smoothness_order,
k_dim,
p_order,
s_order,
2 * j,
min_power
);
}
let kappa = 1.0 / length_scale.max(1e-300);
let mut total_pure = KahanSum::default();
let mut total_log = KahanSum::default();
let mut total_log_abs_scale = KahanSum::default();
for (m, &a_m) in coeffs.a.iter().enumerate().skip(1) {
if a_m == 0.0 {
continue;
}
let (pure, log) = duchon_polyharmonic_block_taylor_r2j(m, k_dim, j);
total_pure.add(a_m * pure);
total_log.add(a_m * log);
total_log_abs_scale.add((a_m * log).abs());
}
for (n, &b_n) in coeffs.b.iter().enumerate().skip(1) {
if b_n == 0.0 {
continue;
}
let (pure, log) = duchon_matern_block_taylor_r2j(kappa, n, k_dim, j);
total_pure.add(b_n * pure);
total_log.add(b_n * log);
total_log_abs_scale.add((b_n * log).abs());
}
let total_pure = total_pure.sum();
let total_log = total_log.sum();
let total_log_abs_scale = total_log_abs_scale.sum();
let log_cancel_tol = 1e-10 * total_log_abs_scale.max(total_pure.abs()).max(1e-30);
if total_log.abs() > log_cancel_tol {
crate::bail_invalid_basis!(
"Duchon Taylor a_{} log-coefficient did not cancel: log={total_log:.6e}, pure={total_pure:.6e}; \
log_abs_scale={total_log_abs_scale:.6e}, tol={log_cancel_tol:.6e}; p={p_order}, s={s_order}, d={k_dim}",
2 * j
);
}
let factorial_2j = gamma_lanczos((2 * j + 1) as f64);
Ok(factorial_2j * total_pure)
}
pub(crate) fn duchon_phi_even_derivative_collision_psi_triplet(
length_scale: f64,
p_order: usize,
s_order: usize,
k_dim: usize,
coeffs: &DuchonPartialFractionCoeffs,
j: usize,
) -> Result<(f64, f64, f64), BasisError> {
let smoothness_order = 2 * (p_order + s_order);
let required = k_dim + 2 * j;
if smoothness_order <= required {
let min_power = (required / 2 + 1).saturating_sub(p_order);
crate::bail_invalid_basis!(
"Duchon collision derivative phi^({}) psi triplet requires 2*(p+s) > dimension+{}; got 2*(p+s)={}, dimension={}, p={}, s={}. \
The exact two-block / transformation-normal path needs analytic length-scale derivatives of the kernel, which are finite only for a smoother spline: raise power to >= {} (or reduce the joint smooth's dimension).",
2 * j,
2 * j,
smoothness_order,
k_dim,
p_order,
s_order,
min_power
);
}
let kappa = 1.0 / length_scale.max(1e-300);
let mut value = KahanSum::default();
let mut psi = KahanSum::default();
let mut psi_psi = KahanSum::default();
let mut log_value = KahanSum::default();
let mut log_psi = KahanSum::default();
let mut log_psi_psi = KahanSum::default();
let mut log_abs_scale = KahanSum::default();
for (m, &a_m) in coeffs.a.iter().enumerate().skip(1) {
if a_m == 0.0 {
continue;
}
let alpha_m = duchon_coeff_exponents(p_order, s_order, m);
let (pure, log) = duchon_polyharmonic_block_taylor_r2j(m, k_dim, j);
value.add(a_m * pure);
psi.add(alpha_m * a_m * pure);
psi_psi.add(alpha_m * alpha_m * a_m * pure);
log_value.add(a_m * log);
log_psi.add(alpha_m * a_m * log);
log_psi_psi.add(alpha_m * alpha_m * a_m * log);
log_abs_scale.add((a_m * log).abs());
log_abs_scale.add((alpha_m * a_m * log).abs());
log_abs_scale.add((alpha_m * alpha_m * a_m * log).abs());
}
for (n, &b_n) in coeffs.b.iter().enumerate().skip(1) {
if b_n == 0.0 {
continue;
}
let beta_n = duchon_coeff_exponents(p_order, s_order, n);
let (pure, log) = duchon_matern_block_taylor_r2j_triplet(kappa, n, k_dim, j);
value.add(b_n * pure.0);
psi.add(beta_n * b_n * pure.0 + b_n * pure.1);
psi_psi.add(beta_n * beta_n * b_n * pure.0 + 2.0 * beta_n * b_n * pure.1 + b_n * pure.2);
log_value.add(b_n * log.0);
log_psi.add(beta_n * b_n * log.0 + b_n * log.1);
log_psi_psi.add(beta_n * beta_n * b_n * log.0 + 2.0 * beta_n * b_n * log.1 + b_n * log.2);
let log_v = b_n * log.0;
let log_p = beta_n * b_n * log.0 + b_n * log.1;
let log_pp = beta_n * beta_n * b_n * log.0 + 2.0 * beta_n * b_n * log.1 + b_n * log.2;
log_abs_scale.add(log_v.abs());
log_abs_scale.add(log_p.abs());
log_abs_scale.add(log_pp.abs());
}
let value = value.sum();
let psi = psi.sum();
let psi_psi = psi_psi.sum();
let log_value = log_value.sum();
let log_psi = log_psi.sum();
let log_psi_psi = log_psi_psi.sum();
let log_abs_scale = log_abs_scale.sum();
let scale = value.abs().max(psi.abs()).max(psi_psi.abs()).max(1e-30);
let log_cancel_tol = 1e-10 * log_abs_scale.max(scale);
if log_value.abs().max(log_psi.abs()).max(log_psi_psi.abs()) > log_cancel_tol {
crate::bail_invalid_basis!(
"Duchon Taylor a_{} log-coefficient derivative did not cancel: \
log=({log_value:.6e}, {log_psi:.6e}, {log_psi_psi:.6e}), \
value=({value:.6e}, {psi:.6e}, {psi_psi:.6e}), log_abs_scale={log_abs_scale:.6e}, tol={log_cancel_tol:.6e}; \
p={p_order}, s={s_order}, d={k_dim}",
2 * j
);
}
let factorial_2j = gamma_lanczos((2 * j + 1) as f64);
Ok((
factorial_2j * value,
factorial_2j * psi,
factorial_2j * psi_psi,
))
}
pub(crate) fn duchon_phi_rrrr_collision(
length_scale: f64,
p_order: usize,
s_order: usize,
k_dim: usize,
coeffs: &DuchonPartialFractionCoeffs,
) -> Result<f64, BasisError> {
duchon_phi_even_derivative_collision(length_scale, p_order, s_order, k_dim, coeffs, 2)
}
pub(crate) fn duchon_phi_rrrrrr_collision(
length_scale: f64,
p_order: usize,
s_order: usize,
k_dim: usize,
coeffs: &DuchonPartialFractionCoeffs,
) -> Result<f64, BasisError> {
duchon_phi_even_derivative_collision(length_scale, p_order, s_order, k_dim, coeffs, 3)
}
pub(crate) fn build_duchon_design_psi_derivativeswithworkspace(
data: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
spec: &DuchonBasisSpec,
identifiability_transform: Option<&Array2<f64>>,
workspace: &mut BasisWorkspace,
) -> Result<ScalarDesignPsiDerivatives, BasisError> {
let length_scale = spec.length_scale.ok_or_else(|| {
BasisError::InvalidInput(
"exact Duchon log-kappa derivatives require hybrid Duchon with length_scale"
.to_string(),
)
})?;
let effective_nullspace_order = duchon_effective_nullspace_order(centers, spec.nullspace_order);
let p_order = duchon_p_from_nullspace_order(effective_nullspace_order);
let s_order = spec.power_as_usize();
let kappa = 1.0 / length_scale;
let coeffs = duchon_partial_fraction_coeffs(p_order, s_order, kappa);
let mut z_kernel =
kernel_constraint_nullspace(centers, effective_nullspace_order, &mut workspace.cache)?;
if let Some(v) = spec.radial_reparam.as_ref() {
if v.nrows() != z_kernel.ncols() {
crate::bail_dim_basis!(
"Duchon frozen radial reparam shape {:?} does not match constrained kernel dimension {}",
v.dim(),
z_kernel.ncols()
);
}
z_kernel = fast_ab(&z_kernel, v);
}
let poly_cols = polynomial_block_from_order(data, effective_nullspace_order).ncols();
let p_padded = z_kernel.ncols() + poly_cols;
if let Some(zf) = identifiability_transform
&& p_padded != zf.nrows()
{
crate::bail_dim_basis!(
"Duchon identifiability transform mismatch in design derivatives: local cols={}, transform rows={}",
p_padded,
zf.nrows()
);
}
let p_final = identifiability_transform
.map(|zf| zf.ncols())
.unwrap_or(p_padded);
build_scalar_design_psi_derivatives_shared(
data,
centers,
spec.aniso_log_scales.as_deref(),
p_final,
Some(z_kernel),
identifiability_transform.cloned(),
poly_cols,
RadialScalarKind::Duchon {
length_scale,
p_order,
s_order,
dim: data.ncols(),
coeffs,
},
duchon_scaling_exponent(p_order, s_order, data.ncols()),
)
}
pub fn build_duchon_basis_log_kappa_derivative(
data: ArrayView2<'_, f64>,
spec: &DuchonBasisSpec,
) -> Result<BasisPsiDerivativeResult, BasisError> {
let mut workspace = BasisWorkspace::default();
build_duchon_basis_log_kappa_derivativewithworkspace(data, spec, &mut workspace)
}
pub fn build_duchon_basis_log_kappa_derivativewithworkspace(
data: ArrayView2<'_, f64>,
spec: &DuchonBasisSpec,
workspace: &mut BasisWorkspace,
) -> Result<BasisPsiDerivativeResult, BasisError> {
let mut bundle = build_duchon_basis_log_kappa_derivativeswithworkspace(data, spec, workspace)?;
bundle.first.implicit_operator = bundle.implicit_operator;
Ok(bundle.first)
}
pub fn build_duchon_basis_log_kappa_derivatives(
data: ArrayView2<'_, f64>,
spec: &DuchonBasisSpec,
) -> Result<BasisPsiDerivativeBundle, BasisError> {
let mut workspace = BasisWorkspace::default();
build_duchon_basis_log_kappa_derivativeswithworkspace(data, spec, &mut workspace)
}
pub(crate) fn duchon_operator_penalties_requested(spec: &DuchonOperatorPenaltySpec) -> bool {
matches!(spec.mass, OperatorPenaltySpec::Active { .. })
|| matches!(spec.tension, OperatorPenaltySpec::Active { .. })
|| matches!(spec.stiffness, OperatorPenaltySpec::Active { .. })
}
pub fn build_duchon_basis_log_kappa_derivativeswithworkspace(
data: ArrayView2<'_, f64>,
spec: &DuchonBasisSpec,
workspace: &mut BasisWorkspace,
) -> Result<BasisPsiDerivativeBundle, BasisError> {
if spec.periodic.is_some() {
return build_periodic_duchon_basis_log_kappa_derivativeswithworkspace(
data, spec, workspace,
);
}
let (centers, identifiability_transform) =
prepare_duchon_derivative_contextwithworkspace(data, spec, workspace)?;
let operator_collocation_points =
if duchon_operator_penalties_requested(&spec.operator_penalties) {
let m = (DUCHON_COLLOCATION_OVERSAMPLE * centers.nrows()).min(data.nrows());
Some(select_thin_plate_knots(data, m)?)
} else {
None
};
build_duchon_basis_log_kappa_derivativeswith_collocationwithworkspace(
data,
spec,
centers.view(),
identifiability_transform.as_ref(),
operator_collocation_points
.as_ref()
.map(|points| points.view()),
workspace,
)
}
pub(crate) fn build_duchon_basis_log_kappa_derivativeswith_collocationwithworkspace(
data: ArrayView2<'_, f64>,
spec: &DuchonBasisSpec,
centers: ArrayView2<'_, f64>,
identifiability_transform: Option<&Array2<f64>>,
operator_collocation_points: Option<ArrayView2<'_, f64>>,
workspace: &mut BasisWorkspace,
) -> Result<BasisPsiDerivativeBundle, BasisError> {
let design_derivatives = build_duchon_design_psi_derivativeswithworkspace(
data,
centers,
spec,
identifiability_transform,
workspace,
)?;
let (native_sources, native_first, native_second) =
build_duchon_native_penalty_psi_derivatives(
centers,
spec,
identifiability_transform,
workspace,
)?;
let (operator_sources, operator_first, operator_second) = if duchon_operator_penalties_requested(
&spec.operator_penalties,
) {
let Some(collocation_points) = operator_collocation_points else {
crate::bail_invalid_basis!(
"Duchon log-kappa operator penalty derivatives require realized collocation points"
);
};
build_duchon_operator_penalty_psi_derivatives(
collocation_points,
centers,
spec,
identifiability_transform,
workspace,
)?
} else {
(Vec::new(), Vec::new(), Vec::new())
};
let mut penalties_derivative = Vec::with_capacity(native_first.len() + operator_first.len());
penalties_derivative.extend(native_first);
penalties_derivative.extend(operator_first);
let mut penaltiessecond_derivative =
Vec::with_capacity(native_second.len() + operator_second.len());
penaltiessecond_derivative.extend(native_second);
penaltiessecond_derivative.extend(operator_second);
let expected_derivative_count = native_sources.len() + operator_sources.len();
if penalties_derivative.len() != expected_derivative_count {
crate::bail_invalid_basis!(
"Duchon penalty derivative count mismatch: assembled {}, expected {} from active penalty sources",
penalties_derivative.len(),
expected_derivative_count
);
}
Ok(BasisPsiDerivativeBundle {
first: BasisPsiDerivativeResult {
design_derivative: design_derivatives.design_first,
penalties_derivative,
implicit_operator: None,
},
second: BasisPsiSecondDerivativeResult {
designsecond_derivative: design_derivatives.design_second_diag,
penaltiessecond_derivative,
implicit_operator: None,
},
implicit_operator: design_derivatives.implicit_operator,
})
}
pub fn build_duchon_basis_log_kappasecond_derivative(
data: ArrayView2<'_, f64>,
spec: &DuchonBasisSpec,
) -> Result<BasisPsiSecondDerivativeResult, BasisError> {
let mut workspace = BasisWorkspace::default();
build_duchon_basis_log_kappasecond_derivativewithworkspace(data, spec, &mut workspace)
}
pub fn build_duchon_basis_log_kappasecond_derivativewithworkspace(
data: ArrayView2<'_, f64>,
spec: &DuchonBasisSpec,
workspace: &mut BasisWorkspace,
) -> Result<BasisPsiSecondDerivativeResult, BasisError> {
let mut bundle = build_duchon_basis_log_kappa_derivativeswithworkspace(data, spec, workspace)?;
bundle.second.implicit_operator = bundle.implicit_operator;
Ok(bundle.second)
}
pub(crate) fn duchon_kernel_amplification(
centers: ArrayView2<'_, f64>,
length_scale: Option<f64>,
p_order: usize,
s_order: usize,
d: usize,
aniso_log_scales: Option<&[f64]>,
coeffs: Option<&DuchonPartialFractionCoeffs>,
pure_poly_coeff: Option<&PolyharmonicBlockCoeff>,
) -> f64 {
let k = centers.nrows();
if k == 0 {
return 1.0;
}
let axis_scales = aniso_log_scales.map(aniso_axis_scales);
let mut max_abs = 0.0_f64;
for i in 0..k {
for j in i..k {
let r = if let Some(scales) = axis_scales.as_deref() {
aniso_distance_rows_with_scales(centers, i, centers, j, scales)
} else {
euclidean_distance_rows(centers, i, centers, j)
};
let val = if let Some(ppc) = pure_poly_coeff {
ppc.eval(r)
} else {
match duchon_matern_kernel_general_from_distance(
r,
length_scale,
p_order,
s_order,
d,
coeffs,
) {
Ok(v) => v,
Err(_) => continue,
}
};
if val.abs() > max_abs {
max_abs = val.abs();
}
}
}
if max_abs > 0.0 && max_abs < 1e-10 {
1.0 / max_abs
} else {
1.0
}
}
pub fn duchon_pure_kernel_amplification(
centers: ArrayView2<'_, f64>,
order: DuchonNullspaceOrder,
power: f64,
) -> f64 {
let dim = centers.ncols();
if dim == 0 || centers.nrows() == 0 {
return 1.0;
}
let effective_order = duchon_effective_nullspace_order(centers, order);
let p_order = duchon_p_from_nullspace_order(effective_order);
let s_order: f64 = power;
let pure_poly_coeff =
PolyharmonicBlockCoeff::new(pure_duchon_block_order(p_order, s_order), dim);
duchon_kernel_amplification(
centers,
None,
p_order,
duchon_power_to_usize(s_order),
dim,
None,
None,
Some(&pure_poly_coeff),
)
}
pub(crate) fn build_duchon_basis_designwithworkspace(
data: ArrayView2<'_, f64>,
centers: ArrayView2<'_, f64>,
length_scale: Option<f64>,
power: f64,
nullspace_order: DuchonNullspaceOrder,
aniso_log_scales: Option<&[f64]>,
radial_reparam: Option<&Array2<f64>>,
workspace: &mut BasisWorkspace,
) -> Result<DuchonBasisDesign, BasisError> {
let n = data.nrows();
let d = data.ncols();
let k = centers.nrows();
if d == 0 {
crate::bail_invalid_basis!("Duchon basis requires at least one covariate dimension");
}
if k == 0 {
crate::bail_invalid_basis!("Duchon basis requires at least one center");
}
if centers.ncols() != d {
crate::bail_dim_basis!(
"Duchon basis dimension mismatch: data has {d} columns, centers have {}",
centers.ncols()
);
}
if data.iter().any(|v| !v.is_finite()) || centers.iter().any(|v| !v.is_finite()) {
crate::bail_invalid_basis!("Duchon basis requires finite data and center values");
}
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 validation_power = if length_scale.is_some() {
duchon_power_to_usize(s_order) as f64
} else {
s_order
};
validate_duchon_kernel_orders(length_scale, p_order, validation_power, d)?;
let center_mean: Vec<f64> = (0..d)
.map(|c| centers.column(c).sum() / (k.max(1) as f64))
.collect();
let mut data_centered = data.to_owned();
for c in 0..d {
let mu = center_mean[c];
data_centered.column_mut(c).mapv_inplace(|v| v - mu);
}
let poly_block = polynomial_block_from_order(data_centered.view(), nullspace_order);
let z_raw = kernel_constraint_nullspace(centers, nullspace_order, &mut workspace.cache)?;
let z = if let Some(v) = radial_reparam {
if v.nrows() != z_raw.ncols() {
crate::bail_dim_basis!(
"Duchon radial reparam shape {:?} does not match constrained kernel dimension {}",
v.dim(),
z_raw.ncols()
);
}
fast_ab(&z_raw, v)
} else {
z_raw
};
let coeffs = length_scale.map(|ls| {
duchon_partial_fraction_coeffs(
p_order,
duchon_power_to_usize(s_order),
1.0 / ls.max(1e-300),
)
});
let warn_bounds = match (length_scale, aniso_log_scales) {
(Some(_), Some(eta)) => {
let y_centers = points_in_aniso_y_space(centers, eta);
pairwise_distance_bounds(y_centers.view())
}
(Some(_), None) => pairwise_distance_bounds(centers),
(None, _) => None,
};
if let (Some(length_scale), Some((r_min, r_max))) = (length_scale, warn_bounds) {
let kappa = 1.0 / length_scale.max(1e-300);
let kappa_lo = 1e-2 / r_max;
let kappa_hi = 1e2 / r_min;
if kappa < kappa_lo || kappa > kappa_hi {
log::debug!(
"Duchon κ={} is outside recommended range [{}, {}] derived from centers (r_min={}, r_max={}); numerical conditioning may degrade",
kappa,
kappa_lo,
kappa_hi,
r_min,
r_max
);
}
}
let kernel_cols = z.ncols();
let poly_cols = poly_block.ncols();
let total_cols = kernel_cols + poly_cols;
let pure_poly_coeff = if length_scale.is_none() {
Some(PolyharmonicBlockCoeff::new(
(pure_duchon_block_order(p_order, s_order)) as f64,
d,
))
} else {
None
};
let axis_scales = aniso_log_scales.map(aniso_axis_scales);
let kernel_amp = duchon_kernel_amplification(
centers,
length_scale,
p_order,
duchon_power_to_usize(s_order),
d,
aniso_log_scales,
coeffs.as_ref(),
pure_poly_coeff.as_ref(),
);
let hybrid_kind = match (length_scale, coeffs.as_ref()) {
(Some(ls), Some(c)) if pure_poly_coeff.is_none() => Some(RadialScalarKind::Duchon {
length_scale: ls,
p_order,
s_order: duchon_power_to_usize(s_order),
dim: d,
coeffs: c.clone(),
}),
_ => None,
};
let value_profile = hybrid_kind.as_ref().and_then(|kind| {
if n.saturating_mul(k) < RADIAL_PROFILE_MIN_PAIRS {
return None;
}
let (r_lo, r_hi) = (0..n)
.into_par_iter()
.map(|i| {
let mut lo = f64::INFINITY;
let mut hi = 0.0_f64;
for j in 0..k {
let r = if let Some(scales) = axis_scales.as_deref() {
aniso_distance_rows_with_scales(data, i, centers, j, scales)
} else {
euclidean_distance_rows(data, i, centers, j)
};
if r > 0.0 {
lo = lo.min(r);
hi = hi.max(r);
}
}
(lo, hi)
})
.reduce(
|| (f64::INFINITY, 0.0_f64),
|a, b| (a.0.min(b.0), a.1.max(b.1)),
);
if r_lo.is_finite() && r_hi > r_lo {
radial_profile::RadialProfile::build(kind, r_lo, r_hi)
} else {
None
}
});
let mut basis = Array2::<f64>::zeros((n, total_cols));
let chunk_size = 1024.min(n);
let basis_result: Result<(), BasisError> = basis
.axis_chunks_iter_mut(Axis(0), chunk_size)
.into_par_iter()
.enumerate()
.try_for_each(|(ci, mut chunk)| {
let mut kernel_row = vec![0.0; k];
let chunk_start = ci * chunk_size;
for local_i in 0..chunk.nrows() {
let i = chunk_start + local_i;
for j in 0..k {
let r = if let Some(scales) = axis_scales.as_deref() {
aniso_distance_rows_with_scales(data, i, centers, j, scales)
} else {
euclidean_distance_rows(data, i, centers, j)
};
let raw = if let Some(ref ppc) = pure_poly_coeff {
ppc.eval(r)
} else if let (Some(profile), Some(kind)) =
(value_profile.as_ref(), hybrid_kind.as_ref())
{
profile.eval_or_exact(kind, r)?.0
} else {
duchon_matern_kernel_general_from_distance(
r,
length_scale,
p_order,
duchon_power_to_usize(s_order),
d,
coeffs.as_ref(),
)?
};
kernel_row[j] = raw * kernel_amp;
}
let mut row = chunk.row_mut(local_i);
row.slice_mut(s![..kernel_cols]).fill(0.0);
for j in 0..k {
let kv = kernel_row[j];
if kv != 0.0 {
let z_row = z.row(j);
for col in 0..kernel_cols {
row[col] += kv * z_row[col];
}
}
}
}
Ok(())
});
basis_result?;
if poly_cols > 0 {
basis.slice_mut(s![.., kernel_cols..]).assign(&poly_block);
}
Ok(DuchonBasisDesign { basis })
}
pub(crate) fn build_cyclic_duchon_basis_1dwithworkspace(
data: ArrayView2<'_, f64>,
spec: &DuchonBasisSpec,
start: f64,
end: f64,
) -> Result<BasisBuildResult, BasisError> {
if data.ncols() != 1 {
crate::bail_invalid_basis!("cyclic Duchon smooths currently require exactly one covariate");
}
if end <= start {
return Err(BasisError::InvalidRange(start, end));
}
let centers = select_centers_by_strategy(data, &spec.center_strategy)?;
if centers.ncols() != 1 {
crate::bail_dim_basis!(
"cyclic Duchon centers must have one column, got {}",
centers.ncols()
);
}
let k = centers.nrows();
let s_order_usize = spec.power_as_usize();
if k <= s_order_usize.max(1) {
crate::bail_invalid_basis!(
"cyclic Duchon basis requires more centers ({k}) than power ({})",
spec.power
);
}
let period = end - start;
let p_order = duchon_p_from_nullspace_order(DuchonNullspaceOrder::Zero);
let validation_power = if spec.length_scale.is_some() {
s_order_usize as f64
} else {
spec.power
};
validate_duchon_kernel_orders(spec.length_scale, p_order, validation_power, 1)?;
let coeffs = spec
.length_scale
.map(|ls| duchon_partial_fraction_coeffs(p_order, s_order_usize, 1.0 / ls.max(1e-300)));
let pure_poly_coeff = if spec.length_scale.is_none() {
Some(PolyharmonicBlockCoeff::new(
pure_duchon_block_order(p_order, spec.power),
1,
))
} else {
None
};
let kernel_amp = duchon_kernel_amplification(
centers.view(),
spec.length_scale,
p_order,
duchon_power_to_usize(spec.power),
1,
None,
coeffs.as_ref(),
pure_poly_coeff.as_ref(),
);
let mut basis = Array2::<f64>::zeros((data.nrows(), k + 1));
for i in 0..data.nrows() {
let x = wrap_to_period(data[[i, 0]], start, period);
for j in 0..k {
let c = wrap_to_period(centers[[j, 0]], start, period);
let r = cyclic_distance_1d(x, c, period);
let raw = if let Some(ref ppc) = pure_poly_coeff {
ppc.eval(r)
} else {
duchon_matern_kernel_general_from_distance(
r,
spec.length_scale,
p_order,
s_order_usize,
1,
coeffs.as_ref(),
)?
};
basis[[i, j]] = raw * kernel_amp;
}
basis[[i, k]] = 1.0;
}
let identifiability_transform = spatial_identifiability_transform_from_design(
data,
basis.view(),
&spec.identifiability,
"cyclic Duchon",
)?;
let (design_matrix, transform_for_penalty) = if let Some(z) = identifiability_transform.as_ref()
{
(fast_ab(&basis, z), Some(z))
} else {
(basis, None)
};
let mut s_kernel = Array2::<f64>::zeros((k + 1, k + 1));
let s_cyclic = create_cyclic_difference_penalty_matrix(k, s_order_usize.max(1).min(k - 1))?;
s_kernel.slice_mut(s![..k, ..k]).assign(&s_cyclic);
let s_final = if let Some(z) = transform_for_penalty {
fast_ab(&fast_atb(z, &s_kernel), z)
} else {
s_kernel
};
let (s_final_norm, s_final_scale) = normalize_penalty(&s_final);
let candidates = vec![PenaltyCandidate {
matrix: s_final_norm,
nullspace_dim_hint: 1,
source: PenaltySource::Primary,
normalization_scale: s_final_scale,
kronecker_factors: None,
op: None,
}];
let (penalties, nullspace_dims, penaltyinfo, null_eigenvectors, ops) =
filter_active_penalty_candidates_with_ops(candidates)?;
Ok(BasisBuildResult {
design: DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(design_matrix)),
penalties,
nullspace_dims,
penaltyinfo,
metadata: BasisMetadata::Duchon {
centers,
length_scale: spec.length_scale,
periodic: Some(vec![Some(period)]),
power: spec.power,
nullspace_order: DuchonNullspaceOrder::Zero,
identifiability_transform,
input_scales: None,
aniso_log_scales: None,
operator_collocation_points: None,
radial_reparam: None,
},
kronecker_factored: None,
ops,
null_eigenvectors,
joint_null_rotation: None,
})
}
pub fn build_duchon_basis(
data: ArrayView2<'_, f64>,
spec: &DuchonBasisSpec,
) -> Result<BasisBuildResult, BasisError> {
let mut workspace = BasisWorkspace::default();
build_duchon_basiswithworkspace(data, spec, &mut workspace)
}
pub fn create_duchon_basis_1d_derivative_dense(
t: ArrayView1<'_, f64>,
centers: ArrayView1<'_, f64>,
power: f64,
nullspace_order: DuchonNullspaceOrder,
periodic: bool,
period: Option<f64>,
order: usize,
) -> Result<Array2<f64>, BasisError> {
if order > 2 {
crate::bail_invalid_basis!(
"Duchon basis derivative supports orders 0, 1, and 2; got order={order}"
);
}
if t.is_empty() || centers.is_empty() {
crate::bail_invalid_basis!("Duchon basis derivative requires non-empty t and centers");
}
if t.iter().any(|v| !v.is_finite()) || centers.iter().any(|v| !v.is_finite()) {
crate::bail_invalid_basis!("Duchon basis derivative requires finite t and center values");
}
if !periodic && period.is_some() {
crate::bail_invalid_basis!(
"Duchon basis derivative period is only valid when periodic=true"
);
}
let data = t.to_owned().insert_axis(Axis(1));
let center_matrix = centers.to_owned().insert_axis(Axis(1));
let mut workspace = BasisWorkspace::default();
let user_m = duchon_p_from_nullspace_order(nullspace_order);
let effective_order = if periodic {
DuchonNullspaceOrder::Zero
} else {
duchon_effective_nullspace_order(center_matrix.view(), nullspace_order)
};
let p_order = duchon_p_from_nullspace_order(effective_order);
let s_order = duchon_power_to_usize(power);
validate_duchon_kernel_orders(None, p_order, s_order as f64, 1)?;
if periodic {
let (collapsed_centers, left, resolved_period) =
prepare_periodic_duchon_centers_1d_with_period(center_matrix, period)?;
let z = kernel_constraint_nullspace(
collapsed_centers.view(),
effective_order,
&mut workspace.cache,
)?;
let kernel_cols = z.ncols();
let k_centers = collapsed_centers.nrows();
let centers_col0: Vec<f64> = collapsed_centers.column(0).to_vec();
let mut raw_kernel = Array2::<f64>::zeros((t.len(), k_centers));
for i in 0..t.len() {
let x = wrap_to_period(t[i], left, resolved_period);
for j in 0..k_centers {
let mut delta = (x - centers_col0[j]).rem_euclid(resolved_period);
if delta > 0.5 * resolved_period {
delta -= resolved_period;
}
let r = delta.abs();
let sign = if delta > 0.0 {
1.0
} else if delta < 0.0 {
-1.0
} else {
0.0
};
let (phi, phi_r, phi_rr) =
periodic_duchon_kernel_bernoulli_triplet(r, user_m, resolved_period)?;
raw_kernel[[i, j]] = match order {
0 => phi,
1 => phi_r * sign,
2 => phi_rr,
other => {
crate::bail_invalid_basis!(
"Duchon basis derivative supports orders 0, 1, and 2; got order={other}"
);
}
};
}
}
let mut basis = Array2::<f64>::zeros((t.len(), kernel_cols + 1));
let design_kernel = fast_ab(&raw_kernel, &z);
basis
.slice_mut(s![.., 0..kernel_cols])
.assign(&design_kernel);
if order == 0 {
basis.column_mut(kernel_cols).fill(1.0);
}
return Ok(basis);
}
let z =
kernel_constraint_nullspace(center_matrix.view(), effective_order, &mut workspace.cache)?;
let kernel_cols = z.ncols();
let poly_cols = polynomial_block_from_order(data.view(), effective_order).ncols();
let pure_coeff =
PolyharmonicBlockCoeff::new((pure_duchon_block_order(p_order, s_order as f64)) as f64, 1);
let kernel_amp = duchon_kernel_amplification(
center_matrix.view(),
None,
p_order,
s_order,
1,
None,
None,
Some(&pure_coeff),
);
let mut raw_kernel = Array2::<f64>::zeros((t.len(), centers.len()));
for i in 0..t.len() {
let x = t[i];
for j in 0..centers.len() {
let delta = x - centers[j];
let r = delta.abs();
let sign = if delta > 0.0 {
1.0
} else if delta < 0.0 {
-1.0
} else {
0.0
};
let (phi, phi_r, phi_rr) =
duchon_kernel_radial_triplet(r, None, p_order, s_order as f64, 1, None)?;
raw_kernel[[i, j]] = match order {
0 => phi,
1 => phi_r * sign,
2 => phi_rr,
other => {
crate::bail_invalid_basis!(
"Duchon basis derivative supports orders 0, 1, and 2; got order={other}"
);
}
} * kernel_amp;
}
}
let mut basis = Array2::<f64>::zeros((t.len(), kernel_cols + poly_cols));
let design_kernel = fast_ab(&raw_kernel, &z);
basis
.slice_mut(s![.., 0..kernel_cols])
.assign(&design_kernel);
fill_duchon_1d_polynomial_derivative(&mut basis, kernel_cols, t, effective_order, order);
Ok(basis)
}