Spaces:
Running
Running
import math | |
from ..libmp.backend import xrange | |
class QuadratureRule(object): | |
""" | |
Quadrature rules are implemented using this class, in order to | |
simplify the code and provide a common infrastructure | |
for tasks such as error estimation and node caching. | |
You can implement a custom quadrature rule by subclassing | |
:class:`QuadratureRule` and implementing the appropriate | |
methods. The subclass can then be used by :func:`~mpmath.quad` by | |
passing it as the *method* argument. | |
:class:`QuadratureRule` instances are supposed to be singletons. | |
:class:`QuadratureRule` therefore implements instance caching | |
in :func:`~mpmath.__new__`. | |
""" | |
def __init__(self, ctx): | |
self.ctx = ctx | |
self.standard_cache = {} | |
self.transformed_cache = {} | |
self.interval_count = {} | |
def clear(self): | |
""" | |
Delete cached node data. | |
""" | |
self.standard_cache = {} | |
self.transformed_cache = {} | |
self.interval_count = {} | |
def calc_nodes(self, degree, prec, verbose=False): | |
r""" | |
Compute nodes for the standard interval `[-1, 1]`. Subclasses | |
should probably implement only this method, and use | |
:func:`~mpmath.get_nodes` method to retrieve the nodes. | |
""" | |
raise NotImplementedError | |
def get_nodes(self, a, b, degree, prec, verbose=False): | |
""" | |
Return nodes for given interval, degree and precision. The | |
nodes are retrieved from a cache if already computed; | |
otherwise they are computed by calling :func:`~mpmath.calc_nodes` | |
and are then cached. | |
Subclasses should probably not implement this method, | |
but just implement :func:`~mpmath.calc_nodes` for the actual | |
node computation. | |
""" | |
key = (a, b, degree, prec) | |
if key in self.transformed_cache: | |
return self.transformed_cache[key] | |
orig = self.ctx.prec | |
try: | |
self.ctx.prec = prec+20 | |
# Get nodes on standard interval | |
if (degree, prec) in self.standard_cache: | |
nodes = self.standard_cache[degree, prec] | |
else: | |
nodes = self.calc_nodes(degree, prec, verbose) | |
self.standard_cache[degree, prec] = nodes | |
# Transform to general interval | |
nodes = self.transform_nodes(nodes, a, b, verbose) | |
if key in self.interval_count: | |
self.transformed_cache[key] = nodes | |
else: | |
self.interval_count[key] = True | |
finally: | |
self.ctx.prec = orig | |
return nodes | |
def transform_nodes(self, nodes, a, b, verbose=False): | |
r""" | |
Rescale standardized nodes (for `[-1, 1]`) to a general | |
interval `[a, b]`. For a finite interval, a simple linear | |
change of variables is used. Otherwise, the following | |
transformations are used: | |
.. math :: | |
\lbrack a, \infty \rbrack : t = \frac{1}{x} + (a-1) | |
\lbrack -\infty, b \rbrack : t = (b+1) - \frac{1}{x} | |
\lbrack -\infty, \infty \rbrack : t = \frac{x}{\sqrt{1-x^2}} | |
""" | |
ctx = self.ctx | |
a = ctx.convert(a) | |
b = ctx.convert(b) | |
one = ctx.one | |
if (a, b) == (-one, one): | |
return nodes | |
half = ctx.mpf(0.5) | |
new_nodes = [] | |
if ctx.isinf(a) or ctx.isinf(b): | |
if (a, b) == (ctx.ninf, ctx.inf): | |
p05 = -half | |
for x, w in nodes: | |
x2 = x*x | |
px1 = one-x2 | |
spx1 = px1**p05 | |
x = x*spx1 | |
w *= spx1/px1 | |
new_nodes.append((x, w)) | |
elif a == ctx.ninf: | |
b1 = b+1 | |
for x, w in nodes: | |
u = 2/(x+one) | |
x = b1-u | |
w *= half*u**2 | |
new_nodes.append((x, w)) | |
elif b == ctx.inf: | |
a1 = a-1 | |
for x, w in nodes: | |
u = 2/(x+one) | |
x = a1+u | |
w *= half*u**2 | |
new_nodes.append((x, w)) | |
elif a == ctx.inf or b == ctx.ninf: | |
return [(x,-w) for (x,w) in self.transform_nodes(nodes, b, a, verbose)] | |
else: | |
raise NotImplementedError | |
else: | |
# Simple linear change of variables | |
C = (b-a)/2 | |
D = (b+a)/2 | |
for x, w in nodes: | |
new_nodes.append((D+C*x, C*w)) | |
return new_nodes | |
def guess_degree(self, prec): | |
""" | |
Given a desired precision `p` in bits, estimate the degree `m` | |
of the quadrature required to accomplish full accuracy for | |
typical integrals. By default, :func:`~mpmath.quad` will perform up | |
to `m` iterations. The value of `m` should be a slight | |
overestimate, so that "slightly bad" integrals can be dealt | |
with automatically using a few extra iterations. On the | |
other hand, it should not be too big, so :func:`~mpmath.quad` can | |
quit within a reasonable amount of time when it is given | |
an "unsolvable" integral. | |
The default formula used by :func:`~mpmath.guess_degree` is tuned | |
for both :class:`TanhSinh` and :class:`GaussLegendre`. | |
The output is roughly as follows: | |
+---------+---------+ | |
| `p` | `m` | | |
+=========+=========+ | |
| 50 | 6 | | |
+---------+---------+ | |
| 100 | 7 | | |
+---------+---------+ | |
| 500 | 10 | | |
+---------+---------+ | |
| 3000 | 12 | | |
+---------+---------+ | |
This formula is based purely on a limited amount of | |
experimentation and will sometimes be wrong. | |
""" | |
# Expected degree | |
# XXX: use mag | |
g = int(4 + max(0, self.ctx.log(prec/30.0, 2))) | |
# Reasonable "worst case" | |
g += 2 | |
return g | |
def estimate_error(self, results, prec, epsilon): | |
r""" | |
Given results from integrations `[I_1, I_2, \ldots, I_k]` done | |
with a quadrature of rule of degree `1, 2, \ldots, k`, estimate | |
the error of `I_k`. | |
For `k = 2`, we estimate `|I_{\infty}-I_2|` as `|I_2-I_1|`. | |
For `k > 2`, we extrapolate `|I_{\infty}-I_k| \approx |I_{k+1}-I_k|` | |
from `|I_k-I_{k-1}|` and `|I_k-I_{k-2}|` under the assumption | |
that each degree increment roughly doubles the accuracy of | |
the quadrature rule (this is true for both :class:`TanhSinh` | |
and :class:`GaussLegendre`). The extrapolation formula is given | |
by Borwein, Bailey & Girgensohn. Although not very conservative, | |
this method seems to be very robust in practice. | |
""" | |
if len(results) == 2: | |
return abs(results[0]-results[1]) | |
try: | |
if results[-1] == results[-2] == results[-3]: | |
return self.ctx.zero | |
D1 = self.ctx.log(abs(results[-1]-results[-2]), 10) | |
D2 = self.ctx.log(abs(results[-1]-results[-3]), 10) | |
except ValueError: | |
return epsilon | |
D3 = -prec | |
D4 = min(0, max(D1**2/D2, 2*D1, D3)) | |
return self.ctx.mpf(10) ** int(D4) | |
def summation(self, f, points, prec, epsilon, max_degree, verbose=False): | |
""" | |
Main integration function. Computes the 1D integral over | |
the interval specified by *points*. For each subinterval, | |
performs quadrature of degree from 1 up to *max_degree* | |
until :func:`~mpmath.estimate_error` signals convergence. | |
:func:`~mpmath.summation` transforms each subintegration to | |
the standard interval and then calls :func:`~mpmath.sum_next`. | |
""" | |
ctx = self.ctx | |
I = total_err = ctx.zero | |
for i in xrange(len(points)-1): | |
a, b = points[i], points[i+1] | |
if a == b: | |
continue | |
# XXX: we could use a single variable transformation, | |
# but this is not good in practice. We get better accuracy | |
# by having 0 as an endpoint. | |
if (a, b) == (ctx.ninf, ctx.inf): | |
_f = f | |
f = lambda x: _f(-x) + _f(x) | |
a, b = (ctx.zero, ctx.inf) | |
results = [] | |
err = ctx.zero | |
for degree in xrange(1, max_degree+1): | |
nodes = self.get_nodes(a, b, degree, prec, verbose) | |
if verbose: | |
print("Integrating from %s to %s (degree %s of %s)" % \ | |
(ctx.nstr(a), ctx.nstr(b), degree, max_degree)) | |
result = self.sum_next(f, nodes, degree, prec, results, verbose) | |
results.append(result) | |
if degree > 1: | |
err = self.estimate_error(results, prec, epsilon) | |
if verbose: | |
print("Estimated error:", ctx.nstr(err), " epsilon:", ctx.nstr(epsilon), " result: ", ctx.nstr(result)) | |
if err <= epsilon: | |
break | |
I += results[-1] | |
total_err += err | |
if total_err > epsilon: | |
if verbose: | |
print("Failed to reach full accuracy. Estimated error:", ctx.nstr(total_err)) | |
return I, total_err | |
def sum_next(self, f, nodes, degree, prec, previous, verbose=False): | |
r""" | |
Evaluates the step sum `\sum w_k f(x_k)` where the *nodes* list | |
contains the `(w_k, x_k)` pairs. | |
:func:`~mpmath.summation` will supply the list *results* of | |
values computed by :func:`~mpmath.sum_next` at previous degrees, in | |
case the quadrature rule is able to reuse them. | |
""" | |
return self.ctx.fdot((w, f(x)) for (x,w) in nodes) | |
class TanhSinh(QuadratureRule): | |
r""" | |
This class implements "tanh-sinh" or "doubly exponential" | |
quadrature. This quadrature rule is based on the Euler-Maclaurin | |
integral formula. By performing a change of variables involving | |
nested exponentials / hyperbolic functions (hence the name), the | |
derivatives at the endpoints vanish rapidly. Since the error term | |
in the Euler-Maclaurin formula depends on the derivatives at the | |
endpoints, a simple step sum becomes extremely accurate. In | |
practice, this means that doubling the number of evaluation | |
points roughly doubles the number of accurate digits. | |
Comparison to Gauss-Legendre: | |
* Initial computation of nodes is usually faster | |
* Handles endpoint singularities better | |
* Handles infinite integration intervals better | |
* Is slower for smooth integrands once nodes have been computed | |
The implementation of the tanh-sinh algorithm is based on the | |
description given in Borwein, Bailey & Girgensohn, "Experimentation | |
in Mathematics - Computational Paths to Discovery", A K Peters, | |
2003, pages 312-313. In the present implementation, a few | |
improvements have been made: | |
* A more efficient scheme is used to compute nodes (exploiting | |
recurrence for the exponential function) | |
* The nodes are computed successively instead of all at once | |
**References** | |
* [Bailey]_ | |
* http://users.cs.dal.ca/~jborwein/tanh-sinh.pdf | |
""" | |
def sum_next(self, f, nodes, degree, prec, previous, verbose=False): | |
""" | |
Step sum for tanh-sinh quadrature of degree `m`. We exploit the | |
fact that half of the abscissas at degree `m` are precisely the | |
abscissas from degree `m-1`. Thus reusing the result from | |
the previous level allows a 2x speedup. | |
""" | |
h = self.ctx.mpf(2)**(-degree) | |
# Abscissas overlap, so reusing saves half of the time | |
if previous: | |
S = previous[-1]/(h*2) | |
else: | |
S = self.ctx.zero | |
S += self.ctx.fdot((w,f(x)) for (x,w) in nodes) | |
return h*S | |
def calc_nodes(self, degree, prec, verbose=False): | |
r""" | |
The abscissas and weights for tanh-sinh quadrature of degree | |
`m` are given by | |
.. math:: | |
x_k = \tanh(\pi/2 \sinh(t_k)) | |
w_k = \pi/2 \cosh(t_k) / \cosh(\pi/2 \sinh(t_k))^2 | |
where `t_k = t_0 + hk` for a step length `h \sim 2^{-m}`. The | |
list of nodes is actually infinite, but the weights die off so | |
rapidly that only a few are needed. | |
""" | |
ctx = self.ctx | |
nodes = [] | |
extra = 20 | |
ctx.prec += extra | |
tol = ctx.ldexp(1, -prec-10) | |
pi4 = ctx.pi/4 | |
# For simplicity, we work in steps h = 1/2^n, with the first point | |
# offset so that we can reuse the sum from the previous degree | |
# We define degree 1 to include the "degree 0" steps, including | |
# the point x = 0. (It doesn't work well otherwise; not sure why.) | |
t0 = ctx.ldexp(1, -degree) | |
if degree == 1: | |
#nodes.append((mpf(0), pi4)) | |
#nodes.append((-mpf(0), pi4)) | |
nodes.append((ctx.zero, ctx.pi/2)) | |
h = t0 | |
else: | |
h = t0*2 | |
# Since h is fixed, we can compute the next exponential | |
# by simply multiplying by exp(h) | |
expt0 = ctx.exp(t0) | |
a = pi4 * expt0 | |
b = pi4 / expt0 | |
udelta = ctx.exp(h) | |
urdelta = 1/udelta | |
for k in xrange(0, 20*2**degree+1): | |
# Reference implementation: | |
# t = t0 + k*h | |
# x = tanh(pi/2 * sinh(t)) | |
# w = pi/2 * cosh(t) / cosh(pi/2 * sinh(t))**2 | |
# Fast implementation. Note that c = exp(pi/2 * sinh(t)) | |
c = ctx.exp(a-b) | |
d = 1/c | |
co = (c+d)/2 | |
si = (c-d)/2 | |
x = si / co | |
w = (a+b) / co**2 | |
diff = abs(x-1) | |
if diff <= tol: | |
break | |
nodes.append((x, w)) | |
nodes.append((-x, w)) | |
a *= udelta | |
b *= urdelta | |
if verbose and k % 300 == 150: | |
# Note: the number displayed is rather arbitrary. Should | |
# figure out how to print something that looks more like a | |
# percentage | |
print("Calculating nodes:", ctx.nstr(-ctx.log(diff, 10) / prec)) | |
ctx.prec -= extra | |
return nodes | |
class GaussLegendre(QuadratureRule): | |
r""" | |
This class implements Gauss-Legendre quadrature, which is | |
exceptionally efficient for polynomials and polynomial-like (i.e. | |
very smooth) integrands. | |
The abscissas and weights are given by roots and values of | |
Legendre polynomials, which are the orthogonal polynomials | |
on `[-1, 1]` with respect to the unit weight | |
(see :func:`~mpmath.legendre`). | |
In this implementation, we take the "degree" `m` of the quadrature | |
to denote a Gauss-Legendre rule of degree `3 \cdot 2^m` (following | |
Borwein, Bailey & Girgensohn). This way we get quadratic, rather | |
than linear, convergence as the degree is incremented. | |
Comparison to tanh-sinh quadrature: | |
* Is faster for smooth integrands once nodes have been computed | |
* Initial computation of nodes is usually slower | |
* Handles endpoint singularities worse | |
* Handles infinite integration intervals worse | |
""" | |
def calc_nodes(self, degree, prec, verbose=False): | |
r""" | |
Calculates the abscissas and weights for Gauss-Legendre | |
quadrature of degree of given degree (actually `3 \cdot 2^m`). | |
""" | |
ctx = self.ctx | |
# It is important that the epsilon is set lower than the | |
# "real" epsilon | |
epsilon = ctx.ldexp(1, -prec-8) | |
# Fairly high precision might be required for accurate | |
# evaluation of the roots | |
orig = ctx.prec | |
ctx.prec = int(prec*1.5) | |
if degree == 1: | |
x = ctx.sqrt(ctx.mpf(3)/5) | |
w = ctx.mpf(5)/9 | |
nodes = [(-x,w),(ctx.zero,ctx.mpf(8)/9),(x,w)] | |
ctx.prec = orig | |
return nodes | |
nodes = [] | |
n = 3*2**(degree-1) | |
upto = n//2 + 1 | |
for j in xrange(1, upto): | |
# Asymptotic formula for the roots | |
r = ctx.mpf(math.cos(math.pi*(j-0.25)/(n+0.5))) | |
# Newton iteration | |
while 1: | |
t1, t2 = 1, 0 | |
# Evaluates the Legendre polynomial using its defining | |
# recurrence relation | |
for j1 in xrange(1,n+1): | |
t3, t2, t1 = t2, t1, ((2*j1-1)*r*t1 - (j1-1)*t2)/j1 | |
t4 = n*(r*t1-t2)/(r**2-1) | |
a = t1/t4 | |
r = r - a | |
if abs(a) < epsilon: | |
break | |
x = r | |
w = 2/((1-r**2)*t4**2) | |
if verbose and j % 30 == 15: | |
print("Computing nodes (%i of %i)" % (j, upto)) | |
nodes.append((x, w)) | |
nodes.append((-x, w)) | |
ctx.prec = orig | |
return nodes | |
class QuadratureMethods(object): | |
def __init__(ctx, *args, **kwargs): | |
ctx._gauss_legendre = GaussLegendre(ctx) | |
ctx._tanh_sinh = TanhSinh(ctx) | |
def quad(ctx, f, *points, **kwargs): | |
r""" | |
Computes a single, double or triple integral over a given | |
1D interval, 2D rectangle, or 3D cuboid. A basic example:: | |
>>> from mpmath import * | |
>>> mp.dps = 15; mp.pretty = True | |
>>> quad(sin, [0, pi]) | |
2.0 | |
A basic 2D integral:: | |
>>> f = lambda x, y: cos(x+y/2) | |
>>> quad(f, [-pi/2, pi/2], [0, pi]) | |
4.0 | |
**Interval format** | |
The integration range for each dimension may be specified | |
using a list or tuple. Arguments are interpreted as follows: | |
``quad(f, [x1, x2])`` -- calculates | |
`\int_{x_1}^{x_2} f(x) \, dx` | |
``quad(f, [x1, x2], [y1, y2])`` -- calculates | |
`\int_{x_1}^{x_2} \int_{y_1}^{y_2} f(x,y) \, dy \, dx` | |
``quad(f, [x1, x2], [y1, y2], [z1, z2])`` -- calculates | |
`\int_{x_1}^{x_2} \int_{y_1}^{y_2} \int_{z_1}^{z_2} f(x,y,z) | |
\, dz \, dy \, dx` | |
Endpoints may be finite or infinite. An interval descriptor | |
may also contain more than two points. In this | |
case, the integration is split into subintervals, between | |
each pair of consecutive points. This is useful for | |
dealing with mid-interval discontinuities, or integrating | |
over large intervals where the function is irregular or | |
oscillates. | |
**Options** | |
:func:`~mpmath.quad` recognizes the following keyword arguments: | |
*method* | |
Chooses integration algorithm (described below). | |
*error* | |
If set to true, :func:`~mpmath.quad` returns `(v, e)` where `v` is the | |
integral and `e` is the estimated error. | |
*maxdegree* | |
Maximum degree of the quadrature rule to try before | |
quitting. | |
*verbose* | |
Print details about progress. | |
**Algorithms** | |
Mpmath presently implements two integration algorithms: tanh-sinh | |
quadrature and Gauss-Legendre quadrature. These can be selected | |
using *method='tanh-sinh'* or *method='gauss-legendre'* or by | |
passing the classes *method=TanhSinh*, *method=GaussLegendre*. | |
The functions :func:`~mpmath.quadts` and :func:`~mpmath.quadgl` are also available | |
as shortcuts. | |
Both algorithms have the property that doubling the number of | |
evaluation points roughly doubles the accuracy, so both are ideal | |
for high precision quadrature (hundreds or thousands of digits). | |
At high precision, computing the nodes and weights for the | |
integration can be expensive (more expensive than computing the | |
function values). To make repeated integrations fast, nodes | |
are automatically cached. | |
The advantages of the tanh-sinh algorithm are that it tends to | |
handle endpoint singularities well, and that the nodes are cheap | |
to compute on the first run. For these reasons, it is used by | |
:func:`~mpmath.quad` as the default algorithm. | |
Gauss-Legendre quadrature often requires fewer function | |
evaluations, and is therefore often faster for repeated use, but | |
the algorithm does not handle endpoint singularities as well and | |
the nodes are more expensive to compute. Gauss-Legendre quadrature | |
can be a better choice if the integrand is smooth and repeated | |
integrations are required (e.g. for multiple integrals). | |
See the documentation for :class:`TanhSinh` and | |
:class:`GaussLegendre` for additional details. | |
**Examples of 1D integrals** | |
Intervals may be infinite or half-infinite. The following two | |
examples evaluate the limits of the inverse tangent function | |
(`\int 1/(1+x^2) = \tan^{-1} x`), and the Gaussian integral | |
`\int_{\infty}^{\infty} \exp(-x^2)\,dx = \sqrt{\pi}`:: | |
>>> mp.dps = 15 | |
>>> quad(lambda x: 2/(x**2+1), [0, inf]) | |
3.14159265358979 | |
>>> quad(lambda x: exp(-x**2), [-inf, inf])**2 | |
3.14159265358979 | |
Integrals can typically be resolved to high precision. | |
The following computes 50 digits of `\pi` by integrating the | |
area of the half-circle defined by `x^2 + y^2 \le 1`, | |
`-1 \le x \le 1`, `y \ge 0`:: | |
>>> mp.dps = 50 | |
>>> 2*quad(lambda x: sqrt(1-x**2), [-1, 1]) | |
3.1415926535897932384626433832795028841971693993751 | |
One can just as well compute 1000 digits (output truncated):: | |
>>> mp.dps = 1000 | |
>>> 2*quad(lambda x: sqrt(1-x**2), [-1, 1]) #doctest:+ELLIPSIS | |
3.141592653589793238462643383279502884...216420199 | |
Complex integrals are supported. The following computes | |
a residue at `z = 0` by integrating counterclockwise along the | |
diamond-shaped path from `1` to `+i` to `-1` to `-i` to `1`:: | |
>>> mp.dps = 15 | |
>>> chop(quad(lambda z: 1/z, [1,j,-1,-j,1])) | |
(0.0 + 6.28318530717959j) | |
**Examples of 2D and 3D integrals** | |
Here are several nice examples of analytically solvable | |
2D integrals (taken from MathWorld [1]) that can be evaluated | |
to high precision fairly rapidly by :func:`~mpmath.quad`:: | |
>>> mp.dps = 30 | |
>>> f = lambda x, y: (x-1)/((1-x*y)*log(x*y)) | |
>>> quad(f, [0, 1], [0, 1]) | |
0.577215664901532860606512090082 | |
>>> +euler | |
0.577215664901532860606512090082 | |
>>> f = lambda x, y: 1/sqrt(1+x**2+y**2) | |
>>> quad(f, [-1, 1], [-1, 1]) | |
3.17343648530607134219175646705 | |
>>> 4*log(2+sqrt(3))-2*pi/3 | |
3.17343648530607134219175646705 | |
>>> f = lambda x, y: 1/(1-x**2 * y**2) | |
>>> quad(f, [0, 1], [0, 1]) | |
1.23370055013616982735431137498 | |
>>> pi**2 / 8 | |
1.23370055013616982735431137498 | |
>>> quad(lambda x, y: 1/(1-x*y), [0, 1], [0, 1]) | |
1.64493406684822643647241516665 | |
>>> pi**2 / 6 | |
1.64493406684822643647241516665 | |
Multiple integrals may be done over infinite ranges:: | |
>>> mp.dps = 15 | |
>>> print(quad(lambda x,y: exp(-x-y), [0, inf], [1, inf])) | |
0.367879441171442 | |
>>> print(1/e) | |
0.367879441171442 | |
For nonrectangular areas, one can call :func:`~mpmath.quad` recursively. | |
For example, we can replicate the earlier example of calculating | |
`\pi` by integrating over the unit-circle, and actually use double | |
quadrature to actually measure the area circle:: | |
>>> f = lambda x: quad(lambda y: 1, [-sqrt(1-x**2), sqrt(1-x**2)]) | |
>>> quad(f, [-1, 1]) | |
3.14159265358979 | |
Here is a simple triple integral:: | |
>>> mp.dps = 15 | |
>>> f = lambda x,y,z: x*y/(1+z) | |
>>> quad(f, [0,1], [0,1], [1,2], method='gauss-legendre') | |
0.101366277027041 | |
>>> (log(3)-log(2))/4 | |
0.101366277027041 | |
**Singularities** | |
Both tanh-sinh and Gauss-Legendre quadrature are designed to | |
integrate smooth (infinitely differentiable) functions. Neither | |
algorithm copes well with mid-interval singularities (such as | |
mid-interval discontinuities in `f(x)` or `f'(x)`). | |
The best solution is to split the integral into parts:: | |
>>> mp.dps = 15 | |
>>> quad(lambda x: abs(sin(x)), [0, 2*pi]) # Bad | |
3.99900894176779 | |
>>> quad(lambda x: abs(sin(x)), [0, pi, 2*pi]) # Good | |
4.0 | |
The tanh-sinh rule often works well for integrands having a | |
singularity at one or both endpoints:: | |
>>> mp.dps = 15 | |
>>> quad(log, [0, 1], method='tanh-sinh') # Good | |
-1.0 | |
>>> quad(log, [0, 1], method='gauss-legendre') # Bad | |
-0.999932197413801 | |
However, the result may still be inaccurate for some functions:: | |
>>> quad(lambda x: 1/sqrt(x), [0, 1], method='tanh-sinh') | |
1.99999999946942 | |
This problem is not due to the quadrature rule per se, but to | |
numerical amplification of errors in the nodes. The problem can be | |
circumvented by temporarily increasing the precision:: | |
>>> mp.dps = 30 | |
>>> a = quad(lambda x: 1/sqrt(x), [0, 1], method='tanh-sinh') | |
>>> mp.dps = 15 | |
>>> +a | |
2.0 | |
**Highly variable functions** | |
For functions that are smooth (in the sense of being infinitely | |
differentiable) but contain sharp mid-interval peaks or many | |
"bumps", :func:`~mpmath.quad` may fail to provide full accuracy. For | |
example, with default settings, :func:`~mpmath.quad` is able to integrate | |
`\sin(x)` accurately over an interval of length 100 but not over | |
length 1000:: | |
>>> quad(sin, [0, 100]); 1-cos(100) # Good | |
0.137681127712316 | |
0.137681127712316 | |
>>> quad(sin, [0, 1000]); 1-cos(1000) # Bad | |
-37.8587612408485 | |
0.437620923709297 | |
One solution is to break the integration into 10 intervals of | |
length 100:: | |
>>> quad(sin, linspace(0, 1000, 10)) # Good | |
0.437620923709297 | |
Another is to increase the degree of the quadrature:: | |
>>> quad(sin, [0, 1000], maxdegree=10) # Also good | |
0.437620923709297 | |
Whether splitting the interval or increasing the degree is | |
more efficient differs from case to case. Another example is the | |
function `1/(1+x^2)`, which has a sharp peak centered around | |
`x = 0`:: | |
>>> f = lambda x: 1/(1+x**2) | |
>>> quad(f, [-100, 100]) # Bad | |
3.64804647105268 | |
>>> quad(f, [-100, 100], maxdegree=10) # Good | |
3.12159332021646 | |
>>> quad(f, [-100, 0, 100]) # Also good | |
3.12159332021646 | |
**References** | |
1. http://mathworld.wolfram.com/DoubleIntegral.html | |
""" | |
rule = kwargs.get('method', 'tanh-sinh') | |
if type(rule) is str: | |
if rule == 'tanh-sinh': | |
rule = ctx._tanh_sinh | |
elif rule == 'gauss-legendre': | |
rule = ctx._gauss_legendre | |
else: | |
raise ValueError("unknown quadrature rule: %s" % rule) | |
else: | |
rule = rule(ctx) | |
verbose = kwargs.get('verbose') | |
dim = len(points) | |
orig = prec = ctx.prec | |
epsilon = ctx.eps/8 | |
m = kwargs.get('maxdegree') or rule.guess_degree(prec) | |
points = [ctx._as_points(p) for p in points] | |
try: | |
ctx.prec += 20 | |
if dim == 1: | |
v, err = rule.summation(f, points[0], prec, epsilon, m, verbose) | |
elif dim == 2: | |
v, err = rule.summation(lambda x: \ | |
rule.summation(lambda y: f(x,y), \ | |
points[1], prec, epsilon, m)[0], | |
points[0], prec, epsilon, m, verbose) | |
elif dim == 3: | |
v, err = rule.summation(lambda x: \ | |
rule.summation(lambda y: \ | |
rule.summation(lambda z: f(x,y,z), \ | |
points[2], prec, epsilon, m)[0], | |
points[1], prec, epsilon, m)[0], | |
points[0], prec, epsilon, m, verbose) | |
else: | |
raise NotImplementedError("quadrature must have dim 1, 2 or 3") | |
finally: | |
ctx.prec = orig | |
if kwargs.get("error"): | |
return +v, err | |
return +v | |
def quadts(ctx, *args, **kwargs): | |
""" | |
Performs tanh-sinh quadrature. The call | |
quadts(func, *points, ...) | |
is simply a shortcut for: | |
quad(func, *points, ..., method=TanhSinh) | |
For example, a single integral and a double integral: | |
quadts(lambda x: exp(cos(x)), [0, 1]) | |
quadts(lambda x, y: exp(cos(x+y)), [0, 1], [0, 1]) | |
See the documentation for quad for information about how points | |
arguments and keyword arguments are parsed. | |
See documentation for TanhSinh for algorithmic information about | |
tanh-sinh quadrature. | |
""" | |
kwargs['method'] = 'tanh-sinh' | |
return ctx.quad(*args, **kwargs) | |
def quadgl(ctx, *args, **kwargs): | |
""" | |
Performs Gauss-Legendre quadrature. The call | |
quadgl(func, *points, ...) | |
is simply a shortcut for: | |
quad(func, *points, ..., method=GaussLegendre) | |
For example, a single integral and a double integral: | |
quadgl(lambda x: exp(cos(x)), [0, 1]) | |
quadgl(lambda x, y: exp(cos(x+y)), [0, 1], [0, 1]) | |
See the documentation for quad for information about how points | |
arguments and keyword arguments are parsed. | |
See documentation for TanhSinh for algorithmic information about | |
tanh-sinh quadrature. | |
""" | |
kwargs['method'] = 'gauss-legendre' | |
return ctx.quad(*args, **kwargs) | |
def quadosc(ctx, f, interval, omega=None, period=None, zeros=None): | |
r""" | |
Calculates | |
.. math :: | |
I = \int_a^b f(x) dx | |
where at least one of `a` and `b` is infinite and where | |
`f(x) = g(x) \cos(\omega x + \phi)` for some slowly | |
decreasing function `g(x)`. With proper input, :func:`~mpmath.quadosc` | |
can also handle oscillatory integrals where the oscillation | |
rate is different from a pure sine or cosine wave. | |
In the standard case when `|a| < \infty, b = \infty`, | |
:func:`~mpmath.quadosc` works by evaluating the infinite series | |
.. math :: | |
I = \int_a^{x_1} f(x) dx + | |
\sum_{k=1}^{\infty} \int_{x_k}^{x_{k+1}} f(x) dx | |
where `x_k` are consecutive zeros (alternatively | |
some other periodic reference point) of `f(x)`. | |
Accordingly, :func:`~mpmath.quadosc` requires information about the | |
zeros of `f(x)`. For a periodic function, you can specify | |
the zeros by either providing the angular frequency `\omega` | |
(*omega*) or the *period* `2 \pi/\omega`. In general, you can | |
specify the `n`-th zero by providing the *zeros* arguments. | |
Below is an example of each:: | |
>>> from mpmath import * | |
>>> mp.dps = 15; mp.pretty = True | |
>>> f = lambda x: sin(3*x)/(x**2+1) | |
>>> quadosc(f, [0,inf], omega=3) | |
0.37833007080198 | |
>>> quadosc(f, [0,inf], period=2*pi/3) | |
0.37833007080198 | |
>>> quadosc(f, [0,inf], zeros=lambda n: pi*n/3) | |
0.37833007080198 | |
>>> (ei(3)*exp(-3)-exp(3)*ei(-3))/2 # Computed by Mathematica | |
0.37833007080198 | |
Note that *zeros* was specified to multiply `n` by the | |
*half-period*, not the full period. In theory, it does not matter | |
whether each partial integral is done over a half period or a full | |
period. However, if done over half-periods, the infinite series | |
passed to :func:`~mpmath.nsum` becomes an *alternating series* and this | |
typically makes the extrapolation much more efficient. | |
Here is an example of an integration over the entire real line, | |
and a half-infinite integration starting at `-\infty`:: | |
>>> quadosc(lambda x: cos(x)/(1+x**2), [-inf, inf], omega=1) | |
1.15572734979092 | |
>>> pi/e | |
1.15572734979092 | |
>>> quadosc(lambda x: cos(x)/x**2, [-inf, -1], period=2*pi) | |
-0.0844109505595739 | |
>>> cos(1)+si(1)-pi/2 | |
-0.0844109505595738 | |
Of course, the integrand may contain a complex exponential just as | |
well as a real sine or cosine:: | |
>>> quadosc(lambda x: exp(3*j*x)/(1+x**2), [-inf,inf], omega=3) | |
(0.156410688228254 + 0.0j) | |
>>> pi/e**3 | |
0.156410688228254 | |
>>> quadosc(lambda x: exp(3*j*x)/(2+x+x**2), [-inf,inf], omega=3) | |
(0.00317486988463794 - 0.0447701735209082j) | |
>>> 2*pi/sqrt(7)/exp(3*(j+sqrt(7))/2) | |
(0.00317486988463794 - 0.0447701735209082j) | |
**Non-periodic functions** | |
If `f(x) = g(x) h(x)` for some function `h(x)` that is not | |
strictly periodic, *omega* or *period* might not work, and it might | |
be necessary to use *zeros*. | |
A notable exception can be made for Bessel functions which, though not | |
periodic, are "asymptotically periodic" in a sufficiently strong sense | |
that the sum extrapolation will work out:: | |
>>> quadosc(j0, [0, inf], period=2*pi) | |
1.0 | |
>>> quadosc(j1, [0, inf], period=2*pi) | |
1.0 | |
More properly, one should provide the exact Bessel function zeros:: | |
>>> j0zero = lambda n: findroot(j0, pi*(n-0.25)) | |
>>> quadosc(j0, [0, inf], zeros=j0zero) | |
1.0 | |
For an example where *zeros* becomes necessary, consider the | |
complete Fresnel integrals | |
.. math :: | |
\int_0^{\infty} \cos x^2\,dx = \int_0^{\infty} \sin x^2\,dx | |
= \sqrt{\frac{\pi}{8}}. | |
Although the integrands do not decrease in magnitude as | |
`x \to \infty`, the integrals are convergent since the oscillation | |
rate increases (causing consecutive periods to asymptotically | |
cancel out). These integrals are virtually impossible to calculate | |
to any kind of accuracy using standard quadrature rules. However, | |
if one provides the correct asymptotic distribution of zeros | |
(`x_n \sim \sqrt{n}`), :func:`~mpmath.quadosc` works:: | |
>>> mp.dps = 30 | |
>>> f = lambda x: cos(x**2) | |
>>> quadosc(f, [0,inf], zeros=lambda n:sqrt(pi*n)) | |
0.626657068657750125603941321203 | |
>>> f = lambda x: sin(x**2) | |
>>> quadosc(f, [0,inf], zeros=lambda n:sqrt(pi*n)) | |
0.626657068657750125603941321203 | |
>>> sqrt(pi/8) | |
0.626657068657750125603941321203 | |
(Interestingly, these integrals can still be evaluated if one | |
places some other constant than `\pi` in the square root sign.) | |
In general, if `f(x) \sim g(x) \cos(h(x))`, the zeros follow | |
the inverse-function distribution `h^{-1}(x)`:: | |
>>> mp.dps = 15 | |
>>> f = lambda x: sin(exp(x)) | |
>>> quadosc(f, [1,inf], zeros=lambda n: log(n)) | |
-0.25024394235267 | |
>>> pi/2-si(e) | |
-0.250243942352671 | |
**Non-alternating functions** | |
If the integrand oscillates around a positive value, without | |
alternating signs, the extrapolation might fail. A simple trick | |
that sometimes works is to multiply or divide the frequency by 2:: | |
>>> f = lambda x: 1/x**2+sin(x)/x**4 | |
>>> quadosc(f, [1,inf], omega=1) # Bad | |
1.28642190869861 | |
>>> quadosc(f, [1,inf], omega=0.5) # Perfect | |
1.28652953559617 | |
>>> 1+(cos(1)+ci(1)+sin(1))/6 | |
1.28652953559617 | |
**Fast decay** | |
:func:`~mpmath.quadosc` is primarily useful for slowly decaying | |
integrands. If the integrand decreases exponentially or faster, | |
:func:`~mpmath.quad` will likely handle it without trouble (and generally be | |
much faster than :func:`~mpmath.quadosc`):: | |
>>> quadosc(lambda x: cos(x)/exp(x), [0, inf], omega=1) | |
0.5 | |
>>> quad(lambda x: cos(x)/exp(x), [0, inf]) | |
0.5 | |
""" | |
a, b = ctx._as_points(interval) | |
a = ctx.convert(a) | |
b = ctx.convert(b) | |
if [omega, period, zeros].count(None) != 2: | |
raise ValueError( \ | |
"must specify exactly one of omega, period, zeros") | |
if a == ctx.ninf and b == ctx.inf: | |
s1 = ctx.quadosc(f, [a, 0], omega=omega, zeros=zeros, period=period) | |
s2 = ctx.quadosc(f, [0, b], omega=omega, zeros=zeros, period=period) | |
return s1 + s2 | |
if a == ctx.ninf: | |
if zeros: | |
return ctx.quadosc(lambda x:f(-x), [-b,-a], lambda n: zeros(-n)) | |
else: | |
return ctx.quadosc(lambda x:f(-x), [-b,-a], omega=omega, period=period) | |
if b != ctx.inf: | |
raise ValueError("quadosc requires an infinite integration interval") | |
if not zeros: | |
if omega: | |
period = 2*ctx.pi/omega | |
zeros = lambda n: n*period/2 | |
#for n in range(1,10): | |
# p = zeros(n) | |
# if p > a: | |
# break | |
#if n >= 9: | |
# raise ValueError("zeros do not appear to be correctly indexed") | |
n = 1 | |
s = ctx.quadgl(f, [a, zeros(n)]) | |
def term(k): | |
return ctx.quadgl(f, [zeros(k), zeros(k+1)]) | |
s += ctx.nsum(term, [n, ctx.inf]) | |
return s | |
def quadsubdiv(ctx, f, interval, tol=None, maxintervals=None, **kwargs): | |
""" | |
Computes the integral of *f* over the interval or path specified | |
by *interval*, using :func:`~mpmath.quad` together with adaptive | |
subdivision of the interval. | |
This function gives an accurate answer for some integrals where | |
:func:`~mpmath.quad` fails:: | |
>>> from mpmath import * | |
>>> mp.dps = 15; mp.pretty = True | |
>>> quad(lambda x: abs(sin(x)), [0, 2*pi]) | |
3.99900894176779 | |
>>> quadsubdiv(lambda x: abs(sin(x)), [0, 2*pi]) | |
4.0 | |
>>> quadsubdiv(sin, [0, 1000]) | |
0.437620923709297 | |
>>> quadsubdiv(lambda x: 1/(1+x**2), [-100, 100]) | |
3.12159332021646 | |
>>> quadsubdiv(lambda x: ceil(x), [0, 100]) | |
5050.0 | |
>>> quadsubdiv(lambda x: sin(x+exp(x)), [0,8]) | |
0.347400172657248 | |
The argument *maxintervals* can be set to limit the permissible | |
subdivision:: | |
>>> quadsubdiv(lambda x: sin(x**2), [0,100], maxintervals=5, error=True) | |
(-5.40487904307774, 5.011) | |
>>> quadsubdiv(lambda x: sin(x**2), [0,100], maxintervals=100, error=True) | |
(0.631417921866934, 1.10101120134116e-17) | |
Subdivision does not guarantee a correct answer since, the error | |
estimate on subintervals may be inaccurate:: | |
>>> quadsubdiv(lambda x: sech(10*x-2)**2 + sech(100*x-40)**4 + sech(1000*x-600)**6, [0,1], error=True) | |
(0.210802735500549, 1.0001111101e-17) | |
>>> mp.dps = 20 | |
>>> quadsubdiv(lambda x: sech(10*x-2)**2 + sech(100*x-40)**4 + sech(1000*x-600)**6, [0,1], error=True) | |
(0.21080273550054927738, 2.200000001e-24) | |
The second answer is correct. We can get an accurate result at lower | |
precision by forcing a finer initial subdivision:: | |
>>> mp.dps = 15 | |
>>> quadsubdiv(lambda x: sech(10*x-2)**2 + sech(100*x-40)**4 + sech(1000*x-600)**6, linspace(0,1,5)) | |
0.210802735500549 | |
The following integral is too oscillatory for convergence, but we can get a | |
reasonable estimate:: | |
>>> v, err = fp.quadsubdiv(lambda x: fp.sin(1/x), [0,1], error=True) | |
>>> round(v, 6), round(err, 6) | |
(0.504067, 1e-06) | |
>>> sin(1) - ci(1) | |
0.504067061906928 | |
""" | |
queue = [] | |
for i in range(len(interval)-1): | |
queue.append((interval[i], interval[i+1])) | |
total = ctx.zero | |
total_error = ctx.zero | |
if maxintervals is None: | |
maxintervals = 10 * ctx.prec | |
count = 0 | |
quad_args = kwargs.copy() | |
quad_args["verbose"] = False | |
quad_args["error"] = True | |
if tol is None: | |
tol = +ctx.eps | |
orig = ctx.prec | |
try: | |
ctx.prec += 5 | |
while queue: | |
a, b = queue.pop() | |
s, err = ctx.quad(f, [a, b], **quad_args) | |
if kwargs.get("verbose"): | |
print("subinterval", count, a, b, err) | |
if err < tol or count > maxintervals: | |
total += s | |
total_error += err | |
else: | |
count += 1 | |
if count == maxintervals and kwargs.get("verbose"): | |
print("warning: number of intervals exceeded maxintervals") | |
if a == -ctx.inf and b == ctx.inf: | |
m = 0 | |
elif a == -ctx.inf: | |
m = min(b-1, 2*b) | |
elif b == ctx.inf: | |
m = max(a+1, 2*a) | |
else: | |
m = a + (b - a) / 2 | |
queue.append((a, m)) | |
queue.append((m, b)) | |
finally: | |
ctx.prec = orig | |
if kwargs.get("error"): | |
return +total, +total_error | |
else: | |
return +total | |
if __name__ == '__main__': | |
import doctest | |
doctest.testmod() | |