Skip to content

Commit 7f437e7

Browse files
committed
Complex special functions
Algorithms for calculating gamma, log-gamma and digamma in the complex field
1 parent cdc6439 commit 7f437e7

File tree

7 files changed

+167
-30
lines changed

7 files changed

+167
-30
lines changed

math-doc/math/scribblings/math-special-functions.scrbl

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
The term ``special function'' has no formal definition. However, for the purposes of the
2020
@racketmodname[math] library, a @deftech{special function} is one that is not @tech{elementary}.
2121

22-
The special functions are split into two groups: @secref{real-functions} and
22+
The special functions are split into three groups: @secref{complex-functions}, @secref{real-functions} and
2323
@secref{flonum-functions}. Functions that accept real arguments are usually defined
2424
in terms of their flonum counterparts, but are different in two crucial ways:
2525
@itemlist[
@@ -28,13 +28,13 @@ in terms of their flonum counterparts, but are different in two crucial ways:
2828
@racket[exn:fail:contract] instead of returning @racket[+nan.0].}
2929
]
3030

31-
Currently, @racketmodname[math/special-functions] does not export any functions that accept
32-
or return complex numbers. Mathematically, some of them could return complex numbers given
31+
Only functions in @secref{complex-functions} support accepting or returning complex arguments.
32+
Mathematically, some of the other functions could return complex numbers given
3333
real numbers, such @racket[hurwitz-zeta] when given a negative second argument. In
3434
these cases, they raise an @racket[exn:fail:contract] (for an exact argument) or return
3535
@racket[+nan.0] (for an inexact argument).
3636

37-
Most real functions have more than one type, but they are documented as having only
37+
Most real and complex functions have more than one type, but they are documented as having only
3838
one. The documented type is the most general type, which is used to generate a contract for
3939
uses in untyped code. Use @racket[:print-type] to see all of a function's types.
4040

@@ -56,11 +56,11 @@ The most general type @racket[Real -> (U Zero Flonum)] is used to generate
5656
@racket[lambert]'s contract when it is used in untyped code. Except for this discussion,
5757
this the only type documented for @racket[lambert].
5858

