racket/collects/math/scribblings/math-special-functions.scrbl
Neil Toronto 60dd8d065f Fixes for errors Pierpaolo Bernardi found by reviewing the docs; also,
renamed `partition-count' to `partitions' to be consistent with
`permutations', and gave better examples in `multinomial' docs

 * (flulp-error +inf.0 +nan.0) was returning +nan.0 instead of +inf.0

 * Type of `multinomial' didn't match its docs or `flmultinomial'

 * Reworded docs for `diagonal-array'

 * Reworked/reordered quite a few things in docs for `math/bigfloat'

 * Fixed first identity given in `gamma-inc' docs

 * Fixed descrption for `+max.0', etc.
2012-12-17 11:40:37 -07:00

417 lines
19 KiB
Racket

#lang scribble/manual
@(require scribble/eval
racket/sandbox
(for-label racket/base racket/unsafe/ops
math plot
(only-in typed/racket/base
Flonum Real Integer Natural Zero Positive-Integer Exact-Rational
Boolean Any Listof U case-> -> :print-type))
"utils.rkt")
@(define untyped-eval (make-untyped-math-eval))
@title[#:tag "special"]{Special Functions}
@(author-neil)
@defmodule[math/special-functions]
The term ``special function'' has no formal definition. However, for the purposes of the
@racketmodname[math] library, a @deftech{special function} is one that is not @tech{elementary}.
The special functions are split into two groups: @secref{real-functions} and
@secref{flonum-functions}. Functions that accept real arguments are usually defined
in terms of their flonum counterparts, but are different in two crucial ways:
@itemlist[
@item{Many return exact values for certain exact arguments.}
@item{When applied to exact arguments outside their domains, they raise an
@racket[exn:fail:contract] instead of returning @racket[+nan.0].}
]
Currently, @racketmodname[math/special-functions] does not export any functions that accept
or return complex numbers. Mathematically, some of them could return complex numbers given
real numbers, such @racket[hurwitz-zeta] when given a negative second argument. In
these cases, they raise an @racket[exn:fail:contract] (for an exact argument) or return
@racket[+nan.0] (for an inexact argument).
Most real functions have more than one type, but they are documented as having only
one. The documented type is the most general type, which is used to generate a contract for
uses in untyped code. Use @racket[:print-type] to see all of a function's types.
A function's types state theorems about its behavior in a way that Typed Racket can understand
and check. For example, @racket[lambert] has these types:
@racketblock[(case-> (Zero -> Zero)
(Flonum -> Flonum)
(Real -> (U Zero Flonum)))]
Because @racket[lambert : Zero -> Zero], Typed Racket proves during typechecking that one
of its exact cases is @racket[(lambert 0) = 0].
Because the theorem @racket[lambert : Flonum -> Flonum] is stated as a type and proved
by typechecking, Typed Racket's optimizer can transform the expressions around its use
into bare-metal floating-point operations. For example, @racket[(+ 2.0 (lambert 3.0))] is
transformed into @racket[(unsafe-fl+ 2.0 (lambert 3.0))].
The most general type @racket[Real -> (U Zero Flonum)] is used to generate
@racket[lambert]'s contract when it is used in untyped code. Except for this discussion,
this the only type documented for @racket[lambert].
@section[#:tag "real-functions"]{Real Functions}
@defproc[(gamma [x Real]) (U Positive-Integer Flonum)]{
Computes the @hyperlink["http://en.wikipedia.org/wiki/Gamma_function"]{gamma function},
a generalization of the factorial function to the entire real line, except nonpositive integers.
When @racket[x] is an exact integer, @racket[(gamma x)] is exact.
@examples[#:eval untyped-eval
(plot (list (function (λ (x) (gamma (+ 1 x))) 0 4.5
#:label "gamma(x+1)")
(function (λ (x) (factorial (truncate x))) #:color 2
#:label "factorial(floor(x))")))
(plot (function gamma -2.5 5.5) #:y-min -50 #:y-max 50)
(gamma 5)
(gamma 5.0)
(factorial 4)
(gamma -1)
(gamma -1.0)
(gamma 0.0)
(gamma -0.0)
(gamma 172.0)
(eval:alts
(bf (gamma 172))
(eval:result @racketresultfont{(bf "1.241018070217667823424840524103103992618e309")}))]
Error is no more than 10 @tech{ulps} everywhere that has been tested, and is usually no more than 4
ulps.
}
@defproc[(log-gamma [x Real]) (U Zero Flonum)]{
Like @racket[(log (abs (gamma x)))], but more accurate and without unnecessary overflow.
The only exact cases are @racket[(log-gamma 1) = 0] and @racket[(log-gamma 2) = 0].
@examples[#:eval untyped-eval
(plot (list (function log-gamma -5.5 10.5 #:label "log-gamma(x)")
(function (λ (x) (log (abs (gamma x))))
#:color 2 #:style 'long-dash #:width 2
#:label "log(abs(gamma(x)))")))
(log-gamma 5)
(log (abs (gamma 5)))
(log-gamma -1)
(log-gamma -1.0)
(log-gamma 0.0)
(log (abs (gamma 172.0)))
(log-gamma 172.0)]
Error is no more than 11 @tech{ulps} everywhere that has been tested, and is usually no more than 2
ulps. Error reaches its maximum near negative roots.
}
@defproc[(psi0 [x Real]) Flonum]{
Computes the @hyperlink["http://en.wikipedia.org/wiki/Digamma_function"]{digamma function},
the logarithmic derivative of the gamma function.
@examples[#:eval untyped-eval
(plot (function psi0 -2.5 4.5) #:y-min -5 #:y-max 5)
(psi0 0)
(psi0 1)
(- gamma.0)]
Except near negative roots, maximum observed error is 2 @tech{ulps}, but is usually no more
than 1.
Near negative roots, which occur singly between each pair of negative integers, @racket[psi0]
exhibits @tech{catastrophic cancellation} from using the reflection formula, meaning that
relative error is effectively unbounded. However, maximum observed @racket[absolute-error]
is @racket[(* 5 epsilon.0)]. This is the best we can do for now, because there are currently
no reasonably fast algorithms for computing @racket[psi0] near negative roots with low relative
error.
If you need low relative error near negative roots, use @racket[bfpsi0].
}
@defproc[(psi [m Integer] [x Real]) Flonum]{
Computes a @hyperlink["http://en.wikipedia.org/wiki/Polygamma_function"]{polygamma function},
or the @racket[m]th logarithmic derivative of the gamma function. The order @racket[m] must be
a natural number, and @racket[x] may not be zero or a negative integer. Note that
@racket[(psi 0 x) = (psi0 x)].
@examples[#:eval untyped-eval
(plot (for/list ([m (in-range 4)])
(function (λ (x) (psi m x)) -2.5 2.5
#:color m #:style m #:label (format "psi~a(x)" m)))
#:y-min -300 #:y-max 300 #:legend-anchor 'top-right)
(psi -1 2.3)
(psi 0 -1.1)
(psi0 -1.1)]
From spot checks with @racket[m > 0], error appears to be as with @racket[psi0]: very low except
near negative roots. Near negative roots, relative error is apparently unbounded, but absolute error
is low.
}
@deftogether[(@defproc[(erf [x Real]) Real]
@defproc[(erfc [x Real]) Real])]{
Compute the @hyperlink["http://en.wikipedia.org/wiki/Error_function"]{error function and
complementary error function}, respectively. The only exact cases are @racket[(erf 0) = 0]
and @racket[(erfc 0) = 1].
@examples[#:eval untyped-eval
(plot (list (function erf -2 2 #:label "erf(x)")
(function erfc #:color 2 #:label "erfc(x)")))
(erf 0)
(erf 1)
(- 1 (erfc 1))
(erf -1)
(- (erfc 1) 1)]
Mathematically, erfc(@italic{x}) = 1 - erf(@italic{x}), but having separate implementations
can help maintain accuracy. To compute an expression containing erf, use @racket[erf] for
@racket[x] near @racket[0.0]. For positive @racket[x] away from @racket[0.0],
manipulate @racket[(- 1.0 (erfc x))] and its surrounding expressions to avoid the subtraction:
@interaction[#:eval untyped-eval
(define x 5.2)
(bf-precision 128)
(eval:alts (define log-erf-x (bigfloat->rational (bflog (bferf (bf x)))))
(define log-erf-x (/ -288077151382475259515535085154389899879
1496577676626844588240573268701473812127674924007424)))
(flulp-error (log (erf x)) log-erf-x)
(flulp-error (log (- 1.0 (erfc x))) log-erf-x)
(flulp-error (fllog1p (- (erfc x))) log-erf-x)]
For negative @racket[x] away from @racket[0.0], do the same with @racket[(- (erfc (- x)) 1.0)].
For @racket[erf], error is no greater than 2 @tech{ulps} everywhere that has been tested, and
is almost always no greater than 1. For @racket[erfc], observed error is no greater than 4 ulps,
and is usually no greater than 2.
}
@deftogether[(@defproc[(lambert [x Real]) (U Zero Flonum)]
@defproc[(lambert- [x Real]) Flonum])]{
Compute the @hyperlink["http://en.wikipedia.org/wiki/Lambert_W_function"]{Lambert W function},
or the inverse of @racket[x = (* y (exp y))].
This function has two real branches. The @racket[lambert] variant computes the upper branch,
and is defined for @racket[x >= (- (exp -1))]. The @racket[lambert-] variant computes the
lower branch, and is defined for @italic{negative} @racket[x >= (- (exp -1))].
The only exact case is @racket[(lambert 0) = 0].
@examples[#:eval untyped-eval
(plot (list (function lambert (- (exp -1)) 1)
(function lambert- (- (exp -1)) -min.0 #:color 2))
#:y-min -4)
(lambert 0)
(lambert (- (exp -1)))
(lambert -1/2)
(lambert- 0)
(define y0 (lambert -0.1))
(define y1 (lambert- -0.1))
y0
y1
(* y0 (exp y0))
(* y1 (exp y1))]
The Lambert W function often appears in solutions to equations that contain
@italic{n} log(@italic{n}), such as those that describe the running time of divide-and-conquer
algorithms.
For example, suppose we have a sort that takes @racket[t = (* c n (log n))] time, and we measure
the time it takes to sort an @racket[n = 10000]-element list at @racket[t = 0.245] ms. Solving for
@racket[c], we get
@interaction[#:eval untyped-eval
(define n 10000)
(define t 0.245)
(define c (/ t (* n (log n))))
c]
Now we would like to know how many elements we can sort in 100ms. We solve for @racket[n] and
use the solution to define a function @racket[time->sort-size]:
@interaction[#:eval untyped-eval
(define (time->sort-size t)
(exact-floor (exp (lambert (/ t c)))))
(time->sort-size 100)]
Testing the solution, we get
@interaction[#:eval untyped-eval
(eval:alts (define lst2 (build-list 2548516 values))
(eval:result ""))
(eval:alts (time (sort lst2 <))
(eval:result "" "cpu time: 80 real time: 93 gc time: 0"))]
For both branches, error is no more than 2 @tech{ulps} everywhere tested.
}
@defproc[(zeta [x Real]) Real]{
Computes the @hyperlink["http://en.wikipedia.org/wiki/Riemann_zeta_function"]{Riemann zeta function}.
If @racket[x] is a nonpositive exact integer, @racket[(zeta x)] is exact.
@examples[#:eval untyped-eval
(plot (function zeta -2 10) #:y-min -4 #:y-max 4)
(plot (function zeta -14 -2))
(zeta 0)
(zeta 1)
(zeta 1.0)
(zeta -1)
(eval:alts
(define num 1000000)
(eval:result ""))
(eval:alts
(define num-coprime
(for/sum ([_ (in-range num)])
(if (coprime? (random-bits 16) (random-bits 16)) 1 0)))
(eval:result ""))
(eval:alts (fl (/ num-coprime num))
(eval:result @racketresultfont{0.607901}))
(/ 1 (zeta 2))]
When @racket[s] is an odd, negative exact integer, @racket[(zeta s)] computes
@racket[(bernoulli (- 1 s))], which can be rather slow.
Maximum observed error is 6 @tech{ulps}, but is usually 3 or less.
}
@defproc[(eta [x Real]) Real]{
Computes the @hyperlink["http://en.wikipedia.org/wiki/Dirichlet_eta_function"]{Dirichlet eta function}.
If @racket[x] is a nonpositive exact integer, @racket[(eta x)] is exact.
@examples[#:eval untyped-eval
(plot (function eta -10 6))
(eta 0)
(eta -1)
(eta 1)
(log 2)]
When @racket[s] is an odd, negative exact integer, @racket[(eta s)] computes
@racket[(bernoulli (- 1 s))], which can be rather slow.
Maximum observed error is 11 @tech{ulps}, but is usually 4 or less.
}
@defproc[(hurwitz-zeta [s Real] [q Real]) Real]{
Computes the @hyperlink["http://en.wikipedia.org/wiki/Hurwitz_zeta_function"]{Hurwitz
zeta function} for @racket[s > 1] and @racket[q > 0]. When @racket[s = 1.0] or @racket[q = 0.0],
@racket[(hurwitz-zeta s q) = +inf.0].
@examples[#:eval untyped-eval
(plot (list (function zeta 1.5 5)
(function (λ (s) (hurwitz-zeta s 1))
#:color 2 #:style 'long-dash #:width 2)))
(hurwitz-zeta 1 1)
(hurwitz-zeta 1.0 1.0)
(hurwitz-zeta 2 1/4)
(+ (sqr pi) (* 8 catalan.0))]
While @racket[hurwitz-zeta] currently raises an exception for @racket[s < 1], it may in the future
return real values.
Maximum observed error is 6 @tech{ulps}, but is usually 2 or less.
}
@defproc[(beta [x Real] [y Real]) (U Exact-Rational Flonum)]{
Computes the @hyperlink["http://en.wikipedia.org/wiki/Beta_function"]{beta function} for positive
real @racket[x] and @racket[y]. Like @racket[(/ (* (gamma x) (gamma y)) (gamma (+ x y)))],
but more accurate.
@examples[#:eval untyped-eval
(plot3d (contour-intervals3d beta 0.25 2 0.25 2) #:angle 250)
(beta 0 0)
(beta 1 5)
(beta 1.0 5.0)]
}
@defproc[(log-beta [x Real] [y Real]) (U Zero Flonum)]{
Like @racket[(log (beta x y))], but more accurate and without unnecessary overflow.
The only exact case is @racket[(log-beta 1 1) = 0].
}
@defproc[(gamma-inc [k Real] [x Real] [upper? Any #f] [regularized? Any #f]) Flonum]{
Computes the @hyperlink["http://en.wikipedia.org/wiki/Incomplete_gamma_function"]{incomplete
gamma integral} for @racket[k > 0] and @racket[x >= 0]. When @racket[upper? = #f], it integrates
from zero to @racket[x]; otherwise it integrates from @racket[x] to infinity.
If you are doing statistical work, you should probably use @racket[gamma-dist] instead, which
is defined in terms of @racket[gamma-inc] and is more flexible (e.g. it allows negative @racket[x]).
The following identities should hold:
@itemlist[
@item{@racket[(gamma-inc k 0) = 0]}
@item{@racket[(gamma-inc k +inf.0) = (gamma k)]}
@item{@racket[(+ (gamma-inc k x #f) (gamma-inc k x #t)) = (gamma k)] (approximately)}
@item{@racket[(gamma-inc k x upper? #t) = (/ (gamma-inc k x upper? #f) (gamma k))] (approximately)}
@item{@racket[(gamma-inc k +inf.0 #t #t) = 1.0]}
@item{@racket[(+ (gamma-inc k x #f #t) (gamma-inc k x #t #t)) = 1.0] (approximately)}
]
@examples[#:eval untyped-eval
(list
(plot3d (contour-intervals3d gamma-inc 0.1 4.5 0 10)
#:x-label "k" #:y-label "x" #:width 210 #:height 210)
(plot3d (contour-intervals3d
(λ (k x) (gamma-inc k x #t)) 0.1 4.5 0 10)
#:x-label "k" #:y-label "x" #:width 210 #:height 210))
(plot3d (contour-intervals3d
(λ (k x) (gamma-inc k x #f #t)) 0.1 20 0 20)
#:x-label "k" #:y-label "x")
(gamma 4.0)
(+ (gamma-inc 4.0 0.5 #f) (gamma-inc 4.0 0.5 #t))
(gamma-inc 4.0 +inf.0)
(/ (gamma-inc 200.0 50.0 #f) (gamma 200.0))
(gamma-inc 200.0 50.0 #f #t)
(gamma-inc 0 5.0)
(gamma-inc 0.0 5.0)]
}
@defproc[(log-gamma-inc [k Real] [x Real] [upper? Any #f] [regularized? Any #f]) Flonum]{
Like @racket[(log (gamma-inc k x upper? regularized?))], but more accurate and without unnecessary
overflow.
}
@defproc[(beta-inc [a Real] [b Real] [x Real] [upper? Any #f] [regularized? Any #f]) Flonum]{
Computes the @hyperlink["http://en.wikipedia.org/wiki/Beta_function#Incomplete_beta_function"]{
incomplete beta integral} for @racket[a > 0], @racket[b > 0] and @racket[0 <= x <= 1].
When @racket[upper? = #f], it integrates from zero to @racket[x]; otherwise, it integrates from
@racket[x] to one.
If you are doing statistical work, you should probably use @racket[beta-dist] instead, which
is defined in terms of @racket[beta-inc] and is more flexible (e.g. it allows negative @racket[x]).
Similar identities should hold as with @racket[gamma-inc].
@examples[#:eval untyped-eval
(plot3d (isosurfaces3d (λ (a b x) (beta-inc a b x #f #t))
0.1 2.5 0.1 2.5 0 1 #:label "beta(a,b,x)")
#:x-label "a" #:y-label "b" #:z-label "x"
#:angle 20 #:altitude 20 #:legend-anchor 'top)]
}
@defproc[(log-beta-inc [a Real] [b Real] [x Real] [upper? Any #f] [regularized? Any #f]) Flonum]{
Like @racket[(log (beta-inc a b x upper? regularized?))], but more accurate and without unnecessary
overflow.
While most areas of this function have error less than @racket[5e-15], when @racket[a] and @racket[b]
have very dissimilar magnitudes (e.g. @racket[1e-16] and @racket[1e16]), it exhibits
@tech{catastrophic cancellation}. We are working on it.
}
@section[#:tag "flonum-functions"]{Flonum Functions}
@defproc[(flgamma [x Flonum]) Flonum]{}
@defproc[(fllog-gamma [x Flonum]) Flonum]{}
@defproc[(flpsi0 [x Flonum]) Flonum]{}
@defproc[(flpsi [m Integer] [x Flonum]) Flonum]{}
@defproc[(flerf [x Flonum]) Flonum]{}
@defproc[(flerfc [x Flonum]) Flonum]{}
@defproc[(fllambert [x Flonum]) Flonum]{}
@defproc[(fllambert- [x Flonum]) Flonum]{}
@defproc[(flzeta [x Flonum]) Flonum]{}
@defproc[(fleta [x Flonum]) Flonum]{}
@defproc[(flhurwitz-zeta [s Flonum] [q Flonum]) Flonum]{}
@defproc[(flbeta [x Flonum] [y Flonum]) Flonum]{}
@defproc[(fllog-beta [x Flonum] [y Flonum]) Flonum]{}
@defproc[(flgamma-inc [k Flonum] [x Flonum] [upper? Any] [regularized? Any]) Flonum]{}
@defproc[(fllog-gamma-inc [k Flonum] [x Flonum] [upper? Any] [regularized? Any]) Flonum]{}
@defproc[(flbeta-inc [a Flonum] [b Flonum] [x Flonum] [upper? Any] [regularized? Any]) Flonum]{}
@defproc[(fllog-beta-inc [a Flonum] [b Flonum] [x Flonum] [upper? Any] [regularized? Any]) Flonum]{
Flonum versions of the above functions. These return @racket[+nan.0] instead of raising errors and do
not have optional arguments. They can be a little faster to apply because they check fewer special
cases.
}
@(close-eval untyped-eval)