Skip to content

Commit c885098

Browse files
committed
Implement BigMath.erf(x, prec) and BigMath.erfc(x, prec)
Uses asymptotic expansion of erfc if possible and fallback to taylor series of erf
1 parent 07696bc commit c885098

File tree

2 files changed

+172
-0
lines changed

2 files changed

+172
-0
lines changed

lib/bigdecimal/math.rb

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
# cos (x, prec)
1010
# tan (x, prec)
1111
# atan(x, prec)
12+
# erf (x, prec)
13+
# erfc(x, prec)
1214
# PI (prec)
1315
# E (prec) == exp(1.0,prec)
1416
#
@@ -246,4 +248,130 @@ def E(prec)
246248
BigDecimal::Internal.validate_prec(prec, :E)
247249
BigMath.exp(1, prec)
248250
end
251+
252+
# call-seq:
253+
# erf(decimal, numeric) -> BigDecimal
254+
#
255+
# Computes the error function of +decimal+ to the specified number of digits of
256+
# precision, +numeric+.
257+
#
258+
# If +decimal+ is NaN, returns NaN.
259+
#
260+
# BigMath.erf(BigDecimal('1'), 32).to_s
261+
# #=> "0.84270079294971486934122063508261e0"
262+
#
263+
def erf(x, prec)
264+
BigDecimal::Internal.validate_prec(prec, :erf)
265+
x = BigDecimal::Internal.coerce_to_bigdecimal(x, prec, :erf)
266+
return BigDecimal::Internal.nan_computation_result if x.nan?
267+
return BigDecimal(x.infinite?) if x.infinite?
268+
return BigDecimal(0) if x == 0
269+
return -erf(-x, prec) if x < 0
270+
271+
if x > 8 && (erfc1 = _erfc_asymptotic(x.abs, 1))
272+
erfc2 = _erfc_asymptotic(x.abs, [prec + erfc1.exponent, 1].max)
273+
return BigDecimal(1).sub(erfc2, prec) if erfc2
274+
end
275+
276+
prec2 = prec + BigDecimal.double_fig
277+
base = BigDecimal::BASE ** 2
278+
x_smallprec = (x * base).fix / base
279+
# Taylor series of x with small precision is fast
280+
erf1 = _erf_taylor(x_smallprec, BigDecimal(0), BigDecimal(0), prec2)
281+
# Taylor series converges quickly for small x
282+
v = _erf_taylor(x - x_smallprec, x_smallprec, erf1, prec2).mult(1, prec)
283+
[BigDecimal(1), v].min
284+
end
285+
286+
# call-seq:
287+
# erfc(decimal, numeric) -> BigDecimal
288+
#
289+
# Computes the complementary error function of +decimal+ to the specified number of digits of
290+
# precision, +numeric+.
291+
#
292+
# If +decimal+ is NaN, returns NaN.
293+
#
294+
# BigMath.erfc(BigDecimal('10'), 32).to_s
295+
# #=> "0.20884875837625447570007862949578e-44"
296+
#
297+
def erfc(x, prec)
298+
BigDecimal::Internal.validate_prec(prec, :erfc)
299+
x = BigDecimal::Internal.coerce_to_bigdecimal(x, prec, :erfc)
300+
return BigDecimal::Internal.nan_computation_result if x.nan?
301+
return BigDecimal(1 - x.infinite?) if x.infinite?
302+
return BigDecimal(1).sub(erf(x, prec), prec) if x < 0
303+
304+
if x >= 8
305+
y = _erfc_asymptotic(x, prec)
306+
return y.mult(1, prec) if y
307+
end
308+
309+
# erfc(x) = 1 - erf(x) < exp(-x**2)/x/sqrt(pi)
310+
# Precision of erf(x) needs about log10(exp(-x**2)) extra digits
311+
log10 = 2.302585092994046
312+
high_prec = prec + BigDecimal.double_fig + (x**2 / log10).ceil
313+
BigDecimal(1).sub(erf(x, high_prec), prec)
314+
end
315+
316+
317+
private def _erf_taylor(x, a, erf_a, prec)
318+
# Let f(x) = erf(x+a)*exp((x+a)**2)*sqrt(pi)/2
319+
# = c0 + c1*x + c2*x**2 + c3*x**3 + c4*x**4 + ...
320+
# f'(x) = 1+2*(x+a)*f(x)
321+
# f'(x) = c1 + 2*c2*x + 3*c3*x**2 + 4*c4*x**3 + 5*c5*x**4 + ...
322+
# = 1+2*(x+a)*(c0 + c1*x + c2*x**2 + c3*x**3 + c4*x**4 + ...)
323+
# therefore,
324+
# c0 = f(0)
325+
# c1 = 2 * a * c0 + 1
326+
# c2 = (2 * c0 + 2 * a * c1) / 2
327+
# c3 = (2 * c1 + 2 * a * c2) / 3
328+
# c4 = (2 * c2 + 2 * a * c3) / 4
329+
330+
return erf_a if x.zero?
331+
332+
scale = BigDecimal(2).div(sqrt(PI(prec), prec), prec)
333+
c_prev = erf_a.div(scale.mult(BigMath.exp(-a*a, prec), prec), prec)
334+
c_next = (2 * a * c_prev).add(1, prec).mult(x, prec)
335+
v = c_prev.add(c_next, prec)
336+
337+
2.step do |k|
338+
c = (c_prev.mult(x, prec) + a * c_next).mult(2, prec).mult(x, prec).div(k, prec)
339+
v = v.add(c, prec)
340+
c_prev, c_next = c_next, c
341+
break if [c_prev, c_next].all? { |c| c.zero? || (c.exponent < v.exponent - prec) }
342+
end
343+
v = v.mult(scale.mult(BigMath.exp(-(x + a).mult(x + a, prec), prec), prec), prec)
344+
v > 1 ? BigDecimal(1) : v
345+
end
346+
347+
private def _erfc_asymptotic(x, prec)
348+
# Let f(x) = erfc(x)*sqrt(pi)*exp(x**2)/2
349+
# f(x) satisfies the following differential equation:
350+
# 2*x*f(x) = f'(x) + 1
351+
# From the above equation, we can derive the following asymptotic expansion:
352+
# f(x) = sum { (-1)**k * (2*k)! / 4***k / k! / x**(2*k)) } / x
353+
354+
# This asymptotic expansion does not converge.
355+
# But if there is a k that satisfies (2*k)! / 4***k / k! / x**(2*k) < 10**(-prec),
356+
# It is enough to calculate erfc within the given precision.
357+
# (2*k)! / 4**k / k! can be approximated as sqrt(2) * (k/e)**k by using Stirling's approximation.
358+
prec += BigDecimal.double_fig
359+
xf = x.to_f
360+
log10xf = Math.log10(xf)
361+
kmax = 1
362+
until kmax * Math.log10(kmax / Math::E) + 1 - 2 * kmax * log10xf < -prec
363+
kmax += 1
364+
return if xf * xf < kmax # Unable to calculate with the given precision
365+
end
366+
367+
sum = BigDecimal(1)
368+
x2 = x.mult(x, prec)
369+
d = BigDecimal(1)
370+
(1..kmax).each do |k|
371+
d = d.div(x2, prec).mult(1 - 2 * k, prec).div(2, prec)
372+
sum = sum.add(d, prec)
373+
end
374+
expx2 = BigMath.exp(x.mult(x, prec), prec)
375+
sum.div(expx2.mult(PI(prec).sqrt(prec), prec), prec).div(x, prec)
376+
end
249377
end