59-
@section[#:tag "real-functions"]{Real Functions}
59+
@section[#:tag "complex-functions"]{Complex Functions}
6060

61-
@defproc[(gamma [x Real]) (U Positive-Integer Flonum)]{
61+
@defproc[(gamma [x Number]) Number]{
6262
Computes the @hyperlink["http://en.wikipedia.org/wiki/Gamma_function"]{gamma function},
63-
a generalization of the factorial function to the entire real line, except nonpositive integers.
63+
a generalization of the factorial function to the entire complex plane, except nonpositive exact integers.
6464
When @racket[x] is an exact integer, @racket[(gamma x)] is exact.
6565

6666
@examples[#:eval untyped-eval
@@ -76,16 +76,17 @@ When @racket[x] is an exact integer, @racket[(gamma x)] is exact.
7676
(gamma -1.0)
7777
(gamma 0.0)
7878
(gamma -0.0)
79+
(gamma 1+i)
7980
(gamma 172.0)
8081
(eval:alts
8182
(bf (gamma 172))
8283
(eval:result @racketresultfont{(bf "1.241018070217667823424840524103103992618e309")}))]
8384

84-
Error is no more than 10 @tech{ulps} everywhere that has been tested, and is usually no more than 4
85-
ulps.
85+
On the real line the error is no more than 10 @tech{ulps} everywhere that has been tested, and is usually no more than 4
86+
ulps. In the rest of the complex plane the relative error is smaller than 1e-13 (@tech{ulps}=450).
8687
}
8788

88-
@defproc[(log-gamma [x Real]) (U Zero Flonum)]{
89+
@defproc[(log-gamma [x Number]) Number]{
8990
Like @racket[(log (abs (gamma x)))], but more accurate and without unnecessary overflow.
9091
The only exact cases are @racket[(log-gamma 1) = 0] and @racket[(log-gamma 2) = 0].
9192

@@ -99,25 +100,27 @@ The only exact cases are @racket[(log-gamma 1) = 0] and @racket[(log-gamma 2) =
99100
(log-gamma -1)
100101
(log-gamma -1.0)
101102
(log-gamma 0.0)
103+
(log-gamma 1+i)
102104
(log (abs (gamma 172.0)))
103105
(log-gamma 172.0)]
104106

105-
Error is no more than 11 @tech{ulps} everywhere that has been tested, and is usually no more than 2
107+
On the real line error is no more than 11 @tech{ulps} everywhere that has been tested, and is usually no more than 2
106108
ulps. Error reaches its maximum near negative roots.
107109
}
108110

109-
@defproc[(psi0 [x Real]) Flonum]{
111+
@defproc[(psi0 [x Number]) Number]{
110112
Computes the @hyperlink["http://en.wikipedia.org/wiki/Digamma_function"]{digamma function},
111113
the logarithmic derivative of the gamma function.
112114

113115
@examples[#:eval untyped-eval
114116
(plot (function psi0 -2.5 4.5) #:y-min -5 #:y-max 5)
115117
(psi0 0)
116118
(psi0 1)
119+
(psi0 1+i)
117120
(- gamma.0)]
118121

119-
Except near negative roots, maximum observed error is 2 @tech{ulps}, but is usually no more
120-
than 1.
122+
On the real line, except near negative roots, maximum observed error is 2 @tech{ulps}, but is usually no more
123+
than 1. In the complex plane the relative error is around 1e-13.
121124

122125
Near negative roots, which occur singly between each pair of negative integers, @racket[psi0]
123126
exhibits @tech{catastrophic cancellation} from using the reflection formula, meaning that
@@ -129,6 +132,8 @@ error.
129132
If you need low relative error near negative roots, use @racket[bfpsi0].
130133
}
131134

135+
@section[#:tag "real-functions"]{Real Functions}
136+
132137
@defproc[(psi [m Integer] [x Real]) Flonum]{
133138
Computes a @hyperlink["http://en.wikipedia.org/wiki/Polygamma_function"]{polygamma function},
134139
or the @racket[m]th logarithmic derivative of the gamma function. The order @racket[m] must be

math-lib/math/private/functions/gamma.rkt

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,13 +272,38 @@ Approximations:
272272
[(and (x . fl> . -4.5) (x . fl< . 4.5)) (flgamma-taylor x)]
273273
[else (flgamma-lanczos x)])))]))
274274

275+
(define √2π 2.5066282746310005024157652848110)
276+
(: complex-gamma (Number -> Number))
277+
(define (complex-gamma z+1)
278+
(define neg? (< (real-part z+1)0))
279+
(define z (- (if neg? (- z+1) z+1) 1))
280+
(cond
281+
[(or (= z 1)(= z 0))1]
282+
[else
283+
(define zh (+ z 0.5))
284+
(define zgh (+ zh lanczos-complex-g))
285+
(define zp (expt zgh (* 0.5 zh)))
286+
287+
(define ans
288+
(* zp (exp (- zgh)) zp
289+
√2π (+ (car lanczos-complex-c)
290+
(for/sum : Number ([a (in-list (cdr lanczos-complex-c))]
291+
[i (in-naturals 1)])
292+
(/ a (+ z i))))))
293+
(if neg?
294+
(- (/ pi (* ans z+1 (sin (* pi z+1)))))
295+
ans)]))
296+
275297
(: gamma (case-> (One -> One)
276298
(Integer -> Positive-Integer)
277299
(Float -> Float)
278-
(Real -> (U Positive-Integer Flonum))))
279-
(define (gamma x)
300+
(Real -> (U Positive-Integer Flonum))
301+
(Number -> Number)))
302+
(define (gamma z)
303+
(define x (if (= (imag-part z) 0) (real-part z) z))
280304
(cond [(double-flonum? x) (flgamma x)]
281305
[(exact-integer? x)
282306
(cond [(x . > . 0) (factorial (- x 1))]
283307
[else (raise-argument-error 'gamma "Real, not Zero or Negative-Integer" x)])]
284-
[else (flgamma (fl x))]))
308+
[(real? x) (flgamma (fl x))]
309+
[else (complex-gamma z)]))

math-lib/math/private/functions/lanczos.rkt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
(require "../../flonum.rkt")
44

5-
(provide lanczos-sum lanczos-g)
5+
(provide (all-defined-out))
66

77
;; Lanczos polynomial for N=13 G=6.024680040776729583740234375
88
;; Max experimental error (with arbitary precision arithmetic) is 1.196214e-17
@@ -36,3 +36,25 @@
3636
1.0)))
3737

3838
(define lanczos-g 6.024680040776729583740234375)
39+
40+
41+
;; Lanczos series approximation for the complex plane
42+
;; ref Paul Godfrey's MATLAB implementations | https://my.fit.edu/~gabdo/paulbio.html
43+
;; ->relative error < 1e-13
44+
(define lanczos-complex-c
45+
(list 0.99999999999999709182
46+
57.156235665862923517
47+
-59.597960355475491248
48+
14.136097974741747174
49+
-0.49191381609762019978
50+
0.33994649984811888699e-4
51+
0.46523628927048575665e-4
52+
-0.98374475304879564677e-4
53+
0.15808870322491248884e-3
54+
-0.21026444172410488319e-3
55+
0.21743961811521264320e-3
56+
-0.16431810653676389022e-3
57+
0.84418223983852743293e-4
58+
-0.26190838401581408670e-4
59+
0.36899182659531622704e-5))
60+
(define lanczos-complex-g 607/128)

