123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- from ..libmp.backend import xrange
- from .calculus import defun
- #----------------------------------------------------------------------------#
- # Approximation methods #
- #----------------------------------------------------------------------------#
- # The Chebyshev approximation formula is given at:
- # http://mathworld.wolfram.com/ChebyshevApproximationFormula.html
- # The only major changes in the following code is that we return the
- # expanded polynomial coefficients instead of Chebyshev coefficients,
- # and that we automatically transform [a,b] -> [-1,1] and back
- # for convenience.
- # Coefficient in Chebyshev approximation
- def chebcoeff(ctx,f,a,b,j,N):
- s = ctx.mpf(0)
- h = ctx.mpf(0.5)
- for k in range(1, N+1):
- t = ctx.cospi((k-h)/N)
- s += f(t*(b-a)*h + (b+a)*h) * ctx.cospi(j*(k-h)/N)
- return 2*s/N
- # Generate Chebyshev polynomials T_n(ax+b) in expanded form
- def chebT(ctx, a=1, b=0):
- Tb = [1]
- yield Tb
- Ta = [b, a]
- while 1:
- yield Ta
- # Recurrence: T[n+1](ax+b) = 2*(ax+b)*T[n](ax+b) - T[n-1](ax+b)
- Tmp = [0] + [2*a*t for t in Ta]
- for i, c in enumerate(Ta): Tmp[i] += 2*b*c
- for i, c in enumerate(Tb): Tmp[i] -= c
- Ta, Tb = Tmp, Ta
- @defun
- def chebyfit(ctx, f, interval, N, error=False):
- r"""
- Computes a polynomial of degree `N-1` that approximates the
- given function `f` on the interval `[a, b]`. With ``error=True``,
- :func:`~mpmath.chebyfit` also returns an accurate estimate of the
- maximum absolute error; that is, the maximum value of
- `|f(x) - P(x)|` for `x \in [a, b]`.
- :func:`~mpmath.chebyfit` uses the Chebyshev approximation formula,
- which gives a nearly optimal solution: that is, the maximum
- error of the approximating polynomial is very close to
- the smallest possible for any polynomial of the same degree.
- Chebyshev approximation is very useful if one needs repeated
- evaluation of an expensive function, such as function defined
- implicitly by an integral or a differential equation. (For
- example, it could be used to turn a slow mpmath function
- into a fast machine-precision version of the same.)
- **Examples**
- Here we use :func:`~mpmath.chebyfit` to generate a low-degree approximation
- of `f(x) = \cos(x)`, valid on the interval `[1, 2]`::
- >>> from mpmath import *
- >>> mp.dps = 15; mp.pretty = True
- >>> poly, err = chebyfit(cos, [1, 2], 5, error=True)
- >>> nprint(poly)
- [0.00291682, 0.146166, -0.732491, 0.174141, 0.949553]
- >>> nprint(err, 12)
- 1.61351758081e-5
- The polynomial can be evaluated using ``polyval``::
- >>> nprint(polyval(poly, 1.6), 12)
- -0.0291858904138
- >>> nprint(cos(1.6), 12)
- -0.0291995223013
- Sampling the true error at 1000 points shows that the error
- estimate generated by ``chebyfit`` is remarkably good::
- >>> error = lambda x: abs(cos(x) - polyval(poly, x))
- >>> nprint(max([error(1+n/1000.) for n in range(1000)]), 12)
- 1.61349954245e-5
- **Choice of degree**
- The degree `N` can be set arbitrarily high, to obtain an
- arbitrarily good approximation. As a rule of thumb, an
- `N`-term Chebyshev approximation is good to `N/(b-a)` decimal
- places on a unit interval (although this depends on how
- well-behaved `f` is). The cost grows accordingly: ``chebyfit``
- evaluates the function `(N^2)/2` times to compute the
- coefficients and an additional `N` times to estimate the error.
- **Possible issues**
- One should be careful to use a sufficiently high working
- precision both when calling ``chebyfit`` and when evaluating
- the resulting polynomial, as the polynomial is sometimes
- ill-conditioned. It is for example difficult to reach
- 15-digit accuracy when evaluating the polynomial using
- machine precision floats, no matter the theoretical
- accuracy of the polynomial. (The option to return the
- coefficients in Chebyshev form should be made available
- in the future.)
- It is important to note the Chebyshev approximation works
- poorly if `f` is not smooth. A function containing singularities,
- rapid oscillation, etc can be approximated more effectively by
- multiplying it by a weight function that cancels out the
- nonsmooth features, or by dividing the interval into several
- segments.
- """
- a, b = ctx._as_points(interval)
- orig = ctx.prec
- try:
- ctx.prec = orig + int(N**0.5) + 20
- c = [chebcoeff(ctx,f,a,b,k,N) for k in range(N)]
- d = [ctx.zero] * N
- d[0] = -c[0]/2
- h = ctx.mpf(0.5)
- T = chebT(ctx, ctx.mpf(2)/(b-a), ctx.mpf(-1)*(b+a)/(b-a))
- for (k, Tk) in zip(range(N), T):
- for i in range(len(Tk)):
- d[i] += c[k]*Tk[i]
- d = d[::-1]
- # Estimate maximum error
- err = ctx.zero
- for k in range(N):
- x = ctx.cos(ctx.pi*k/N) * (b-a)*h + (b+a)*h
- err = max(err, abs(f(x) - ctx.polyval(d, x)))
- finally:
- ctx.prec = orig
- if error:
- return d, +err
- else:
- return d
- @defun
- def fourier(ctx, f, interval, N):
- r"""
- Computes the Fourier series of degree `N` of the given function
- on the interval `[a, b]`. More precisely, :func:`~mpmath.fourier` returns
- two lists `(c, s)` of coefficients (the cosine series and sine
- series, respectively), such that
- .. math ::
- f(x) \sim \sum_{k=0}^N
- c_k \cos(k m x) + s_k \sin(k m x)
- where `m = 2 \pi / (b-a)`.
- Note that many texts define the first coefficient as `2 c_0` instead
- of `c_0`. The easiest way to evaluate the computed series correctly
- is to pass it to :func:`~mpmath.fourierval`.
- **Examples**
- The function `f(x) = x` has a simple Fourier series on the standard
- interval `[-\pi, \pi]`. The cosine coefficients are all zero (because
- the function has odd symmetry), and the sine coefficients are
- rational numbers::
- >>> from mpmath import *
- >>> mp.dps = 15; mp.pretty = True
- >>> c, s = fourier(lambda x: x, [-pi, pi], 5)
- >>> nprint(c)
- [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
- >>> nprint(s)
- [0.0, 2.0, -1.0, 0.666667, -0.5, 0.4]
- This computes a Fourier series of a nonsymmetric function on
- a nonstandard interval::
- >>> I = [-1, 1.5]
- >>> f = lambda x: x**2 - 4*x + 1
- >>> cs = fourier(f, I, 4)
- >>> nprint(cs[0])
- [0.583333, 1.12479, -1.27552, 0.904708, -0.441296]
- >>> nprint(cs[1])
- [0.0, -2.6255, 0.580905, 0.219974, -0.540057]
- It is instructive to plot a function along with its truncated
- Fourier series::
- >>> plot([f, lambda x: fourierval(cs, I, x)], I) #doctest: +SKIP
- Fourier series generally converge slowly (and may not converge
- pointwise). For example, if `f(x) = \cosh(x)`, a 10-term Fourier
- series gives an `L^2` error corresponding to 2-digit accuracy::
- >>> I = [-1, 1]
- >>> cs = fourier(cosh, I, 9)
- >>> g = lambda x: (cosh(x) - fourierval(cs, I, x))**2
- >>> nprint(sqrt(quad(g, I)))
- 0.00467963
- :func:`~mpmath.fourier` uses numerical quadrature. For nonsmooth functions,
- the accuracy (and speed) can be improved by including all singular
- points in the interval specification::
- >>> nprint(fourier(abs, [-1, 1], 0), 10)
- ([0.5000441648], [0.0])
- >>> nprint(fourier(abs, [-1, 0, 1], 0), 10)
- ([0.5], [0.0])
- """
- interval = ctx._as_points(interval)
- a = interval[0]
- b = interval[-1]
- L = b-a
- cos_series = []
- sin_series = []
- cutoff = ctx.eps*10
- for n in xrange(N+1):
- m = 2*n*ctx.pi/L
- an = 2*ctx.quadgl(lambda t: f(t)*ctx.cos(m*t), interval)/L
- bn = 2*ctx.quadgl(lambda t: f(t)*ctx.sin(m*t), interval)/L
- if n == 0:
- an /= 2
- if abs(an) < cutoff: an = ctx.zero
- if abs(bn) < cutoff: bn = ctx.zero
- cos_series.append(an)
- sin_series.append(bn)
- return cos_series, sin_series
- @defun
- def fourierval(ctx, series, interval, x):
- """
- Evaluates a Fourier series (in the format computed by
- by :func:`~mpmath.fourier` for the given interval) at the point `x`.
- The series should be a pair `(c, s)` where `c` is the
- cosine series and `s` is the sine series. The two lists
- need not have the same length.
- """
- cs, ss = series
- ab = ctx._as_points(interval)
- a = interval[0]
- b = interval[-1]
- m = 2*ctx.pi/(ab[-1]-ab[0])
- s = ctx.zero
- s += ctx.fsum(cs[n]*ctx.cos(m*n*x) for n in xrange(len(cs)) if cs[n])
- s += ctx.fsum(ss[n]*ctx.sin(m*n*x) for n in xrange(len(ss)) if ss[n])
- return s
|