use super::BasisError;
use super::polylog::{dilog_unit, trilog_unit};
use super::sphere_half_angle::HalfAngleSeparation;
use super::sphere_spec::SphereWahbaKernel;
use super::sphere_spectral::{
pseudo_s2_truncated_coefficients, sobolev_s2_truncated_coefficients,
sphere_truncated_spectral_derivative_eval, sphere_truncated_spectral_eval,
};
#[inline]
pub(crate) fn wahba_sphere_kernel_pseudo_coincident(m: usize) -> f64 {
let m_eff = m.clamp(1, 4);
let factorial = (1..=(m_eff + 1)).map(|k| k as f64).product::<f64>();
1.0 / (2.0 * std::f64::consts::PI * (m_eff as f64) * factorial)
}
#[inline]
pub(crate) fn wahba_sphere_kernel_pseudo(sep: HalfAngleSeparation, m: usize) -> f64 {
let w = sep.u;
if w <= 0.0 {
return wahba_sphere_kernel_pseudo_coincident(m);
}
let c0 = w.sqrt();
let a = (1.0 + 1.0 / c0).ln();
let c = 2.0 * c0;
let two_pi = 2.0 * std::f64::consts::PI;
match m {
1 => {
let q1 = 2.0 * a * w - c + 1.0;
(q1 - 0.5) / two_pi
}
2 => {
let w2 = w * w;
let q2 = a * (6.0 * w2 - 2.0 * w) - 3.0 * c * w + 3.0 * w + 0.5;
(q2 / 2.0 - 1.0 / 6.0) / two_pi
}
3 => {
let w2 = w * w;
let w3 = w2 * w;
let q3 = (a * (60.0 * w3 - 36.0 * w2) + 30.0 * w2 + c * (8.0 * w - 30.0 * w2)
- 3.0 * w
+ 1.0)
/ 3.0;
(q3 / 6.0 - 1.0 / 24.0) / two_pi
}
_ => {
let w2 = w * w;
let w3 = w2 * w;
let w4 = w3 * w;
let q4 = a * (70.0 * w4 - 60.0 * w3 + 6.0 * w2)
+ 35.0 * w3 * (1.0 - c)
+ c * 55.0 * w2 / 3.0
- 12.5 * w2
- w / 3.0
+ 0.25;
(q4 / 24.0 - 1.0 / 120.0) / two_pi
}
}
}
#[inline]
fn wahba_sphere_kernel_pseudo_derivative_coincident(m: usize) -> Option<f64> {
let m_eff = m.clamp(1, 4);
if m_eff < 3 {
return None;
}
let m_f = m_eff as f64;
let factorial = (1..=(m_eff + 1)).map(|k| k as f64).product::<f64>();
Some(-(m_f + 1.0) / (std::f64::consts::PI * (m_f - 2.0) * (m_f - 1.0) * factorial))
}
#[inline]
pub(crate) fn wahba_sphere_kernel_pseudo_derivative_dhav(
sep: HalfAngleSeparation,
m: usize,
) -> f64 {
let w = sep.u;
if w <= 0.0 {
return wahba_sphere_kernel_pseudo_derivative_coincident(m).unwrap_or(f64::NEG_INFINITY);
}
let c0 = w.sqrt();
let a = (1.0 + 1.0 / c0).ln();
let c = 2.0 * c0;
let two_pi = 2.0 * std::f64::consts::PI;
let da_dw = -1.0 / (2.0 * c0 * c0 * (c0 + 1.0));
let dc_dw = 1.0 / c0;
let dk_dw = match m {
1 => {
let dq1_dw = 2.0 * a + 2.0 * w * da_dw - dc_dw;
dq1_dw / two_pi
}
2 => {
let dq2_dw = da_dw * (6.0 * w * w - 2.0 * w) + a * (12.0 * w - 2.0)
- 3.0 * (dc_dw * w + c)
+ 3.0;
(dq2_dw / 2.0) / two_pi
}
3 => {
let w2 = w * w;
let w3 = w2 * w;
let dinner_dw = da_dw * (60.0 * w3 - 36.0 * w2)
+ a * (180.0 * w2 - 72.0 * w)
+ 60.0 * w
+ (dc_dw * (8.0 * w - 30.0 * w2) + c * (8.0 - 60.0 * w))
- 3.0;
let dq3_dw = dinner_dw / 3.0;
(dq3_dw / 6.0) / two_pi
}
_ => {
let w2 = w * w;
let w3 = w2 * w;
let w4 = w3 * w;
let dq4_dw = da_dw * (70.0 * w4 - 60.0 * w3 + 6.0 * w2)
+ a * (280.0 * w3 - 180.0 * w2 + 12.0 * w)
+ 35.0 * (3.0 * w2 * (1.0 - c) - w3 * dc_dw)
+ (55.0 / 3.0) * (dc_dw * w2 + c * 2.0 * w)
- 25.0 * w
- 1.0 / 3.0;
(dq4_dw / 24.0) / two_pi
}
};
dk_dw
}
#[inline]
pub(crate) fn wahba_sphere_kernel_sobolev(sep: HalfAngleSeparation, m: usize) -> f64 {
let four_pi = 4.0 * std::f64::consts::PI;
let pi2_6 = std::f64::consts::PI * std::f64::consts::PI / 6.0;
let u = sep.u;
let one_minus_u = sep.v;
match m {
1 => (-u.ln() - 1.0) / four_pi,
2 => (dilog_unit(one_minus_u) + 1.0 - pi2_6) / four_pi,
3 => {
const ZETA3: f64 = 1.2020569031595942853997381615114499907649862923404988817922;
let li3_u = trilog_unit(u);
let li2_one_minus_u = dilog_unit(one_minus_u);
let cross = if u <= 0.0 {
0.0
} else {
u.ln() * dilog_unit(u)
};
(-2.0 * li3_u - li2_one_minus_u + cross + 2.0 * ZETA3 + pi2_6 - 2.0) / four_pi
}
_ => wahba_sphere_kernel_sobolev_spectral(sep.cos_gamma(), m),
}
}
#[inline]
pub(crate) fn wahba_sphere_kernel_sobolev_spectral(cos_gamma: f64, m: usize) -> f64 {
let l_max = match m {
1 => 4096_usize,
2 => 256,
3 => 128,
_ => 96,
};
let x = cos_gamma.clamp(-1.0, 1.0);
let m_i = m as i32;
let four_pi = 4.0 * std::f64::consts::PI;
let mut p_l_minus_1 = 1.0_f64;
let mut p_l = x;
let mut sum = 3.0 * p_l / (four_pi * 2.0_f64.powi(m_i));
for l in 1..l_max {
let p_l_plus_1 =
((2 * l + 1) as f64 * x * p_l - (l as f64) * p_l_minus_1) / ((l + 1) as f64);
let ell = (l + 1) as f64;
let eigen = (ell * (ell + 1.0)).powi(m_i);
let weight = (2.0 * ell + 1.0) / four_pi;
sum += weight * p_l_plus_1 / eigen;
p_l_minus_1 = p_l;
p_l = p_l_plus_1;
}
sum
}
#[inline]
pub(crate) fn wahba_sphere_kernel_kind(
sep: HalfAngleSeparation,
penalty_order: usize,
kernel: SphereWahbaKernel,
) -> Result<f64, BasisError> {
if !(1..=4).contains(&penalty_order) {
crate::bail_invalid_basis!(
"spherical spline penalty_order must be one of 1, 2, 3, 4; got {penalty_order}"
);
}
let value = wahba_sphere_kernel_kind_unchecked(sep, penalty_order, kernel);
if !value.is_finite() {
crate::bail_invalid_basis!("spherical spline kernel produced a non-finite value");
}
Ok(value)
}
#[inline]
fn wahba_sphere_kernel_kind_unchecked(
sep: HalfAngleSeparation,
penalty_order: usize,
kernel: SphereWahbaKernel,
) -> f64 {
match kernel {
SphereWahbaKernel::Sobolev => wahba_sphere_kernel_sobolev(sep, penalty_order),
SphereWahbaKernel::Pseudo => wahba_sphere_kernel_pseudo(sep, penalty_order),
SphereWahbaKernel::SobolevTruncated { lmax } => {
let coeffs = sobolev_s2_truncated_coefficients(lmax as usize, penalty_order);
sphere_truncated_spectral_eval(sep.cos_gamma(), &coeffs)
}
SphereWahbaKernel::PseudoTruncated { lmax } => {
let coeffs = pseudo_s2_truncated_coefficients(lmax as usize, penalty_order);
sphere_truncated_spectral_eval(sep.cos_gamma(), &coeffs)
}
}
}
#[inline]
pub(crate) fn wahba_sphere_kernel_simd_kind(
u: wide::f64x4,
v: wide::f64x4,
penalty_order: usize,
kernel: SphereWahbaKernel,
) -> wide::f64x4 {
use wide::f64x4;
if !(1..=4).contains(&penalty_order) {
return f64x4::from(f64::NAN);
}
let zero = f64x4::ZERO;
let u_lanes = u.fast_max(zero).fast_min(f64x4::ONE).to_array();
let v_lanes = v.fast_max(zero).fast_min(f64x4::ONE).to_array();
let mut out = [0.0_f64; 4];
for lane in 0..4 {
let sep = HalfAngleSeparation {
u: u_lanes[lane],
v: v_lanes[lane],
};
out[lane] = wahba_sphere_kernel_kind_unchecked(sep, penalty_order, kernel);
}
f64x4::from(out)
}
#[inline]
fn wahba_sphere_kernel_sobolev_closed_form_derivative_dhav(
sep: HalfAngleSeparation,
m: usize,
) -> f64 {
let four_pi = 4.0 * std::f64::consts::PI;
let u = sep.u;
let v = sep.v;
assert!(
u > 0.0,
"closed-form Sobolev derivative called at the coincident pole \
(u = sin²(γ/2) = {u}); the caller's POLE_LIMIT_THRESHOLD guard is \
supposed to make this unreachable"
);
let ln_u = if v <= 0.5 { (-v).ln_1p() } else { u.ln() };
let ln_v = if u <= 0.5 { (-u).ln_1p() } else { v.ln() };
let ln_u_over_v = if v == 0.0 { -1.0 } else { ln_u / v };
let dk_du = match m {
1 => -1.0 / (four_pi * u),
2 => ln_u_over_v / four_pi,
3 => {
let li2_u = dilog_unit(u);
let cross = if v == 0.0 { 0.0 } else { ln_u * ln_v / u };
(-li2_u / u - ln_u_over_v - cross) / four_pi
}
other => {
panic!("closed-form Sobolev derivative only defined for m in {{1,2,3}}; got m={other}")
}
};
dk_du
}
pub(crate) fn wahba_sphere_kernel_sobolev_derivative_dhav(
sep: HalfAngleSeparation,
m: usize,
) -> f64 {
const POLE_LIMIT_THRESHOLD: f64 = 1.0e-10;
const POLE_LIMIT_U: f64 = 0.5 * POLE_LIMIT_THRESHOLD;
if (1..=3).contains(&m) && sep.u >= POLE_LIMIT_U {
return wahba_sphere_kernel_sobolev_closed_form_derivative_dhav(sep, m);
}
let l_max = match m {
1 => 4096_usize,
2 => 256,
3 => 128,
_ => 96,
};
let x = sep.cos_gamma();
let m_i = m as i32;
let four_pi = 4.0 * std::f64::consts::PI;
let mut p_prev = 1.0_f64; let mut p_curr = x; let mut d_prev = 0.0_f64; let mut d_curr = 1.0_f64; let mut sum = 3.0 * d_curr / (four_pi * 2.0_f64.powi(m_i));
for l in 2..=l_max {
let ell = l as f64;
let two_l_minus_1 = 2.0 * ell - 1.0;
let d_next = two_l_minus_1 * p_curr + d_prev;
let p_next = (two_l_minus_1 * x * p_curr - (ell - 1.0) * p_prev) / ell;
let eigen = (ell * (ell + 1.0)).powi(m_i);
let weight = (2.0 * ell + 1.0) / four_pi;
sum += weight * d_next / eigen;
p_prev = p_curr;
p_curr = p_next;
d_prev = d_curr;
d_curr = d_next;
}
-2.0 * sum
}
#[inline]
pub(crate) fn wahba_sphere_kernel_derivative_dhav_kind(
sep: HalfAngleSeparation,
penalty_order: usize,
kernel: SphereWahbaKernel,
) -> f64 {
match kernel {
SphereWahbaKernel::Sobolev => {
wahba_sphere_kernel_sobolev_derivative_dhav(sep, penalty_order)
}
SphereWahbaKernel::Pseudo => wahba_sphere_kernel_pseudo_derivative_dhav(sep, penalty_order),
SphereWahbaKernel::SobolevTruncated { lmax } => {
let coeffs = sobolev_s2_truncated_coefficients(lmax as usize, penalty_order);
-2.0 * sphere_truncated_spectral_derivative_eval(sep.cos_gamma(), &coeffs)
}
SphereWahbaKernel::PseudoTruncated { lmax } => {
let coeffs = pseudo_s2_truncated_coefficients(lmax as usize, penalty_order);
-2.0 * sphere_truncated_spectral_derivative_eval(sep.cos_gamma(), &coeffs)
}
}
}