Skip to main content

normal_cdf

Function normal_cdf 

Source
pub fn normal_cdf(x: f64) -> f64
Expand description

Standard normal CDF Phi(x) evaluated via the exact special-function identity

Phi(x) = 0.5 * erfc(-x / sqrt(2)).

This is the exact Gaussian CDF semantics used throughout the codebase. The numerical erfc implementation may use internal approximations, but the returned function is the standard normal CDF itself rather than a separate polynomial surrogate surface.

Examples found in repository?
examples/special_audit_dump.rs (line 79)
33fn main() {
34    // ---- Bessel channels -------------------------------------------------
35    for eta in geometric_grid(1e-6, 1e12, 400) {
36        let (centered, ratio, d1) = special::bessel_i0_centered_terms(eta);
37        emit("bessel_centered_log", eta, centered);
38        emit("bessel_ratio", eta, ratio);
39        emit("bessel_d1", eta, d1);
40        emit(
41            "bessel_d2",
42            eta,
43            special::bessel_i0_centered_second_log_derivative_from_log_abs(eta.ln()),
44        );
45    }
46    // Dense sweep across the ascending/asymptotic seam at 20.
47    for eta in linear_grid(15.0, 25.0, 201) {
48        let (centered, ratio, d1) = special::bessel_i0_centered_terms(eta);
49        emit("bessel_centered_log", eta, centered);
50        emit("bessel_ratio", eta, ratio);
51        emit("bessel_d1", eta, d1);
52        emit(
53            "bessel_d2",
54            eta,
55            special::bessel_i0_centered_second_log_derivative_from_log_abs(eta.ln()),
56        );
57    }
58
59    // ---- Polygamma family ------------------------------------------------
60    for x in geometric_grid(1e-8, 1e10, 400) {
61        emit("digamma", x, special::digamma(x));
62        emit("trigamma", x, special::trigamma(x));
63        emit("tetragamma", x, special::tetragamma(x));
64        emit("pentagamma", x, special::pentagamma(x));
65    }
66    for x in linear_grid(0.5, 30.0, 300) {
67        emit("digamma", x, special::digamma(x));
68        emit("trigamma", x, special::trigamma(x));
69        emit("tetragamma", x, special::tetragamma(x));
70        emit("pentagamma", x, special::pentagamma(x));
71    }
72
73    // ---- Normal-distribution channels ------------------------------------
74    let mut normal_args: Vec<f64> = linear_grid(-40.0, 40.0, 801);
75    normal_args.extend(geometric_grid(1e-8, 1e2, 200));
76    normal_args.extend(geometric_grid(1e-8, 1e2, 200).into_iter().map(|v| -v));
77    for x in normal_args {
78        emit("normal_pdf", x, prob::normal_pdf(x));
79        emit("normal_cdf", x, prob::normal_cdf(x));
80        // The upper tail as its own quantity. `1 - normal_cdf(x)` is exactly
81        // zero above x ~ 8.3 and 7% high already at x = 8 (#2562); this channel
82        // exists so that cannot silently return.
83        emit("normal_sf", x, prob::normal_sf(x));
84        emit("normal_logcdf", x, prob::normal_logcdf(x));
85        emit("normal_logsf", x, prob::normal_logsf(x));
86        let (log_cdf, mills) = prob::signed_probit_logcdf_and_mills_ratio(x);
87        emit("probit_logcdf", x, log_cdf);
88        emit("probit_mills", x, mills);
89        let derivatives = prob::normal_logcdf_derivatives(x);
90        for (order, value) in derivatives.iter().enumerate() {
91            emit(&format!("logcdf_d{order}"), x, *value);
92        }
93        if x >= 0.0 {
94            emit("erfcx", x, prob::erfcx_nonnegative(x));
95        }
96    }
97    for x in geometric_grid(1e-3, 1e6, 400) {
98        emit("erfcx", x, prob::erfcx_nonnegative(x));
99    }
100
101    // ---- log1mexp --------------------------------------------------------
102    for a in geometric_grid(1e-14, 50.0, 300) {
103        emit("log1mexp", a, prob::log1mexp_positive(a));
104    }
105
106    // ---- Normal quantile -------------------------------------------------
107    for p in geometric_grid(1e-300, 0.5, 400) {
108        if let Ok(q) = prob::standard_normal_quantile(p) {
109            emit("normal_quantile", p, q);
110        }
111    }
112    for p in linear_grid(0.001, 0.999, 400) {
113        if let Ok(q) = prob::standard_normal_quantile(p) {
114            emit("normal_quantile", p, q);
115        }
116    }
117    for log_p in linear_grid(-700.0, -1e-6, 400) {
118        if let Ok(q) = prob::standard_normal_quantile_from_log_cdf(log_p) {
119            emit("normal_quantile_from_log", log_p, q);
120        }
121    }
122
123    // ---- Gauss-Legendre --------------------------------------------------
124    for n in [
125        3usize, 4, 5, 7, 8, 12, 15, 16, 20, 24, 31, 32, 40, 48, 63, 64, 80, 96, 100, 127, 128, 160,
126        200, 256,
127    ] {
128        let (nodes, weights) = special::gauss_legendre(n);
129        for (i, (node, weight)) in nodes.iter().zip(weights.iter()).enumerate() {
130            println!("gl_node\t{n}\t{i}\t{node:e}");
131            println!("gl_weight\t{n}\t{i}\t{weight:e}");
132        }
133    }
134
135    // ---- Binomial coefficient --------------------------------------------
136    for n in 0usize..=60 {
137        for k in 0..=n {
138            println!(
139                "binomial\t{n}\t{k}\t{:e}",
140                special::binomial_coefficient_f64(n, k)
141            );
142        }
143    }
144
145    // Beta quantiles. The shapes are the ones a beta-regression predictive
146    // interval produces from a mean and a variance, plus a direct sweep of
147    // small and large shape pairs. The lower tail here reaches quantiles far
148    // below `f64::EPSILON`, which is exactly where a solver with an absolute
149    // tolerance in `x` stalls rather than degrading (#2528), so the sweep is
150    // deliberately weighted toward small `a`.
151    for (mu, variance_fraction) in [
152        (0.001_f64, 0.3_f64),
153        (0.01, 0.2),
154        (0.01, 0.5),
155        (0.02, 0.3),
156        (0.05, 0.5),
157        (0.1, 0.5),
158        (0.3, 0.5),
159        (0.5, 0.5),
160        (0.7, 0.5),
161        (0.9, 0.2),
162    ] {
163        let bernoulli_variance = mu * (1.0 - mu);
164        let precision = 1.0 / variance_fraction - 1.0;
165        let (a, b) = (mu * precision, (1.0 - mu) * precision);
166        for p in [0.001_f64, 0.025, 0.1, 0.5, 0.9, 0.975, 0.999] {
167            println!(
168                "beta_quantile\t{a:e}\t{b:e}\t{p:e}\t{:e}",
169                prob::beta_quantile(p, a, b)
170            );
171        }
172        // Keep the moment-matched variance in the record so a reader can see
173        // which mean produced which shape pair.
174        println!("beta_shape\t{mu:e}\t{bernoulli_variance:e}\t{a:e}\t{b:e}");
175    }
176    for (a, b) in [
177        (0.1_f64, 0.1_f64),
178        (0.5, 0.5),
179        (0.5, 20.0),
180        (20.0, 0.5),
181        (1.0, 1.0),
182        (2.0, 3.0),
183        (2.5, 7.5),
184        (100.0, 100.0),
185        (1000.0, 5.0),
186        (5.0, 1000.0),
187    ] {
188        for p in [
189            1.0e-8_f64, 1.0e-4, 0.001, 0.01, 0.025, 0.1, 0.25, 0.5, 0.75, 0.9, 0.975, 0.999,
190        ] {
191            println!(
192                "beta_quantile\t{a:e}\t{b:e}\t{p:e}\t{:e}",
193                prob::beta_quantile(p, a, b)
194            );
195        }
196    }
197
198    // ---- Student-t survival function -------------------------------------
199    // Two-argument channel (nu, t). The complement `1 - cdf` saturates SOONER
200    // the larger nu is -- already at nu = 500, t = 10, where the true tail is
201    // 6.9e-22 -- because the loss is in the subtraction, not in the cdf (#2562).
202    for nu in [1.0_f64, 2.5, 5.0, 30.0, 120.0, 500.0, 5000.0, 1.0e4] {
203        for t in [
204            0.0_f64, 0.5, 1.0, 2.0, 4.0, 6.0, 8.0, 10.0, 15.0, 20.0, 30.0, 40.0, 80.0,
205        ] {
206            println!(
207                "students_t_sf\t{nu:e}\t{t:e}\t{:e}",
208                prob::student_t_sf(t, nu)
209            );
210            println!(
211                "students_t_sf\t{nu:e}\t{:e}\t{:e}",
212                -t,
213                prob::student_t_sf(-t, nu)
214            );
215        }
216    }
217}