test/bigdecimal/test_bigmath.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,48 @@ def test_log
162162
end
163163
SRC
164164
end
165+
166+
def test_erf
167+
[-0.5, 0.1, 0.3, 2.1, 3.3].each do |x|
168+
assert_in_epsilon(Math.erf(x), BigMath.erf(BigDecimal(x.to_s), N))
169+
end
170+
assert_equal(1, BigMath.erf(PINF, N))
171+
assert_equal(-1, BigMath.erf(MINF, N))
172+
assert_equal(1, BigMath.erf(BigDecimal(1000), 100))
173+
assert_equal(-1, BigMath.erf(BigDecimal(-1000), 100))
174+
assert_not_equal(1, BigMath.erf(BigDecimal(10), 45))
175+
assert_not_equal(1, BigMath.erf(BigDecimal(15), 100))
176+
assert_equal(
177+
BigDecimal("0.9953222650189527341620692563672529286108917970400600767383523262004372807199951773676290080196806805"),
178+
BigMath.erf(BigDecimal("2"), 100)
179+
)
180+
assert_converge_in_precision {|n| BigMath.erf(BigDecimal("1e-30"), n) }
181+
assert_converge_in_precision {|n| BigMath.erf(BigDecimal("0.3"), n) }
182+
assert_converge_in_precision {|n| BigMath.erf(SQRT2, n) }
183+
end
184+
185+
def test_erfc
186+
[-0.5, 0.1, 0.3, 2.1, 3.3].each do |x|
187+
assert_in_epsilon(Math.erfc(x), BigMath.erfc(BigDecimal(x.to_s), N))
188+
end
189+
assert_equal(0, BigMath.erfc(PINF, N))
190+
assert_equal(2, BigMath.erfc(MINF, N))
191+
192+
# erfc with taylor series
193+
assert_equal(
194+
BigDecimal("2.088487583762544757000786294957788611560818119321163727012213713938174695833440290610766384285723554e-45"),
195+
BigMath.erfc(BigDecimal("10"), 100)
196+
)
197+
assert_converge_in_precision {|n| BigMath.erfc(BigDecimal("0.3"), n) }
198+
assert_converge_in_precision {|n| BigMath.erfc(SQRT2, n) }
199+
assert_converge_in_precision {|n| BigMath.erfc(BigDecimal("8"), n) }
200+
# erfc with asymptotic expansion
201+
assert_equal(
202+
BigDecimal("1.896961059966276509268278259713415434936907563929186183462834752900411805205111886605256690776760041e-697"),
203+
BigMath.erfc(BigDecimal("40"), 100)
204+
)
205+
assert_converge_in_precision {|n| BigMath.erfc(BigDecimal("30"), n) }
206+
assert_converge_in_precision {|n| BigMath.erfc(30 * SQRT2, n) }
207+
assert_converge_in_precision {|n| BigMath.erfc(BigDecimal("50"), n) }
208+
end
165209
end

0 commit comments

Comments
 (0)