Shared Rust kernels

ported/normal_dist_common

All modules · quoted at commit 9e1884369a67 of DnaCalc/DnaCalc. Comments are the source's own.

phi_kernel · 19 lines · calc-func/src/ported/normal_dist_common.rs on GitHub
/// Worksheet `PHI` numerical graph observed on the reference Excel host.
/// W111 retains a fresh 9,130-row exact numerical replay; surface overflow
/// admission and contextual coercion remain separate obligations.
///
/// ```text
/// sq  = RN53(RN64(x·x))            (x87 double-rounded square)
/// t   = -(sq/2)                    (exact halving + negation)
/// e   = EXP(t)                     (the x87 exponential, `excel_exp`)
/// v   = RN53(RN64(e · RN(1/sqrt(2π))))
/// PHI = +0.0 if |v| < DBL_MIN      (live-pinned publication flush;
///        normal values publish unchanged — boundary probed at bit level)
/// ```
pub fn phi_kernel(x: f64) -> f64 {
    use crate::ported::excel_numeric::{excel_exp, excel_x87_mul};
    let sq = excel_x87_mul(x, x);
    let e = excel_exp(-(sq / 2.0));
    let v = excel_x87_mul(e, INV_SQRT_2PI);
    if v.abs() < f64::MIN_POSITIVE { 0.0 } else { v }
}

Used by 10 functions.

stored_horner · 10 lines · calc-func/src/ported/normal_dist_common.rs on GitHub
/// One Horner evaluation with every product and sum stored to binary64, as `erf_approx` needs.
fn stored_horner(coefficients: &[u64], t: f64) -> f64 {
    use crate::ported::excel_numeric::{excel_x87_add, excel_x87_mul};
    let mut value = 0.0;
    for (i, &bits) in coefficients.iter().enumerate().rev() {
        let c = f64::from_bits(bits);
        value = if i + 1 == coefficients.len() { c } else { excel_x87_add(excel_x87_mul(value, t), c) };
    }
    value
}

Used by 20 functions.

erf_approx · 38 lines · calc-func/src/ported/normal_dist_common.rs on GitHub
/// The shared ERF body (ERF, ERF.PRECISE, the below-half ERFC complement and GAUSS's tiny
/// route).
///
/// For `|x| < 0.5` Excel evaluates a (4, 5) rational `x * N(t) / D(t)` as compiled double
/// arithmetic on the x87 at precision 64: every operation is `RN53(RN64(..))`, that is, its
/// result is stored to binary64 before the next one reads it, in the order
///
/// ```text
/// t = x*x;  N = Horner(t);  D = Horner(t);  p = x*N;  y = p/D
/// ```
///
/// Excel shows this directly. Stored `N` and `D` step at edges found by bisection to one input
/// ulp, and the two `D` edges measured agree to 1e-15 only with the 64-bit register's half unit on
/// each. Of the store placements raced on 2,000 consecutive answers at each of about 20 sites up
/// to 0.5, all stores leaves 0.5% of the rows unexplained (the coefficients' last bits) and every
/// other placement more than twice that. The measured coefficients give `N1/N0 - D1/D0 = -1/3`
/// to 2e-16 and reproduce erf to a flat offset of 9e-17 up to 0.5, past which the error climbs at
/// once, which places the approximation's design interval.
///
/// Above 0.5 the magnitude is the stored-PC64 complement of the upper ERFC body. Independent
/// public calls agree on 3,328 adjacent inputs; exponent-edge controls distinguish the final
/// PC64 store from strict binary64 subtraction. Up to 4 ERFC's `F` remains a stand-in, so this measured
/// composition does not establish its last bits there. Exactly 0.5 keeps its existing boundary body.
/// Subnormal arguments cannot be written to a cell (Excel reads them back as zero).
pub fn erf_approx(x: f64) -> f64 {
    if x.is_normal() && x.abs() < 0.5 {
        use crate::ported::excel_numeric::{excel_x87_div, excel_x87_mul};
        let t = excel_x87_mul(x, x);
        let numerator = stored_horner(&ERF_N, t);
        let denominator = stored_horner(&ERF_D, t);
        return excel_x87_div(excel_x87_mul(x, numerator), denominator);
    }
    if x.abs() > 0.5 {
        use crate::ported::excel_numeric::excel_x87_sub;
        return excel_x87_sub(1.0, erfc_upper(x.abs())).copysign(x);
    }
    libm::erf(x)
}

Used by 20 functions.

erfc_upper · 44 lines · calc-func/src/ported/normal_dist_common.rs on GitHub
/// The upper branch of Excel's erfc for `x >= 0.5`: `Q = E * F`.
///
/// `E` is measured. `z = RN53(RN64(x*x))` and `E = exp(-z)` are stored to binary64 before use: over
/// 220,000 Excel answers up to `x = 26.5` the residual of `Q` against this `E` times the true
/// `exp(x*x)*erfc(x)` stays within about 10 units of 2^-53, where the exact `exp(-x*x)` spreads by
/// hundreds of units. `z`'s double rounding shows directly: plain binary64 `x*x` leaves outliers of
/// up to 1,024 units.
///
/// `F` has two branches. `x = 4` itself falls below: bisected on consecutive doubles, Excel leaves the table below at the next double up.
///
/// Above 4, `F = N(x) / D(x)`, degrees (8, 9), Horner in `x`, each product, sum and the final
/// quotient stored to binary64 from precision 64, and `Q` is the stored product `E * F`. The
/// coefficients were read from Excel one stored stage at a time, from the top of each polynomial:
/// the rounding of a stored stage leaves a sawtooth in the answers, so runs of consecutive doubles
/// at geometrically spaced `x` give each stage's phase, and the next stage's phase over a long
/// baseline gives the coefficient above it. The answers' last bits then fix the low bits, band by
/// band downward from `x = 26.5`. `D(0)` reads as 1 and `N/D` follows `exp(x*x)*erfc(x)`'s
/// expansion at infinity: the top two coefficients agree between `N` and `D` to 1e-14, the next
/// differs by 1/2.
///
/// Up to 4 `F` is a stand-in with the same store discipline, and last bits differ. The stage tones
/// of the table above stop between 3.95 and 4.05. Excel's own table there is not identified.
pub fn erfc_upper(x: f64) -> f64 {
    use crate::ported::excel_numeric::{excel_exp, excel_x87_add, excel_x87_div, excel_x87_mul};
    let step = |value: f64, c: f64, by: f64| excel_x87_mul(excel_x87_add(value, c), by);
    let z = excel_x87_mul(x, x);
    let e = excel_exp(-z);
    if e == 0.0 {
        // Past about 27 the exponential is gone and the polynomials below can overflow.
        return 0.0;
    }
    let f = if x > 4.0 {
        excel_x87_div(stored_horner(&ERFC_TAIL_N, x), stored_horner(&ERFC_TAIL_D, x))
    } else {
        let mut num = excel_x87_mul(ERFC_C[8], x);
        let mut den = x;
        for i in 0..7 {
            num = step(num, ERFC_C[i], x);
            den = step(den, ERFC_D[i], x);
        }
        excel_x87_div(excel_x87_add(num, ERFC_C[7]), excel_x87_add(den, ERFC_D[7]))
    };
    excel_x87_mul(e, f)
}

Used by 20 functions.