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 {
u_lo: f64,
u_hi: f64,
m: usize,
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
}
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,
})
}
fn certify(&self, kind: &RadialScalarKind) -> bool {
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 > PROFILE_CERT_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]
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)
}
}
}
#[cfg(test)]
mod tests {
use super::super::duchon_partial_fraction_coeffs;
use super::*;
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]
fn duchon_profile_certifies_and_matches_exact_on_dense_grid() {
let kind = production_duchon_kind();
let (r_min, r_max) = (0.05_f64, 30.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]
fn out_of_range_radii_fall_back_to_exact() {
let kind = production_duchon_kind();
let profile = RadialProfile::build(&kind, 0.1, 10.0).expect("profile certifies");
for &r in &[0.01_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]
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());
}
}