Skip to content

Commit 02e22d4

Browse files
committed
Implement BigMath.erf(x, prec) and BigMath.erfc(x, prec)
erf(x) uses taylor series erfc(x) uses asymptotic expansion if possible and fallback to 1-erf(x)
1 parent bf22f51 commit 02e22d4

File tree

2 files changed

+147
-0
lines changed

2 files changed

+147
-0
lines changed

lib/bigdecimal/math.rb

Lines changed: 106 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,108 @@ 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+
# erf(x) = (2 / sqrt(pi)) * exp(-x**2) * sum { 4**k * k! * x**(2k + 1) / (2k + 1)! }
253+
# Controlling precision of this series is easier than normal erf taylor series.
254+
prec += BigDecimal.double_fig
255+
x2 = x.mult(x, prec)
256+
257+
log10 = 2.302585092994046
258+
return BigDecimal(x > 0 ? 1 : -1) if x2 > prec * log10
259+
260+
a = BigDecimal(2).div(sqrt(PI(prec), prec), prec).mult(BigMath.exp(-x2, prec), prec)
261+
b = x
262+
d = x
263+
1.step do |k|
264+
d = d.mult(2, prec).div(2 * k + 1, prec).mult(x2, prec)
265+
b = b.add(d, prec)
266+
if d.exponent < b.exponent - prec
267+
break
268+
end
269+
end
270+
v = a.mult(b, prec)
271+
v > 1 ? BigDecimal(1) : v
272+
end
273+
274+
# call-seq:
275+
# erfc(decimal, numeric) -> BigDecimal
276+
#
277+
# Computes the complementary error function of +decimal+ to the specified number of digits of
278+
# precision, +numeric+.
279+
#
280+
# If +decimal+ is NaN, returns NaN.
281+
#
282+
# BigMath.erfc(BigDecimal('10'), 16).to_s
283+
# #=> "0.20884875837625447570007862949578e-44"
284+
#
285+
def erfc(x, prec)
286+
raise ArgumentError, "Zero or negative precision for erfc" if prec <= 0
287+
return BigDecimal("NaN") if x.nan?
288+
return BigDecimal(1).sub(erf(x, prec), prec + BigDecimal.double_fig) if x < 0
289+
290+
if x >= 8
291+
# Faster asymptotic expansion can be used if x is large enough
292+
y = _erfc_asymptotic(x, prec)
293+
return y if y
294+
end
295+
296+
prec += BigDecimal.double_fig
297+
298+
# erfc(x) = 1 - erf(x) < exp(-x**2)/x/sqrt(pi)
299+
# Precision of erf(x) needs about log10(exp(-x**2)) extra digits
300+
log10 = 2.302585092994046
301+
high_prec = prec + (x**2 / log10).ceil
302+
BigDecimal(1).sub(erf(x, high_prec), prec)
303+
end
304+
305+
private def _erfc_asymptotic(x, prec)
306+
# Let f(x) = erfc(x)*sqrt(pi)*exp(x**2)/2
307+
# f(x) satisfies the following differential equation:
308+
# 2*x*f(x) = f'(x) + 1
309+
# From the above equation, we can derive the following asymptotic expansion:
310+
# f(x) = sum { (-1)**k * (2*k)! / 4***k / k! / x**(2*k)) } / x
311+
312+
# This asymptotic expansion does not converge.
313+
# But if there is a k that satisfies (2*k)! / 4***k / k! / x**(2*k) < 10**(-prec),
314+
# It is enough to calculate erfc within the given precision.
315+
# (2*k)! / 4**k / k! can be approximated as sqrt(2) * (k/e)**k by using Stirling's approximation.
316+
prec += BigDecimal.double_fig
317+
xf = x.to_f
318+
log10xf = Math.log10(xf)
319+
kmax = 1
320+
until kmax * Math.log10(kmax / Math::E) + 1 - 2 * kmax * log10xf < -prec
321+
kmax += 1
322+
return if xf * xf < kmax # unable to calculate with the given precision
323+
end
324+
325+
sum = BigDecimal(1)
326+
xinv = BigDecimal(1).div(x, prec)
327+
xinv2 = xinv.mult(xinv, prec)
328+
d = BigDecimal(1)
329+
(1..kmax).each do |k|
330+
d = d.mult(xinv2, prec).mult(1 - 2 * k, prec).div(2, prec)
331+
sum = sum.add(d, prec)
332+
end
333+
expx2 = BigMath.exp(x.mult(x, prec), prec)
334+
335+
# Workaround for https://github.com/ruby/bigdecimal/issues/345
336+
expx2 = BigDecimal(expx2) unless expx2.is_a?(BigDecimal)
337+
338+
sum.div(expx2.mult(PI(prec).sqrt(prec), prec), prec).mult(xinv, prec)
339+
end
234340
end

test/bigdecimal/test_bigmath.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,45 @@ def test_log
115115
end
116116
SRC
117117
end
118+
119+
def test_erf
120+
[-0.5, 0.1, 0.3, 2.1, 3.3].each do |x|
121+
assert_in_epsilon(Math.erf(x), BigMath.erf(BigDecimal(x.to_s), N))
122+
end
123+
assert_equal(1, BigMath.erf(BigDecimal(1000), 100))
124+
assert_equal(-1, BigMath.erf(BigDecimal(-1000), 100))
125+
assert_not_equal(1, BigMath.erf(BigDecimal(10), 45))
126+
assert_not_equal(1, BigMath.erf(BigDecimal(15), 100))
127+
assert_in_epsilon(
128+
BigDecimal("0.995322265018952734162069256367252928610891797040060076738352326200437280719995177367629008019680680487939328715594755785"),
129+
BigMath.erf(BigDecimal("2"), 100),
130+
BigDecimal("1e-100")
131+
)
132+
assert_relative_precision {|n| BigMath.erf(BigDecimal("1e-30"), n) }
133+
assert_relative_precision {|n| BigMath.erf(BigDecimal("0.3"), n) }
134+
assert_relative_precision {|n| BigMath.erf(BigDecimal("1.2"), n) }
135+
end
136+
137+
def test_erfc
138+
[-0.5, 0.1, 0.3, 2.1, 3.3].each do |x|
139+
assert_in_epsilon(Math.erfc(x), BigMath.erfc(BigDecimal(x.to_s), N))
140+
end
141+
# erfc with taylor series
142+
assert_in_epsilon(
143+
BigDecimal("2.08848758376254475700078629495778861156081811932116372701221371393817469583344029061076638428572355398152593923652403986e-45"),
144+
BigMath.erfc(BigDecimal("10"), 100),
145+
BigDecimal("1e-100")
146+
)
147+
assert_relative_precision {|n| BigMath.erfc(BigDecimal("0.3"), n) }
148+
assert_relative_precision {|n| BigMath.erfc(BigDecimal("1.2"), n) }
149+
assert_relative_precision {|n| BigMath.erfc(BigDecimal("30"), n) }
150+
# erfc with asymptotic expansion
151+
assert_in_epsilon(
152+
BigDecimal("1.89696105996627650926827825971341543493690756392918618346283475290041180520511188660525669077676004136530598303468056210e-697"),
153+
BigMath.erfc(BigDecimal("40"), 100),
154+
BigDecimal("1e-100")
155+
)
156+
assert_relative_precision {|n| BigMath.erfc(BigDecimal("30"), n) }
157+
assert_relative_precision {|n| BigMath.erfc(BigDecimal("50"), n) }
158+
end
118159
end

0 commit comments

Comments
 (0)