Skip to content

Commit c6647ff

Browse files
committed
feat: complete bignumber gamma implementation and add tests
1 parent 9a0a8e7 commit c6647ff

File tree

3 files changed

+71
-38
lines changed

3 files changed

+71
-38
lines changed

src/function/probability/factorial.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { deepMap } from '../../utils/collection.js'
22
import { factory } from '../../utils/factory.js'
33

44
const name = 'factorial'
5-
const dependencies = ['typed', 'isInteger', 'bignumber', 'gamma']
5+
const dependencies = ['typed', 'isInteger', 'gamma']
66

77
export const createFactorial = /* #__PURE__ */ factory(name, dependencies, ({
88
typed, isInteger, bignumber, gamma
@@ -42,7 +42,7 @@ export const createFactorial = /* #__PURE__ */ factory(name, dependencies, ({
4242
if (n.isNegative()) {
4343
throw new Error('Value must be non-negative')
4444
}
45-
if (!n.isFinite()) return bignumber(Infinity)
45+
if (!n.isFinite()) return n
4646
if (!isInteger(n)) throw new Error('Value must be integer')
4747

4848
return gamma(n.plus(1))

src/function/probability/gamma.js

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { factory } from '../../utils/factory.js'
22
import { gammaG, gammaNumber, gammaP } from '../../plain/number/index.js'
3-
3+
import { nearlyEqual } from '../../utils/bignumber/nearlyEqual.js'
44
const name = 'gamma'
55
const dependencies = [
66
'typed', 'config', 'BigNumber', 'Complex',
@@ -78,17 +78,24 @@ export const createGamma = /* #__PURE__ */ factory(name, dependencies, ({
7878
return x.mul(twoPiSqrt).mul(tpow).mul(expt)
7979
}
8080

81-
const piB = BigNumber.acos(-1)
82-
const halflog2piB = piB.times(2).ln().div(2)
83-
const sqrtpiB = piB.sqrt()
84-
const zeroB = new BigNumber(0)
85-
const twoB = new BigNumber(2)
86-
const neg2B = new BigNumber(-2)
81+
let piB = BigNumber.acos(-1)
82+
let halflog2piB = piB.times(2).ln().div(2)
83+
let sqrtpiB = piB.sqrt()
84+
let zeroB = new BigNumber(0)
85+
let twoB = new BigNumber(2)
86+
let neg2B = new BigNumber(-2)
8787

8888
return typed(name, {
8989
number: gammaNumber,
9090
Complex: gammaComplex,
9191
BigNumber: function (n) {
92+
// recalculate constants in case precision changed etc.
93+
piB = BigNumber.acos(-1)
94+
halflog2piB = piB.times(2).ln().div(2)
95+
sqrtpiB = piB.sqrt()
96+
zeroB = new BigNumber(0)
97+
twoB = new BigNumber(2)
98+
neg2B = new BigNumber(-2)
9299
// Handle special values here
93100
if (!n.isFinite()) {
94101
return new BigNumber(n.isNegative() ? NaN : Infinity)
@@ -125,7 +132,9 @@ export const createGamma = /* #__PURE__ */ factory(name, dependencies, ({
125132
// Note for future the same core algorithm can be used for 1/gamma
126133
// or log-gamma, see details in the paper.
127134
// Our goal is to compute relTol digits of gamma
128-
let digits = Math.abs(Math.log10(config.relTol))
135+
let relTol = config.relTol
136+
let absTol = config.absTol
137+
let digits = Math.ceil(Math.abs(Math.log10(relTol)))
129138
const bits = Math.min(digits * 10 / 3, 3)
130139
const beta = 0.2 // tuning parameter prescribed by reference
131140
const reflect = n.lessThan(-5)
@@ -139,20 +148,21 @@ export const createGamma = /* #__PURE__ */ factory(name, dependencies, ({
139148
let inaccurate = true
140149
let mainsum = new BigNumber(0)
141150
let shifted = z.plus(r)
142-
// console.trace('Trying for', digits, 'with shift', r)
143151
while (inaccurate) {
144152
const needPrec = digits + 3 + Math.floor(
145153
shifted.ln().times(shifted).log().toNumber())
146154
if (needPrec > config.precision) {
147-
digits -= needPrec - config.precision
155+
const lessDigits = needPrec - config.precision
156+
digits -= lessDigits
148157
if (digits < 3) {
149158
throw new Error(`Cannot compute gamma(${n}) with useful accuracy`)
150159
}
160+
relTol *= 10 ** lessDigits
161+
absTol *= 10 ** lessDigits
151162
let message = `Insufficient bignumber precision for gamma(${n}), `
152163
message += `limiting to ${digits} digits.`
153-
console.warning(message)
164+
console.warn(message)
154165
}
155-
const threshold = new BigNumber(0.1).pow(digits)
156166
let lastTerm = new BigNumber(Infinity)
157167
let index = 1
158168
let powShifted = shifted
@@ -162,20 +172,21 @@ export const createGamma = /* #__PURE__ */ factory(name, dependencies, ({
162172
const term = bernoulli(new BigNumber(double))
163173
.div(powShifted.times(double * (double - 1)))
164174
// See if we already converged
165-
const absTerm = term.abs()
166-
if (absTerm.lessThan(threshold)) {
175+
const newsum = mainsum.plus(term)
176+
if (nearlyEqual(mainsum, newsum, relTol, absTol)) {
167177
inaccurate = false
168178
break
169179
}
180+
const absTerm = term.abs()
170181
if (lastTerm.lessThan(absTerm)) {
171182
// oops, diverging
172183
mainsum = new BigNumber(0)
173-
r = 2 * r
184+
r = 2 * r + 1
174185
shifted = z.plus(r)
175186
break
176187
}
177188
lastTerm = absTerm
178-
mainsum = mainsum.plus(term)
189+
mainsum = newsum
179190
index += 1
180191
powShifted = powShifted.times(shiftedSq)
181192
}

test/unit-tests/function/probability/gamma.test.js

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import assert from 'assert'
44
import { approxEqual, approxDeepEqual } from '../../../../tools/approx.js'
55
import math from '../../../../src/defaultInstance.js'
6+
import {
7+
createBigNumberPhi
8+
} from '../../../../src/utils/bignumber/constants.js'
69
const bignumber = math.bignumber
710
const gamma = math.gamma
811

@@ -68,34 +71,53 @@ describe('gamma', function () {
6871
assert.deepStrictEqual(gamma(bignumber(-2)).toString(), 'Infinity')
6972
assert.ok(gamma(bignumber(-Infinity)).isNaN())
7073
})
71-
/*
72-
it('should calculate the gamma of a rational bignumber', function () {
73-
assert.deepStrictEqual(gamma(bignumber(0.125)), bignumber('7.5339415987976'))
74-
assert.deepStrictEqual(gamma(bignumber(0.25)), bignumber('3.62560990822191'))
75-
assert.deepStrictEqual(gamma(bignumber(0.5)), bignumber('1.77245385090552'))
76-
assert.deepStrictEqual(gamma(bignumber(1.5)), bignumber('0.886226925452758'))
77-
assert.deepStrictEqual(gamma(bignumber(2.5)), bignumber('1.32934038817914'))
78-
79-
const bigmath = math.create({ precision: 15 })
80-
assert.deepStrictEqual(bigmath.gamma(bignumber(30.5)), '4.82269693349091e+31')
8174

82-
bigmath.config({ precision: 13 })
83-
assert.deepStrictEqual(bigmath.gamma(bignumber(-1.5)), bigmath.bignumber('2.363271801207'))
84-
assert.deepStrictEqual(gamma(bignumber(-2.5)), bignumber('-0.9453087205'))
75+
it('should calculate the gamma of a rational bignumber', function () {
76+
assert.deepStrictEqual(gamma(bignumber(0.125)), bignumber('7.533941598798'))
77+
assert.deepStrictEqual(gamma(bignumber(0.25)), bignumber('3.625609908222'))
78+
// gamma(n + 1/2) are special values, so always return lots of digits
79+
approxEqual(gamma(bignumber(0.5)), bignumber('1.77245385090552'))
80+
approxEqual(gamma(bignumber(1.5)), bignumber('0.886226925452758'))
81+
approxEqual(gamma(bignumber(2.5)), bignumber('1.32934038817914'))
82+
83+
const bigmath = math.create({ relTol: 1e-15, absTol: 1e-18, precision: 24 })
84+
const bigresult = bigmath.gamma(bigmath.bignumber(30.5))
85+
approxEqual(bigresult, bigmath.bignumber('4.82269693349091e+31'))
86+
assert.deepStrictEqual(
87+
bigmath.gamma(bigmath.bignumber(1).div(3)),
88+
bigmath.bignumber('2.678938534707748'))
89+
90+
bigmath.config({ relTol: 1e-13, absTol: 1e-16, precision: 18 })
91+
approxEqual(
92+
bigmath.gamma(bigmath.bignumber(-1.5)),
93+
bigmath.bignumber('2.363271801207'))
94+
assert.deepStrictEqual(
95+
bigmath.gamma(bigmath.bignumber(0.2)),
96+
bigmath.bignumber('4.5908437119988'))
97+
98+
bigmath.config({ relTol: 1e-300, absTol: 1e-310, precision: 500 })
99+
assert.deepStrictEqual(
100+
bigmath.gamma(bigmath.bignumber(2).div(3)),
101+
bigmath.bignumber('1.354117939426400416945288028154513785519327266056793698394022467963782965401742541675834147952972911106434823610033058854142261552586211826607191148114322833434155915620917505682592366523385211910858011501770153617023853945368317754599736504155930691384228034622762716150366499013846393144654597536751')
102+
)
103+
approxEqual(gamma(bignumber(-2.5)), bignumber('-0.9453087205'))
85104
})
86105

87106
it('should calculate the gamma of an irrational bignumber', function () {
88-
assert.deepStrictEqual(gamma(bigUtil.phi(math.precision).neg()), bignumber('2.3258497469'))
89-
assert.deepStrictEqual(gamma(bigUtil.phi(math.precision)), bignumber('0.895673151705288'))
107+
const phi = createBigNumberPhi(math.BigNumber)
108+
assert.deepStrictEqual(gamma(phi.neg()), bignumber('2.325849746874'))
109+
assert.deepStrictEqual(gamma(phi), bignumber('0.895673151705'))
90110

91-
assert.deepStrictEqual(gamma(bigUtil.pi(20)), bignumber('2.28803779534003'))
92-
assert.deepStrictEqual(gamma(bigUtil.e(math.precision)), bignumber('1.56746825577405'))
111+
const pi = math.acos(math.bignumber(-1))
112+
assert.deepStrictEqual(gamma(pi), bignumber('2.2880377953400'))
113+
const bige = math.exp(math.bignumber(1))
114+
assert.deepStrictEqual(gamma(bige), bignumber('1.5674682557740'))
93115

94116
const bigmath = math.create({ number: 'BigNumber' })
95-
assert.deepStrictEqual(gamma(bigmath.SQRT2), bignumber('0.886581428719259'))
96-
assert.deepStrictEqual(gamma(bigmath.SQRT2.neg()), bignumber('2.59945990753'))
117+
assert.deepStrictEqual(gamma(bigmath.SQRT2), bignumber('0.886581428719'))
118+
assert.deepStrictEqual(gamma(bigmath.SQRT2.neg()), bignumber('2.599459907525'))
97119
})
98-
*/
120+
99121
it('should calculate the gamma of an imaginary unit', function () {
100122
approxDeepEqual(gamma(math.i), math.complex(-0.154949828301810685124955130,
101123
-0.498015668118356042713691117))

0 commit comments

Comments
 (0)