' $ IN229/Simulation: Lecture 3 Knut–Andreas Lie Department of Informatics University of Oslo February 2003 & % ' $ IN229/Simulation: Lecture 3 Ordinary Differential Equations (ODEs) What is an ODE? An equation relating a function to its derivatives (in such a way that the function itself can be determined). Very simple example from high school physics: Consider the motion of a body with mass m under constant force f , which is initially at rest at position x0. Newton’s second law reads f = ma = mx00(t) where x(t) is the position of the body at time t. The corresponding ODE then reads f = mx00(t), x(0) = x0, x0(0) = 0 This equation is easily integrated f 2 t. x(t) = x0 + 2m & 1 % ' $ IN229/Simulation: Lecture 3 ODEs contd. An ODE model from modern research, describing the dynamics of HIV-1 infection in vivo (Perelson& Nelson, SIAM Review 41/1, 1999) : The rate of change of uninfected cells T , productively infected cells T ∗, and virus V : dT = s + pT 1 − T /Tmax − dT T − kV T dt dT ∗ = kV T − δT ∗ dt dV = N δT ∗ − cV. dt Here: dT – death rate of uninfected cells δ – death rate of infected cells p – rate of proliferation N – virus production per infected cell c – clearance rate & 2 % & y(a) = α, a≤t≤b i = 0, . . . , N − 1 This gives an explicit formula for each y(ti+1) once y0 is known. y(ti+1) = y(ti) + hf (y(ti), ti), • apply forward differences to the equation • generate a mesh: ti = a + ih, for each i = 0, . . . , N h = (b − a)/N is called stepsize Obvious solution – use finite differences: y 0 = f (y, t), We wish to solve the equation: Numerical solution of ODEs – Euler’s method ' $ IN229/Simulation: Lecture 3 3 % & 0 2 4 6 8 10 12 14 16 18 20 0 0.1 Exact h=1/4 h=1/16 h=1/64 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1 Graphical illustration of Euler’s method: u0 = 3u ' $ IN229/Simulation: Lecture 3 4 % & y(a) = α, a≤t≤b i = 0, . . . , N − 1 This gives an equation for each y(ti+1) once y0 is known. y(ti+1) = y(ti) + hf (y(ti+1), ti+1), This time we apply backward differences and introduce a mesh: ti = a + ih, for each i = 0, . . . , N y 0 = f (y, t), Once again we consider: Another Euler method – backward Euler ' $ IN229/Simulation: Lecture 3 5 % & the new value is given by an algebraic equation y(ti+1) = y(ti) + hf (y(ti+1), ti+1) • backward Euler: implicit method the new value is given by a formula y(ti+1) = y(ti) + hf (y(ti), ti) • forward Euler: explicit method Two different methods ' $ IN229/Simulation: Lecture 3 6 % & −1 −0.8 −0.6 −0.4 −0.2 0 0.2 0.4 0.6 0.8 1 0 0.1 0.2 0.3 Exact Forward Euler Backward Euler 0.4 u(xi) = u(xi−1) − 4π sin(4πxi−1) 0.5 0.6 0.7 0.8 0.9 1 u(xi) = u(xi−1) − 4π sin(4πxi) The two Euler methods: u0 = −4π sin(4πx) u(0) = 1 ' $ IN229/Simulation: Lecture 3 7 % & error = ÿ(τ )h/2 = d f (y(τ ))h/2 dt We say that forward Euler is a first-order method. for ti ≤ τ ≤ ti+1. Now since ẏ(ti) = f (y(ti)), y(ti) + hẏ(ti) + ÿ(τ )h2/2 − y(ti) = hf (y(ti)) + h · error y(ti+1) − y(ti) = f (y(ti)) + error h Expanding y(ti+1) by a Taylor polynomial: Consider ẏ = f (y). When using forward Euler, we make an error, Truncation errors – the error we make at each point ' $ IN229/Simulation: Lecture 3 8 % & yi+1 = yi + hf (yi + hf (yi)/2) We have a new method: This means that β = hf (yi)/2. f (yi) + f 0(yi)f (yi)h/2 = f (yi + b) = f (yi) + βf 0(yi) + β 2 + . . . T aylor Assume now that error = f¨(τ )h2/6. Then f (yi)h + f˙(yi)h2/2 + f¨(τ )h3/6 = hf (yi + β) + h · error Consider the forward Euler method: yi+1 = yi + hf (yi). What if we evaluate f () at some other point? Higher order methods – Runge–Kutta ' $ IN229/Simulation: Lecture 3 9 % & For higher-order methods we use more intermediate steps. • Combine yi, w, f (yi), f (w) to get a more accurate solution yi+1. • Approximate the solution at a point ti ≤ τ ≤ ti+1 by the intermediate step w = yi + (τ − ti)f (yi) The methods are all on the form: There are other alternatives also, e.g., yi+1 = yi + (h/2) f (yi) + f (yi + hf (yi)) Runge-Kutta methods.... ' $ IN229/Simulation: Lecture 3 10 % ' $ IN229/Simulation: Lecture 3 ODE Solver Environment The purpose of this example: • OO-design for a simple problem • Continue the overview of C++ features • Principles apply to advanced simulations Mathematical problem: dyi = fi(y1, . . . , yn, t), dt yi(0) = yi0, i = 1, . . . , n • A system of ordinary differential equations (ODEs). • A plenitude of numerical methods exist. • Occurs frequently in numerics1 1 You will encounter solution of ODEs later when streamlines are taught in the visualization part. & 11 % & • TOL1, TOL2,.. are various parameters • TSTEP is the current time step • WORK1 is a work array (since runtime allocation is not allowed in F77) • F is a function defining the fi’s • T is the value of t • Y is current y Here: SUBROUTINE RK4(Y,T,F,WORK1,N,TSTEP,TOL1,TOL2,...) Typical interface in FORTRAN77: Traditional procedural solution ' $ IN229/Simulation: Lecture 3 12 % ' $ IN229/Simulation: Lecture 3 FORTRAN 77 contd. For a specific ODE: ÿ + c1(ẏ + c2ẏ|ẏ|) + c3(y + c4y 3) = sin ωt Written as a system (ẏ1 = y2, ẏ2 = ÿ) f1 = y2, f2 = −c1(y2 + c2y2|y2|) − c3(y1 + c4y13) + sin ωt Possible FORTRAN function SUBROUTINE F(YDOT,Y,T,C1,C2,C3,C4,OMEGA) Unfortunately this is problem dependent and cannot be used. Instead, SUBROUTINE F(YDOT,Y,T) with C1,C2,C3,C4,OMEGA transferred in COMMON blocks. =⇒ dangerous side effects and possible hidden bugs & 13 % ' $ IN229/Simulation: Lecture 3 OO Design Our software problem consists of • Different ODE solvers • Different problems (fi’s) Design: introduce two class hierarchies • ODESolver (e.g., forward Euler, Runge–Kutta , ..) Base class with: • initialisation of solver • virtual function advance for one time step • ODEProblem (e.g., Newton’s 2.law, HIV-1 virus, ..) Base class with: • the generic system - virtual function equation • a driver function timeLoop • common data: ∆t, T , name of solver,.. ODESolver must access ODEProblem and vice versa & 14 % & // members only visible in subclasses // definition of the ODE in user’s class // tell C++ that this class name exists Notice: protected means public access for derived classes and private access for others. public: // members visible also outside the class ODESolver (ODEProblem* eqdef_) { eqdef = eqdef_; } virtual ~ODESolver () {} // always needed, does nothing here... virtual void init() {} // initialize solver data structures virtual void advance (MyArray<double>& y, double& t, double& dt); }; class ODESolver { protected: ODEProblem* eqdef; class ODEProblem; Implementation in C++ ' $ IN229/Simulation: Lecture 3 15 % & // tell C++ that this class name exists • timeLoop can be written in base class • scan and print extended in subclasses • equation and size only in subclasses class ODEProblem { protected: ODESolver* solver; // some ODE solver MyArray<double> y, y0; // solution (y) and initial cond. (y0) double t, dt, T; // time loop parameters public: ODEProblem () {} virtual ~ODEProblem (); virtual void timeLoop (); virtual void equation (MyArray<double>& f, const MyArray<double>& y, double t); virtual int size (); // no of equations in the ODE system virtual void scan (); virtual void print (ostream& os); }; class ODESolver; The ODEProblem class ' $ IN229/Simulation: Lecture 3 16 % & This function is generic and can be coded in the base class. void ODEProblem:: timeLoop () { ofstream outfile("y.out"); t = 0; y = y0; outfile << t << " "; y.print(outfile); outfile << endl; while (t <= T) { solver->advance (y, t, dt); outfile << t << " "; y.print(outfile); outfile << endl; } } The timeLoop function ' $ IN229/Simulation: Lecture 3 17 % & f2 = −c1(y2 + c2y2|y2|) − c3(y1 + c4y13) + sin ωt We inherit y, y0, t, dt, T and functions from ODEProblem. timeLoop can be reused and does not appear explicitly class Oscillator : public ODEProblem { protected: double c1,c2,c3,c4,omega; // problem dependent paramters public: Oscillator () {} virtual void equation (MyArray<double>& f, const MyArray<double>& y, double t); virtual int size () { return 2; } // 2x2 system of ODEs virtual void scan (); virtual void print (ostream& os); }; f1 = y2, Right-hand side of ẏ(t) = f (y) One specific problem — Oscillator ' $ IN229/Simulation: Lecture 3 18 % & void Oscillator::equation(MyArray<double>& f, const MyArray<double>& y, double t) { f(1) = y(2); f(2) = -c1*(y(2)+c2*y(2)*fabs(y(2))) - c3*y(1)*(1.0+c4*y(1)*y(1)) + sin(omega*t); } Oscillator contd. ' $ IN229/Simulation: Lecture 3 19 % & void ForwardEuler::advance(MyArray<double>& y, double& t, double& dt) { eqdef->equation (scratch1, y, t); // evaluate scratch1 (as f) const int n = y.size(); for (int i = 1; i <= n; i++) y(i) += dt * scratch1(i); t += dt; } class ForwardEuler : public ODESolver { MyArray<double> scratch1; // needed in the algorithm public: ForwardEuler (ODEProblem* eqdef_); virtual void init (); // for allocating scratch1 virtual void advance (MyArray<double>& y, double& t, double& dt); }; yi`+1 = yi` + ∆tf (y1` , . . . , y `, n, tm) Forward Euler is the simplest possible scheme A specific solver — ForwardEuler ' $ IN229/Simulation: Lecture 3 20 % & ∆t f (y ` , tm ) 2 ∆t `+1/4 = yi + f (y `+1/4 , tm + ∆t/2) 2 ∆t = yi` + ∆tf (y `+2/4 , tm + ) 2 ∆t h ` ∆t ` = yi + f (y , tm ) + 2f (y `+1/4 , tm + ) 6 2 i ∆t `+2/4 `+3/4 +2f (y , tm + ) + f (y , tm + ∆t) 2 = yi` + class RungeKutta4 : public ODESolver { MyArray<double> scratch1, scratch2, scratch3; // needed in algorithm public: RungeKutta4 (ODEProblem* eqdef_); virtual void init (); virtual void advance (MyArray<double>& y, double& t, double& dt); }; yi`+1 yi `+3/4 yi `+2/4 `+1/4 yi Another solver – RungeKutta4 ' $ IN229/Simulation: Lecture 3 21 % & } for (i = 1; i <= n; i++) y(i) = y(i) + dt6*(scratch1(i) + scratch2(i) + 2*scratch3(i)); t += dt; void RungeKutta4:: advance (MyArray<double>& y, double& t, double& dt) { const double dt2 = 0.5*dt; const double dt6 = dt/6.0; const int n = y.size(); eqdef->equation (scratch1, y, t); int i; for (i = 1; i <= n; i++) scratch2(i) = y(i) + dt2 * scratch1(i); eqdef->equation (scratch1, scratch2, t+dt2); for (i = 1; i <= n; i++) scratch2(i) = y(i) + dt2 * scratch1(i); eqdef->equation (scratch3, scratch2, t+dt2); for (i = 1; i <= n; i++) { scratch2(i) = y(i) + dt * scratch3(i); scratch3(i) = scratch1(i) + scratch3(i); } eqdef->equation (scratch1, scratch2, t+dt); eqdef->equation (scratch2, y, t); RungeKutta4 contd. ' $ IN229/Simulation: Lecture 3 22 % & Oscillator ODEProblem ODESolver The class hierarchy ........ RungeKutta4A RungeKutta4 RungeKutta2 ForwardEuler ' $ IN229/Simulation: Lecture 3 23 % & // name of subclass in ODESolver hierarchy // pointer to user’s problem class // create correct subclass of ODESolver ODESolver* ODESolver_prm::create () { ODESolver* ptr = NULL; if (strcmp(method, "ForwardEuler") == 0) ptr = new ForwardEuler (problem); else if (strcmp(method, "RungeKutta4") == 0) ptr = new RungeKutta4 (problem); else { cout << "\n\nODESolver_prm::create:\n\t" << "Method " << method << " is not available\n\\n"; exit(1); } return ptr; } class ODESolver_prm { public: char method[30]; ODEProblem* problem; ODESolver* create (); }; How to connect to initialize? ' $ IN229/Simulation: Lecture 3 24 % & } ODESolver_prm solver_prm; cout << "Give name of ODE solver: "; cin >> solver_prm.method; solver_prm.problem = this; solver = solver_prm.create(); solver->init(); // more reading in user’s subclass cout << "Give time step: "; cin >> dt; cout << "Give final time T: "; cin >> T; cout << "Give " << n << " initial conditions: "; // y0.scan(cin); int i; for(i=1; (i<=n) && (cin>>y0(i)); i++); cout << "Read " << i-1 << "elements"; y0.print(cout); cout<<endl; void ODEProblem:: scan () { const int n = size(); // call size in actual subclass y.redim(n); y0.redim(n); Initialization of ODEProblem ' $ IN229/Simulation: Lecture 3 25 % & void Oscillator:: scan () { // first we need to do everything that ODEProblem::scan does: ODEProblem::scan(); // additional reading here: cout << "Give c1, c2, c3, c4, and omega: "; cin >> c1 >> c2 >> c3 >> c4 >> omega; print(cout); // convenient check for the user } Initialization of a specific problem ' $ IN229/Simulation: Lecture 3 26 % & • faster start for the next ODE we wish to solve.... • introduction to general code design principles • a flexible ODE library Conclusion: int main (int argc, const char* argv[]) { Oscillator problem; problem.scan(); // read input data and initialize problem.timeLoop(); // solve problem } #include "Oscillator.h" ... and finally the main program ' $ IN229/Simulation: Lecture 3 27 % & $(OBJ) $(CXX) $(CXXFLAGS) $(OBJ) -o $@ main.o ForwardEuler.o: ForwardEuler.h ODESolver.h MyArray.h : : # DO NOT DELETE THIS LINE -- make depend depends on it. depend :; makedepend -f Makefile -- *.C clean:; rm -f $(OBJ) $(EXE) .PHONY: clean depend all $(EXE): all: depend $(EXE) EXE = myprog OBJ = ForwardEuler.o ODEProblem.o ODESolver.o \ ODESolver_prm.o RungeKutta4.o Oscillator.o CXXFLAGS = -Wall -g CXX = g++ And while we are at it - Makefile ' $ IN229/Simulation: Lecture 3 28 % & # # # # # y0 dt final time method c1 c2 c3 c4 omega y(0) = 0, ẏ(0) = 1 And run the program: myprog < data.in The comments “# ...” will not work with the current implementation. 0.0 1.0 0.001 30 RungeKutta4 0 0 1 0 0.5 Create an input file: ÿ + y = sin(0.5t), Consider the following equation: An example ' $ IN229/Simulation: Lecture 3 29 % & −1.5 −1 −0.5 0 0.5 1 1.5 0 5 y1=y y2=dy/dt 10 15 20 Plot of the solution 25 30 ' $ IN229/Simulation: Lecture 3 30 %