math-lib/math/private/functions/log-gamma.rkt

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"../../flonum.rkt"
55
"../../base.rkt"
66
"log-gamma-zeros.rkt"
7-
"gamma.rkt")
7+
"gamma.rkt"
8+
"lanczos.rkt")
89

910
(provide fllog-gamma log-gamma)
1011

@@ -398,15 +399,43 @@
398399
[(x . fl< . 4.5) (fllog-gamma-small-positive x)]
399400
[else (fllog-gamma-large-positive x)])))]))
400401

402+
(define log-√2π 0.9189385332046727417803297)
403+
(define log-π 1.14472988584940017414342735)
404+
(: complex-log-gamma (Number -> Number))
405+
(define (complex-log-gamma z)
406+
(define neg? (< (real-part z)0))
407+
(define z* (if neg? (- z) z))
408+
(cond
409+
[(or (= z* 1)(= z* 2))0]
410+
[else
411+
(define z- (- z* 0.5))
412+
(define zg (+ z- lanczos-complex-g))
413+
414+
(define ans
415+
(+ (- zg) (* z- (log zg))
416+
log-√2π (log (+ (car lanczos-complex-c)
417+
(for/sum : Number ([a (in-list (cdr lanczos-complex-c))]
418+
[i (in-naturals 0)])
419+
(/ a (+ z* i)))))))
420+
(cond
421+
[neg?
422+
(define lpi (+ log-π (* (if (< (imag-part z) 0) +i -i) pi)))
423+
(- lpi (log z) ans (log (sin (* pi z))))]
424+
[else ans])]))
425+
426+
401427
(: log-gamma (case-> (One -> Zero)
402428
(Flonum -> Flonum)
403-
(Real -> (U Zero Flonum))))
404-
(define (log-gamma x)
429+
(Real -> (U Zero Flonum))
430+
(Number -> Number)))
431+
(define (log-gamma z)
432+
(define x (if (= (imag-part z) 0) (real-part z) z))
405433
(cond [(flonum? x) (fllog-gamma x)]
406434
[(single-flonum? x) (fllog-gamma (fl x))]
407435
[(integer? x)
408436
(cond [(x . <= . 0)
409437
(raise-argument-error 'log-gamma "Real, not Zero or Negative-Integer" x)]
410438
[(eqv? x 1) 0]
411439
[else (fllog-factorial (fl (- x 1)))])]
412-
[else (fllog-gamma (fl x))]))
440+
[(real? x) (fllog-gamma (fl x))]
441+
[else (complex-log-gamma z)]))

math-lib/math/private/functions/psi.rkt

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"../number-theory/bernoulli.rkt"
77
"gamma.rkt"
88
"hurwitz-zeta.rkt"
9-
"tan-diff.rkt")
9+
"tan-diff.rkt"
10+
"lanczos.rkt")
1011

1112
(provide flpsi0 flpsi psi0 psi)
1213

@@ -208,6 +209,29 @@
208209
[(x . = . +inf.0) +inf.0]
209210
[else +nan.0]))
210211

212+
(: complex-psi0 (Number -> Number))
213+
(define (complex-psi0 z*)
214+
(define neg? (< (real-part z*) 0.5))
215+
(define z (if neg? (- 1 z*) z*))
216+
217+
(define-values (n d)
218+
(for/fold : (Values Number Number)
219+
([n : Number 0][d : Number 0])
220+
([c (in-list (reverse (cdr lanczos-complex-c)))]
221+
[k (in-range (length lanczos-complex-c) -1 -1)])
222+
(define dz (/ 1 (+ z k -2)))
223+
(define dd (* c dz))
224+
(values (- n (* dd dz)) (+ d dd))))
225+
226+
(define gg (+ z lanczos-complex-g -0.5))
227+
228+
(define ans (+ (log gg) (- (/ n (+ d (car lanczos-complex-c)))
229+
(/ lanczos-complex-g gg))))
230+
231+
(if neg?
232+
(- ans (/ pi (tan (* pi z*))))
233+
ans))
234+
211235
(define pi.128 267257146016241686964920093290467695825/85070591730234615865843651857942052864)
212236

213237
(: flexppi (Flonum -> Flonum))
@@ -232,13 +256,16 @@
232256
(fl- (fl* sgn (flpsi m (fl- 1.0 x))) t)]
233257
[else +nan.0]))
234258

