use super::*;
use gam_linalg::utils::KahanSum;
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Mutex, OnceLock};
const PANEL_ORDER: usize = 32;
pub(crate) const MAX_DERIVATIVE_ORDER: usize = 4;
const CHANNELS: usize = MAX_DERIVATIVE_ORDER + 1;
const ORDER_SHIFTS: usize = 2 * MAX_DERIVATIVE_ORDER + 1;
pub(crate) const REFERENCE_RTOL: f64 = 256.0 * f64::EPSILON;
const SERIES_CROSSOVER_Z: f64 = 2.5;
const SERIES_TERMS: usize = 20;
const EULER_MASCHERONI: f64 = 0.577_215_664_901_532_9;
const REFERENCE_MAX_BISECTIONS: u32 = 6;
struct PanelEstimate {
values: [f64; CHANNELS],
abs_sums: [f64; CHANNELS],
last_delta: [f64; CHANNELS],
converged: bool,
}
pub(crate) fn chebyshev_tail_tolerance() -> f64 {
4.0 * REFERENCE_RTOL
}
const PANEL_DYNAMIC_RANGE: f64 = std::f64::consts::E;
const TANH_SINH_T_MAX: f64 = 3.0;
const TANH_SINH_FIRST_LEVEL: u32 = 3;
const TANH_SINH_LAST_LEVEL: u32 = 8;
pub(crate) fn rho_value_floor(d: usize, p: usize) -> f64 {
let leading = (d - 2 * p).min(2) as f64;
0.1 * f64::EPSILON.powf(1.0 / leading)
}
pub(crate) fn rho_derivative_floor() -> f64 {
DUCHON_DERIVATIVE_R_FLOOR_REL * 1e-2
}
pub(crate) fn rho_ceiling() -> f64 {
1.0 / f64::EPSILON.sqrt()
}
fn pow_b(z: f64, b: f64) -> f64 {
let two_b = (2.0 * b).round() as i32;
if two_b % 2 == 0 {
z.powi(two_b / 2)
} else {
z.powi(two_b / 2) * z.sqrt()
}
}
#[derive(Clone, Debug)]
struct Panel {
u_lo: f64,
u_hi: f64,
coeff: [[f64; PANEL_ORDER]; CHANNELS],
scale: [f64; CHANNELS],
}
impl Panel {
fn eval(&self, m: usize, u: f64) -> f64 {
let x = (2.0 * u - (self.u_lo + self.u_hi)) / (self.u_hi - self.u_lo);
let x2 = 2.0 * x;
let coeff = &self.coeff[m];
let mut b1 = 0.0_f64;
let mut b2 = 0.0_f64;
for &c in coeff.iter().skip(1).rev() {
let b0 = c + x2 * b1 - b2;
b2 = b1;
b1 = b0;
}
0.5 * coeff[0] + x * b1 - b2
}
}
#[derive(Clone, Debug)]
struct TermTable {
coef: [[[f64; ORDER_SHIFTS]; CHANNELS]; CHANNELS],
}
impl TermTable {
fn new(b: f64) -> Self {
let mut coef = [[[0.0_f64; ORDER_SHIFTS]; CHANNELS]; CHANNELS];
coef[0][0][MAX_DERIVATIVE_ORDER] = 1.0;
for m in 0..MAX_DERIVATIVE_ORDER {
for k in 0..=m {
let a = b - k as f64;
for j in 1..ORDER_SHIFTS - 1 {
let c = coef[m][k][j];
if c == 0.0 {
continue;
}
if a != 0.0 {
coef[m + 1][k + 1][j] += c * a;
}
coef[m + 1][k][j - 1] -= 0.5 * c;
coef[m + 1][k][j + 1] -= 0.5 * c;
}
}
}
Self { coef }
}
fn evaluate(&self, z: f64, z_pow_b: f64, k: &[f64; ORDER_SHIFTS]) -> [f64; CHANNELS] {
let inv_z = 1.0 / z;
let mut z_pow = [0.0_f64; CHANNELS];
z_pow[0] = z_pow_b;
for k_idx in 1..CHANNELS {
z_pow[k_idx] = z_pow[k_idx - 1] * inv_z;
}
let mut out = [0.0_f64; CHANNELS];
for (m, slot) in out.iter_mut().enumerate() {
let mut acc = KahanSum::default();
for (k_idx, row) in self.coef[m].iter().enumerate().take(m + 1) {
for (j, &c) in row.iter().enumerate() {
if c != 0.0 {
acc.add(c * z_pow[k_idx] * k[j]);
}
}
}
*slot = acc.sum();
}
out
}
}
fn bessel_k_ladder(b: f64, z: f64) -> [f64; ORDER_SHIFTS] {
let two_b = (2.0 * b).round() as i64;
let half_integer = two_b % 2 != 0;
let top = (b - MAX_DERIVATIVE_ORDER as f64)
.abs()
.max((b + MAX_DERIVATIVE_ORDER as f64).abs());
let count = if half_integer {
(top - 0.5).round() as usize + 1
} else {
top.round() as usize + 1
};
let mut ladder = vec![0.0_f64; count];
if half_integer {
ladder[0] = bessel_k_half_integer_order(0, z);
if count > 1 {
ladder[1] = ladder[0] * (1.0 + 1.0 / z);
}
for idx in 2..count {
let nu = (idx - 1) as f64 + 0.5;
ladder[idx] = ladder[idx - 2] + 2.0 * nu * ladder[idx - 1] / z;
}
} else {
ladder[0] = bessel_k_integer_order(0, z);
if count > 1 {
ladder[1] = bessel_k_integer_order(1, z);
}
for idx in 2..count {
let nu = (idx - 1) as f64;
ladder[idx] = ladder[idx - 2] + 2.0 * nu * ladder[idx - 1] / z;
}
}
let mut out = [0.0_f64; ORDER_SHIFTS];
for (j, slot) in out.iter_mut().enumerate() {
let order = (b + j as f64 - MAX_DERIVATIVE_ORDER as f64).abs();
let idx = if half_integer {
(order - 0.5).round() as usize
} else {
order.round() as usize
};
*slot = ladder[idx];
}
out
}
fn tanh_sinh_level(level: u32) -> Vec<(f64, bool, f64)> {
let h = 2.0_f64.powi(-(level as i32));
let first = level == TANH_SINH_FIRST_LEVEL;
let max_k = (TANH_SINH_T_MAX / h).floor() as i64;
let mut nodes = Vec::new();
let mut k: i64 = if first { 0 } else { 1 };
while k <= max_k {
for sign in [-1.0_f64, 1.0] {
if k == 0 && sign > 0.0 {
continue;
}
let t = sign * k as f64 * h;
let y = std::f64::consts::FRAC_PI_2 * t.sinh();
let offset_unit = 2.0 / ((2.0 * y.abs()).exp() + 1.0);
let weight_unit =
std::f64::consts::FRAC_PI_2 * t.cosh() / (y.cosh() * y.cosh());
nodes.push((offset_unit, t < 0.0, weight_unit * h));
}
k += if first { 1 } else { 2 };
}
nodes
}
fn tanh_sinh_levels() -> &'static [Vec<(f64, bool, f64)>] {
static LEVELS: OnceLock<Vec<Vec<(f64, bool, f64)>>> = OnceLock::new();
LEVELS.get_or_init(|| {
(TANH_SINH_FIRST_LEVEL..=TANH_SINH_LAST_LEVEL)
.map(tanh_sinh_level)
.collect()
})
}
#[derive(Clone, Copy, Debug)]
struct Monomial {
coef: f64,
power: i32,
logged: bool,
}
fn normalize_monomials(mut terms: Vec<Monomial>) -> Vec<Monomial> {
terms.sort_by(|a, b| (a.logged, a.power).cmp(&(b.logged, b.power)));
let mut out: Vec<Monomial> = Vec::with_capacity(terms.len());
for term in terms {
match out.last_mut() {
Some(last) if last.logged == term.logged && last.power == term.power => {
last.coef += term.coef;
}
_ => out.push(term),
}
}
out.retain(|term| term.coef != 0.0);
out
}
fn differentiate_monomials(terms: &[Monomial]) -> Vec<Monomial> {
let mut out = Vec::with_capacity(2 * terms.len());
for &Monomial { coef, power, logged } in terms {
if power != 0 {
out.push(Monomial {
coef: coef * power as f64,
power: power - 1,
logged,
});
}
if logged {
out.push(Monomial {
coef,
power: power - 1,
logged: false,
});
}
}
normalize_monomials(out)
}
fn evaluate_monomials(terms: &[Monomial], z: f64, ln_half_z: f64) -> f64 {
let mut plain = KahanSum::default();
let mut logged = KahanSum::default();
for &Monomial { coef, power, logged: is_logged } in terms {
let value = coef * z.powi(power);
if is_logged {
logged.add(value);
} else {
plain.add(value);
}
}
plain.sum() + ln_half_z * logged.sum()
}
#[derive(Clone, Debug)]
struct TermEvaluator {
b: f64,
recurrence: TermTable,
mode: TermMode,
}
#[derive(Clone, Debug)]
enum TermMode {
HalfInteger {
laurent: [Vec<Monomial>; CHANNELS],
},
Integer {
series: [Vec<Monomial>; CHANNELS],
},
}
impl TermEvaluator {
fn new(b: f64) -> Self {
let two_b = (2.0 * b).round() as i64;
let recurrence = TermTable::new(b);
let mode = if two_b % 2 != 0 {
let n = ((two_b.abs() - 1) / 2) as usize;
let base_power = (two_b - 1) / 2; let mut base: Vec<Monomial> = Vec::with_capacity(n + 1);
for j in 0..=n {
let num: f64 = (1..=(n + j)).map(|k| k as f64).product();
let den_j: f64 = (1..=j).map(|k| k as f64).product();
let den_nj: f64 = (1..=(n - j)).map(|k| k as f64).product();
base.push(Monomial {
coef: num / (den_j * den_nj * 2.0_f64.powi(j as i32)),
power: base_power as i32 - j as i32,
logged: false,
});
}
let mut laurent: [Vec<Monomial>; CHANNELS] = Default::default();
laurent[0] = normalize_monomials(base);
for m in 1..CHANNELS {
let mut next = differentiate_monomials(&laurent[m - 1]);
next.extend(laurent[m - 1].iter().map(|t| Monomial {
coef: -t.coef,
power: t.power,
logged: false,
}));
laurent[m] = normalize_monomials(next);
}
TermMode::HalfInteger { laurent }
} else {
let n = (two_b.abs() / 2) as usize;
let power_shift: i32 = if two_b < 0 { -2 * n as i32 } else { 0 };
let factorial = |k: usize| -> f64 { (1..=k).map(|i| i as f64).product() };
let digamma_int = |k: usize| -> f64 {
-EULER_MASCHERONI + (1..k).map(|i| 1.0 / i as f64).sum::<f64>()
};
let mut base: Vec<Monomial> = Vec::new();
for k in 0..n {
base.push(Monomial {
coef: 2.0_f64.powi(n as i32 - 1) * factorial(n - k - 1) / factorial(k)
* (-0.25_f64).powi(k as i32),
power: 2 * k as i32,
logged: false,
});
}
let sign_n = if n % 2 == 0 { 1.0 } else { -1.0 };
for k in 0..SERIES_TERMS {
let shared = 0.25_f64.powi(k as i32) / (factorial(k) * factorial(n + k));
base.push(Monomial {
coef: -sign_n * 2.0_f64.powi(-(n as i32)) * shared,
power: 2 * (n + k) as i32,
logged: true,
});
base.push(Monomial {
coef: sign_n * 2.0_f64.powi(-(n as i32) - 1)
* (digamma_int(k + 1) + digamma_int(n + k + 1))
* shared,
power: 2 * (n + k) as i32,
logged: false,
});
}
for term in &mut base {
term.power += power_shift;
}
let mut series: [Vec<Monomial>; CHANNELS] = Default::default();
series[0] = normalize_monomials(base);
for m in 1..CHANNELS {
series[m] = differentiate_monomials(&series[m - 1]);
}
TermMode::Integer { series }
};
Self {
b,
recurrence,
mode,
}
}
fn evaluate(&self, z: f64) -> [f64; CHANNELS] {
match &self.mode {
TermMode::HalfInteger { laurent } => {
let scale = std::f64::consts::FRAC_PI_2.sqrt() * (-z).exp();
std::array::from_fn(|m| scale * evaluate_monomials(&laurent[m], z, 0.0))
}
TermMode::Integer { series } => {
if z <= SERIES_CROSSOVER_Z {
let ln_half_z = (0.5 * z).ln();
std::array::from_fn(|m| evaluate_monomials(&series[m], z, ln_half_z))
} else {
let ladder = bessel_k_ladder(self.b, z);
self.recurrence.evaluate(z, pow_b(z, self.b), &ladder)
}
}
}
}
}
#[derive(Clone, Debug)]
struct ProfileShape {
p: usize,
s: usize,
d: usize,
b: f64,
terms: TermEvaluator,
}
impl ProfileShape {
fn integrand(&self, rho: f64, v: f64) -> [f64; CHANNELS] {
let z = rho * v;
let t = self.terms.evaluate(z);
let weight = (1.0 - v * v).powi(self.p as i32 - 1) * 2.0_f64.powf(2.0 - self.b);
let mut v_pow = v.powi(self.d as i32 - 2 * self.p as i32 - 1);
let mut out = [0.0_f64; CHANNELS];
for (m, slot) in out.iter_mut().enumerate() {
*slot = weight * v_pow * t[m];
v_pow *= v;
}
out
}
fn integrate_panel(
&self,
rho: f64,
a: f64,
b: f64,
channels: usize,
scale_floor: &[f64; CHANNELS],
) -> Result<PanelEstimate, BasisError> {
let half = 0.5 * (b - a);
let mut sums: [KahanSum; CHANNELS] = Default::default();
let mut abs_sums = [0.0_f64; CHANNELS];
let mut previous: Option<[f64; CHANNELS]> = None;
let mut last_delta = [f64::NAN; CHANNELS];
for (level_idx, level) in tanh_sinh_levels().iter().enumerate() {
if level_idx > 0 {
for m in 0..channels {
let s = sums[m].sum();
sums[m] = KahanSum::default();
sums[m].add(0.5 * s);
abs_sums[m] *= 0.5;
}
}
for &(offset_unit, left, weight_unit) in level {
let v = if left {
a + half * offset_unit
} else {
b - half * offset_unit
};
if v <= 0.0 || v >= 1.0 {
continue;
}
let f = self.integrand(rho, v);
let w = half * weight_unit;
for m in 0..channels {
if !f[m].is_finite() {
crate::bail_invalid_basis!(
"Duchon radial profile reference integrand is not finite at rho={rho:e}, \
v={v:e} (p={}, s={}, d={}, derivative order {m})",
self.p,
self.s,
self.d
);
}
let term = w * f[m];
sums[m].add(term);
abs_sums[m] += term.abs();
}
}
let current: [f64; CHANNELS] = std::array::from_fn(|m| sums[m].sum());
if let Some(prev) = previous {
for m in 0..channels {
last_delta[m] = (current[m] - prev[m]).abs()
/ abs_sums[m].max(scale_floor[m]).max(f64::MIN_POSITIVE);
}
if (0..channels).all(|m| last_delta[m] <= REFERENCE_RTOL) {
return Ok(PanelEstimate {
values: current,
abs_sums,
last_delta,
converged: true,
});
}
}
previous = Some(current);
}
Ok(PanelEstimate {
values: std::array::from_fn(|m| sums[m].sum()),
abs_sums,
last_delta,
converged: false,
})
}
fn integrate_adaptive(
&self,
rho: f64,
a: f64,
b: f64,
channels: usize,
scale_floor: &[f64; CHANNELS],
depth: u32,
) -> Result<[f64; CHANNELS], BasisError> {
let estimate = self.integrate_panel(rho, a, b, channels, scale_floor)?;
if estimate.converged {
return Ok(estimate.values);
}
if depth >= REFERENCE_MAX_BISECTIONS {
let report: Vec<String> = (0..channels)
.map(|m| {
format!(
"m={m}: |Δ|/scale={:.2e} at the last level (value {:e}, Σ|terms| {:e})",
estimate.last_delta[m], estimate.values[m], estimate.abs_sums[m]
)
})
.collect();
crate::bail_invalid_basis!(
"Duchon radial profile reference integral did not converge at rho={rho:e} on \
[{a:e}, {b:e}] after {depth} bisections (p={}, s={}, d={}; bar {:.2e}): {}",
self.p,
self.s,
self.d,
REFERENCE_RTOL,
report.join("; ")
);
}
let floor: [f64; CHANNELS] =
std::array::from_fn(|m| scale_floor[m].max(estimate.abs_sums[m]));
let mid = 0.5 * (a + b);
let left = self.integrate_adaptive(rho, a, mid, channels, &floor, depth + 1)?;
let right = self.integrate_adaptive(rho, mid, b, channels, &floor, depth + 1)?;
Ok(std::array::from_fn(|m| left[m] + right[m]))
}
fn reference(&self, rho: f64, channels: usize) -> Result<[f64; CHANNELS], BasisError> {
let largest_power = (self.d as i32 - 2 * self.p as i32 - 1 + MAX_DERIVATIVE_ORDER as i32) as f64;
let cut = (2.0 / f64::EPSILON).ln() + 3.0 * largest_power;
let v_star = (cut / rho).min(1.0);
let mut total = self.integrate_adaptive(rho, 0.0, v_star, channels, &[0.0; CHANNELS], 0)?;
if v_star < 1.0 {
let floor: [f64; CHANNELS] = std::array::from_fn(|m| total[m].abs());
let tail = self.integrate_adaptive(rho, v_star, 1.0, channels, &floor, 0)?;
for m in 0..channels {
total[m] += tail[m];
}
}
Ok(total)
}
}
#[derive(Clone, Debug)]
pub(crate) struct DuchonRadialProfile {
shape: ProfileShape,
prefactor: f64,
g0: Option<f64>,
u_value_lo: f64,
u_lo: f64,
u_hi: f64,
low: Vec<Panel>,
main: Vec<Panel>,
}
fn envelope(alpha: i32, rho: f64) -> f64 {
(1.0 + rho * rho).sqrt().powi(alpha)
}
impl DuchonRadialProfile {
fn alpha(&self, m: usize) -> i32 {
self.shape.d as i32 - 2 * self.shape.p as i32 + m as i32
}
pub(crate) fn build(p: usize, s: usize, d: usize) -> Result<Self, BasisError> {
if !(p >= 1 && s >= 1 && 2 * p < d) {
crate::bail_invalid_basis!(
"Duchon radial profile requires p ≥ 1, s ≥ 1 and 2p < d; got p={p}, s={s}, d={d}"
);
}
let b = p as f64 + s as f64 - 0.5 * d as f64;
let shape = ProfileShape {
p,
s,
d,
b,
terms: TermEvaluator::new(b),
};
let g0 = if b > 0.0 {
Some(
gamma_lanczos(b) * gamma_lanczos(s as f64 - b) * gamma_lanczos(p as f64)
/ gamma_lanczos(s as f64 - b + p as f64),
)
} else {
None
};
let prefactor = (4.0 * std::f64::consts::PI).powf(-0.5 * d as f64)
/ (gamma_lanczos(p as f64) * gamma_lanczos(s as f64));
let u_value_lo = rho_value_floor(d, p).ln();
let u_lo = rho_derivative_floor().ln();
let u_hi = rho_ceiling().ln();
let build_start = std::time::Instant::now();
let low = build_panels(&shape, 1, u_value_lo, u_lo)?;
let main = build_panels(&shape, CHANNELS, u_lo, u_hi)?;
let profile = Self {
shape,
prefactor,
g0,
u_value_lo,
u_lo,
u_hi,
low,
main,
};
profile.spot_check()?;
let (low_count, main_count) = profile.panel_counts();
log::info!(
"[duchon-profile] (p={p}, s={s}, d={d}): {low_count} value-only + {main_count} all-channel \
panels certified in {:.3}s",
build_start.elapsed().as_secs_f64()
);
Ok(profile)
}
fn spot_check(&self) -> Result<(), BasisError> {
let sets: [(&[Panel], usize); 2] = [(&self.low, 1), (&self.main, CHANNELS)];
for (set, channels) in sets {
for panel in set {
let u = panel.u_lo + 0.37 * (panel.u_hi - panel.u_lo);
let rho = u.exp();
let reference = if channels == CHANNELS {
self.reference(rho)?
} else {
self.shape.reference(rho, channels)?
};
for m in 0..channels {
let got = self.derivative(m, rho);
let bar = self.resolution(m, rho);
if !((got - reference[m]).abs() <= bar) {
crate::bail_invalid_basis!(
"Duchon radial profile (p={}, s={}, d={}) channel {m} misses its reference at \
rho={rho:e}: {got:e} vs {:e}, |Δ|={:e} > {bar:e}",
self.shape.p,
self.shape.s,
self.shape.d,
reference[m],
(got - reference[m]).abs()
);
}
}
}
}
Ok(())
}
pub(crate) fn origin_value(&self) -> Result<f64, BasisError> {
self.g0.ok_or_else(|| {
BasisError::InvalidInput(format!(
"the hybrid Duchon kernel is singular at the origin for 2(p+s) ≤ d (p={}, s={}, d={})",
self.shape.p, self.shape.s, self.shape.d
))
})
}
fn locate(&self, m: usize, u: f64) -> (&Panel, f64) {
let (set, lo) = if m == 0 && u < self.u_lo {
(&self.low, self.u_value_lo)
} else {
(&self.main, self.u_lo)
};
let u_eval = u.clamp(lo, self.u_hi);
let idx = set
.partition_point(|panel| panel.u_hi < u_eval)
.min(set.len() - 1);
(&set[idx], u_eval)
}
#[inline]
fn channel(&self, m: usize, rho: f64, u: f64) -> f64 {
if m == 0
&& u < self.u_value_lo
&& let Some(g0) = self.g0
{
return g0;
}
let (panel, u_eval) = self.locate(m, u);
panel.eval(m, u_eval) / envelope(self.alpha(m), rho)
}
pub(crate) fn derivative(&self, m: usize, rho: f64) -> f64 {
assert!(
m <= MAX_DERIVATIVE_ORDER,
"Duchon radial profile carries derivatives up to order {MAX_DERIVATIVE_ORDER}, asked {m}"
);
assert!(
rho > 0.0 && rho.is_finite(),
"Duchon radial profile needs a finite positive rho, got {rho}"
);
self.channel(m, rho, rho.ln())
}
pub(crate) fn value(&self, rho: f64) -> f64 {
self.derivative(0, rho)
}
pub(crate) fn derivatives(&self, rho: f64) -> [f64; CHANNELS] {
assert!(
rho > 0.0 && rho.is_finite(),
"Duchon radial profile needs a finite positive rho, got {rho}"
);
let u = rho.ln();
let (panel, u_eval) = self.locate(1, u);
std::array::from_fn(|m| {
if m == 0 {
self.channel(0, rho, u)
} else {
panel.eval(m, u_eval) / envelope(self.alpha(m), rho)
}
})
}
pub(crate) fn kappa_scale(&self, kappa: f64) -> f64 {
self.prefactor * kappa.powf(-2.0 * self.shape.b)
}
pub(crate) fn resolution(&self, m: usize, rho: f64) -> f64 {
let (panel, _) = self.locate(m, rho.ln());
2.0 * (chebyshev_tail_tolerance() + REFERENCE_RTOL) * panel.scale[m]
/ envelope(self.alpha(m), rho)
}
pub(crate) fn reference(&self, rho: f64) -> Result<[f64; CHANNELS], BasisError> {
self.shape.reference(rho, CHANNELS)
}
pub(crate) fn panel_counts(&self) -> (usize, usize) {
(self.low.len(), self.main.len())
}
}
fn chebyshev_nodes() -> &'static [f64; PANEL_ORDER] {
static NODES: OnceLock<[f64; PANEL_ORDER]> = OnceLock::new();
NODES.get_or_init(|| {
std::array::from_fn(|i| {
(std::f64::consts::PI * (i as f64 + 0.5) / PANEL_ORDER as f64).cos()
})
})
}
fn chebyshev_coefficients(values: &[f64; PANEL_ORDER]) -> [f64; PANEL_ORDER] {
let n = PANEL_ORDER as f64;
std::array::from_fn(|k| {
let mut acc = KahanSum::default();
for (i, &v) in values.iter().enumerate() {
acc.add(v * (std::f64::consts::PI * k as f64 * (i as f64 + 0.5) / n).cos());
}
2.0 * acc.sum() / n
})
}
enum PanelVerdict {
Certified(Panel),
Split([(f64, f64); CHANNELS]),
}
fn sample_panel(
shape: &ProfileShape,
channels: usize,
u_lo: f64,
u_hi: f64,
) -> Result<PanelVerdict, BasisError> {
use rayon::prelude::*;
let sampled: Vec<Result<[f64; CHANNELS], BasisError>> = chebyshev_nodes()
.par_iter()
.map(|&x| {
let u = 0.5 * (u_lo + u_hi) + 0.5 * (u_hi - u_lo) * x;
let rho = u.exp();
let g = shape.reference(rho, channels)?;
Ok(std::array::from_fn(|m| {
let alpha = shape.d as i32 - 2 * shape.p as i32 + m as i32;
g[m] * envelope(alpha, rho)
}))
})
.collect();
let mut values = [[0.0_f64; PANEL_ORDER]; CHANNELS];
for (i, node) in sampled.into_iter().enumerate() {
let node = node?;
for m in 0..channels {
values[m][i] = node[m];
}
}
let mut coeff = [[0.0_f64; PANEL_ORDER]; CHANNELS];
let mut scale = [0.0_f64; CHANNELS];
let mut certified = true;
let mut measured = [(0.0_f64, 0.0_f64); CHANNELS];
for m in 0..channels {
let samples = &values[m];
scale[m] = samples.iter().fold(0.0_f64, |acc, v| acc.max(v.abs()));
if !scale[m].is_finite() {
crate::bail_invalid_basis!(
"Duchon radial profile channel {m} is not finite on u ∈ [{u_lo:.3}, {u_hi:.3}] \
(p={}, s={}, d={})",
shape.p,
shape.s,
shape.d
);
}
coeff[m] = chebyshev_coefficients(samples);
let largest = coeff[m].iter().fold(0.0_f64, |acc, c| acc.max(c.abs()));
let tail = coeff[m][PANEL_ORDER - 1]
.abs()
.max(coeff[m][PANEL_ORDER - 2].abs());
let tail_ok = tail <= chebyshev_tail_tolerance() * largest;
let sign_definite = m == 0
&& (samples.iter().all(|v| *v > 0.0) || samples.iter().all(|v| *v < 0.0));
let range = if sign_definite {
let smallest = samples.iter().fold(f64::INFINITY, |acc, v| acc.min(v.abs()));
scale[m] / smallest
} else {
0.0
};
let range_ok = range <= PANEL_DYNAMIC_RANGE;
measured[m] = (if largest > 0.0 { tail / largest } else { 0.0 }, range);
certified &= tail_ok && range_ok;
}
if certified {
Ok(PanelVerdict::Certified(Panel {
u_lo,
u_hi,
coeff,
scale,
}))
} else {
Ok(PanelVerdict::Split(measured))
}
}
fn build_panels(
shape: &ProfileShape,
channels: usize,
u_lo: f64,
u_hi: f64,
) -> Result<Vec<Panel>, BasisError> {
use rayon::prelude::*;
let min_width = (u_hi - u_lo) * 2.0_f64.powi(-10);
let mut certified: Vec<Panel> = Vec::new();
let mut frontier = vec![(u_lo, u_hi)];
while !frontier.is_empty() {
let verdicts: Vec<Result<(f64, f64, PanelVerdict), BasisError>> = frontier
.par_iter()
.map(|&(a, b)| sample_panel(shape, channels, a, b).map(|v| (a, b, v)))
.collect();
let mut next = Vec::new();
for verdict in verdicts {
let (a, b, verdict) = verdict?;
match verdict {
PanelVerdict::Certified(panel) => certified.push(panel),
PanelVerdict::Split(measured) => {
if b - a < min_width {
let report: Vec<String> = measured
.iter()
.take(channels)
.enumerate()
.map(|(m, (tail, range))| {
format!("m={m}: tail/largest={tail:.2e} range={range:.2e}")
})
.collect();
crate::bail_invalid_basis!(
"Duchon radial profile does not certify on u ∈ [{a:.6}, {b:.6}] \
(p={}, s={}, d={}; bars tail ≤ {:.2e}, range ≤ {:.3}): {}",
shape.p,
shape.s,
shape.d,
chebyshev_tail_tolerance(),
PANEL_DYNAMIC_RANGE,
report.join("; ")
);
}
let mid = 0.5 * (a + b);
next.push((a, mid));
next.push((mid, b));
}
}
}
frontier = next;
}
certified.sort_by(|x, y| x.u_lo.total_cmp(&y.u_lo));
Ok(certified)
}
const PROFILE_INDEX_SLOTS: usize = 64;
static PROFILE_STORE: [OnceLock<DuchonRadialProfile>; PROFILE_INDEX_SLOTS] =
[const { OnceLock::new() }; PROFILE_INDEX_SLOTS];
static PROFILE_INDEX: [OnceLock<((usize, usize, usize), &'static DuchonRadialProfile)>;
PROFILE_INDEX_SLOTS] = [const { OnceLock::new() }; PROFILE_INDEX_SLOTS];
static PROFILE_INTERN_CALLS: AtomicUsize = AtomicUsize::new(0);
fn intern_duchon_radial_profile(
shape: (usize, usize, usize),
) -> Result<&'static DuchonRadialProfile, BasisError> {
PROFILE_INTERN_CALLS.fetch_add(1, Ordering::Relaxed);
static PROFILE_SLOTS: OnceLock<Mutex<HashMap<(usize, usize, usize), usize>>> = OnceLock::new();
let slots = PROFILE_SLOTS.get_or_init(|| Mutex::new(HashMap::new()));
if let Some(&slot) = slots
.lock()
.expect("Duchon radial profile cache poisoned")
.get(&shape)
{
return published_profile(slot);
}
let (p, s, d) = shape;
let built = DuchonRadialProfile::build(p, s, d)?;
let mut guard = slots.lock().expect("Duchon radial profile cache poisoned");
if let Some(&slot) = guard.get(&shape) {
return published_profile(slot);
}
let slot = guard.len();
let cell = PROFILE_STORE.get(slot).ok_or_else(|| {
BasisError::InvalidInput(format!(
"this process has already interned {PROFILE_INDEX_SLOTS} distinct Duchon radial \
shapes and (p={p}, s={s}, d={d}) is one more; the profile store is fixed for the \
process's life"
))
})?;
let profile = cell.get_or_init(|| built);
guard.insert(shape, slot);
Ok(profile)
}
fn published_profile(slot: usize) -> Result<&'static DuchonRadialProfile, BasisError> {
PROFILE_STORE
.get(slot)
.and_then(OnceLock::get)
.ok_or_else(|| {
BasisError::InvalidInput(format!(
"Duchon radial profile slot {slot} is mapped but carries no published profile"
))
})
}
pub(crate) fn duchon_radial_profile(
p: usize,
s: usize,
d: usize,
) -> Result<&'static DuchonRadialProfile, BasisError> {
let shape = (p, s, d);
for slot in PROFILE_INDEX.iter() {
match slot.get() {
Some(&(indexed, profile)) if indexed == shape => return Ok(profile),
Some(_) => continue,
None => break,
}
}
let profile = intern_duchon_radial_profile(shape)?;
for slot in PROFILE_INDEX.iter() {
if slot.get().is_some() {
continue;
}
if slot.set((shape, profile)).is_ok() {
break;
}
}
Ok(profile)
}
#[cfg(test)]
mod tests {
use super::*;
const SHAPES: [(usize, usize, usize); 13] = [
(6, 1, 3),
(6, 2, 2),
(5, 1, 3),
(5, 2, 2),
(3, 1, 2),
(4, 1, 3),
(16, 1, 9),
(10, 2, 5),
(9, 3, 4),
(3, 1, 1),
(3, 1, 9),
(10, 1, 4),
(16, 2, 4),
];
fn probe_radii() -> Vec<f64> {
let lo = rho_derivative_floor().ln();
let hi = rho_ceiling().ln();
(0..40)
.map(|i| (lo + (i as f64 + 0.37) / 40.0 * (hi - lo)).exp())
.collect()
}
#[test]
fn every_channel_matches_its_reference_integral_within_the_certificate() {
for &(d, p, s) in &SHAPES {
let profile = duchon_radial_profile(p, s, d).expect("profile builds");
let (low, main) = profile.panel_counts();
assert!(low >= 1 && main >= 4, "(d={d}, p={p}, s={s}) panel counts {low}/{main}");
for rho in probe_radii() {
let reference = profile.reference(rho).expect("reference converges");
for m in 0..CHANNELS {
let got = profile.derivative(m, rho);
let bar = profile.resolution(m, rho);
assert!(
(got - reference[m]).abs() <= bar,
"(d={d}, p={p}, s={s}) m={m} rho={rho:.3e}: profile {got:.16e} vs reference \
{:.16e}, |Δ|={:.3e} > resolution {bar:.3e}",
reference[m],
(got - reference[m]).abs()
);
}
}
}
}
#[test]
fn the_value_channel_below_the_derivative_floor_matches_its_reference() {
for &(d, p, s) in &SHAPES {
let profile = duchon_radial_profile(p, s, d).expect("profile builds");
for rho in [2.0e-9_f64, 1.0e-8, 3.0e-8, 2.0e-7]
.into_iter()
.filter(|&rho| rho > rho_value_floor(d, p))
{
let reference = profile.reference(rho).expect("reference converges")[0];
let got = profile.value(rho);
let bar = profile.resolution(0, rho);
assert!(
(got - reference).abs() <= bar,
"(d={d}, p={p}, s={s}) rho={rho:.1e}: {got:.16e} vs {reference:.16e} (bar {bar:.2e})"
);
}
}
}
#[test]
fn the_origin_value_is_the_closed_form_and_the_profile_reaches_it() {
for &(d, p, s) in SHAPES.iter().filter(|(d, p, s)| 2 * (p + s) > *d) {
let profile = duchon_radial_profile(p, s, d).expect("profile builds");
let g0 = profile.origin_value().expect("b > 0 has an origin value");
let rho = 1.5 * rho_value_floor(d, p);
let near = profile.value(rho);
let bar = profile.resolution(0, rho) + 8.0 * f64::EPSILON * g0;
assert!(
(near - g0).abs() <= bar,
"(d={d}, p={p}, s={s}): G(ρ_lo) = {near:.16e} vs G(0) = {g0:.16e} (bar {bar:.2e})"
);
let below = profile.value(0.5 * rho_value_floor(d, p));
assert_eq!(below.to_bits(), g0.to_bits(), "below the value floor the profile is G(0)");
}
}
#[test]
fn the_large_radius_profile_is_the_polyharmonic_tail() {
for &(d, p, s) in SHAPES.iter().filter(|(_, p, _)| *p == 1) {
let profile = duchon_radial_profile(p, s, d).expect("profile builds");
let b = p as f64 + s as f64 - 0.5 * d as f64;
let tail_constant = 2.0_f64.powi(d as i32 - 2 * p as i32)
* gamma_lanczos(s as f64 - b)
* gamma_lanczos(s as f64);
for rho in [1.0e3_f64, 1.0e5, 1.0e9] {
let law = tail_constant * rho.powi(-(d as i32 - 2 * p as i32));
let got = profile.value(rho);
assert!(
((got - law) / law).abs() <= 1e-12,
"(d={d}, p={p}, s={s}): G({rho:.0e}) = {got:.16e} vs tail law {law:.16e}"
);
}
}
}
#[test]
fn the_profile_agrees_with_an_independent_quadpack_oracle() {
let rows: [(usize, usize, usize, f64, [f64; 5]); 27] = [
(6, 1, 3, 3.0e-01, [4.6985918744347777e-01, -1.5280509899838315e-01, -1.9816847111553884e-01, 9.2561975226344528e-01, -3.9694341175201573e+00]),
(6, 1, 3, 3.0e+00, [1.0914697710866623e-01, -6.5216440555166594e-02, 3.9215058819385841e-02, -2.1276924973236151e-02, 6.9381035837886417e-03]),
(6, 1, 3, 3.0e+01, [3.9506172790671214e-05, -5.2674896620681878e-06, 8.7791490102848121e-07, -1.7558293827270790e-07, 4.0969311059236578e-08]),
(6, 1, 3, 3.0e+02, [3.9506172839506188e-09, -5.2674897119341561e-11, 8.7791495198902613e-13, -1.7558299039780523e-14, 4.0969364426154538e-16]),
(6, 2, 2, 1.0e+00, [4.0505129895177810e-01, -1.1956079034782202e-01, 4.1440616094682572e-03, 7.2017547769282639e-02, -1.6811933626324055e-01]),
(6, 2, 2, 1.0e+01, [3.6810901080102675e-02, -6.7329643588438281e-03, 1.7756624741639996e-03, -5.9526359784749206e-04, 2.3537927072176507e-04]),
(6, 2, 2, 3.0e+02, [4.4440493827160507e-05, -2.9624362139917695e-07, 2.9620850480109740e-09, -3.9488614540466389e-11, 6.5802652034750789e-13]),
(6, 2, 2, 1.0e+04, [3.9999996800000020e-08, -7.9999987200000017e-12, 2.3999993600000012e-15, -9.5999961600000003e-19, 4.7999973120000002e-22]),
(5, 1, 3, 3.0e-01, [5.7728516709470501e-01, -8.2898535984488389e-02, -2.0775229494176811e-01, 3.9871733020347994e-01, -5.3363865834983748e-01]),
(5, 1, 3, 1.0e+01, [1.4059892518100654e-02, -4.1294515470580041e-03, 1.5713113384848453e-03, -7.1323331693790200e-04, 3.6356456589205617e-04]),
(5, 1, 3, 1.0e+03, [1.4179630807244130e-08, -4.2538892421732390e-11, 1.7015556968692956e-13, -8.5077784843464781e-16, 5.1046670906078874e-18]),
(5, 2, 2, 1.0e+00, [1.1021648736861631e+00, -1.2897521047617824e-01, -5.3421239984428652e-02, 1.0165169935384380e-01, -1.1354669441816742e-01]),
(5, 2, 2, 3.0e+01, [1.1763841854900088e-01, -3.8862691842207011e-03, 2.5558346888487324e-04, -2.5091527780010377e-05, 3.2677338632772836e-06]),
(5, 2, 2, 3.0e+02, [1.1815833834525396e-02, -3.9382611638342123e-05, 2.6251573282152214e-07, -2.6246905090939952e-09, 3.4988093135899478e-11]),
(3, 1, 2, 3.0e-01, [1.7495188441799354e+00, -1.4177634563722352e-01, -3.6789047048678736e-01, 6.1508378604185920e-01, -7.6040920150538838e-01]),
(3, 1, 2, 3.0e+00, [1.0345604321804829e+00, -2.2719310265493881e-01, 6.3216787382709264e-02, -4.3866000134315120e-03, -2.3566293666730211e-02]),
(3, 1, 2, 1.0e+02, [3.5449077018110321e-02, -3.5449077018110320e-04, 7.0898154036220673e-06, -2.1269446210866188e-07, 8.5077784843464801e-09]),
(4, 1, 3, 1.0e+00, [8.9873717526205488e-01, -1.7263545188893351e-01, -8.4000874530434450e-02, 1.5512070616520965e-01, -1.1443744630115851e-01]),
(4, 1, 3, 3.0e+01, [8.8888888881474906e-03, -5.9259259186006819e-04, 5.9259258535687199e-05, -7.9012338533590316e-06, 1.3168717225478964e-06]),
(8, 1, 4, 3.0e+00, [6.0953872337774703e-02, -4.1594882419161060e-02, 2.7575716872150619e-02, -1.6382052326866139e-02, 6.4381022935026989e-03]),
(8, 1, 4, 1.0e+02, [7.6800000000000004e-10, -4.6079999999999998e-11, 3.2255999999999995e-12, -2.5804799999999990e-13, 2.3224319999999999e-14]),
(16, 1, 9, 1.0e+00, [1.1886003289332642e-01, -3.9201561871392100e-02, -1.3883802126352989e-02, 4.1258042065121049e-02, -4.0220630582165504e-02]),
(16, 1, 9, 1.0e+01, [3.3036650726599696e-04, -2.4741494010306798e-04, 1.8463467561634622e-04, -1.3626363127830295e-04, 9.8500625583362153e-05]),
(16, 1, 9, 3.0e+02, [9.9443269149350568e-24, -4.6406858936363596e-25, 2.3203429468181794e-26, -1.2375162383030290e-27, 7.0125920170504993e-29]),
(10, 2, 5, 3.0e+00, [3.6612100958410623e-02, -1.5401471911417455e-02, 4.7637380557147158e-03, 5.0292547448374211e-04, -2.8214782576543706e-03]),
(10, 2, 5, 1.0e+03, [3.0718156800000003e-15, -1.8430525439999997e-17, 1.2901072896000000e-19, -1.0320592895999999e-21, 9.2882681855999984e-24]),
(9, 3, 4, 1.0e+01, [2.7040056431742739e-02, -5.8916911171334472e-03, 1.5341464221588009e-03, -4.3208855617272632e-04, 1.1618407984814464e-04]),
];
for &(d, p, s, rho, want) in &rows {
let profile = duchon_radial_profile(p, s, d).expect("profile builds");
let got = profile.derivatives(rho);
for m in 0..CHANNELS {
let rel = ((got[m] - want[m]) / want[m]).abs();
assert!(
rel <= 1e-11,
"(d={d}, p={p}, s={s}) rho={rho}: m={m} profile {:.16e} vs QUADPACK {:.16e} (rel {rel:.2e})",
got[m],
want[m]
);
}
}
}
#[test]
fn derivative_channels_are_finite_differences_of_the_lower_channel() {
for &(d, p, s) in &SHAPES {
let profile = duchon_radial_profile(p, s, d).expect("profile builds");
for rho in [0.3_f64, 1.0, 4.0, 20.0, 200.0] {
for m in 0..2 {
let h = rho * f64::EPSILON.cbrt();
let fd = (profile.derivative(m, rho + h) - profile.derivative(m, rho - h))
/ (2.0 * h);
let exact = profile.derivative(m + 1, rho);
let truncation = h * h / 6.0 * profile.derivative(m + 3, rho).abs();
let rounding = 2.0 * profile.resolution(m, rho) / h
+ 4.0 * f64::EPSILON * profile.derivative(m, rho).abs() / h;
let bar = 2.0 * (truncation + rounding) + profile.resolution(m + 1, rho);
assert!(
(fd - exact).abs() <= bar,
"(d={d}, p={p}, s={s}) m={m} rho={rho}: FD {fd:.10e} vs channel {exact:.10e}, \
|Δ|={:.3e} > {bar:.3e}",
(fd - exact).abs()
);
}
}
}
}
#[test]
fn term_evaluator_forms_agree_where_they_meet() {
for b in [1.0_f64, 2.0, 3.0] {
let evaluator = TermEvaluator::new(b);
let z = SERIES_CROSSOVER_Z;
let series = evaluator.evaluate(z);
let ladder = bessel_k_ladder(b, z);
let recurrence = evaluator.recurrence.evaluate(z, pow_b(z, b), &ladder);
for m in 0..CHANNELS {
let scale = recurrence[m].abs().max(1e-300);
assert!(
((series[m] - recurrence[m]) / scale).abs() <= 1e-11,
"b={b} m={m}: series {:.16e} vs recurrence {:.16e}",
series[m],
recurrence[m]
);
}
}
for b in [0.5_f64, 1.5, 2.5] {
let evaluator = TermEvaluator::new(b);
let z = 6.0;
let closed = evaluator.evaluate(z);
let ladder = bessel_k_ladder(b, z);
let recurrence = evaluator.recurrence.evaluate(z, pow_b(z, b), &ladder);
for m in 0..CHANNELS {
let scale = recurrence[m].abs().max(1e-300);
assert!(
((closed[m] - recurrence[m]) / scale).abs() <= 1e-12,
"b={b} m={m}: closed form {:.16e} vs recurrence {:.16e}",
closed[m],
recurrence[m]
);
}
}
}
#[test]
fn profile_builds_report_their_panel_counts_and_build_time() {
for &(d, p, s) in &SHAPES {
let start = std::time::Instant::now();
let built = DuchonRadialProfile::build(p, s, d);
let elapsed = start.elapsed().as_secs_f64();
match built {
Ok(profile) => {
let (low, main) = profile.panel_counts();
eprintln!("[profile-build] (d={d}, p={p}, s={s}): low={low} main={main} in {elapsed:.3}s");
}
Err(error) => {
eprintln!("[profile-build] (d={d}, p={p}, s={s}): REFUSED after {elapsed:.3}s: {error}");
panic!("(d={d}, p={p}, s={s}) must build: {error}");
}
}
}
}
#[test]
fn the_profile_index_hands_every_thread_one_interned_profile_per_shape() {
let shapes = [(1_usize, 3_usize, 6_usize), (2, 2, 6), (1, 2, 3)];
let first: Vec<&'static DuchonRadialProfile> = shapes
.iter()
.map(|&(p, s, d)| duchon_radial_profile(p, s, d).expect("profile builds"))
.collect();
let handles: Vec<_> = (0..4)
.map(|_| {
std::thread::spawn(move || {
shapes
.iter()
.map(|&(p, s, d)| duchon_radial_profile(p, s, d).expect("profile builds"))
.collect::<Vec<&'static DuchonRadialProfile>>()
})
})
.collect();
for handle in handles {
let seen = handle.join().expect("lookup thread joins");
for (got, want) in seen.iter().zip(first.iter()) {
assert!(
std::ptr::eq(*got, *want),
"a second thread got a different profile for the same shape"
);
}
}
let interned_before = PROFILE_INTERN_CALLS.load(Ordering::Relaxed);
let hammer: Vec<_> = (0..4)
.map(|_| {
std::thread::spawn(move || {
for _ in 0..64 {
for &(p, s, d) in shapes.iter() {
duchon_radial_profile(p, s, d).expect("profile builds");
}
}
})
})
.collect();
for handle in hammer {
handle.join().expect("lookup thread joins");
}
let interned_after = PROFILE_INTERN_CALLS.load(Ordering::Relaxed);
assert!(
interned_after - interned_before <= SHAPES.len() + shapes.len(),
"a published shape must be answered from the lock-free index, not the interning map: {} interns across {} lookups of already-published shapes",
interned_after - interned_before,
4 * 64 * shapes.len()
);
for (&(p, s, d), profile) in shapes.iter().zip(first.iter()) {
let want = (4.0 * std::f64::consts::PI).powf(-0.5 * d as f64)
/ (gamma_lanczos(p as f64) * gamma_lanczos(s as f64));
assert_eq!(profile.kappa_scale(1.0), want);
}
}
#[test]
fn shapes_outside_the_single_integral_regime_are_refused() {
assert!(
DuchonRadialProfile::build(3, 1, 6).is_err(),
"2p = d is the partial-fraction regime"
);
assert!(
DuchonRadialProfile::build(1, 0, 6).is_err(),
"s = 0 is pure polyharmonic"
);
let singular = duchon_radial_profile(1, 1, 6).expect("2(p + s) ≤ d builds: finite away from the origin");
assert!(singular.origin_value().is_err(), "but it has no origin value");
assert!(
DuchonRadialProfile::build(0, 1, 16).is_err(),
"p = 0 is a bare Matérn block, outside the single-integral reduction"
);
}
}