use super::{BasisError, RadialScalarKind};
pub const PROFILE_CERT_RTOL: f64 = 1.0e-13;
pub const PROFILE_SPOT_RTOL: f64 = 1.0e-12;
pub const PROFILE_NODE_LADDER: [usize; 3] = [64, 128, 256];
pub const PROFILE_SPOT_CHECK_POINTS: usize = 5;
pub struct RadialProfile {
pub(crate) u_lo: f64,
pub(crate) u_hi: f64,
pub(crate) m: usize,
pub(crate) coeff: [Vec<f64>; 3],
}
impl RadialProfile {
pub fn build(kind: &RadialScalarKind, r_min: f64, r_max: f64) -> Option<Self> {
if !(r_min.is_finite() && r_max.is_finite()) || r_min <= 0.0 || r_max <= r_min {
return None;
}
let u_lo = r_min.ln();
let u_hi = r_max.ln();
for &m in PROFILE_NODE_LADDER.iter() {
let Some(profile) = Self::build_at(kind, u_lo, u_hi, m) else {
return None;
};
if profile.certify(kind) {
return Some(profile);
}
}
None
}
pub(crate) fn build_at(
kind: &RadialScalarKind,
u_lo: f64,
u_hi: f64,
m: usize,
) -> Option<Self> {
let mut values: [Vec<f64>; 3] = [vec![0.0; m], vec![0.0; m], vec![0.0; m]];
let mut nodes_x = vec![0.0_f64; m];
for (i, x_slot) in nodes_x.iter_mut().enumerate() {
let x = (std::f64::consts::PI * (2 * i + 1) as f64 / (2 * m) as f64).cos();
*x_slot = x;
let u = 0.5 * (u_lo + u_hi) + 0.5 * (u_hi - u_lo) * x;
let r = u.exp();
let (phi, q, t) = kind.eval_design_triplet(r).ok()?;
if !(phi.is_finite() && q.is_finite() && t.is_finite()) {
return None;
}
values[0][i] = phi;
values[1][i] = q;
values[2][i] = t;
}
let mut basis = vec![0.0_f64; m * m];
for (i, &x) in nodes_x.iter().enumerate() {
basis[i * m] = 1.0;
if m > 1 {
basis[i * m + 1] = x;
}
for p in 2..m {
basis[i * m + p] = 2.0 * x * basis[i * m + p - 1] - basis[i * m + p - 2];
}
}
let coeff = values.map(|vals| {
let mut c = vec![0.0_f64; m];
for (p, c_slot) in c.iter_mut().enumerate() {
let mut acc = 0.0_f64;
for (i, &v) in vals.iter().enumerate() {
acc += v * basis[i * m + p];
}
let gamma = if p == 0 { 1.0 } else { 2.0 };
*c_slot = gamma * acc / m as f64;
}
c
});
Some(Self {
u_lo,
u_hi,
m,
coeff,
})
}
pub(crate) fn certify(&self, kind: &RadialScalarKind) -> bool {
let tail_rtol = PROFILE_CERT_RTOL;
let tail_band = (self.m / 16).max(2);
for c in &self.coeff {
let scale = c.iter().fold(0.0_f64, |a, &v| a.max(v.abs()));
if scale == 0.0 {
continue;
}
let tail = c[self.m - tail_band..]
.iter()
.fold(0.0_f64, |a, &v| a.max(v.abs()));
if tail > tail_rtol * scale {
return false;
}
}
let phi_ratio = 0.618_033_988_749_894_9_f64;
for s in 1..=PROFILE_SPOT_CHECK_POINTS {
let f = (0.37 + s as f64 * phi_ratio).fract();
let u = self.u_lo + f * (self.u_hi - self.u_lo);
let r = u.exp();
let Ok((phi_e, q_e, t_e)) = kind.eval_design_triplet(r) else {
return false;
};
let (phi_i, q_i, t_i) = self.eval_inside(r);
for (interp, exact) in [(phi_i, phi_e), (q_i, q_e), (t_i, t_e)] {
let scale = exact.abs().max(interp.abs()).max(f64::MIN_POSITIVE);
if (interp - exact).abs() > PROFILE_SPOT_RTOL * scale {
return false;
}
}
}
true
}
#[inline]
pub fn covers(&self, r: f64) -> bool {
if !(r > 0.0) {
return false;
}
let u = r.ln();
u >= self.u_lo && u <= self.u_hi
}
#[inline]
pub(crate) fn eval_inside(&self, r: f64) -> (f64, f64, f64) {
let u = r.ln();
let x = (2.0 * u - (self.u_lo + self.u_hi)) / (self.u_hi - self.u_lo);
let two_x = 2.0 * x;
let mut out = [0.0_f64; 3];
for (c, slot) in self.coeff.iter().zip(out.iter_mut()) {
let mut b1 = 0.0_f64;
let mut b2 = 0.0_f64;
for &a in c.iter().skip(1).rev() {
let b0 = a + two_x * b1 - b2;
b2 = b1;
b1 = b0;
}
*slot = c[0] + x * b1 - b2;
}
(out[0], out[1], out[2])
}
#[inline]
pub fn eval_or_exact(
&self,
kind: &RadialScalarKind,
r: f64,
) -> Result<(f64, f64, f64), BasisError> {
if self.covers(r) {
Ok(self.eval_inside(r))
} else {
kind.eval_design_triplet(r)
}
}
pub(crate) fn differentiate_coeffs(a: &[f64]) -> Vec<f64> {
let len = a.len();
let mut b = vec![0.0_f64; len];
if len <= 1 {
return b;
}
let big_d = len - 1;
b[big_d - 1] = 2.0 * big_d as f64 * a[big_d];
for d in (1..big_d).rev() {
b[d - 1] = b[d + 1] + 2.0 * d as f64 * a[d];
}
b[0] *= 0.5;
b
}
#[inline]
pub(crate) fn clenshaw(c: &[f64], x: f64, two_x: f64) -> f64 {
let mut b1 = 0.0_f64;
let mut b2 = 0.0_f64;
for &a in c.iter().skip(1).rev() {
let b0 = a + two_x * b1 - b2;
b2 = b1;
b1 = b0;
}
c.first().copied().unwrap_or(0.0) + x * b1 - b2
}
#[inline]
pub fn eval_jets_inside(&self, r: f64) -> [[f64; 4]; 3] {
let u = r.ln();
let x = (2.0 * u - (self.u_lo + self.u_hi)) / (self.u_hi - self.u_lo);
let two_x = 2.0 * x;
let s = 2.0 / (self.u_hi - self.u_lo); let inv_r = 1.0 / r;
let mut out = [[0.0_f64; 4]; 3];
for (ci, c0) in self.coeff.iter().enumerate() {
let c1 = Self::differentiate_coeffs(c0); let c2 = Self::differentiate_coeffs(&c1); let c3 = Self::differentiate_coeffs(&c2); let j_val = Self::clenshaw(c0, x, two_x);
let j_x = Self::clenshaw(&c1, x, two_x);
let j_xx = Self::clenshaw(&c2, x, two_x);
let j_xxx = Self::clenshaw(&c3, x, two_x);
let j_u = s * j_x;
let j_uu = s * s * j_xx;
let j_uuu = s * s * s * j_xxx;
out[ci][0] = j_val; out[ci][1] = j_u * inv_r; out[ci][2] = (j_uu - j_u) * inv_r * inv_r; out[ci][3] = (j_uuu - 3.0 * j_uu + 2.0 * j_u) * inv_r * inv_r * inv_r; }
out
}
}
#[cfg(test)]
mod tests {
use super::super::duchon_partial_fraction_coeffs;
use super::*;
pub(crate) fn production_duchon_kind() -> RadialScalarKind {
let (p_order, s_order, dim, length_scale) = (1usize, 9usize, 16usize, 1.0_f64);
RadialScalarKind::Duchon {
length_scale,
p_order,
s_order,
dim,
coeffs: duchon_partial_fraction_coeffs(p_order, s_order, 1.0 / length_scale),
}
}
#[test]
pub(crate) fn duchon_profile_certifies_and_matches_exact_on_dense_grid() {
let kind = production_duchon_kind();
let (r_min, r_max) = (1.0_f64, 10.0_f64);
let profile =
RadialProfile::build(&kind, r_min, r_max).expect("production Duchon profile certifies");
let n = 2_000usize;
for i in 0..n {
let r = r_min * (r_max / r_min).powf((i as f64 + 0.5) / n as f64);
let (phi_e, q_e, t_e) = kind.eval_design_triplet(r).expect("exact triplet");
let (phi_i, q_i, t_i) = profile.eval_or_exact(&kind, r).expect("profile eval");
for (interp, exact) in [(phi_i, phi_e), (q_i, q_e), (t_i, t_e)] {
let scale = exact.abs().max(interp.abs()).max(f64::MIN_POSITIVE);
assert!(
(interp - exact).abs() <= 1.0e-11 * scale,
"profile vs exact at r={r}: {interp:e} vs {exact:e}"
);
}
}
}
#[test]
pub(crate) fn out_of_range_radii_fall_back_to_exact() {
let kind = production_duchon_kind();
let profile = RadialProfile::build(&kind, 1.0, 10.0).expect("profile certifies");
for &r in &[0.5_f64, 50.0] {
assert!(!profile.covers(r));
let exact = kind.eval_design_triplet(r).expect("exact");
let via = profile.eval_or_exact(&kind, r).expect("fallback");
assert_eq!(exact, via, "fallback must be the exact evaluator verbatim");
}
}
#[test]
pub(crate) fn jet_tower_matches_central_differences_of_value_clenshaw() {
let kind = production_duchon_kind();
let (r_min, r_max) = (1.0_f64, 10.0_f64);
let profile =
RadialProfile::build(&kind, r_min, r_max).expect("production Duchon profile certifies");
let n = 200usize;
let (lo, hi) = (r_min * 1.2, r_max / 1.2);
for i in 0..n {
let r = lo * (hi / lo).powf((i as f64 + 0.5) / n as f64);
let h = r * 1.0e-4;
let jets = profile.eval_jets_inside(r);
for (ci, jet) in jets.iter().enumerate() {
let v = |rr: f64| profile.eval_inside(rr);
let val = |rr: f64| {
let (p, q, t) = v(rr);
[p, q, t][ci]
};
let f_m2 = val(r - 2.0 * h);
let f_m1 = val(r - h);
let f_p1 = val(r + h);
let f_p2 = val(r + 2.0 * h);
let f_0 = val(r);
let d1 = (-f_p2 + 8.0 * f_p1 - 8.0 * f_m1 + f_m2) / (12.0 * h);
let d2 = (-f_p2 + 16.0 * f_p1 - 30.0 * f_0 + 16.0 * f_m1 - f_m2) / (12.0 * h * h);
let d3 = (f_p2 - 2.0 * f_p1 + 2.0 * f_m1 - f_m2) / (2.0 * h * h * h);
let checks = [(jet[0], f_0), (jet[1], d1), (jet[2], d2), (jet[3], d3)];
let rtols = [1.0e-10_f64, 1.0e-5, 1.0e-3, 5.0e-2];
for (k, (jet_v, fd_v)) in checks.iter().enumerate() {
let scale = jet_v.abs().max(fd_v.abs()).max(1.0e-8);
assert!(
(jet_v - fd_v).abs() <= rtols[k] * scale,
"channel {ci} order {k} at r={r}: jet={jet_v:e} fd={fd_v:e}"
);
}
}
}
}
#[test]
pub(crate) fn degenerate_range_refuses() {
let kind = production_duchon_kind();
assert!(RadialProfile::build(&kind, 1.0, 1.0).is_none());
assert!(RadialProfile::build(&kind, -1.0, 2.0).is_none());
}
#[test]
pub(crate) fn production_duchon_operator_samples_are_stable_not_cancellation_noisy() {
let kind = production_duchon_kind();
assert!(
RadialProfile::build(&kind, 1.0, 10.0).is_some(),
"production Duchon profile must certify on [1, 10] under the strict \
tail gate now that the operator core is cancellation-free"
);
for &r in &[1.3_f64, 2.7, 5.0, 8.0] {
let (_phi, q, t) = kind.eval_design_triplet(r).expect("triplet");
let h = r * 1.0e-5;
let phi = |rr: f64| kind.eval_design_triplet(rr).expect("phi").0;
let phi_p = (phi(r + h) - phi(r - h)) / (2.0 * h);
let phi_pp = (phi(r + h) - 2.0 * phi(r) + phi(r - h)) / (h * h);
let q_fd = phi_p / r;
let t_fd = (phi_pp - q_fd) / (r * r);
assert!(
(q - q_fd).abs() <= 1.0e-6 * q.abs().max(1.0e-300),
"q at r={r}: stable={q:e} vs φ′/r central-diff={q_fd:e}"
);
assert!(
(t - t_fd).abs() <= 1.0e-4 * t.abs().max(1.0e-300),
"t at r={r}: stable={t:e} vs (φ″−q)/r² central-diff={t_fd:e}"
);
}
}
}