235-
(: psi0 (Real -> Flonum))
236-
(define (psi0 x)
259+
(: psi0 (case-> (Real -> Flonum)
260+
(Number -> Number)))
261+
(define (psi0 z)
262+
(define x (if (= (imag-part z) 0) (real-part z) z))
237263
(cond [(flonum? x) (flpsi0 x)]
238264
[(single-flonum? x) (flpsi0 (fl x))]
239265
[(and (integer? x) (x . <= . 0))
240266
(raise-argument-error 'psi0 "Real, not Zero or Negative-Integer" x)]
241-
[else (flpsi0 (fl x))]))
267+
[(real? x) (flpsi0 (fl x))]
268+
[else (complex-psi0 z)]))
242269

243270
(: psi (Integer Real -> Flonum))
244271
(define (psi m x)

math-lib/math/special-functions.rkt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,23 @@
99
(except-in "private/functions/lambert.rkt" lambert)
1010
(except-in "private/functions/zeta.rkt" eta zeta)
1111
(except-in "private/functions/hurwitz-zeta.rkt" hurwitz-zeta)
12-
"private/functions/psi.rkt"
12+
(except-in "private/functions/psi.rkt" psi0)
1313
"private/functions/incomplete-gamma.rkt"
1414
"private/functions/incomplete-beta.rkt"
1515
"private/functions/stirling-error.rkt"
1616
"private/functions/fresnel.rkt")
1717

1818
(require/untyped-contract
1919
"private/functions/gamma.rkt"
20-
[gamma (Real -> Real)])
20+
[gamma (Number -> Number)])
2121

2222
(require/untyped-contract
2323
"private/functions/log-gamma.rkt"
24-
[log-gamma (Real -> Real)])
24+
[log-gamma (Number -> Number)])
25+
26+
(require/untyped-contract
27+
"private/functions/psi.rkt"
28+
[psi0 (Number -> Number)])
2529

2630
(require/untyped-contract
2731
"private/functions/beta.rkt"
@@ -61,6 +65,7 @@
6165
"private/functions/fresnel.rkt")
6266
gamma
6367
log-gamma
68+
psi0
6469
beta log-beta
6570
erf erfc
6671
lambert

math-test/math/tests/function-tests.rkt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,31 @@
4646
(check-exn exn:fail:contract? (λ () (gamma 0)))
4747
(check-equal? (gamma 4) 6)
4848
(check-equal? (gamma 4.0) (flgamma 4.0))
49-
49+
;checks calculated at https://wolframalpha.com/input/?i=gamma(z) / log-gamma(z) / digamma(z)
50+
(let ([z 1.5+0.2i]
51+
[ans 0.869862133805271779271655350309163858030873370542544143+0.00730170831118402614972641679459165642341675697339983422i])
52+
(check-= (/ (gamma z) ans) 1 1e-13))
53+
(let ([z 1.5+4i]
54+
[ans -0.018686297879039387689174933326088586248898218845351961+0.0026241318932335593901663964582546948097937382268327333i])
55+
(check-= (/ (gamma z) ans) 1 1e-13))
56+
(let ([z 196+285i]
57+
[ans 5.72038483529989178989490686567823916571020691258673e291-2.00024207656771597752271221062051820963168450311374e291i])
58+
(check-= (/ (gamma z) ans) 1 1e-13))
59+
(let ([z 28.45-0.05i]
60+
[ans 4.788665109060112126493799529515601713890011469557001e28-8.048791086601393564384851295197107978228121888100461e27i])
61+
(check-= (/ (gamma z) ans) 1 1e-13))
62+
(let ([z 0.5-0.00333i]
63+
[ans 1.772367471310459385811039514905244407455502656029765525+0.01158858570803772503302975596977297632272041615128277137i])
64+
(check-= (/ (gamma z) ans) 1 1e-13))
65+
(let ([z -0.5-0.00333i]
66+
[ans -3.544732073864574326606517758100934969124507449000061+0.0004307441958626149491398963294062742489283873077748653i])
67+
(check-= (/ (gamma z) ans) 1 1e-13))
68+
(let ([z -1+i]
69+
[ans -0.9974967895818289935938328922330359491032588243198502190-4.228631137454774745965835886896275145906361523162647513i])
70+
(check-= (/ (log-gamma z) ans) 1 1e-13))
71+
(let ([z -1+i]
72+
[ans 0.59465032062247697727187848272191072247626297176354162323+2.5766740474685811741340507947500004904456562664038166655i])
73+
(check-= (/ (psi0 z) ans) 1 1e-13))
5074
;; ---------------------------------------------------------------------------------------------------
5175
;; hypot
5276

0 commit comments

Comments
 (0)