Spaces:
Paused
Paused
| 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 | |
| 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 | |
| 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 | |
| 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 | |