Spaces:
Paused
Paused
| from bisect import bisect | |
| from ..libmp.backend import xrange | |
| class ODEMethods(object): | |
| pass | |
| def ode_taylor(ctx, derivs, x0, y0, tol_prec, n): | |
| h = tol = ctx.ldexp(1, -tol_prec) | |
| dim = len(y0) | |
| xs = [x0] | |
| ys = [y0] | |
| x = x0 | |
| y = y0 | |
| orig = ctx.prec | |
| try: | |
| ctx.prec = orig*(1+n) | |
| # Use n steps with Euler's method to get | |
| # evaluation points for derivatives | |
| for i in range(n): | |
| fxy = derivs(x, y) | |
| y = [y[i]+h*fxy[i] for i in xrange(len(y))] | |
| x += h | |
| xs.append(x) | |
| ys.append(y) | |
| # Compute derivatives | |
| ser = [[] for d in range(dim)] | |
| for j in range(n+1): | |
| s = [0]*dim | |
| b = (-1) ** (j & 1) | |
| k = 1 | |
| for i in range(j+1): | |
| for d in range(dim): | |
| s[d] += b * ys[i][d] | |
| b = (b * (j-k+1)) // (-k) | |
| k += 1 | |
| scale = h**(-j) / ctx.fac(j) | |
| for d in range(dim): | |
| s[d] = s[d] * scale | |
| ser[d].append(s[d]) | |
| finally: | |
| ctx.prec = orig | |
| # Estimate radius for which we can get full accuracy. | |
| # XXX: do this right for zeros | |
| radius = ctx.one | |
| for ts in ser: | |
| if ts[-1]: | |
| radius = min(radius, ctx.nthroot(tol/abs(ts[-1]), n)) | |
| radius /= 2 # XXX | |
| return ser, x0+radius | |
| def odefun(ctx, F, x0, y0, tol=None, degree=None, method='taylor', verbose=False): | |
| r""" | |
| Returns a function `y(x) = [y_0(x), y_1(x), \ldots, y_n(x)]` | |
| that is a numerical solution of the `n+1`-dimensional first-order | |
| ordinary differential equation (ODE) system | |
| .. math :: | |
| y_0'(x) = F_0(x, [y_0(x), y_1(x), \ldots, y_n(x)]) | |
| y_1'(x) = F_1(x, [y_0(x), y_1(x), \ldots, y_n(x)]) | |
| \vdots | |
| y_n'(x) = F_n(x, [y_0(x), y_1(x), \ldots, y_n(x)]) | |
| The derivatives are specified by the vector-valued function | |
| *F* that evaluates | |
| `[y_0', \ldots, y_n'] = F(x, [y_0, \ldots, y_n])`. | |
| The initial point `x_0` is specified by the scalar argument *x0*, | |
| and the initial value `y(x_0) = [y_0(x_0), \ldots, y_n(x_0)]` is | |
| specified by the vector argument *y0*. | |
| For convenience, if the system is one-dimensional, you may optionally | |
| provide just a scalar value for *y0*. In this case, *F* should accept | |
| a scalar *y* argument and return a scalar. The solution function | |
| *y* will return scalar values instead of length-1 vectors. | |
| Evaluation of the solution function `y(x)` is permitted | |
| for any `x \ge x_0`. | |
| A high-order ODE can be solved by transforming it into first-order | |
| vector form. This transformation is described in standard texts | |
| on ODEs. Examples will also be given below. | |
| **Options, speed and accuracy** | |
| By default, :func:`~mpmath.odefun` uses a high-order Taylor series | |
| method. For reasonably well-behaved problems, the solution will | |
| be fully accurate to within the working precision. Note that | |
| *F* must be possible to evaluate to very high precision | |
| for the generation of Taylor series to work. | |
| To get a faster but less accurate solution, you can set a large | |
| value for *tol* (which defaults roughly to *eps*). If you just | |
| want to plot the solution or perform a basic simulation, | |
| *tol = 0.01* is likely sufficient. | |
| The *degree* argument controls the degree of the solver (with | |
| *method='taylor'*, this is the degree of the Taylor series | |
| expansion). A higher degree means that a longer step can be taken | |
| before a new local solution must be generated from *F*, | |
| meaning that fewer steps are required to get from `x_0` to a given | |
| `x_1`. On the other hand, a higher degree also means that each | |
| local solution becomes more expensive (i.e., more evaluations of | |
| *F* are required per step, and at higher precision). | |
| The optimal setting therefore involves a tradeoff. Generally, | |
| decreasing the *degree* for Taylor series is likely to give faster | |
| solution at low precision, while increasing is likely to be better | |
| at higher precision. | |
| The function | |
| object returned by :func:`~mpmath.odefun` caches the solutions at all step | |
| points and uses polynomial interpolation between step points. | |
| Therefore, once `y(x_1)` has been evaluated for some `x_1`, | |
| `y(x)` can be evaluated very quickly for any `x_0 \le x \le x_1`. | |
| and continuing the evaluation up to `x_2 > x_1` is also fast. | |
| **Examples of first-order ODEs** | |
| We will solve the standard test problem `y'(x) = y(x), y(0) = 1` | |
| which has explicit solution `y(x) = \exp(x)`:: | |
| >>> from mpmath import * | |
| >>> mp.dps = 15; mp.pretty = True | |
| >>> f = odefun(lambda x, y: y, 0, 1) | |
| >>> for x in [0, 1, 2.5]: | |
| ... print((f(x), exp(x))) | |
| ... | |
| (1.0, 1.0) | |
| (2.71828182845905, 2.71828182845905) | |
| (12.1824939607035, 12.1824939607035) | |
| The solution with high precision:: | |
| >>> mp.dps = 50 | |
| >>> f = odefun(lambda x, y: y, 0, 1) | |
| >>> f(1) | |
| 2.7182818284590452353602874713526624977572470937 | |
| >>> exp(1) | |
| 2.7182818284590452353602874713526624977572470937 | |
| Using the more general vectorized form, the test problem | |
| can be input as (note that *f* returns a 1-element vector):: | |
| >>> mp.dps = 15 | |
| >>> f = odefun(lambda x, y: [y[0]], 0, [1]) | |
| >>> f(1) | |
| [2.71828182845905] | |
| :func:`~mpmath.odefun` can solve nonlinear ODEs, which are generally | |
| impossible (and at best difficult) to solve analytically. As | |
| an example of a nonlinear ODE, we will solve `y'(x) = x \sin(y(x))` | |
| for `y(0) = \pi/2`. An exact solution happens to be known | |
| for this problem, and is given by | |
| `y(x) = 2 \tan^{-1}\left(\exp\left(x^2/2\right)\right)`:: | |
| >>> f = odefun(lambda x, y: x*sin(y), 0, pi/2) | |
| >>> for x in [2, 5, 10]: | |
| ... print((f(x), 2*atan(exp(mpf(x)**2/2)))) | |
| ... | |
| (2.87255666284091, 2.87255666284091) | |
| (3.14158520028345, 3.14158520028345) | |
| (3.14159265358979, 3.14159265358979) | |
| If `F` is independent of `y`, an ODE can be solved using direct | |
| integration. We can therefore obtain a reference solution with | |
| :func:`~mpmath.quad`:: | |
| >>> f = lambda x: (1+x**2)/(1+x**3) | |
| >>> g = odefun(lambda x, y: f(x), pi, 0) | |
| >>> g(2*pi) | |
| 0.72128263801696 | |
| >>> quad(f, [pi, 2*pi]) | |
| 0.72128263801696 | |
| **Examples of second-order ODEs** | |
| We will solve the harmonic oscillator equation `y''(x) + y(x) = 0`. | |
| To do this, we introduce the helper functions `y_0 = y, y_1 = y_0'` | |
| whereby the original equation can be written as `y_1' + y_0' = 0`. Put | |
| together, we get the first-order, two-dimensional vector ODE | |
| .. math :: | |
| \begin{cases} | |
| y_0' = y_1 \\ | |
| y_1' = -y_0 | |
| \end{cases} | |
| To get a well-defined IVP, we need two initial values. With | |
| `y(0) = y_0(0) = 1` and `-y'(0) = y_1(0) = 0`, the problem will of | |
| course be solved by `y(x) = y_0(x) = \cos(x)` and | |
| `-y'(x) = y_1(x) = \sin(x)`. We check this:: | |
| >>> f = odefun(lambda x, y: [-y[1], y[0]], 0, [1, 0]) | |
| >>> for x in [0, 1, 2.5, 10]: | |
| ... nprint(f(x), 15) | |
| ... nprint([cos(x), sin(x)], 15) | |
| ... print("---") | |
| ... | |
| [1.0, 0.0] | |
| [1.0, 0.0] | |
| --- | |
| [0.54030230586814, 0.841470984807897] | |
| [0.54030230586814, 0.841470984807897] | |
| --- | |
| [-0.801143615546934, 0.598472144103957] | |
| [-0.801143615546934, 0.598472144103957] | |
| --- | |
| [-0.839071529076452, -0.54402111088937] | |
| [-0.839071529076452, -0.54402111088937] | |
| --- | |
| Note that we get both the sine and the cosine solutions | |
| simultaneously. | |
| **TODO** | |
| * Better automatic choice of degree and step size | |
| * Make determination of Taylor series convergence radius | |
| more robust | |
| * Allow solution for `x < x_0` | |
| * Allow solution for complex `x` | |
| * Test for difficult (ill-conditioned) problems | |
| * Implement Runge-Kutta and other algorithms | |
| """ | |
| if tol: | |
| tol_prec = int(-ctx.log(tol, 2))+10 | |
| else: | |
| tol_prec = ctx.prec+10 | |
| degree = degree or (3 + int(3*ctx.dps/2.)) | |
| workprec = ctx.prec + 40 | |
| try: | |
| len(y0) | |
| return_vector = True | |
| except TypeError: | |
| F_ = F | |
| F = lambda x, y: [F_(x, y[0])] | |
| y0 = [y0] | |
| return_vector = False | |
| ser, xb = ode_taylor(ctx, F, x0, y0, tol_prec, degree) | |
| series_boundaries = [x0, xb] | |
| series_data = [(ser, x0, xb)] | |
| # We will be working with vectors of Taylor series | |
| def mpolyval(ser, a): | |
| return [ctx.polyval(s[::-1], a) for s in ser] | |
| # Find nearest expansion point; compute if necessary | |
| def get_series(x): | |
| if x < x0: | |
| raise ValueError | |
| n = bisect(series_boundaries, x) | |
| if n < len(series_boundaries): | |
| return series_data[n-1] | |
| while 1: | |
| ser, xa, xb = series_data[-1] | |
| if verbose: | |
| print("Computing Taylor series for [%f, %f]" % (xa, xb)) | |
| y = mpolyval(ser, xb-xa) | |
| xa = xb | |
| ser, xb = ode_taylor(ctx, F, xb, y, tol_prec, degree) | |
| series_boundaries.append(xb) | |
| series_data.append((ser, xa, xb)) | |
| if x <= xb: | |
| return series_data[-1] | |
| # Evaluation function | |
| def interpolant(x): | |
| x = ctx.convert(x) | |
| orig = ctx.prec | |
| try: | |
| ctx.prec = workprec | |
| ser, xa, xb = get_series(x) | |
| y = mpolyval(ser, x-xa) | |
| finally: | |
| ctx.prec = orig | |
| if return_vector: | |
| return [+yk for yk in y] | |
| else: | |
| return +y[0] | |
| return interpolant | |
| ODEMethods.odefun = odefun | |
| if __name__ == "__main__": | |
| import doctest | |
| doctest.testmod() | |