Skip to content

Commit 3be6cd0

Browse files
committed
Implement BigMath.gamma and BigMath.lgamma
Implement gamma and lgamma with Spouge's approximation
1 parent 150d01d commit 3be6cd0

File tree

2 files changed

+166
-0
lines changed

2 files changed

+166
-0
lines changed

lib/bigdecimal/math.rb

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
# log10(x, prec)
2525
# log1p(x, prec)
2626
# expm1(x, prec)
27+
# gamma(x, prec)
28+
# lgamma(x, prec)
2729
# PI (prec)
2830
# E (prec) == exp(1.0,prec)
2931
#
@@ -568,6 +570,123 @@ def expm1(x, prec)
568570
exp_prec > 0 ? exp(x, exp_prec).sub(1, prec) : BigDecimal(-1)
569571
end
570572

573+
# call-seq:
574+
# BigMath.gamma(decimal, numeric) -> BigDecimal
575+
#
576+
# Computes the gamma function of +decimal+ to the specified number of
577+
# digits of precision, +numeric+.
578+
#
579+
# BigMath.gamma(BigDecimal('0.5'), 32).to_s
580+
# #=> "0.17724538509055160272981674833411e1"
581+
#
582+
def gamma(x, prec)
583+
prec = BigDecimal::Internal.coerce_validate_prec(prec, :gamma)
584+
x = BigDecimal::Internal.coerce_to_bigdecimal(x, prec, :gamma)
585+
prec2 = prec + BigDecimal.double_fig
586+
if x < 0.5
587+
raise Math::DomainError 'Numerical argument is out of domain - gamma' if x.frac.zero?
588+
589+
# Euler's reflection formula: gamma(z) * gamma(1-z) = pi/sin(pi*z)
590+
pi = PI(prec2)
591+
sin = _sinpix(x, pi, prec2)
592+
return pi.div(gamma(1 - x, prec).mult(sin, prec2), prec)
593+
elsif x.frac.zero? && x < 1000 * prec
594+
return _gamma_positive_integer(x, prec2).mult(1, prec)
595+
end
596+
597+
a, sum = _gamma_spouge_sum_part(x, prec2)
598+
(x + (a - 1)).power(x - 0.5, prec2).mult(BigMath.exp(1 - x, prec2), prec2).mult(sum, prec)
599+
end
600+
601+
# call-seq:
602+
# BigMath.lgamma(decimal, numeric) -> [BigDecimal, Integer]
603+
#
604+
# Computes the natural logarithm of the absolute value of the gamma function
605+
# of +decimal+ to the specified number of digits of precision, +numeric+ and its sign.
606+
#
607+
# BigMath.lgamma(BigDecimal('0.5'), 32)
608+
# #=> [0.57236494292470008707171367567653e0, 1]
609+
#
610+
def lgamma(x, prec)
611+
prec = BigDecimal::Internal.coerce_validate_prec(prec, :lgamma)
612+
x = BigDecimal::Internal.coerce_to_bigdecimal(x, prec, :lgamma)
613+
prec2 = prec + BigDecimal.double_fig
614+
if x < 0.5
615+
return [BigDecimal::INFINITY, 1] if x.frac.zero?
616+
617+
# Euler's reflection formula: gamma(z) * gamma(1-z) = pi/sin(pi*z)
618+
pi = PI(prec2)
619+
sin = _sinpix(x, pi, prec2)
620+
log_gamma = BigMath.log(pi, prec2).sub(lgamma(1 - x, prec).first + BigMath.log(sin.abs, prec2), prec)
621+
[log_gamma, sin > 0 ? 1 : -1]
622+
elsif x.frac.zero? && x < 1000 * prec
623+
log_gamma = BigMath.log(_gamma_positive_integer(x, prec2), prec)
624+
[log_gamma, 1]
625+
else
626+
a, sum = _gamma_spouge_sum_part(x, prec2)
627+
log_gamma = BigMath.log(sum, prec2).add((x - 0.5).mult(BigMath.log(x.add(a - 1, prec2), prec2), prec2) + 1 - x, prec)
628+
[log_gamma, 1]
629+
end
630+
end
631+
632+
# Returns sum part: sqrt(2*pi) and c[k]/(x+k) terms of Spouge's approximation
633+
private_class_method def _gamma_spouge_sum_part(x, prec) # :nodoc:
634+
x -= 1
635+
# Spouge's approximation
636+
# x! = (x + a)**(x + 0.5) * exp(-x - a) * (sqrt(2 * pi) + (1..a - 1).sum{|k| c[k] / (x + k) } + epsilon)
637+
# where c[k] = (-1)**k * (a - k)**(k - 0.5) * exp(a - k) / (k - 1)!
638+
# and epsilon is bounded by a**(-0.5) * (2 * pi) ** (-a - 0.5)
639+
640+
# Estimate required a for given precision
641+
a = (prec / Math.log10(2 * Math::PI)).ceil
642+
643+
# Calculate exponent of c[k] in low precision to estimate required precision
644+
low_prec = 16
645+
log10f = Math.log(10)
646+
x_low_prec = x.mult(1, low_prec)
647+
loggamma_k = 0
648+
ck_exponents = (1..a-1).map do |k|
649+
loggamma_k += Math.log10(k - 1) if k > 1
650+
-loggamma_k - k / log10f + (k - 0.5) * Math.log10(a - k) - BigMath.log10(x_low_prec.add(k, low_prec), low_prec)
651+
end
652+
653+
# Estimate exponent of sum by Stirling's approximation
654+
approx_sum_exponent = x < 1 ? -Math.log10(a) / 2 : Math.log10(2 * Math::PI) / 2 + x_low_prec.add(0.5, low_prec) * Math.log10(x_low_prec / x_low_prec.add(a, low_prec))
655+
656+
# Determine required precision of c[k]
657+
prec2 = [ck_exponents.max.ceil - approx_sum_exponent.floor, 0].max + prec
658+
659+
einv = BigMath.exp(-1, prec2)
660+
sum = (PI(prec) * 2).sqrt(prec).mult(BigMath.exp(-a, prec), prec)
661+
y = BigDecimal(1)
662+
(1..a - 1).each do |k|
663+
# c[k] = (-1)**k * (a - k)**(k - 0.5) * exp(-k) / (k-1)! / (x + k)
664+
y = y.div(1 - k, prec2) if k > 1
665+
y = y.mult(einv, prec2)
666+
z = y.mult(BigDecimal((a - k) ** k), prec2).div(BigDecimal(a - k).sqrt(prec2).mult(x.add(k, prec2), prec2), prec2)
667+
# sum += c[k] / (x + k)
668+
sum = sum.add(z, prec2)
669+
end
670+
[a, sum]
671+
end
672+
673+
private_class_method def _gamma_positive_integer(x, prec) # :nodoc:
674+
return x if x == 1
675+
numbers = (1..x - 1).map {|i| BigDecimal(i) }
676+
while numbers.size > 1
677+
numbers = numbers.each_slice(2).map {|a, b| b ? a.mult(b, prec) : a }
678+
end
679+
numbers.first
680+
end
681+
682+
# Returns sin(pi * x), for gamma reflection formula calculation
683+
private_class_method def _sinpix(x, pi, prec) # :nodoc:
684+
x = x % 2
685+
sign = x > 1 ? -1 : 1
686+
x %= 1
687+
x = 1 - x if x > 0.5 # to avoid sin(pi*x) loss of precision for x close to 1
688+
sign * sin(x.mult(pi, prec), prec)
689+
end
571690

