In this section, we will consider the mathematical task of numerically solving a system of ordinary equations for given initial values:
y'(t) = f(t, y) y(t0) = y0∈ ℝn
The solution of this problem is a function y. A numerical method aims at computing good approximations, yi≈ y(ti) at discrete points, the communications points ti, within the interval of interest [t0, te]. We collect the data that describes the problem in a class, as follows:
class IV_Problem: """ Initial value problem (IVP) class """ def __init__(self, rhs, y0, interval, name='IVP'): """ rhs 'right hand side' function of the ordinary differential equation f(t,y) y0 array with initial values interval start and end value of the interval of independent variables often initial and end time name descriptive name of the problem """ self.rhs = rhs self.y0 = y0 self.t0, self.tend = interval self.name = name
The differential equation:
describes a mathematical pendulum; y1 describes its angle with respect to the vertical axis, g is the gravitation constant, and l is its length. The initial angle is π/2 and the initial angular velocity is zero.
The pendulum problem becomes an instance of the problem class, as follows:
def rhs(t,y): g = 9.81 l = 1. yprime = array([y[1], g / l * sin(y[0])]) return yprime pendulum = IV_Problem(rhs, array([pi / 2, 0.]), [0., 10.] , 'mathem. pendulum')
There might be different views on the problem at hand, leading to a different design of the class. For example, one might want to consider the interval of independent variables as a part of a solution process instead of the problem definition. The same holds when considering initial values. They might, as we did here, be considered a part of the mathematical problem, while other authors might want to allow variation of initial values by putting them as a part of the solution process.
The solution process is modeled as another class:
class IVPsolver: """ IVP solver class for explicit one-step discretization methods with constant step size """ def __init__(self, problem, discretization, stepsize): self.problem = problem self.discretization = discretization self.stepsize = stepsize def one_stepper(self): yield self.problem.t0, self.problem.y0 ys = self.problem.y0 ts = self.problem.t0 while ts <= self.problem.tend: ts, ys = self.discretization(self.problem.rhs, ts, ys, self.stepsize) yield ts, ys def solve(self): return list(self.one_stepper())
We continue by first defining two discretization schemes:
def expliciteuler(rhs, ts, ys, h): return ts + h, ys + h * rhs(ts, ys)
def rungekutta4(rhs, ts, ys, h): k1 = h * rhs(ts, ys) k2 = h * rhs(ts + h/2., ys + k1/2.) k3 = h * rhs(ts + h/2., ys + k2/2.) k4 = h * rhs(ts + h, ys + k3) return ts + h, ys + (k1 + 2*k2 + 2*k3 + k4)/6.
With these, we can create instances to obtain the corresponding discretized versions of the pendulum ODE:
pendulum_Euler = IVPsolver(pendulum, expliciteuler, 0.001) pendulum_RK4 = IVPsolver(pendulum, rungekutta4, 0.001)
We can solve the two discrete models and plot the solution and the angle difference:
sol_Euler = pendulum_Euler.solve() sol_RK4 = pendulum_RK4.solve() tEuler, yEuler = zip(*sol_Euler) tRK4, yRK4 = zip(*sol_RK4) subplot(1,2,1), plot(tEuler,yEuler), title('Pendulum result with Explicit Euler'), xlabel('Time'), ylabel('Angle and angular velocity') subplot(1,2,2), plot(tRK4,abs(array(yRK4)-array(yEuler))), title('Difference between both methods'), xlabel('Time'), ylabel('Angle and angular velocity')
Figure14.4: Pendulum simulation with the explicit Euler method and comparison with the results of the more accurate Runge–Kutta 4 method
It is worthwhile discussing alternative class designs. What should be put in separate classes, what should be bundled into the same class?
IVPsolver
class appropriate for a future extension of adaptive methods, where a tolerance rather than a step size is given?IVPsolver.onestepper
?scipy.integrate.ode
and scipy.integrate.odeint
.3.145.179.85