use crate::spec::{BinomialLink, Family, GammaLink};
use pulp::Simd;
const EXP_C: [f64; 12] = [
f64::from_bits(0x3ff0000000000000),
f64::from_bits(0x3ff0000000000000),
f64::from_bits(0x3fe0000000000010),
f64::from_bits(0x3fc55555555554a2),
f64::from_bits(0x3fa555555554f370),
f64::from_bits(0x3f81111111130dd6),
f64::from_bits(0x3f56c16c1878111c),
f64::from_bits(0x3f2a01a0110572b2),
f64::from_bits(0x3efa01992d0fe736),
f64::from_bits(0x3ec71df4520aaeeb),
f64::from_bits(0x3e928b311c7eb84f),
f64::from_bits(0x3e5ad661c903688b),
];
const LOG1P_H: [f64; 10] = [
f64::from_bits(0x3fe5555555555555),
f64::from_bits(0x3fd999999999a455),
f64::from_bits(0x3fd24924923cd3a0),
f64::from_bits(0x3fcc71c727660721),
f64::from_bits(0x3fc745cefc3caf8b),
f64::from_bits(0x3fc3b18cab0fef6e),
f64::from_bits(0x3fc10ab0536ce75b),
f64::from_bits(0x3fbebaa07b021d58),
f64::from_bits(0x3fb67ff2751e342c),
f64::from_bits(0x3fc4b8585fced69a),
];
const EXP_DEG: usize = EXP_C.len() - 1; const LOG1P_DEG: usize = LOG1P_H.len() - 1;
const LN2HI: f64 = f64::from_bits(0x3fe62e42fee00000); const LN2LO: f64 = f64::from_bits(0x3dea39ef35793c76); const LOG2E: f64 = f64::from_bits(0x3ff71547652b82fe); const RND_MAGIC: f64 = 1.5 * (1u64 << 52) as f64;
const BIAS_MAGIC: f64 = (1u64 << 52) as f64 + 1023.0;
const MANT_MASK: u64 = 0x000F_FFFF_FFFF_FFFF;
const SHIFT52: u64 = 1u64 << 52;
const EXP_ARG_FLOOR: f64 = -700.0;
const EXP_ARG_CEIL: f64 = 700.0;
pub(crate) const FUSED_DEFAULT: bool = cfg!(not(target_arch = "wasm32"));
#[inline(always)]
fn fmadd<S: Simd, const FUSED: bool>(simd: S, a: S::f64s, b: S::f64s, c: S::f64s) -> S::f64s {
if FUSED {
simd.mul_add_f64s(a, b, c)
} else {
simd.add_f64s(simd.mul_f64s(a, b), c)
}
}
#[inline(always)]
fn fmadd_scalar<const FUSED: bool>(a: f64, b: f64, c: f64) -> f64 {
if FUSED {
a.mul_add(b, c)
} else {
a * b + c
}
}
#[inline(always)]
fn simd_exp_reduced<S: Simd, const FUSED: bool>(simd: S, x: S::f64s) -> S::f64s {
let t = fmadd::<S, FUSED>(simd, x, simd.splat_f64s(LOG2E), simd.splat_f64s(RND_MAGIC));
let kf = simd.sub_f64s(t, simd.splat_f64s(RND_MAGIC));
let neg_kf = simd.neg_f64s(kf);
let hi = fmadd::<S, FUSED>(simd, neg_kf, simd.splat_f64s(LN2HI), x); let r = fmadd::<S, FUSED>(simd, neg_kf, simd.splat_f64s(LN2LO), hi); let mut acc = simd.splat_f64s(EXP_C[EXP_DEG]);
let mut j = EXP_DEG;
while j > 0 {
j -= 1;
acc = fmadd::<S, FUSED>(simd, acc, r, simd.splat_f64s(EXP_C[j]));
}
let e = simd.add_f64s(kf, simd.splat_f64s(BIAS_MAGIC));
let m = simd.and_u64s(simd.transmute_u64s_f64s(e), simd.splat_u64s(MANT_MASK));
let pow2 = simd.transmute_f64s_u64s(simd.mul_u64s(m, simd.splat_u64s(SHIFT52)));
simd.mul_f64s(acc, pow2)
}
#[inline(always)]
fn simd_log1p_unit<S: Simd, const FUSED: bool>(simd: S, z: S::f64s) -> S::f64s {
let f = z;
let hfsq = simd.mul_f64s(simd.splat_f64s(0.5), simd.mul_f64s(f, f));
let s = simd.div_f64s(f, simd.add_f64s(simd.splat_f64s(2.0), f));
let w = simd.mul_f64s(s, s);
let mut acc = simd.splat_f64s(LOG1P_H[LOG1P_DEG]);
let mut j = LOG1P_DEG;
while j > 0 {
j -= 1;
acc = fmadd::<S, FUSED>(simd, acc, w, simd.splat_f64s(LOG1P_H[j]));
}
let rr = simd.mul_f64s(w, acc);
let inner = simd.mul_f64s(s, simd.add_f64s(hfsq, rr));
simd.sub_f64s(f, simd.sub_f64s(hfsq, inner))
}
#[inline(always)]
fn simd_z_mask<S: Simd, const FUSED: bool>(simd: S, eta: S::f64s) -> (S::f64s, S::m64s) {
let neg_abs = simd.max_f64s(
simd.neg_f64s(simd.abs_f64s(eta)),
simd.splat_f64s(EXP_ARG_FLOOR),
);
let z = simd_exp_reduced::<S, FUSED>(simd, neg_abs);
let mask = simd.greater_than_or_equal_f64s(eta, simd.splat_f64s(0.0));
(z, mask)
}
#[inline(always)]
fn simd_fused<S: Simd, const FUSED: bool>(simd: S, eta: S::f64s) -> (S::f64s, S::f64s, S::f64s) {
let one = simd.splat_f64s(1.0);
let (z, mask) = simd_z_mask::<S, FUSED>(simd, eta);
let l = simd_log1p_unit::<S, FUSED>(simd, z);
let opz = simd.add_f64s(one, z);
let p = simd.select_f64s(mask, simd.div_f64s(one, opz), simd.div_f64s(z, opz));
let lp = simd.select_f64s(mask, simd.add_f64s(eta, l), l);
let w = simd.max_f64s(
simd.mul_f64s(p, simd.sub_f64s(one, p)),
simd.splat_f64s(crate::glm::WEIGHT_CLAMP),
);
(p, w, lp)
}
#[inline]
fn scalar_exp_reduced<const FUSED: bool>(x: f64) -> f64 {
let kf = fmadd_scalar::<FUSED>(x, LOG2E, RND_MAGIC) - RND_MAGIC;
let hi = fmadd_scalar::<FUSED>(-kf, LN2HI, x);
let r = fmadd_scalar::<FUSED>(-kf, LN2LO, hi);
let mut acc = EXP_C[EXP_DEG];
let mut j = EXP_DEG;
while j > 0 {
j -= 1;
acc = fmadd_scalar::<FUSED>(acc, r, EXP_C[j]);
}
let m = (kf + BIAS_MAGIC).to_bits() & MANT_MASK;
acc * f64::from_bits(m.wrapping_mul(SHIFT52))
}
#[inline]
fn scalar_log1p_unit<const FUSED: bool>(z: f64) -> f64 {
let f = z;
let hfsq = 0.5 * (f * f);
let s = f / (2.0 + f);
let w = s * s;
let mut acc = LOG1P_H[LOG1P_DEG];
let mut j = LOG1P_DEG;
while j > 0 {
j -= 1;
acc = fmadd_scalar::<FUSED>(acc, w, LOG1P_H[j]);
}
let rr = w * acc;
f - (hfsq - s * (hfsq + rr))
}
#[inline]
fn scalar_z<const FUSED: bool>(eta: f64) -> f64 {
scalar_exp_reduced::<FUSED>((-eta.abs()).max(EXP_ARG_FLOOR))
}
#[inline]
fn scalar_fused<const FUSED: bool>(eta: f64) -> (f64, f64, f64) {
let z = scalar_z::<FUSED>(eta);
let l = scalar_log1p_unit::<FUSED>(z);
let (p, lp) = if eta >= 0.0 {
(1.0 / (1.0 + z), eta + l)
} else {
(z / (1.0 + z), l)
};
let w = (p * (1.0 - p)).max(crate::glm::WEIGHT_CLAMP);
(p, w, lp)
}
struct PwLog1pexpOp<'a, const FUSED: bool> {
eta: &'a [f64],
p: &'a mut [f64],
w: &'a mut [f64],
}
impl<const FUSED: bool> pulp::WithSimd for PwLog1pexpOp<'_, FUSED> {
type Output = f64;
#[inline(always)]
fn with_simd<S: Simd>(self, simd: S) -> f64 {
let (eh, et) = S::as_simd_f64s(self.eta);
let (ph, pt) = S::as_mut_simd_f64s(self.p);
let (wh, wt) = S::as_mut_simd_f64s(self.w);
let mut dsum = simd.splat_f64s(0.0);
for i in 0..eh.len() {
let (p, w, lp) = simd_fused::<S, FUSED>(simd, eh[i]);
ph[i] = p;
wh[i] = w;
dsum = simd.add_f64s(dsum, lp);
}
let mut acc = simd.reduce_sum_f64s(dsum);
for i in 0..et.len() {
let (p, w, lp) = scalar_fused::<FUSED>(et[i]);
pt[i] = p;
wt[i] = w;
acc += lp;
}
acc
}
}
pub(crate) fn pw_and_log1pexp_sum(eta: &[f64], p: &mut [f64], w: &mut [f64]) -> f64 {
debug_assert_eq!(eta.len(), p.len());
debug_assert_eq!(eta.len(), w.len());
pulp::Arch::new().dispatch(PwLog1pexpOp::<{ FUSED_DEFAULT }> { eta, p, w })
}
struct SigmoidInplaceOp<'a, const FUSED: bool> {
buf: &'a mut [f64],
}
impl<const FUSED: bool> pulp::WithSimd for SigmoidInplaceOp<'_, FUSED> {
type Output = ();
#[inline(always)]
fn with_simd<S: Simd>(self, simd: S) {
let one = simd.splat_f64s(1.0);
let (head, tail) = S::as_mut_simd_f64s(self.buf);
for x in head.iter_mut() {
let (z, mask) = simd_z_mask::<S, FUSED>(simd, *x);
let opz = simd.add_f64s(one, z);
*x = simd.select_f64s(mask, simd.div_f64s(one, opz), simd.div_f64s(z, opz));
}
for x in tail.iter_mut() {
let z = scalar_z::<FUSED>(*x);
*x = if *x >= 0.0 {
1.0 / (1.0 + z)
} else {
z / (1.0 + z)
};
}
}
}
#[inline]
pub fn sigmoid_fill(buf: &mut [f64]) {
pulp::Arch::new().dispatch(SigmoidInplaceOp::<{ FUSED_DEFAULT }> { buf });
}
#[inline]
pub fn exp_nonpos(x: f64) -> f64 {
scalar_exp_reduced::<{ FUSED_DEFAULT }>(x.max(EXP_ARG_FLOOR))
}
pub(crate) fn exp_clamped(x: f64) -> f64 {
scalar_exp_reduced::<{ FUSED_DEFAULT }>(x.clamp(EXP_ARG_FLOOR, EXP_ARG_CEIL))
}
struct ExpInplaceOp<'a, const FUSED: bool> {
buf: &'a mut [f64],
}
impl<const FUSED: bool> pulp::WithSimd for ExpInplaceOp<'_, FUSED> {
type Output = ();
#[inline(always)]
fn with_simd<S: Simd>(self, simd: S) {
let lo = simd.splat_f64s(EXP_ARG_FLOOR);
let hi = simd.splat_f64s(EXP_ARG_CEIL);
let (head, tail) = S::as_mut_simd_f64s(self.buf);
for x in head.iter_mut() {
*x = simd_exp_reduced::<S, FUSED>(simd, simd.min_f64s(simd.max_f64s(*x, lo), hi));
}
for x in tail.iter_mut() {
*x = exp_clamped(*x);
}
}
}
#[inline]
pub fn exp_fill(buf: &mut [f64]) {
pulp::Arch::new().dispatch(ExpInplaceOp::<{ FUSED_DEFAULT }> { buf });
}
const LN_U_FLOOR: f64 = 4.8828125e-4; const LN_U_CEIL: f64 = f64::from_bits(0x3FEF_FFFF_FFFF_FFFF); const ONE_BITS: u64 = 0x3FF0_0000_0000_0000;
#[inline]
fn scalar_ln_unit<const FUSED: bool>(u: f64) -> f64 {
let u = u.clamp(LN_U_FLOOR, LN_U_CEIL);
let mut kf = 0.0f64;
let mut th = std::f64::consts::SQRT_2 * 0.5;
for _ in 0..11 {
if u < th {
kf -= 1.0;
}
th *= 0.5;
}
let m = f64::from_bits((u.to_bits() & MANT_MASK) | ONE_BITS);
let m = if m < std::f64::consts::SQRT_2 {
m
} else {
0.5 * m
};
let l = scalar_log1p_unit::<FUSED>(m - 1.0);
fmadd_scalar::<FUSED>(kf, LN2HI, fmadd_scalar::<FUSED>(kf, LN2LO, l))
}
struct LnInplaceOp<'a, const FUSED: bool> {
buf: &'a mut [f64],
}
impl<const FUSED: bool> pulp::WithSimd for LnInplaceOp<'_, FUSED> {
type Output = ();
#[inline(always)]
fn with_simd<S: Simd>(self, simd: S) {
let one = simd.splat_f64s(1.0);
let (head, tail) = S::as_mut_simd_f64s(self.buf);
for v in head.iter_mut() {
let u = simd.min_f64s(
simd.max_f64s(*v, simd.splat_f64s(LN_U_FLOOR)),
simd.splat_f64s(LN_U_CEIL),
);
let mut kf = simd.splat_f64s(0.0);
let mut th = std::f64::consts::SQRT_2 * 0.5;
for _ in 0..11 {
let mask = simd.less_than_f64s(u, simd.splat_f64s(th));
kf = simd.select_f64s(mask, simd.sub_f64s(kf, one), kf);
th *= 0.5;
}
let m = simd.transmute_f64s_u64s(simd.or_u64s(
simd.and_u64s(simd.transmute_u64s_f64s(u), simd.splat_u64s(MANT_MASK)),
simd.splat_u64s(ONE_BITS),
));
let lt = simd.less_than_f64s(m, simd.splat_f64s(std::f64::consts::SQRT_2));
let m = simd.select_f64s(lt, m, simd.mul_f64s(simd.splat_f64s(0.5), m));
let l = simd_log1p_unit::<S, FUSED>(simd, simd.sub_f64s(m, one));
let inner = fmadd::<S, FUSED>(simd, kf, simd.splat_f64s(LN2LO), l);
*v = fmadd::<S, FUSED>(simd, kf, simd.splat_f64s(LN2HI), inner);
}
for v in tail.iter_mut() {
*v = scalar_ln_unit::<FUSED>(*v);
}
}
}
#[inline]
pub fn ln_owned(u: f64) -> f64 {
scalar_ln_unit::<{ FUSED_DEFAULT }>(u)
}
#[inline]
pub fn ln_fill(buf: &mut [f64]) {
pulp::Arch::new().dispatch(LnInplaceOp::<{ FUSED_DEFAULT }> { buf });
}
const ERF_A1: f64 = 0.254829592;
const ERF_A2: f64 = -0.284496736;
const ERF_A3: f64 = 1.421413741;
const ERF_A4: f64 = -1.453152027;
const ERF_A5: f64 = 1.061405429;
const ERF_P: f64 = 0.3275911;
struct PhiInplaceOp<'a, const FUSED: bool> {
buf: &'a mut [f64],
}
impl<const FUSED: bool> pulp::WithSimd for PhiInplaceOp<'_, FUSED> {
type Output = ();
#[inline(always)]
fn with_simd<S: Simd>(self, simd: S) {
let one = simd.splat_f64s(1.0);
let half = simd.splat_f64s(0.5);
let c = simd.splat_f64s(std::f64::consts::FRAC_1_SQRT_2);
let (head, tail) = S::as_mut_simd_f64s(self.buf);
for v in head.iter_mut() {
let x = simd.mul_f64s(simd.neg_f64s(*v), c); let neg = simd.less_than_f64s(x, simd.splat_f64s(0.0));
let ax = simd.abs_f64s(x);
let t = simd.div_f64s(
one,
simd.add_f64s(one, simd.mul_f64s(simd.splat_f64s(ERF_P), ax)),
);
let mut poly = simd.add_f64s(
simd.mul_f64s(simd.splat_f64s(ERF_A5), t),
simd.splat_f64s(ERF_A4),
);
poly = simd.add_f64s(simd.mul_f64s(poly, t), simd.splat_f64s(ERF_A3));
poly = simd.add_f64s(simd.mul_f64s(poly, t), simd.splat_f64s(ERF_A2));
poly = simd.add_f64s(simd.mul_f64s(poly, t), simd.splat_f64s(ERF_A1));
poly = simd.mul_f64s(poly, t);
let e = simd_exp_reduced::<S, FUSED>(
simd,
simd.max_f64s(
simd.mul_f64s(simd.neg_f64s(ax), ax),
simd.splat_f64s(EXP_ARG_FLOOR),
),
);
let y = simd.sub_f64s(one, simd.mul_f64s(poly, e));
let erf = simd.select_f64s(neg, simd.neg_f64s(y), y);
*v = simd.mul_f64s(half, simd.sub_f64s(one, erf));
}
for v in tail.iter_mut() {
*v = scalar_phi(*v);
}
}
}
#[inline]
pub(crate) fn scalar_phi(z: f64) -> f64 {
let x = -z * std::f64::consts::FRAC_1_SQRT_2;
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let ax = x.abs();
let t = 1.0 / (1.0 + ERF_P * ax);
let poly = (((((ERF_A5 * t + ERF_A4) * t) + ERF_A3) * t + ERF_A2) * t + ERF_A1) * t;
let y = 1.0 - poly * exp_nonpos(-ax * ax);
0.5 * (1.0 - sign * y)
}
#[inline]
pub(crate) fn phi_hp(z: f64) -> f64 {
0.5 * erfc_cody(-z * std::f64::consts::FRAC_1_SQRT_2)
}
#[allow(clippy::excessive_precision)]
const ERFC_A: [f64; 5] = [
3.16112374387056560e00,
1.13864154151050156e02,
3.77485237685302021e02,
3.20937758913846947e03,
1.85777706184603153e-1,
];
#[allow(clippy::excessive_precision)]
const ERFC_B: [f64; 4] = [
2.36012909523441209e01,
2.44024637934444173e02,
1.28261652607737228e03,
2.84423683343917062e03,
];
#[allow(clippy::excessive_precision)]
const ERFC_C: [f64; 9] = [
5.64188496988670089e-1,
8.88314979438837594e00,
6.61191906371416295e01,
2.98635138197400131e02,
8.81952221241769090e02,
1.71204761263407058e03,
2.05107837782607147e03,
1.23033935479799725e03,
2.15311535474403846e-8,
];
#[allow(clippy::excessive_precision)]
const ERFC_D: [f64; 8] = [
1.57449261107098347e01,
1.17693950891312499e02,
5.37181101862009858e02,
1.62138957456669019e03,
3.29079923573345963e03,
4.36261909014324716e03,
3.43936767414372164e03,
1.23033935480374942e03,
];
#[allow(clippy::excessive_precision)]
const ERFC_P: [f64; 6] = [
3.05326634961232344e-1,
3.60344899949804439e-1,
1.25781726111229246e-1,
1.60837851487422766e-2,
6.58749161529837803e-4,
1.63153871373020978e-2,
];
#[allow(clippy::excessive_precision)]
const ERFC_Q: [f64; 5] = [
2.56852019228982242e00,
1.87295284992346047e00,
5.27905102951428412e-1,
6.05183413124413191e-2,
2.33520497626869185e-3,
];
#[allow(clippy::excessive_precision)]
const ERFC_SQRPI: f64 = 5.6418958354775628695e-1;
const ERFC_THRESH: f64 = 0.46875;
const ERFC_SIXTEN: f64 = 16.0;
const ERFC_XSMALL: f64 = 1.11e-16;
const ERFC_XBIG: f64 = 26.543;
fn erfc_cody(x: f64) -> f64 {
const A: [f64; 5] = ERFC_A;
const B: [f64; 4] = ERFC_B;
const C: [f64; 9] = ERFC_C;
const D: [f64; 8] = ERFC_D;
const P: [f64; 6] = ERFC_P;
const Q: [f64; 5] = ERFC_Q;
const SQRPI: f64 = ERFC_SQRPI;
const THRESH: f64 = ERFC_THRESH;
const SIXTEN: f64 = ERFC_SIXTEN;
const XSMALL: f64 = ERFC_XSMALL;
const XBIG: f64 = ERFC_XBIG;
let y = x.abs();
if y <= THRESH {
let ysq = if y > XSMALL { y * y } else { 0.0 };
let mut xnum = A[4] * ysq;
let mut xden = ysq;
for i in 0..3 {
xnum = (xnum + A[i]) * ysq;
xden = (xden + B[i]) * ysq;
}
return 1.0 - x * (xnum + A[3]) / (xden + B[3]);
}
let mut result = if y <= 4.0 {
let mut xnum = C[8] * y;
let mut xden = y;
for i in 0..7 {
xnum = (xnum + C[i]) * y;
xden = (xden + D[i]) * y;
}
(xnum + C[7]) / (xden + D[7])
} else if y >= XBIG {
0.0
} else {
let ysq = 1.0 / (y * y);
let mut xnum = P[5] * ysq;
let mut xden = ysq;
for i in 0..4 {
xnum = (xnum + P[i]) * ysq;
xden = (xden + Q[i]) * ysq;
}
(SQRPI - ysq * (xnum + P[4]) / (xden + Q[4])) / y
};
if y < XBIG {
let ysq = (y * SIXTEN).trunc() / SIXTEN;
let del = (y - ysq) * (y + ysq);
result *= (-ysq * ysq).exp() * (-del).exp();
}
if x < 0.0 {
2.0 - result
} else {
result
}
}
#[allow(clippy::excessive_precision)]
pub(crate) fn ln_gamma(x: f64) -> f64 {
const C: [f64; 9] = [
0.999_999_999_999_809_93,
676.520_368_121_885_1,
-1_259.139_216_722_402_8,
771.323_428_777_653_13,
-176.615_029_162_140_59,
12.507_343_278_686_905,
-0.138_571_095_265_720_12,
9.984_369_578_019_571_6e-6,
1.505_632_735_149_311_6e-7,
];
const G: f64 = 7.0;
const LN_SQRT_2PI: f64 = 0.918_938_533_204_672_74; let x = x - 1.0;
let mut a = C[0];
let t = x + G + 0.5;
for (i, &c) in C.iter().enumerate().skip(1) {
a += c / (x + i as f64);
}
LN_SQRT_2PI + (x + 0.5) * t.ln() - t + a.ln()
}
#[inline]
pub fn phi_fill(buf: &mut [f64]) {
pulp::Arch::new().dispatch(PhiInplaceOp::<{ FUSED_DEFAULT }> { buf });
}
#[inline(always)]
fn simd_erfc<S: Simd, const FUSED: bool>(simd: S, x: S::f64s) -> S::f64s {
let one = simd.splat_f64s(1.0);
let y = simd.abs_f64s(x);
let ysq = simd.select_f64s(
simd.less_than_f64s(simd.splat_f64s(ERFC_XSMALL), y),
simd.mul_f64s(y, y),
simd.splat_f64s(0.0),
);
let mut xnum = simd.mul_f64s(simd.splat_f64s(ERFC_A[4]), ysq);
let mut xden = ysq;
for i in 0..3 {
xnum = simd.mul_f64s(simd.add_f64s(xnum, simd.splat_f64s(ERFC_A[i])), ysq);
xden = simd.mul_f64s(simd.add_f64s(xden, simd.splat_f64s(ERFC_B[i])), ysq);
}
let r1 = simd.sub_f64s(
one,
simd.div_f64s(
simd.mul_f64s(x, simd.add_f64s(xnum, simd.splat_f64s(ERFC_A[3]))),
simd.add_f64s(xden, simd.splat_f64s(ERFC_B[3])),
),
);
let mut xnum = simd.mul_f64s(simd.splat_f64s(ERFC_C[8]), y);
let mut xden = y;
for i in 0..7 {
xnum = simd.mul_f64s(simd.add_f64s(xnum, simd.splat_f64s(ERFC_C[i])), y);
xden = simd.mul_f64s(simd.add_f64s(xden, simd.splat_f64s(ERFC_D[i])), y);
}
let r2 = simd.div_f64s(
simd.add_f64s(xnum, simd.splat_f64s(ERFC_C[7])),
simd.add_f64s(xden, simd.splat_f64s(ERFC_D[7])),
);
let yb = simd.max_f64s(y, simd.splat_f64s(4.0));
let iy2 = simd.div_f64s(one, simd.mul_f64s(yb, yb));
let mut xnum = simd.mul_f64s(simd.splat_f64s(ERFC_P[5]), iy2);
let mut xden = iy2;
for i in 0..4 {
xnum = simd.mul_f64s(simd.add_f64s(xnum, simd.splat_f64s(ERFC_P[i])), iy2);
xden = simd.mul_f64s(simd.add_f64s(xden, simd.splat_f64s(ERFC_Q[i])), iy2);
}
let r3 = simd.div_f64s(
simd.sub_f64s(
simd.splat_f64s(ERFC_SQRPI),
simd.div_f64s(
simd.mul_f64s(iy2, simd.add_f64s(xnum, simd.splat_f64s(ERFC_P[4]))),
simd.add_f64s(xden, simd.splat_f64s(ERFC_Q[4])),
),
),
yb,
);
let t16 = simd.mul_f64s(y, simd.splat_f64s(ERFC_SIXTEN));
let rnd = simd.sub_f64s(
simd.add_f64s(t16, simd.splat_f64s(RND_MAGIC)),
simd.splat_f64s(RND_MAGIC),
);
let trunc = simd.select_f64s(simd.less_than_f64s(t16, rnd), simd.sub_f64s(rnd, one), rnd);
let yq = simd.div_f64s(trunc, simd.splat_f64s(ERFC_SIXTEN));
let del = simd.mul_f64s(simd.sub_f64s(y, yq), simd.add_f64s(y, yq));
let e1 = simd_exp_reduced::<S, FUSED>(
simd,
simd.max_f64s(
simd.neg_f64s(simd.mul_f64s(yq, yq)),
simd.splat_f64s(EXP_ARG_FLOOR),
),
);
let e2 = simd_exp_reduced::<S, FUSED>(
simd,
simd.max_f64s(simd.neg_f64s(del), simd.splat_f64s(EXP_ARG_FLOOR)),
);
let tail = simd.mul_f64s(
simd.select_f64s(simd.less_than_f64s(simd.splat_f64s(4.0), y), r3, r2),
simd.mul_f64s(e1, e2),
);
let tail = simd.select_f64s(
simd.greater_than_or_equal_f64s(y, simd.splat_f64s(ERFC_XBIG)),
simd.splat_f64s(0.0),
tail,
);
let tail = simd.select_f64s(
simd.less_than_f64s(x, simd.splat_f64s(0.0)),
simd.sub_f64s(simd.splat_f64s(2.0), tail),
tail,
);
simd.select_f64s(
simd.less_than_f64s(simd.splat_f64s(ERFC_THRESH), y),
tail,
r1,
)
}
#[inline]
fn scalar_erfc_blend<const FUSED: bool>(x: f64) -> f64 {
let y = x.abs();
let ysq = if ERFC_XSMALL < y { y * y } else { 0.0 };
let mut xnum = ERFC_A[4] * ysq;
let mut xden = ysq;
for i in 0..3 {
xnum = (xnum + ERFC_A[i]) * ysq;
xden = (xden + ERFC_B[i]) * ysq;
}
let r1 = 1.0 - x * (xnum + ERFC_A[3]) / (xden + ERFC_B[3]);
let mut xnum = ERFC_C[8] * y;
let mut xden = y;
for i in 0..7 {
xnum = (xnum + ERFC_C[i]) * y;
xden = (xden + ERFC_D[i]) * y;
}
let r2 = (xnum + ERFC_C[7]) / (xden + ERFC_D[7]);
let yb = y.max(4.0);
let iy2 = 1.0 / (yb * yb);
let mut xnum = ERFC_P[5] * iy2;
let mut xden = iy2;
for i in 0..4 {
xnum = (xnum + ERFC_P[i]) * iy2;
xden = (xden + ERFC_Q[i]) * iy2;
}
let r3 = (ERFC_SQRPI - iy2 * (xnum + ERFC_P[4]) / (xden + ERFC_Q[4])) / yb;
let t16 = y * ERFC_SIXTEN;
let rnd = (t16 + RND_MAGIC) - RND_MAGIC;
let trunc = if t16 < rnd { rnd - 1.0 } else { rnd };
let yq = trunc / ERFC_SIXTEN;
let del = (y - yq) * (y + yq);
let e1 = scalar_exp_reduced::<FUSED>((-(yq * yq)).max(EXP_ARG_FLOOR));
let e2 = scalar_exp_reduced::<FUSED>((-del).max(EXP_ARG_FLOOR));
let tail = if 4.0 < y { r3 } else { r2 } * (e1 * e2);
let tail = if y >= ERFC_XBIG { 0.0 } else { tail };
let tail = if x < 0.0 { 2.0 - tail } else { tail };
if ERFC_THRESH < y {
tail
} else {
r1
}
}
#[inline(always)]
fn simd_probit<S: Simd, const FUSED: bool>(simd: S, eta: S::f64s) -> (S::f64s, S::f64s) {
let mu = simd.mul_f64s(
simd.splat_f64s(0.5),
simd_erfc::<S, FUSED>(
simd,
simd.mul_f64s(
simd.neg_f64s(eta),
simd.splat_f64s(std::f64::consts::FRAC_1_SQRT_2),
),
),
);
let dmu = simd.mul_f64s(
simd.splat_f64s(crate::family::FRAC_1_SQRT_2PI),
simd_exp_reduced::<S, FUSED>(
simd,
simd.max_f64s(
simd.mul_f64s(simd.splat_f64s(-0.5), simd.mul_f64s(eta, eta)),
simd.splat_f64s(EXP_ARG_FLOOR),
),
),
);
(mu, dmu)
}
#[inline]
fn scalar_probit<const FUSED: bool>(eta: f64) -> (f64, f64) {
let mu = 0.5 * scalar_erfc_blend::<FUSED>(-eta * std::f64::consts::FRAC_1_SQRT_2);
let dmu = crate::family::FRAC_1_SQRT_2PI
* scalar_exp_reduced::<FUSED>((-0.5 * (eta * eta)).max(EXP_ARG_FLOOR));
(mu, dmu)
}
#[inline(always)]
fn simd_sigmoid<S: Simd, const FUSED: bool>(simd: S, eta: S::f64s) -> S::f64s {
let one = simd.splat_f64s(1.0);
let (z, mask) = simd_z_mask::<S, FUSED>(simd, eta);
let opz = simd.add_f64s(one, z);
simd.select_f64s(mask, simd.div_f64s(one, opz), simd.div_f64s(z, opz))
}
#[inline]
fn scalar_sigmoid_owned<const FUSED: bool>(eta: f64) -> f64 {
let z = scalar_z::<FUSED>(eta);
if eta >= 0.0 {
1.0 / (1.0 + z)
} else {
z / (1.0 + z)
}
}
#[allow(clippy::too_many_arguments)] pub(crate) fn family_pass(
family: Family,
nb_theta: f64,
eta: &mut [f64],
y: &[f64],
prior_w: &[f64],
weighted: bool,
yeta: f64,
prob: &mut [f64],
w: &mut [f64],
z: &mut [f64],
) -> (f64, bool) {
let n = eta.len();
debug_assert_eq!(prob.len(), n);
debug_assert_eq!(w.len(), n);
debug_assert_eq!(y.len(), n);
debug_assert!(z.is_empty() || z.len() == n);
debug_assert!(prior_w.is_empty() || prior_w.len() == n);
if matches!(
family,
Family::Binomial {
link: BinomialLink::Logit
}
) && !weighted
{
let lp_sum = pw_and_log1pexp_sum(eta, prob, w);
if !z.is_empty() {
for i in 0..n {
z[i] = eta[i] + (y[i] - prob[i]) / w[i];
}
}
return (2.0 * (lp_sum - yeta), false);
}
if matches!(family, Family::Gaussian) {
let mut dev = 0.0;
for i in 0..n {
let (mu, w_raw, r) =
crate::family::irls_weight_and_resid(family, nb_theta, y[i], eta[i]);
let pw = if prior_w.is_empty() { 1.0 } else { prior_w[i] };
prob[i] = mu;
w[i] = (pw * w_raw).max(crate::glm::WEIGHT_CLAMP);
if !z.is_empty() {
z[i] = eta[i] + r;
}
dev += pw * crate::family::dev_resid(family, nb_theta, y[i], mu);
}
return (dev, false);
}
let infeasible = pulp::Arch::new().dispatch(FamilyMuWOp::<{ FUSED_DEFAULT }> {
family,
nb_theta,
eta,
y,
prior_w,
prob,
w,
z,
});
let mut dev = 0.0;
for i in 0..n {
let pw = if prior_w.is_empty() { 1.0 } else { prior_w[i] };
dev += pw * crate::family::dev_resid(family, nb_theta, y[i], prob[i]);
}
(dev, infeasible)
}
#[cfg(test)]
fn erfc_fill(buf: &mut [f64]) {
struct Op<'a, const FUSED: bool> {
buf: &'a mut [f64],
}
impl<const FUSED: bool> pulp::WithSimd for Op<'_, FUSED> {
type Output = ();
#[inline(always)]
fn with_simd<S: Simd>(self, simd: S) {
let (head, tail) = S::as_mut_simd_f64s(self.buf);
for v in head.iter_mut() {
*v = simd_erfc::<S, FUSED>(simd, *v);
}
for v in tail.iter_mut() {
*v = scalar_erfc_blend::<FUSED>(*v);
}
}
}
pulp::Arch::new().dispatch(Op::<{ FUSED_DEFAULT }> { buf });
}
struct FamilyMuWOp<'a, const FUSED: bool> {
family: Family,
nb_theta: f64,
eta: &'a mut [f64],
y: &'a [f64],
prior_w: &'a [f64],
prob: &'a mut [f64],
w: &'a mut [f64],
z: &'a mut [f64],
}
impl<const FUSED: bool> pulp::WithSimd for FamilyMuWOp<'_, FUSED> {
type Output = bool; #[inline(always)]
fn with_simd<S: Simd>(self, simd: S) -> bool {
let (eh, et) = S::as_mut_simd_f64s(self.eta);
let (ph, pt) = S::as_mut_simd_f64s(self.prob);
let (wh, wt) = S::as_mut_simd_f64s(self.w);
let (zh, zt) = S::as_mut_simd_f64s(self.z);
let (yh, yt) = S::as_simd_f64s(self.y);
let (gh, gt) = S::as_simd_f64s(self.prior_w);
let nb_theta = self.nb_theta;
let zero = simd.splat_f64s(0.0);
let one = simd.splat_f64s(1.0);
let clampv = simd.splat_f64s(crate::glm::WEIGHT_CLAMP);
let mut bad = zero; let mut bad_tail = false;
macro_rules! run_arm {
(|$e:ident, $yi:ident| $simd_body:block, |$es:ident, $ys:ident| $scalar_body:block) => {{
for i in 0..eh.len() {
let $e = eh[i];
let $yi = yh[i];
let (ec, mu, w_raw, r) = $simd_body;
eh[i] = ec;
ph[i] = mu;
let pw = if gh.is_empty() { one } else { gh[i] };
wh[i] = simd.max_f64s(simd.mul_f64s(pw, w_raw), clampv);
if let Some(slot) = zh.get_mut(i) {
*slot = simd.add_f64s(ec, r);
}
}
for i in 0..et.len() {
let $es = et[i];
let $ys = yt[i];
let (ec, mu, w_raw, r) = $scalar_body;
et[i] = ec;
pt[i] = mu;
let pw = if gt.is_empty() { 1.0 } else { gt[i] };
wt[i] = (pw * w_raw).max(crate::glm::WEIGHT_CLAMP);
if let Some(slot) = zt.get_mut(i) {
*slot = ec + r;
}
}
}};
}
match self.family {
Family::Binomial {
link: BinomialLink::Probit,
} => {
let lo = simd.splat_f64s(crate::family::PROB_EPS);
let hi = simd.splat_f64s(1.0 - crate::family::PROB_EPS);
run_arm!(
|e, yi| {
let (mu_raw, dmu) = simd_probit::<S, FUSED>(simd, e);
let mu = simd.min_f64s(simd.max_f64s(mu_raw, lo), hi);
let v = simd.mul_f64s(mu, simd.sub_f64s(one, mu));
let w_raw = simd.div_f64s(simd.mul_f64s(dmu, dmu), v);
let r = simd.div_f64s(simd.sub_f64s(yi, mu), dmu);
(e, mu, w_raw, r)
},
|e, yi| {
let (mu_raw, dmu) = scalar_probit::<FUSED>(e);
let mu =
mu_raw.clamp(crate::family::PROB_EPS, 1.0 - crate::family::PROB_EPS);
let v = mu * (1.0 - mu);
(e, mu, dmu * dmu / v, (yi - mu) / dmu)
}
);
}
Family::Binomial {
link: BinomialLink::Logit,
} => {
let lo = simd.splat_f64s(crate::family::PROB_EPS);
let hi = simd.splat_f64s(1.0 - crate::family::PROB_EPS);
run_arm!(
|e, yi| {
let mu_raw = simd_sigmoid::<S, FUSED>(simd, e);
let mu = simd.min_f64s(simd.max_f64s(mu_raw, lo), hi);
let v = simd.mul_f64s(mu, simd.sub_f64s(one, mu));
(e, mu, v, simd.div_f64s(simd.sub_f64s(yi, mu), v))
},
|e, yi| {
let mu_raw = scalar_sigmoid_owned::<FUSED>(e);
let mu =
mu_raw.clamp(crate::family::PROB_EPS, 1.0 - crate::family::PROB_EPS);
let v = mu * (1.0 - mu);
(e, mu, v, (yi - mu) / v)
}
);
}
Family::Poisson { .. } => {
let elo = simd.splat_f64s(-crate::family::ETA_MAX);
let ehi = simd.splat_f64s(crate::family::ETA_MAX);
let mfl = simd.splat_f64s(crate::family::MU_FLOOR);
run_arm!(
|e, yi| {
let ec = simd.min_f64s(simd.max_f64s(e, elo), ehi);
let mu = simd.max_f64s(simd_exp_reduced::<S, FUSED>(simd, ec), mfl);
(ec, mu, mu, simd.div_f64s(simd.sub_f64s(yi, mu), mu))
},
|e, yi| {
let ec = e.clamp(-crate::family::ETA_MAX, crate::family::ETA_MAX);
let mu = scalar_exp_reduced::<FUSED>(ec).max(crate::family::MU_FLOOR);
(ec, mu, mu, (yi - mu) / mu)
}
);
}
Family::Gamma {
link: GammaLink::Log,
}
| Family::NegativeBinomial { .. } => {
let is_nb = matches!(self.family, Family::NegativeBinomial { .. });
let elo = simd.splat_f64s(-crate::family::ETA_MAX);
let ehi = simd.splat_f64s(crate::family::ETA_MAX);
let mfl = simd.splat_f64s(crate::family::MU_FLOOR);
let th = simd.splat_f64s(nb_theta);
run_arm!(
|e, yi| {
let ec = simd.min_f64s(simd.max_f64s(e, elo), ehi);
let ex = simd_exp_reduced::<S, FUSED>(simd, ec);
let mu = simd.max_f64s(ex, mfl);
let msq = simd.mul_f64s(mu, mu);
let v = if is_nb {
simd.add_f64s(mu, simd.div_f64s(msq, th))
} else {
msq
};
let w_raw = simd.div_f64s(simd.mul_f64s(ex, ex), v);
(ec, mu, w_raw, simd.div_f64s(simd.sub_f64s(yi, mu), ex))
},
|e, yi| {
let ec = e.clamp(-crate::family::ETA_MAX, crate::family::ETA_MAX);
let ex = scalar_exp_reduced::<FUSED>(ec);
let mu = ex.max(crate::family::MU_FLOOR);
let v = if is_nb {
mu + mu * mu / nb_theta
} else {
mu * mu
};
(ec, mu, ex * ex / v, (yi - mu) / ex)
}
);
}
Family::Gamma {
link: GammaLink::Inverse,
} => {
let elo = simd.splat_f64s(crate::family::MU_FLOOR);
let ehi = simd.splat_f64s(crate::family::ETA_MAX);
for i in 0..eh.len() {
let raw = eh[i];
bad = simd.add_f64s(
bad,
simd.select_f64s(simd.less_than_or_equal_f64s(raw, zero), one, zero),
);
let ec = simd.min_f64s(simd.max_f64s(raw, elo), ehi);
let mu_raw = simd.div_f64s(one, ec);
let mu = simd.max_f64s(mu_raw, elo);
let dmu = simd.neg_f64s(simd.mul_f64s(mu_raw, mu_raw));
let v = simd.mul_f64s(mu, mu);
let w_raw = simd.div_f64s(simd.mul_f64s(dmu, dmu), v);
eh[i] = ec;
ph[i] = mu;
let pw = if gh.is_empty() { one } else { gh[i] };
wh[i] = simd.max_f64s(simd.mul_f64s(pw, w_raw), clampv);
if let Some(slot) = zh.get_mut(i) {
*slot = simd.add_f64s(ec, simd.div_f64s(simd.sub_f64s(yh[i], mu), dmu));
}
}
for i in 0..et.len() {
let raw = et[i];
bad_tail |= raw <= 0.0;
let ec = raw.clamp(crate::family::MU_FLOOR, crate::family::ETA_MAX);
let mu_raw = 1.0 / ec;
let mu = mu_raw.max(crate::family::MU_FLOOR);
let dmu = -(mu_raw * mu_raw);
let v = mu * mu;
et[i] = ec;
pt[i] = mu;
let pw = if gt.is_empty() { 1.0 } else { gt[i] };
wt[i] = (pw * (dmu * dmu / v)).max(crate::glm::WEIGHT_CLAMP);
if let Some(slot) = zt.get_mut(i) {
*slot = ec + (yt[i] - mu) / dmu;
}
}
}
Family::Gaussian => unreachable!("Gaussian is handled before dispatch"),
}
bad_tail || simd.reduce_sum_f64s(bad) > 0.0
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ulp(a: f64, b: f64) -> i128 {
let o = |x: f64| {
let b = x.to_bits() as i64;
(if b < 0 { i64::MIN.wrapping_sub(b) } else { b }) as i128
};
(o(a) - o(b)).abs()
}
fn libm_fused(eta: f64) -> (f64, f64) {
if eta >= 0.0 {
let z = (-eta).exp();
(1.0 / (1.0 + z), eta + z.ln_1p())
} else {
let z = eta.exp();
(z / (1.0 + z), z.ln_1p())
}
}
#[test]
fn simd_kernel_within_1ulp_of_libm() {
let n = 20_003usize; let eta: Vec<f64> = (0..n).map(|k| -40.0 + 80.0 * k as f64 / n as f64).collect();
let (mut pmax, mut lpmax) = (0i128, 0i128);
for &e in &eta {
let (p, w, lp) = scalar_fused::<{ FUSED_DEFAULT }>(e);
let (libp, liblp) = libm_fused(e);
pmax = pmax.max(ulp(p, libp));
lpmax = lpmax.max(ulp(lp, liblp));
assert!(w >= crate::glm::WEIGHT_CLAMP && w.is_finite());
}
assert!(pmax <= 2, "sigmoid p drifted {pmax} ULP from libm");
assert!(lpmax <= 2, "log1pexp drifted {lpmax} ULP from libm");
let mut p = vec![0.0; n];
let mut w = vec![0.0; n];
let lp_sum = pw_and_log1pexp_sum(&eta, &mut p, &mut w);
let mut p_simd_max = 0i128;
let mut ref_sum = 0.0;
for i in 0..n {
let (libp, liblp) = libm_fused(eta[i]);
p_simd_max = p_simd_max.max(ulp(p[i], libp));
ref_sum += liblp;
}
assert!(
p_simd_max <= 2,
"SIMD-path p drifted {p_simd_max} ULP from libm"
);
assert!(
(lp_sum - ref_sum).abs() <= 1e-9 * ref_sum.abs().max(1.0),
"Σlog1pexp drift {lp_sum} vs {ref_sum}"
);
}
#[test]
fn unfused_kernel_within_3ulp_of_libm() {
let n = 20_003usize;
let eta: Vec<f64> = (0..n).map(|k| -40.0 + 80.0 * k as f64 / n as f64).collect();
let (mut pmax, mut lpmax) = (0i128, 0i128);
for &e in &eta {
let (p, w, lp) = scalar_fused::<false>(e);
let (libp, liblp) = libm_fused(e);
pmax = pmax.max(ulp(p, libp));
lpmax = lpmax.max(ulp(lp, liblp));
assert!(w >= crate::glm::WEIGHT_CLAMP && w.is_finite());
}
assert!(pmax <= 3, "unfused sigmoid p drifted {pmax} ULP from libm");
assert!(lpmax <= 3, "unfused log1pexp drifted {lpmax} ULP from libm");
let mut p = vec![0.0; n];
let mut w = vec![0.0; n];
let lp_sum = pulp::Arch::new().dispatch(PwLog1pexpOp::<false> {
eta: &eta,
p: &mut p,
w: &mut w,
});
let mut ref_sum = 0.0;
let mut p_simd_max = 0i128;
for i in 0..n {
let (libp, liblp) = libm_fused(eta[i]);
p_simd_max = p_simd_max.max(ulp(p[i], libp));
ref_sum += liblp;
}
assert!(
p_simd_max <= 3,
"unfused SIMD p drifted {p_simd_max} ULP from libm"
);
assert!((lp_sum - ref_sum).abs() <= 1e-9 * ref_sum.abs().max(1.0));
}
#[test]
fn sigmoid_fill_within_2ulp_of_libm() {
let n = 20_003usize;
let eta: Vec<f64> = (0..n).map(|k| -40.0 + 80.0 * k as f64 / n as f64).collect();
let mut buf = eta.clone();
sigmoid_fill(&mut buf);
let mut pmax = 0i128;
for i in 0..n {
let (libp, _) = libm_fused(eta[i]);
pmax = pmax.max(ulp(buf[i], libp));
assert!(buf[i].is_finite() && (0.0..=1.0).contains(&buf[i]));
}
assert!(pmax <= 2, "sigmoid_fill drifted {pmax} ULP from libm");
}
#[test]
fn exp_fill_within_1ulp_of_libm_full_domain() {
let n = 20_003usize;
let xs: Vec<f64> = (0..n)
.map(|k| -700.0 + 1400.0 * k as f64 / n as f64)
.collect();
let mut emax = 0i128;
for &x in &xs {
emax = emax.max(ulp(exp_clamped(x), x.exp()));
}
assert!(emax <= 1, "exp_clamped drifted {emax} ULP from libm");
let mut buf = xs.clone();
exp_fill(&mut buf);
let mut smax = 0i128;
for i in 0..n {
smax = smax.max(ulp(buf[i], xs[i].exp()));
}
assert!(smax <= 1, "exp_fill drifted {smax} ULP from libm");
let mut edge = vec![-1.0e9, 1.0e9];
exp_fill(&mut edge);
assert!(edge[0] > 0.0 && edge[1].is_finite());
}
#[test]
fn ln_fill_within_2ulp_of_libm() {
let n = 20_003usize;
let lo = 9.5e-4f64;
let us: Vec<f64> = (0..n)
.map(|k| lo + (1.0 - lo) * k as f64 / n as f64)
.collect();
let mut smax = 0i128;
for &u in &us {
smax = smax.max(ulp(ln_owned(u), u.ln()));
}
assert!(smax <= 2, "ln_owned drifted {smax} ULP from libm");
let mut buf = us.clone();
ln_fill(&mut buf);
let mut vmax = 0i128;
for i in 0..n {
vmax = vmax.max(ulp(buf[i], us[i].ln()));
}
assert!(vmax <= 2, "ln_fill drifted {vmax} ULP from libm");
assert!(ln_owned(1.0e-9) < -6.96);
assert!(ln_owned(0.0) < -6.96);
assert!(ln_owned(1.0) < 0.0 && ln_owned(1.0) > -3.0e-16);
}
#[test]
fn phi_fill_bit_identical_to_scalar_phi() {
let n = 20_003usize;
let z: Vec<f64> = (0..n).map(|k| -9.0 + 18.0 * k as f64 / n as f64).collect();
let mut buf = z.clone();
phi_fill(&mut buf);
for i in 0..n {
assert_eq!(
buf[i].to_bits(),
scalar_phi(z[i]).to_bits(),
"phi_fill diverged from scalar phi at z={}",
z[i]
);
}
}
#[test]
fn phi_hp_full_double_precision() {
let cases = [
(-6.0, 9.865_876_450_376_968e-10),
(-3.0, 1.349_898_031_630_095e-3),
(-1.959_963_984_540_054, 0.025),
(-1.0, 0.158_655_253_931_457_05),
(0.0, 0.5),
(0.5, 0.691_462_461_274_013_1),
(1.0, 0.841_344_746_068_542_9),
(1.959_963_984_540_054, 0.975),
(3.0, 0.998_650_101_968_369_9),
(6.0, 0.999_999_999_013_412_3),
];
for (z, want) in cases {
let got = phi_hp(z);
assert!(
(got - want).abs() <= 1e-14 * want.abs().max(1e-12),
"phi_hp({z}) = {got}, want {want}"
);
}
for &z in &[0.3, 1.7, 4.2, 8.0] {
assert!((phi_hp(-z) - (1.0 - phi_hp(z))).abs() <= 1e-15);
}
}
#[test]
fn ln_gamma_full_double_precision() {
let cases = [
(0.5, 0.572_364_942_924_700_1), (1.0, 0.0),
(1.5, -0.120_782_237_635_245_22),
(1.9, -0.038_984_275_923_082_73),
(2.0, 0.0),
(5.0, 3.178_053_830_347_945_6), (10.0, 12.801_827_480_081_469), (100.0, 359.134_205_369_575_4),
];
for (x, want) in cases {
let got = ln_gamma(x);
assert!(
(got - want).abs() <= 1e-13 * want.abs().max(1.0),
"ln_gamma({x}) = {got}, want {want}"
);
}
}
#[test]
fn erfc_blend_accuracy_and_head_tail_identity() {
let n = 20_003usize;
let xs: Vec<f64> = (0..n).map(|k| -26.0 + 52.0 * k as f64 / n as f64).collect();
let mut smax = 0i128;
for &x in &xs {
smax = smax.max(ulp(scalar_erfc_blend::<{ FUSED_DEFAULT }>(x), erfc_cody(x)));
}
assert!(smax <= 5, "scalar erfc blend drifted {smax} ULP from Cody");
let mut buf = xs.clone();
erfc_fill(&mut buf);
for i in 0..n {
assert_eq!(
buf[i].to_bits(),
scalar_erfc_blend::<{ FUSED_DEFAULT }>(xs[i]).to_bits(),
"erfc SIMD head diverged from its scalar twin at x={}",
xs[i]
);
}
let mut zmax = 0i128;
for k in 0..n {
let z = -8.0 + 16.0 * k as f64 / n as f64;
let (mu, _) = scalar_probit::<{ FUSED_DEFAULT }>(z);
zmax = zmax.max(ulp(mu, phi_hp(z)));
}
assert!(zmax <= 5, "probit μ drifted {zmax} ULP from phi_hp");
}
#[test]
fn family_pass_simd_head_matches_scalar_tail() {
use crate::spec::{BinomialLink, Family, GammaLink, NegBinomialLink, PoissonLink};
let n = 1_003usize;
let eta: Vec<f64> = (0..n).map(|k| -4.0 + 8.0 * k as f64 / n as f64).collect();
let pw: Vec<f64> = (0..n).map(|k| 1.0 + (k % 5) as f64).collect();
let cases: [(Family, f64); 5] = [
(
Family::Binomial {
link: BinomialLink::Probit,
},
f64::NAN,
),
(
Family::Binomial {
link: BinomialLink::Logit,
},
f64::NAN,
),
(
Family::Poisson {
link: PoissonLink::Log,
},
f64::NAN,
),
(
Family::Gamma {
link: GammaLink::Log,
},
f64::NAN,
),
(
Family::NegativeBinomial {
link: NegBinomialLink::Log,
},
1.7,
),
];
for (family, nb_theta) in cases {
let y: Vec<f64> = match family {
Family::Binomial { .. } => (0..n).map(|k| (k % 3) as f64 / 2.0).collect(),
Family::Gamma { .. } => (0..n).map(|k| 0.5 + (k % 7) as f64).collect(),
_ => (0..n).map(|k| (k % 9) as f64).collect(),
};
let run = |lo: usize,
hi: usize,
e: &mut [f64],
p: &mut [f64],
w: &mut [f64],
z: &mut [f64]| {
family_pass(
family,
nb_theta,
&mut e[lo..hi],
&y[lo..hi],
&pw[lo..hi],
true,
0.0,
&mut p[lo..hi],
&mut w[lo..hi],
&mut z[lo..hi],
)
};
let (mut e1, mut p1, mut w1, mut z1) =
(eta.clone(), vec![0.0; n], vec![0.0; n], vec![0.0; n]);
run(0, n, &mut e1, &mut p1, &mut w1, &mut z1);
let (mut e2, mut p2, mut w2, mut z2) =
(eta.clone(), vec![0.0; n], vec![0.0; n], vec![0.0; n]);
for i in 0..n {
run(i, i + 1, &mut e2, &mut p2, &mut w2, &mut z2);
}
for i in 0..n {
for (a, b, what) in [
(e1[i], e2[i], "eta"),
(p1[i], p2[i], "mu"),
(w1[i], w2[i], "w"),
(z1[i], z2[i], "z"),
] {
assert_eq!(
a.to_bits(),
b.to_bits(),
"{family:?} {what} head/tail split at row {i}: {a} vs {b}"
);
}
}
}
}
#[test]
fn family_pass_gamma_inverse_flags_infeasible_eta() {
use crate::spec::{Family, GammaLink};
let f = Family::Gamma {
link: GammaLink::Inverse,
};
let n = 37usize;
let y = vec![1.5; n];
let ok: Vec<f64> = (0..n).map(|k| 0.1 + 0.05 * k as f64).collect();
let run = |eta: &[f64]| {
let (mut e, mut p, mut w, mut z) =
(eta.to_vec(), vec![0.0; n], vec![0.0; n], vec![0.0; n]);
family_pass(
f,
f64::NAN,
&mut e,
&y,
&[],
false,
0.0,
&mut p,
&mut w,
&mut z,
)
.1
};
assert!(!run(&ok));
let mut head_bad = ok.clone();
head_bad[2] = -0.5;
assert!(run(&head_bad));
let mut tail_bad = ok.clone();
tail_bad[n - 1] = -0.5;
assert!(run(&tail_bad));
}
#[test]
fn weight_clamped_and_finite() {
let eta: Vec<f64> = vec![-50.0, -10.0, -1e-9, 0.0, 1e-9, 10.0, 50.0, 1e3];
let mut p = vec![0.0; eta.len()];
let mut w = vec![0.0; eta.len()];
pw_and_log1pexp_sum(&eta, &mut p, &mut w);
for i in 0..eta.len() {
assert!(p[i].is_finite() && (0.0..=1.0).contains(&p[i]));
assert!(w[i] >= crate::glm::WEIGHT_CLAMP && w[i].is_finite());
}
}
}