Skip to content

Commit 98d63f9

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 beb3e1e commit 98d63f9

File tree

2 files changed

+172
-0
lines changed

2 files changed

+172
-0
lines changed

lib/bigdecimal/math.rb

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

test/bigdecimal/test_bigmath.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,45 @@ def test_log
130130
end
131131
SRC
132132
end
133+
134+
def test_erf
135+
[-0.5, 0.1, 0.3, 2.1, 3.3].each do |x|
136+
assert_in_epsilon(Math.erf(x), BigMath.erf(BigDecimal(x.to_s), N))
137+
end
138+
assert_equal(1, BigMath.erf(BigDecimal(1000), 100))
139+
assert_equal(-1, BigMath.erf(BigDecimal(-1000), 100))
140+
assert_not_equal(1, BigMath.erf(BigDecimal(10), 45))
141+
assert_not_equal(1, BigMath.erf(BigDecimal(15), 100))
142+
assert_in_epsilon(
143+
BigDecimal("0.995322265018952734162069256367252928610891797040060076738352326200437280719995177367629008019680680487939328715594755785"),
144+
BigMath.erf(BigDecimal("2"), 100),
145+
BigDecimal("1e-100")
146+
)
147+
assert_relative_precision {|n| BigMath.erf(BigDecimal("1e-30"), n) }
148+
assert_relative_precision {|n| BigMath.erf(BigDecimal("0.3"), n) }
149+
assert_relative_precision {|n| BigMath.erf(BigDecimal("1.2"), n) }
150+
end
151+
152+
def test_erfc
153+
[-0.5, 0.1, 0.3, 2.1, 3.3].each do |x|
154+
assert_in_epsilon(Math.erfc(x), BigMath.erfc(BigDecimal(x.to_s), N))
155+
end
156+
# erfc with taylor series
157+
assert_in_epsilon(
158+
BigDecimal("2.08848758376254475700078629495778861156081811932116372701221371393817469583344029061076638428572355398152593923652403986e-45"),
159+
BigMath.erfc(BigDecimal("10"), 100),
160+
BigDecimal("1e-100")
161+
)
162+
assert_relative_precision {|n| BigMath.erfc(BigDecimal("0.3"), n) }
163+
assert_relative_precision {|n| BigMath.erfc(BigDecimal("1.2"), n) }
164+
assert_relative_precision {|n| BigMath.erfc(BigDecimal("30"), n) }
165+
# erfc with asymptotic expansion
166+
assert_in_epsilon(
167+
BigDecimal("1.89696105996627650926827825971341543493690756392918618346283475290041180520511188660525669077676004136530598303468056210e-697"),
168+
BigMath.erfc(BigDecimal("40"), 100),
169+
BigDecimal("1e-100")
170+
)
171+
assert_relative_precision {|n| BigMath.erfc(BigDecimal("30"), n) }
172+
assert_relative_precision {|n| BigMath.erfc(BigDecimal("50"), n) }
173+
end
133174
end

0 commit comments

Comments
 (0)