572691
# call-seq:
573692
# PI(numeric) -> BigDecimal

test/bigdecimal/test_bigmath.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ def test_consistent_precision_acceptance
8282
assert_consistent_precision_acceptance {|prec| BigMath.log10(x, prec) }
8383
assert_consistent_precision_acceptance {|prec| BigMath.log1p(x, prec) }
8484
assert_consistent_precision_acceptance {|prec| BigMath.expm1(x, prec) }
85+
assert_consistent_precision_acceptance {|prec| BigMath.gamma(x, prec) }
86+
assert_consistent_precision_acceptance {|prec| BigMath.lgamma(x, prec) }
8587

8688
assert_consistent_precision_acceptance {|prec| BigMath.E(prec) }
8789
assert_consistent_precision_acceptance {|prec| BigMath.PI(prec) }
@@ -112,6 +114,8 @@ def test_coerce_argument
112114
assert_equal(log10(bd, N), log10(f, N))
113115
assert_equal(log1p(bd, N), log1p(f, N))
114116
assert_equal(expm1(bd, N), expm1(f, N))
117+
assert_equal(gamma(bd, N), gamma(f, N))
118+
assert_equal(lgamma(bd, N), lgamma(f, N))
115119
end
116120

117121
def test_sqrt
@@ -469,4 +473,47 @@ def test_expm1
469473
assert_in_exact_precision(exp(BigDecimal("1.23e-10"), 120) - 1, expm1(BigDecimal("1.23e-10"), 100), 100)
470474
assert_in_exact_precision(exp(123, 120) - 1, expm1(BigDecimal("123"), 100), 100)
471475
end
476+
477+
def test_gamma
478+
[-1.8, -0.7, 0.6, 1.5, 2.4].each do |x|
479+
assert_in_epsilon(Math.gamma(x), gamma(BigDecimal(x.to_s), N))
480+
end
481+
[1, 2, 3, 10, 16].each do |x|
482+
assert_equal(Math.gamma(x).round, gamma(BigDecimal(x), N))
483+
end
484+
sqrt_pi = PI(120).sqrt(120)
485+
assert_equal(sqrt_pi.mult(1, 100), gamma(BigDecimal("0.5"), 100))
486+
assert_equal((sqrt_pi * 4).div(3, 100), gamma(BigDecimal("-1.5"), 100))
487+
assert_equal(
488+
BigDecimal('0.28242294079603478742934215780245355184774949260912e456569'),
489+
BigMath.gamma(100000, 50)
490+
)
491+
assert_converge_in_precision {|n| gamma(BigDecimal("0.3"), n) }
492+
assert_converge_in_precision {|n| gamma(BigDecimal("-1.9" + "9" * 30), n) }
493+
assert_converge_in_precision {|n| gamma(BigDecimal("1234.56789"), n) }
494+
assert_converge_in_precision {|n| gamma(BigDecimal("-987.654321"), n) }
495+
end
496+
497+
def test_lgamma
498+
[-2, -1, 0].each do |x|
499+
l, sign = lgamma(BigDecimal(x), N)
500+
assert(l.infinite?)
501+
assert_equal(1, sign)
502+
end
503+
[-1.8, -0.7, 0.6, 1, 1.5, 2, 2.4, 3, 1e+300].each do |x|
504+
l, sign = Math.lgamma(x)
505+
bigl, bigsign = lgamma(BigDecimal(x.to_s), N)
506+
assert_in_epsilon(l, bigl)
507+
assert_equal(sign, bigsign)
508+
end
509+
assert_equal([BigMath.log(PI(120).sqrt(120), 100), 1], lgamma(BigDecimal("0.5"), 100))
510+
assert_converge_in_precision {|n| lgamma(BigDecimal("-1." + "9" * 30), n).first }
511+
assert_converge_in_precision {|n| lgamma(BigDecimal("-3." + "0" * 30 + "1"), n).first }
512+
assert_converge_in_precision {|n| lgamma(BigDecimal("10"), n).first }
513+
assert_converge_in_precision {|n| lgamma(BigDecimal("0.3"), n).first }
514+
assert_converge_in_precision {|n| lgamma(BigDecimal("-1.9" + "9" * 30), n).first }
515+
assert_converge_in_precision {|n| lgamma(BigDecimal("987.65421"), n).first }
516+
assert_converge_in_precision {|n| lgamma(BigDecimal("-1234.56789"), n).first }
517+
assert_converge_in_precision {|n| lgamma(BigDecimal("1e+400"), n).first }
518+
end
472519
end

0 commit comments

Comments
 (0)