Skip to content

Commit cd5070c

Browse files
committed
Add normal distribution functions
1 parent 5c2c97e commit cd5070c

14 files changed

Lines changed: 739 additions & 5 deletions

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,8 @@ assert_eq!(value.exact_rational(), Some(Rational::fraction(7, 8).unwrap()));
320320
```
321321

322322
`Simple` supports arithmetic, roots, powers, logs, exponentials, trig, inverse trig,
323-
inverse hyperbolic functions, integers, decimals, fractions, `pi`, and `e`.
323+
inverse hyperbolic functions, normal distribution helpers (`erf`, `dnorm`, `pnorm`,
324+
`qnorm`), integers, decimals, fractions, `pi`, and `e`.
324325

325326
## Conversions
326327

src/computable/approximation.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ include!("approximation/trig.rs");
1818
include!("approximation/logarithms.rs");
1919
include!("approximation/inverse_trig.rs");
2020
include!("approximation/inverse_hyperbolic.rs");
21+
include!("approximation/statistics.rs");

src/computable/approximation/dispatch.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ impl Approximation {
7979
PrescaledTan(c) => tan(signal, c, p),
8080
PrescaledTanRational(r) => tan_rational(signal, r, p),
8181
PrescaledCot(c) => cot(signal, c, p),
82+
ErfSeries(c) => erf_series(signal, c, p),
83+
NormalQuantile {
84+
p: prob,
85+
seed,
86+
seed_prec,
87+
} => normal_quantile(signal, prob, seed, *seed_prec, p),
8288
}
8389
}
8490
}
@@ -122,4 +128,3 @@ fn raw(kind: Approximation) -> Computable {
122128
signal: None,
123129
}
124130
}
125-

src/computable/approximation/representation.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ pub(super) enum Approximation {
143143
// the same local quotient kernel once digits are requested.
144144
PrescaledTanRational(Rational),
145145
PrescaledCot(Computable),
146+
ErfSeries(Computable),
147+
NormalQuantile {
148+
p: Computable,
149+
seed: BigInt,
150+
seed_prec: Precision,
151+
},
146152
}
147153

148154
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -165,4 +171,3 @@ pub(super) enum SharedConstant {
165171
AtanInv2,
166172
AtanInv5,
167173
}
168-
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
fn to_prec(n: &BigInt) -> Precision {
2+
n.to_i32().unwrap_or(Precision::MAX)
3+
}
4+
5+
fn erf_series(signal: &Option<Signal>, op: &Computable, p: Precision) -> BigInt {
6+
let rough_x = op.approx_signal(signal, -10);
7+
let x_sq_approx = (&rough_x * &rough_x) >> 20;
8+
9+
let n_estimate = {
10+
let estimate = &x_sq_approx + BigInt::from(-p) + BigInt::from(10);
11+
if estimate < BigInt::one() {
12+
BigInt::one()
13+
} else {
14+
estimate
15+
}
16+
};
17+
let magnitude_bits = to_prec(&((&x_sq_approx * BigInt::from(3)) / BigInt::from(2))) + 2;
18+
let guard_bits = (n_estimate.magnitude().bits() as Precision) + magnitude_bits + 4;
19+
let calc_precision = p - guard_bits;
20+
let op_prec = calc_precision - 8;
21+
let op_appr = op.approx_signal(signal, op_prec);
22+
let max_trunc_error = signed::ONE.deref() << (p - 4 - calc_precision);
23+
24+
let mut n: i64 = 0;
25+
let mut current_term = scale(op_appr.clone(), op_prec - calc_precision);
26+
let mut current_sum = current_term.clone();
27+
loop {
28+
if should_stop(signal) {
29+
break;
30+
}
31+
let prev = current_term;
32+
let mut t = scale(&prev * &op_appr, op_prec);
33+
t = scale(t * &op_appr, op_prec);
34+
t = (t * signed::TWO.deref()) / BigInt::from(2 * n + 3);
35+
n += 1;
36+
if t.is_zero() {
37+
break;
38+
}
39+
40+
let prev_abs = prev.abs();
41+
let cur_abs = t.abs();
42+
if cur_abs < prev_abs {
43+
let denom = &prev_abs - &cur_abs;
44+
if &cur_abs * &prev_abs < &max_trunc_error * &denom {
45+
break;
46+
}
47+
}
48+
current_sum += &t;
49+
current_term = t;
50+
}
51+
scale(current_sum, calc_precision - p)
52+
}
53+
54+
const NORMAL_QUANTILE_RESULT_MSD_BOUND: Precision = 5;
55+
56+
fn normal_quantile(
57+
signal: &Option<Signal>,
58+
p: &Computable,
59+
seed: &BigInt,
60+
seed_prec: Precision,
61+
prec: Precision,
62+
) -> BigInt {
63+
if prec >= seed_prec || should_stop(signal) {
64+
return scale(seed.clone(), seed_prec - prec);
65+
}
66+
67+
let appr_prec = (NORMAL_QUANTILE_RESULT_MSD_BOUND + prec) / 2 - 6;
68+
let xn_int = normal_quantile(signal, p, seed, seed_prec, appr_prec);
69+
let xn = Computable::integer(xn_int).shift_left(appr_prec);
70+
let fx = xn.clone().pnorm();
71+
let phi_xn = xn.clone().dnorm();
72+
let x_next = xn.add(fx.add(p.clone().negate()).multiply(phi_xn.inverse()).negate());
73+
74+
(x_next.approx_signal(signal, prec - 2) + signed::TWO.deref()) >> 2
75+
}

src/computable/node/algebra.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,55 @@ impl Computable {
483483
}
484484
}
485485

486+
/// Error function, erf(x).
487+
pub fn erf(self) -> Computable {
488+
if let Some(rational) = self.exact_rational()
489+
&& rational.sign() == Sign::NoSign
490+
{
491+
return Self::zero();
492+
}
493+
let series = Self {
494+
internal: Box::new(Approximation::ErfSeries(self.clone())),
495+
cache: RefCell::new(Cache::Invalid),
496+
bound: RefCell::new(BoundCache::Invalid),
497+
exact_sign: RefCell::new(ExactSignCache::Invalid),
498+
signal: None,
499+
};
500+
let gaussian = self.square().negate().exp();
501+
let two_over_sqrt_pi = Self::pi().sqrt().inverse().shift_left(1);
502+
two_over_sqrt_pi.multiply(gaussian).multiply(series)
503+
}
504+
505+
/// Standard normal CDF.
506+
pub fn pnorm(self) -> Computable {
507+
if let Some(rational) = self.exact_rational()
508+
&& rational.sign() == Sign::NoSign
509+
{
510+
return Self::rational(HALF_RATIONAL.clone());
511+
}
512+
let sqrt2 = Self::sqrt_constant(2).unwrap_or_else(|| Self::integer(BigInt::from(2)).sqrt());
513+
let z = self.multiply(sqrt2.inverse());
514+
Self::one().add(z.erf()).shift_right(1)
515+
}
516+
517+
/// Standard normal density.
518+
pub fn dnorm(self) -> Computable {
519+
let neg_half_x_sq = self.square().shift_right(1).negate();
520+
let sqrt_2pi = Self::pi().shift_left(1).sqrt();
521+
neg_half_x_sq.exp().multiply(sqrt_2pi.inverse())
522+
}
523+
524+
/// Standard normal quantile by Newton iteration with the analytic density.
525+
pub fn normal_quantile(p: Computable, seed: BigInt, seed_prec: Precision) -> Computable {
526+
Self {
527+
internal: Box::new(Approximation::NormalQuantile { p, seed, seed_prec }),
528+
cache: RefCell::new(Cache::Invalid),
529+
bound: RefCell::new(BoundCache::Invalid),
530+
exact_sign: RefCell::new(ExactSignCache::Invalid),
531+
signal: None,
532+
}
533+
}
534+
486535
/// Attach an abort signal checked by long-running approximation routines.
487536
pub fn abort(&mut self, s: Signal) {
488537
self.signal = Some(s);

src/computable/node/primitive_constructors.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,25 @@ impl Computable {
165165
(Approximation::PrescaledCot(left), Approximation::PrescaledCot(right)) => {
166166
Computable::internal_structural_eq(left, right)
167167
}
168+
(Approximation::ErfSeries(left), Approximation::ErfSeries(right)) => {
169+
Computable::internal_structural_eq(left, right)
170+
}
171+
(
172+
Approximation::NormalQuantile {
173+
p: left_p,
174+
seed: left_seed,
175+
seed_prec: left_seed_prec,
176+
},
177+
Approximation::NormalQuantile {
178+
p: right_p,
179+
seed: right_seed,
180+
seed_prec: right_seed_prec,
181+
},
182+
) => {
183+
left_seed == right_seed
184+
&& left_seed_prec == right_seed_prec
185+
&& Computable::internal_structural_eq(left_p, right_p)
186+
}
168187
_ => false,
169188
}
170189
}
@@ -749,4 +768,3 @@ impl Computable {
749768
}
750769
}
751770
}
752-

src/computable/node/tests.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,53 @@ mod tests {
11921192
assert_close(value.clone().ln().exp(), value, -40, 2);
11931193
}
11941194

1195+
#[test]
1196+
fn erf_known_values() {
1197+
assert_close(
1198+
Computable::zero().erf(),
1199+
Computable::zero(),
1200+
-160,
1201+
2,
1202+
);
1203+
assert_close(
1204+
Computable::rational(Rational::fraction(1, 2).unwrap()).erf(),
1205+
Computable::rational("0.5204998778130465376827466538919645287364".parse().unwrap()),
1206+
-90,
1207+
2,
1208+
);
1209+
assert_close(
1210+
Computable::one().erf(),
1211+
Computable::rational("0.8427007929497148693412206350826092592960".parse().unwrap()),
1212+
-90,
1213+
2,
1214+
);
1215+
}
1216+
1217+
#[test]
1218+
fn normal_density_and_cdf_known_values() {
1219+
assert_close(
1220+
Computable::zero().dnorm(),
1221+
Computable::rational("0.39894228040143267793994605993438186847585863".parse().unwrap()),
1222+
-120,
1223+
2,
1224+
);
1225+
assert_close(
1226+
Computable::one().pnorm(),
1227+
Computable::rational("0.8413447460685429485852325456320379224779".parse().unwrap()),
1228+
-120,
1229+
2,
1230+
);
1231+
}
1232+
1233+
#[test]
1234+
fn normal_quantile_inverts_cdf() {
1235+
let two = Computable::rational(Rational::new(2));
1236+
let p = two.clone().pnorm();
1237+
let seed = BigInt::from((1.9999_f64 * f64::from(1_u32 << 13)).round() as i64);
1238+
let q = Computable::normal_quantile(p, seed, -13);
1239+
assert_close(q, two, -120, 2);
1240+
}
1241+
11951242
#[test]
11961243
fn add() {
11971244
let three: BigInt = "3".parse().unwrap();

src/real/arithmetic.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::computable::Precision;
12
use crate::{
23
CertifiedRealEquality, CertifiedRealOrdering, CertifiedRealSign, Computable, DomainFacts,
34
DomainStatus, ExpressionDegree, IdentityFacts, MagnitudeBits, OrderingFacts, PrimitiveFacts,

0 commit comments

Comments
 (0)