De Solve
De Solve
De Solve
Karline Soetaert
Royal Netherlands Institute of Sea Research (NIOZ) Yerseke, The Netherlands
Thomas Petzoldt
Technische Universit at Dresden Germany
R. Woodrow Setzer
National Center for Computational Toxicology US Environmental Protection Agency
Abstract R package deSolve (Soetaert, Petzoldt, and Setzer 2010b,c) the successor of R package odesolve is a package to solve initial value problems (IVP) of: ordinary dierential equations (ODE), dierential algebraic equations (DAE), partial dierential equations (PDE) and delay dierential equations (DeDE). The implementation includes sti and nonsti integration routines based on the ODEPACK FORTRAN codes (Hindmarsh 1983). It also includes xed and adaptive timestep explicit Runge-Kutta solvers and the Euler method (Press, Teukolsky, Vetterling, and Flannery 1992), and the implicit Runge-Kutta method RADAU (Hairer and Wanner 2010). In this vignette we outline how to implement dierential equations as R -functions. Another vignette (compiledCode) (Soetaert, Petzoldt, and Setzer 2008), deals with differential equations implemented in lower-level languages such as FORTRAN, C, or C++, which are compiled into a dynamically linked library (DLL) and loaded into R (R Development Core Team 2008). Note that another package, bvpSolve provides methods to solve boundary value problems (Soetaert, Cash, and Mazzia 2010a).
Keywords : dierential equations, ordinary dierential equations, dierential algebraic equations, partial dierential equations, initial value problems, R.
dX =aX +Y Z dt dY = b (Y Z ) dt dZ = X Y + c Y Z dt with the initial conditions: X (0) = Y (0) = Z (0) = 1 Where a, b and c are three parameters, with values of -8/3, -10 and 28 respectively. Implementation of an IVP ODE in R can be separated in two parts: the model specication and the model application. Model specication consists of: Dening model parameters and their values, Dening model state variables and their initial conditions, Implementing the model equations that calculate the rate of change (e.g. dX/dt) of the state variables. The model application consists of: Specication of the time at which model output is wanted, Integration of the model equations (uses R-functions from deSolve), Plotting of model results. Below, we discuss the R-code for the Lorenz model.
State variables
The three state variables are also created as a vector, and their initial values given:
Karline Soetaert, Thomas Petzoldt, R. Woodrow Setzer > state <- c(X = 1, + Y = 1, + Z = 1)
Model equations
The model equations are specied in a function (Lorenz) that calculates the rate of change of the state variables. Input to the function is the model time (t, not used here, but required by the calling routine), and the values of the state variables (state) and the parameters, in that order. This function will be called by the R routine that solves the dierential equations (here we use ode, see below). The code is most readable if we can address the parameters and state variables by their names. As both parameters and state variables are vectors, they are converted into a list. The statement with(as.list(c(state, parameters)), ...) then makes available the names of this list. The main part of the model calculates the rate of change of the state variables. At the end of the function, these rates of change are returned, packed as a list. Note that it is necessary to return the rate of change in the same ordering as the specication of the state variables. This is very important. In this case, as state variables are specied X rst, then Y and Z , the rates of changes are returned as dX, dY, dZ . > Lorenz<-function(t, state, parameters) { + with(as.list(c(state, parameters)),{ + # rate of change + dX <- a*X + Y*Z + dY <- b * (Y-Z) + dZ <- -X*Y + c*Y - Z + + # return the rate of change + list(c(dX, dY, dZ)) + }) # end with(as.list ... + }
Model integration
The model is solved using deSolve function ode, which is the default integration routine. Function ode takes as input, a.o. the state variable vector (y), the times at which output is
required (times), the model function that returns the rate of change (func) and the parameter vector (parms). Function ode returns an object of class deSolve with a matrix that contains the values of the state variables (columns) at the requested output times. > library(deSolve) > out <- ode(y = state, times = times, func = Lorenz, parms = parameters) > head(out) time 0.00 0.01 0.02 0.03 0.04 0.05 X 1.0000000 0.9848912 0.9731148 0.9651593 0.9617377 0.9638068 Y 1.000000 1.012567 1.048823 1.107207 1.186866 1.287555 Z 1.000000 1.259918 1.523999 1.798314 2.088545 2.400161
Plotting results
Finally, the model output is plotted. We use the plot method designed for objects of class deSolve, which will neatly arrange the gures in two rows and two columns; before plotting, the size of the outer upper margin (the third margin) is increased (oma), such as to allow writing a gure heading (mtext). First all model variables are plotted versus time, and nally Z versus X: > > > > par(oma = c(0, 0, 3, 0)) plot(out, xlab = "time", ylab = "-") plot(out[, "X"], out[, "Z"], pch = ".") mtext(outer = TRUE, side = 3, "Lorenz model", cex = 1.5)
Lorenz model
X
20
40
30
20 0 10 0 20 40 time 60 80 100
10 0
10
20
40 time
60
80
100
20
10
10
20
20 0
10
10
20
10
20
30
40
out[, "X"]
Figure 1: Solution of the ordinary dierential equation - see text for R-code
> print(system.time(out2 <- lsode (state, times, Lorenz, parameters))) user 1.2 system elapsed 0.0 1.2 <- lsoda (state, times, Lorenz, parameters)))
> print(system.time(out
1
except zvode, the solver used for systems containing complex numbers.
Karline Soetaert, Thomas Petzoldt, R. Woodrow Setzer user 1.09 system elapsed 0.00 1.09 <- daspk (state, times, Lorenz, parameters)))
<- vode
This list also contains implicit Runge-Kuttas (irk..), but they are not yet optimally coded. The only well-implemented implicit Runge-Kutta is the radau method (Hairer and Wanner 2010) that will be discussed in the section dealing with dierential algebraic equations. The properties of a Runge-Kutta method can be displayed as follows: > rkMethod("rk23") $ID [1] "rk23" $varstep [1] TRUE $FSAL
8 [1] FALSE $A
[,1] [,2] [,3] [1,] 0.0 0 0 [2,] 0.5 0 0 [3,] -1.0 2 0 $b1 [1] 0 1 0 $b2 [1] 0.1666667 0.6666667 0.1666667 $c [1] 0.0 0.5 2.0 $stage [1] 3 $Qerr [1] 2 attr(,"class") [1] "list" "rkMethod" Here varstep informs whether the method uses a variable time-step; FSAL whether the rst same as last strategy is used, while stage and Qerr give the number of function evaluations needed for one step, and the order of the local truncation error. A, b1, b2, c are the coecients of the Butcher table. Two formulae (rk45dp7, rk45ck) support dense output. It is also possible to modify the parameters of a method (be very careful with this) or dene and use a new Runge-Kutta method: > func <- function(t, x, parms) { + with(as.list(c(parms, x)),{ + dP <- a * P - b * C * P + dC <- b * P * C - c * C + res <- c(dP, dC) + list(res) + }) + } > rKnew <- rkMethod(ID = "midpoint", + varstep = FALSE, + A = c(0, 1/2), + b1 = c(0, 1), + c = c(0, 1/2), + stage = 2,
Karline Soetaert, Thomas Petzoldt, R. Woodrow Setzer + Qerr = 1 + ) > out <- ode(y = c(P = 2, C = 1), times = 0:100, func, + parms = c(a = 0.1, b = 0.1, c = 0.1), method = rKnew) > head(out) time 0 1 2 3 4 5 P 2.000000 1.990000 1.958387 1.904734 1.830060 1.736925 C 1.000000 1.105000 1.218598 1.338250 1.460298 1.580136
10
-------------------return code (idid) = 0 Integration was successful. -------------------INTEGER values -------------------1 2 3 18 The The The The return code : 0 number of steps taken for the problem so far: 10000 number of function evaluations for the problem so far: 40001 order (or maximum order) of the method: 4
> diagnostics(out2) -------------------lsode return code -------------------return code (idid) = 2 Integration was successful. -------------------INTEGER values -------------------1 2 3 5 6 7 8 9 14 The return code : 2 The number of steps taken for the problem so far: 12778 The number of function evaluations for the problem so far: 16633 The method order last used (successfully): 5 The order of the method to be attempted on the next step: 5 If return flag =-4,-5: the largest component in error vector 0 The length of the real work array actually required: 58 The length of the integer work array actually required: 23 The number of Jacobian evaluations and LU decompositions so far: 721
-------------------RSTATE values -------------------1 2 3 4 The step size in t last used (successfully): 0.01 The step size to be attempted on the next step: 0.01 The current value of the independent variable which the solver has reached: 100.0072 Tolerance scale factor > 1.0 computed when requesting too much accuracy: 0
There is also a summary method for deSolve objects. This is especially handy for multidimensional problems (see below)
Karline Soetaert, Thomas Petzoldt, R. Woodrow Setzer > summary(out1) X Y Z Min. 0.96170 -17.970000 -24.110000 1st Qu. 17.02000 -7.348000 -7.152000 Median 23.06000 -1.947000 -1.451000 Mean 23.69000 -1.385000 -1.402000 3rd Qu. 30.20000 3.607000 2.984000 Max. 47.83000 19.560000 27.180000 N 10001.00000 10001.000000 10001.000000 sd 8.50134 7.846889 8.929121
11
12
13
Note that the values of state variables (here densities) are dened in the centre of boxes (i), whereas the uxes are dened on the box interfaces. We refer to Soetaert and Herman (2009) for more information about this model and its numerical approximation. Here is its implementation in R. First the model equations are dened: > Aphid <- function(t, APHIDS, parameters) { + deltax <- c (0.5, rep(1, numboxes - 1), 0.5) + Flux <- -D * diff(c(0, APHIDS, 0)) / deltax + dAPHIDS <- -diff(Flux) / delx + APHIDS * r + + # the return value + list(dAPHIDS ) + } # end Then the model parameters and spatial grid are dened > > > > > > D <- 0.3 # m2/day diffusion rate r <- 0.01 # /day net growth rate delx <- 1 # m thickness of boxes numboxes <- 60 # distance of boxes on plant, m, 1 m intervals Distance <- seq(from = 0.5, by = delx, length.out = numboxes)
Aphids are initially only present in two central boxes: > > > > # Initial conditions: # ind/m2 APHIDS <- rep(0, times = numboxes) APHIDS[30:31] <- 1 state <- c(APHIDS = APHIDS) # initialise state variables
The model is run for 200 days, producing output every day; the time elapsed in seconds to solve this 60 state-variable model is estimated (system.time): > times <-seq(0, 200, by = 1) > print(system.time( + out <- ode.1D(state, times, Aphid, parms = 0, nspec = 1, names = "Aphid") + )) user 0.03 system elapsed 0.02 0.05
Matrix out consist of times (1st column) followed by the densities (next columns). > head(out[,1:5])
14 time 0 1 2 3 4 5
Package deSolve: Solving Initial Value Dierential Equations in R APHIDS1 0.000000e+00 1.667194e-55 3.630860e-41 2.051210e-34 1.307456e-30 6.839152e-28 APHIDS2 0.000000e+00 9.555028e-52 4.865105e-39 9.207997e-33 3.718598e-29 1.465288e-26 APHIDS3 0.000000e+00 2.555091e-48 5.394287e-37 3.722714e-31 9.635350e-28 2.860056e-25 APHIDS4 0.000000e+00 4.943131e-45 5.053775e-35 1.390691e-29 2.360716e-26 5.334391e-24
The summary method gives the mean, min, max, ... of the entire 1-D variable: > summary(out) Aphid 0.000000e+00 1.705000e-03 4.051000e-02 1.062000e-01 1.931000e-01 1.000000e+00 1.206000e+04 1.303048e-01
Finally, the output is plotted. It is simplest to do this with deSolves S3-method image image(out, method = "filled.contour", grid = Distance, xlab = "time, days", ylab = "Distance on plant, m", main = "Aphid density on a row of plants") As this is a 1-D model, it is best solved with deSolve function ode.1D. A multi-species IVP example can be found in Soetaert and Herman (2009). For 2-D and 3-D problems, we refer to the help-les of functions ode.2D and ode.3D. The output of one-dimensional models can also be plotted using S3-method plot.1D and matplot.1D. In both cases, we can simply take a subset of the output, and add observations. > data <- cbind(dist = c(0,10, 20, 30, 40, 50, 60), + Aphid = c(0,0.1,0.25,0.5,0.25,0.1,0)) > par (mfrow = c(1,2)) > matplot.1D(out, grid = Distance, type = "l", mfrow = NULL, + subset = time %in% seq(0, 200, by = 10), + obs = data, obspar = list(pch = 18, cex = 2, col="red")) > plot.1D(out, grid = Distance, type = "l", mfrow = NULL, + subset = time == 100, + obs = data, obspar = list(pch = 18, cex = 2, col="red"))
15
Figure 2: Solution of the 1-dimensional aphid model - see text for R -code
16
Aphid
1.0 0.5 Aphid 0.4 0.2 0.0 0 10 20 30 x 40 50 60 0.0 0 0.1 0.2 0.3 0.4
0.6
0.8
10
20
30 x
40
50
60
Figure 3: Solution of the Aphid model - plotted with matplot.1D, plot.1D - see text for R-code
17
18
dae
0.0 0
0.5
1.0
1.5
2.0
2.5
3.0
4 time
10
Figure 4: Solution of the dierential algebraic equation model - see text for R-code user 0 system elapsed 0 0
> matplot(out[,1], out[,2:3], type = "l", lwd = 2, + main = "dae", xlab = "time", ylab = "y")
0 = x2 + y 2 1 where the dependent variables are x, y, u, v and . Implemented in R to be used with function radau this becomes: > pendulum <- function (t, Y, parms) { + with (as.list(Y), + list(c(u, + v, + -lam * x, + -lam * y - 9.8, + x^2 + y^2 -1
19
A consistent set of initial conditions are: > yini <- c(x = 1, y = 0, u = 0, v = 1, lam = 1) and the mass matrix M : > M <- diag(nrow = 5) > M[5, 5] <- 0 > M [,1] [,2] [,3] [,4] [,5] 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0
Function radau requires that the index of each equation is specied; there are 2 equations of index 1, two of index 2, one of index 3: > index <- c(2, 2, 1) > times <- seq(from = 0, to = 10, by = 0.01) > out <- radau (y = yini, func = pendulum, parms = NULL, + times = times, mass = M, nind = index) > plot(out, type = "l", lwd = 2) > plot(out[, c("x", "y")], type = "l", lwd = 2)
20
x
1.0 0.0
y
4
0.5
0.0
0.5
1.0
10
10
4 0
10
time
time
time
v
3 30
lam
0.0 0 2 4 6 8 10 1.0 0.8 0.6 0.4 0.2 1.0 y
10
10
15
20
25
0.0 x
0.5
1.0
time
time
Figure 5: Solution of the pendulum problem, an index 3 dierential algebraic equation using radau - see text for R-code
21
The analytical solution is: f (t) = exp(1i t) and g (t) = 1/(f (t) + 1.1) The numerical solution, as produced by zvode matches the analytical solution: > analytical <- cbind(f = exp(1i*times), g = 1/(exp(1i*times)+1.1)) > tail(cbind(out[,2], analytical[,1])) [,1] [95,] 0.9500711-0.3120334i [96,] 0.9679487-0.2511480i [97,] 0.9819287-0.1892512i [98,] 0.9919548-0.1265925i [99,] 0.9979867-0.0634239i [100,] 1.0000000+0.0000000i [,2] 0.9500711-0.3120334i 0.9679487-0.2511480i 0.9819287-0.1892512i 0.9919548-0.1265925i 0.9979867-0.0634239i 1.0000000-0.0000000i
22
and the initial conditions and output times are: > yini <- 1:5 > times <- 1:20 The default solution, using lsode assumes that the model is sti, and the integrator generates the Jacobian, which is assummed to be full : > out <- lsode(yini, times, f1, parms = 0, jactype = "fullint")
It is possible for the user to provide the Jacobian. Especially for large problems this can result in substantial time savings. In a rst case, the Jacobian is written as a full matrix: > fulljac <- function (t, y, parms) { + jac <- matrix(nrow = 5, ncol = 5, byrow = TRUE, + data = c(0.1, -0.2, 0 , 0 , 0 , + -0.3, 0.1, -0.2, 0 , 0 , + 0 , -0.3, 0.1, -0.2, 0 , + 0 , 0 , -0.3, 0.1, -0.2, + 0 , 0 , 0 , -0.3, 0.1)) + return(jac) + } and the model solved as: > out2 <- lsode(yini, times, f1, parms = 0, jactype = "fullusr", + jacfunc = fulljac)
23
The Jacobian matrix is banded, with one nonzero band above (up) and one below(down) the diagonal. First we let lsode estimate the banded Jacobian internally (jactype = "bandint"): > out3 <- lsode(yini, times, f1, parms = 0, jactype = "bandint", + bandup = 1, banddown = 1) It is also possible to provide the nonzero bands of the Jacobian in a function: > bandjac <- function (t, y, parms) { + jac <- matrix(nrow = 3, ncol = 5, byrow = TRUE, + data = c( 0 , -0.2, -0.2, -0.2, -0.2, + 0.1, 0.1, 0.1, 0.1, 0.1, + -0.3, -0.3, -0.3, -0.3, 0)) + return(jac) + } in which case the model is solved as: > out4 <- lsode(yini, times, f1, parms = 0, jactype = "bandusr", + jacfunc = bandjac, bandup = 1, banddown = 1) Finally, if the model is specied as non-sti (by setting mf=10), there is no need to specify the Jacobian: > out5 <- lsode(yini, times, f1, parms = 0, mf = 10)
24
At time 1 and 9 a value is added to variable v1, at time 1 state variable v2 is multiplied with 2, while at time 5 the value of v2 is replaced with 3. These events are specied in a data.frame, eventdat: > eventdat <- data.frame(var = c("v1", "v2", "v2", "v1"), time = c(1, 1, 5, 9), + value = c(1, 2, 3, 4), method = c("add", "mult", "rep", "add")) > eventdat var time value method v1 1 1 add v2 1 2 mult v2 5 3 rep v1 9 4 add
1 2 3 4
The model is solved with ode: > out <- ode(func = eventmod, y = yini, times = times, parms = NULL, + events = list(data = eventdat)) > plot(out, type = "l", lwd = 2)
25
v1
3.5
v2
4 time
10
2.0 0
2.5
3.0
4 time
10
Figure 6: A simple model that contains events > ballode<- function(t, y, parms) { + dy1 <- y[2] + dy2 <- -9.8 + list(c(dy1, dy2)) + } An event is triggered when the ball hits the ground (height = 0) Then velocity (y2) is reversed and reduced by 10 percent. The root function, y[1] = 0, triggers the event: > root <- function(t, y, parms) y[1] The event function imposes the bouncing of the ball > event <- function(t, y, parms) { + y[1]<- 0 + y[2]<- -0.9 * y[2] + return(y) + } After specifying the initial values and times, the model is solved, here using lsode. > yini <> times <> out <+ events c(height = 0, seq(from = 0, lsode(times = = list(func = v = 20) to = 20, by = 0.01) times, y = yini, func = ballode, parms = NULL, event, root = TRUE), rootfun = root)
> plot(out, which = "height", type = "l",lwd = 2, + main = "bouncing ball", ylab = "height")
26
bouncing ball
20 height 0 0 5 10 15
10 time
15
20
27
we get the surprising answer that this is only partly the case, because seq made small numerical errors. The easiest method to get rid of this is rounding: > times2 <- round(times, 1) > times - times2 [1] 0.000000e+00 0.000000e+00 0.000000e+00 5.551115e-17 0.000000e+00 [6] 0.000000e+00 1.110223e-16 1.110223e-16 0.000000e+00 0.000000e+00 [11] 0.000000e+00 The last line shows us that the error was always smaller than, say 1015 , what is typical for ordinary double precision arithmetics. The accuracy of the machine can be determined with .Machine$double.eps. To check if all eventtimes are now contained in the new times vector times2, we use: > eventtimes %in% times2 [1] TRUE TRUE or > all(eventtimes %in% times2) [1] TRUE and see that everything is o.k. now. In few cases, rounding may not work properly, for example if a pharmacokinetic model is simulated with a daily time step, but drug injection occurs at precisely xed times within the day. Then one has to add all additional event times to the ordinary time stepping: > times <- 1:10 > eventtimes <- c(1.3, 3.4, 4, 7.9, 8.5) > newtimes <- sort(unique(c(times, eventtimes))) If, however, an event and a time step are almost (but not exactly) the same, then it is more safe to use: > times <- 1:10 > eventtimes <- c(1.3, 3.4, 4, 7.9999999999999999, 8.5) > newtimes <- sort(c(eventtimes, cleanEventTimes(times, eventtimes))) because cleanEventTimes removes not only the doubled 4 (like unique, but also the almost doubled 8, while keeping the exact event time. The tolerance of cleanEventTimes can be adjusted using an optional argument eps. As said, this is normally done automatically by the dierential equation solvers and in most cases appropriate rounding will be sucient to get rid of the warnings.
28
> plot(yout, which = 1, type = "l", lwd = 2, + main = "Lemming model", mfrow = c(1,2)) > plot(yout[,2], yout[,3], xlab = "y", ylab = "dy", type = "l", lwd = 2)
29
Lemming model
100 100 dy 0 10 20 time 30 40 200 0 0
20
40
60
80
20
40 y
60
80
100
30
= NumStages, byrow = TRUE, data = c( 0, 0, 0, 322.38, 0, 0, 0, 0 , 0.125, 0, 0, 3.448 , 0.125, 0.238, 0, 30.170, 0.038, 0.245, 0.167, 0.862 , 0, 0.023, 0.75, 0 ) )
The dierence function is dened as usual, but does not return the rate of change but rather the new relative stage densities are returned. Thus, each time step, the updated values are divided by the summed densities: > Teasel <- function (t, y, p) { + yNew <- A %*% y + list (yNew / sum(yNew)) + } The model is solved using method iteration: > out <- ode(func = Teasel, y = c(1, rep(0, 5) ), times = 0:50, + parms = 0, method = "iteration") and plotted using R-function matplot: > matplot(out[,1], out[,-1], main = "Teasel stage distribution", type = "l") > legend("topright", legend = Stages, lty = 1:6, col = 1:6)
31
out[, 1]
0.0 0
0.2
0.4
0.6
0.8
10
20 out[, 1]
30
40
50
Figure 9: A dierence model solved with method = iteration How to use it and examples can be found by typing ?plot.deSolve.
32
combustion
1.0 0.6 0.8
0.4
0.0
0.2
Figure 10: Plotting 4 outputs in one gure > out3 <- ode(times = times, y = yini*3, parms = 0, func = combustion) > out4 <- ode(times = times, y = yini*4, parms = 0, func = combustion) The dierent scenarios are plotted at once, and a suitable legend is written. > plot(out, out2, out3, out4, main = "combustion") > legend("bottomright", lty = 1:4, col = 1:4, legend = 1:4, title = "yini*i")
1 2 3 4 5 6
Karline Soetaert, Thomas Petzoldt, R. Woodrow Setzer We select the data from animal A: > obs <- subset (ccl4data, animal == "A", c(time, ChamberConc)) > names(obs) <- c("time", "CP") > head(obs) time 0.083 0.167 0.333 0.500 0.667 0.833 CP 828.4376 779.6795 713.8045 672.0502 631.9522 600.6975
33
1 2 3 4 5 6
After assigning values to the parameters and providing initial conditions, the ccl4model can be run. We run the model three times, each time with a dierent value for the rst parameter. Output is written to matrices out out2, and out3. > + + > > > > > > > > parms <- c(0.182, 4.0, 4.0, 0.08, 0.04, 0.74, 0.05, 0.15, 0.32, 16.17, 281.48, 13.3, 16.17, 5.487, 153.8, 0.04321671, 0.40272550, 951.46, 0.02, 1.0, 3.80000000) yini <- c(AI = 21, AAM = 0, AT = 0, AF = 0, AL = 0, CLT = 0, AM = 0) out <- ccl4model(times = seq(0, 6, by = 0.05), y = yini, parms = parms) par2 <- parms par2[1] <- 0.1 out2 <- ccl4model(times = seq(0, 6, by = 0.05), y = yini, parms = par2) par3 <- parms par3[1] <- 0.05 out3 <- ccl4model(times = seq(0, 6, by = 0.05), y = yini, parms = par3)
We plot all these scenarios and the observed data at once: > plot(out, out2, out3, which = c("AI", "MASS", "CP"), + col = c("black", "red", "green"), lwd = 2, + obs = obs, obspar = list(pch = 18, col = "blue", cex = 1.2)) > legend("topright", lty = c(1,2,3,NA), pch = c(NA, NA, NA, 18), + col = c("black", "red", "green", "blue"), lwd = 2, + legend = c("par1", "par2", "par3", "obs")) If we do not select specic variables, then only the ones for which there are observed data are plotted. Assume we have measured the total mass at the end of day 6. We put this in a second data set: > obs2 <- data.frame(time = 6, MASS = 12) > obs2 time MASS 6 12
34
AI
20 12
MASS
16
12
3 time
0 0
3 time
CP
par1 par2 par3 obs
400 0
600
800
3 time
35
MASS
10 12
CP
q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q q
3 time
Figure 12: Plotting variables in common with observations then we plot the data together with the three model runs as follows: > plot(out, out2, out3, lwd = 2, + obs = list(obs, obs2), + obspar = list(pch = c(16, 18), col = c("blue", "black"), + cex = c(1.2 , 2)) + )
36
AI
40 Frequency Frequency 40
AAM
Frequency 40 60
AT
Frequency
AF
20
20
20
10 time
20
2 time
0.0
0.2 time
0.4
0 0 2 4 6 8 time
AL
20 40 60 Frequency Frequency
CLT
Frequency 20
AM
Frequency 20 40 60
10
20
DOSE
20
10
10
0.00
0.20 time
100 time
250
0 0
14
time
MASS
Frequency Frequency
CP
50
20
14
0 300
20
40
700 time
time
37
Note that it is simpler to implement this model in R-package ReacTran (Soetaert and Meysman 2010). We start by dening the derivative function lvmod <- function (time, state, parms, N, rr, ri, dr, dri) { with (as.list(parms), { PREY <- state[1:N] PRED <- state[(N+1):(2*N)] ## Fluxes due to diffusion ## at internal and external boundaries: zero gradient FluxPrey <- -Da * diff(c(PREY[1], PREY, PREY[N]))/dri FluxPred <- -Da * diff(c(PRED[1], PRED, PRED[N]))/dri ## Biology: Lotka-Volterra model Ingestion <- rIng * PREY * PRED GrowthPrey <- rGrow * PREY * (1-PREY/cap) MortPredator <- rMort * PRED ## Rate of change = Flux gradient + Biology dPREY <- -diff(ri * FluxPrey)/rr/dr + GrowthPrey - Ingestion dPRED <- -diff(ri * FluxPred)/rr/dr + Ingestion * assEff - MortPredator return (list(c(dPREY, dPRED))) }) } Then we dene the parameters, which we put in a list R <- 20 N <- 100 dr <- R/N r <- seq(dr/2,by = dr,len = N) ri <- seq(0,by = dr,len = N+1) dri <- dr parms <- c(Da = 0.05, rIng = 0.2, rGrow = 1.0, rMort = 0.2 , assEff = 0.5, cap = 10) # # # # # # # # # # # # total radius of surface, m 100 concentric circles thickness of each layer distance of center to mid-layer distance to layer interface dispersion distances m2/d, dispersion coefficient /day, rate of ingestion /day, growth rate of prey /day, mortality rate of pred -, assimilation efficiency density, carrying capacity
After dening initial conditions, the model is solved with routine ode.1D state <- rep(0, 2 * N) state[1] <- state[N + 1] <- 10
38
times <- seq(0, 200, by = 1) # output wanted at these time intervals print(system.time( out <- ode.1D(y = state, times = times, func = lvmod, parms = parms, nspec = 2, names = c("PREY", "PRED"), N = N, rr = r, ri = ri, dr = dr, dri = dri) )) user 0.62 system elapsed 0.00 0.63
The summary method provides summaries for both 1-dimensional state variables: summary(out) PREY PRED Min. 0.000000 0.000000 1st Qu. 1.997000 3.971000 Median 2.000000 4.000000 Mean 2.094000 3.333000 3rd Qu. 2.000000 4.000000 Max. 10.000000 10.000000 N 20100.000000 20100.000000 sd 1.648847 1.526742 while the S3-method subset can be used to extract only specic values of the variables: p10 <- subset(out, select = "PREY", subset = time == 10) head(p10, n = 5) [,1] [,2] [,3] [,4] [,5] [,6] [,7] [1,] 6.304707 6.436374 6.687753 7.033897 7.436098 7.843497 8.198683 [,8] [,9] [,10] [,11] [,12] [,13] [,14] [1,] 8.44655 8.542749 8.457464 8.173474 7.682188 6.983525 6.09314 [,15] [,16] [,17] [,18] [,19] [,20] [,21] [1,] 5.054634 3.947258 2.876796 1.946159 1.22074 0.7120999 0.388549 [,22] [,23] [,24] [,25] [,26] [1,] 0.1996948 0.09733824 0.04526957 0.02018686 0.00866459 [,27] [,28] [,29] [,30] [,31] [1,] 0.003590384 0.001439632 0.0005595871 0.0002111643 7.745135e-05 [,32] [,33] [,34] [,35] [,36] [1,] 2.763954e-05 9.605149e-06 3.252992e-06 1.074404e-06 3.462836e-07 [,37] [,38] [,39] [,40] [,41] [1,] 1.089753e-07 3.350363e-08 1.006801e-08 2.958638e-09 8.506159e-10 [,42] [,43] [,44] [,45] [,46] [1,] 2.393626e-10 6.595368e-11 1.780133e-11 4.708254e-12 1.220725e-12 [,47] [,48] [,49] [,50] [,51] [1,] 3.103673e-13 7.740666e-14 1.894361e-14 4.550522e-15 1.07325e-15
Karline Soetaert, Thomas Petzoldt, R. Woodrow Setzer [,52] 2.486015e-16 [,57] 1.276096e-19 [,62] 4.324292e-23 [,67] 9.906072e-27 [,72] 1.562748e-30 [,77] 1.721681e-34 [,82] 1.337994e-38 [,87] 7.383755e-43 [,92] 2.904397e-47 [,97] 8.154381e-52 [,53] 5.657001e-17 [,58] 2.666569e-20 [,63] 8.341186e-24 [,68] 1.771079e-27 [,73] 2.597843e-31 [,78] 2.667263e-35 [,83] 1.934836e-39 [,88] 9.976169e-44 [,93] 3.668048e-48 [,98] 9.626882e-53 [,54] [,55] [,56] 1.264912e-17 2.779928e-18 6.006319e-19 [,59] [,60] [,61] 5.481592e-21 1.108743e-21 2.207026e-22 [,64] [,65] [,66] 1.584226e-24 2.963127e-25 5.458728e-26 [,69] [,70] [,71] 3.120002e-28 5.416316e-29 9.26688e-30 [,74] [,75] [,76] 4.257409e-32 6.87897e-33 1.095928e-33 [,79] [,80] [,81] 4.075197e-36 6.140815e-37 9.12685e-38 [,84] [,85] [,86] 2.75999e-40 3.883822e-41 5.391532e-42 [,89] [,90] [,91] 1.329779e-44 1.748762e-45 2.268934e-46 [,94] [,95] [,96] 4.570454e-49 5.618603e-50 6.814592e-51 [,99] [,100] 1.122896e-53 1.436052e-54
39
[1,] [1,] [1,] [1,] [1,] [1,] [1,] [1,] [1,] [1,]
We rst plot both 1-dimensional state variables at once; we specify that the gures are arranged in two rows, and 2 columns; when we call image, we overrule the default mfrow setting (mfrow = NULL). Next we plot PREY again, once with the default xlim and ylim, and next zooming in. Note that xlim and ylim are a list here. When we call image for the second time, we overrule the default mfrow setting by specifying (mfrow = NULL). image(out, grid = r, mfrow = ticktype = "detailed", image(out, grid = r, which = xlim = list(NULL, c(0, add.contour = c(FALSE, c(2, 2), method = "persp", border = NA, legend = TRUE) c("PREY", "PREY"), mfrow = NULL, 10)), ylim = list(NULL, c(0, 5)), TRUE))
40
41
Then the grid is created, and the consumption rate made a function of grid position (outer). dy <- dx <- 1 # grid size nx <- ny <- 100 x <- seq (dx/2, by = dx, len = nx) y <- seq (dy/2, by = dy, len = ny) # in each grid cell: consumption depending on position r_x2y2 <- outer(x, y, FUN=function(x,y) ((x-50)^2 + (y-50)^2)*1e-4) After dening the initial values, the model is solved using solver ode.2D. We use Runge-Kutta method ode45. C <- matrix(nrow = nx, ncol = ny, 1) ODE3 <- ode.2D(y = C, times = 1:100, func = Simple2D, parms = NULL, dimens = c(nx, ny), names = "C", method = "ode45") We print a summary, and extract the 2-D variable at time = 50 summary(ODE3) C 8.523000e-22 4.332000e-06 2.631000e-03 1.312000e-01 1.203000e-01 1.000000e+00 1.000000e+06 2.489394e-01
t50 <- matrix(nrow = nx, ncol = ny, data = subset(ODE3, select = "C", subset = (time == 50))) We use function contour to plot both the consumption rate and the values of the state variables at time = 50. par(mfrow = c(1, 2)) contour(x, y, r_x2y2, main = "consumption") contour(x, y, t50, main = "Y(t = 50)")
42
consumption
100
4
0.3 5
Y(t = 50)
0.3
0.3 5
0.
0.3
0.25
0.2
0.25
0. 4
80
80
100
0.2
0.4 0.5 0.7 0.
60
60
0.9
40
40
0.6
0.3
0.05
0.1
20
0.1
0.15
0. 4
20
40
60
80
100
0 0
0.3 5
0.3
0.25
0.25
0.3
0.3
0.
20
20
40
60
80
100
43
11. Troubleshooting
11.1. Avoiding numerical errors
The solvers from ODEPACK should be rst choice for any problem and the defaults of the control parameters are reasonable for many practical problems. However, there are cases where they may give dubious results. Consider the following Lotka-Volterra type of model: PCmod <- function(t, x, parms) { with(as.list(c(parms, x)), { dP <- c*P - d*C*P # producer dC <- e*P*C - f*C # consumer res <- c(dP, dC) list(res) }) } and with the following (biologically not very realistic)3 parameter values: parms <- c(c = 10, d = 0.1, e = 0.1, f = 0.1)
After specication of initial conditions and output times, the model is solved using lsoda: xstart times out <func <- c(P = 0.5, C = 1) <- seq(0, 200, 0.1) ode(y = xstart, times = times, = PCmod, parms = parms)
DLSODA- At T(=R1) and step size H(=R2), the error test failed repeatedly or with ABS(H) = HMIN In above message, R1 = 89.9566, R2 = 1.40508e-10 tail(out) time 89.50000 89.60000 89.70000 89.80000 89.90000 89.95657 P C -5.437180e+10 -1.049789e-07 -1.477988e+11 8.127606e-13 -4.017604e+11 -8.353735e-08 -1.092102e+12 -1.136036e-14 -2.968659e+12 1.519005e-12 -5.226781e+12 3.661942e-06
3 they are not realistic because producers grow unlimited with a high rate and consumers with 100 % eciency
44
We see that the simulation was stopped before reaching the nal simulation time and both producers and consumer values may have negative values. What has happened? Being an implicit method, lsoda generates very small negative values for producers, from day 40 on; these negative values, small at rst grow in magnitude until they become innite or even NaNs (not a number). This is because the model equations are not intended to be used with negative numbers, as negative concentrations are not realistic. A quick-and-dirty solution is to reduce the maximum time step to a considerably small value (e.g. hmax = 0.02 which, of course, reduces computational eciency. However, a much better solution is to think about the reason of the failure, i.e in our case the absolute accuracy because the states can reach very small absolute values. Therefore, it helps here to reduce atol to a very small number or even to zero: out <- ode(y = xstart,times = times, func = PCmod, parms = parms, atol = 0) matplot(out[,1], out[,2:3], type = "l", xlab = "time", ylab = "Producer, Consumer") It is, of course, not possible to set both, atol and rtol simultaneously to zero. As we see from this example, it is always a good idea to test simulation results for plausibility. This can be done by theoretical considerations or by comparing the outcome of dierent ODE solvers and parametrizations.
45
help, feel free to ask a question to an appropriate mailing list, e.g. r-help@r-project.org or, more specic, r-sig-dynamic-models@r-project.org.
return(list(c(dPrey, dPredator))) }) } pars <- c(rIng = 0.2, # /day, rate of ingestion rGrow = 1.0, # /day, growth rate of prey rMort = 0.2 , # /day, mortality rate of predator assEff = 0.5, # -, assimilation efficiency K = 10) # mmol/m3, carrying capacity <- c(Prey = 1, Predator = 2) <- seq(0, 200, by = 1) <- ode(func = LVmod, y = yini, parms = pars, times = times)
This model is easily solved by the default integration method, lsoda. Now we change one of the parameters to an unrealistic value: rIng is set to 100. This means that the predator ingests 100 times its own body-weight per day if there are plenty of prey. Needless to say that this is very unhealthy, if not lethal. Also, lsoda cannot solve the model anymore. Thus, if we try: pars["rIng"] <- 100 out2 <- ode(func = LVmod, y = yini, parms = pars, times = times) A lot of seemingly incomprehensible messages will be written to the screen. We repeat the latter part of them:
46
DLSODA- Warning..Internal T (=R1) and H (=R2) are such that in the machine, T + H = T on the next step (H = step size). Solver will continue anyway. In above message, R1 = 53.4272, R2 = 2.44876e-15 DLSODA- Above warning has been issued I1 times. It will not be issued again for this problem. In above message, I1 = 10 DLSODA- At current T (=R1), MXSTEP (=I1) steps taken on this call before reaching TOUT In above message, I1 = 5000 In above message, R1 = 53.4272 Warning messages: 1: In lsoda(y, times, func, parms, ...) : an excessive amount of work (> maxsteps ) was done, but integration was not successful - increase maxsteps 2: In lsoda(y, times, func, parms, ...) : Returning early. Results are accurate, as far as they go The rst sentence tells us that at T = 53.4272, the solver used a step size H = 2.44876e-15. This step size is so small that it cannot tell the dierence between T and T + H. Nevertheless, the solver tried again. The second sentence tells that, as this warning has been occurring 10 times, it will not be outputted again. As expected, this error did not go away, so soon the maximal number of steps (5000) has been exceeded. This is indeed what the next message is about: The third sentence tells that at T = 53.4272, maxstep = 5000 steps have been done. The one before last message tells why the solver returned prematurely, and suggests a solution. Simply increasing maxsteps will not work and it makes more sense to rst see if the output tells what happens: plot(out2, type = "l", lwd = 2, main = "corrupt Lotka-Volterra model") You may, of course, consider to use another solver: pars["rIng"] <- 100 out3 <- ode(func = LVmod, y = yini, parms = pars, times = times, method = "ode45", atol = 1e-14, rtol = 1e-14) but dont forget to think about this too and, for example, increase simulation time to 1000 and try dierent values of atol and rtol. We leave this open as an exercise to the reader.
47
1.2e+63
6.0e+62
10
20
30 time
40
50
48
References
Bogacki P, Shampine LF (1989). A 3(2) Pair of Runge-Kutta Formulas. Applied Mathematics Letters, 2, 19. Brenan KE, Campbell SL, Petzold LR (1996). Numerical Solution of Initial-Value Problems in Dierential-Algebraic Equations. SIAM Classics in Applied Mathematics. Brown PN, Byrne GD, Hindmarsh AC (1989). VODE, A Variable-Coecient ODE Solver. SIAM Journal on Scientic and Statistical Computing, 10, 10381051. Butcher JC (1987). The Numerical Analysis of Ordinary Dierential Equations, Runge-Kutta and General Linear Methods, volume 2. John Wiley & Sons, Chichester and New York. Cash JR, Karp AH (1990). A Variable Order Runge-Kutta Method for Initial Value Problems With Rapidly Varying Right-Hand Sides. ACM Transactions on Mathematical Software, 16, 201222. Dormand JR, Prince PJ (1980). A family of embedded Runge-Kutta formulae. Journal of Computational and Applied Mathematics, 6, 1926. Dormand JR, Prince PJ (1981). High Order Embedded Runge-Kutta Formulae. Journal of Computational and Applied Mathematics, 7, 6775. Fehlberg E (1967). Klassische Runge-Kutta-Formeln fuenfter and siebenter Ordnung mit Schrittweiten-Kontrolle. Computing (Arch. Elektron. Rechnen), 4, 93106. Hairer E, Norsett SP, Wanner G (2009). Solving Ordinary Dierential Equations I: Nonsti Problems. Second Revised Edition. Springer-Verlag, Heidelberg. Hairer E, Wanner G (2010). Solving Ordinary Dierential Equations II: Sti and DierentialAlgebraic Problems. Second Revised Edition. Springer-Verlag, Heidelberg. Hindmarsh AC (1983). ODEPACK, a Systematized Collection of ODE Solvers. In R Stepleman (ed.), Scientic Computing, Vol. 1 of IMACS Transactions on Scientic Computation, pp. 5564. IMACS / North-Holland, Amsterdam. Petzold LR (1983). Automatic Selection of Methods for Solving Sti and Nonsti Systems of Ordinary Dierential Equations. SIAM Journal on Scientic and Statistical Computing, 4, 136148. Press WH, Teukolsky SA, Vetterling WT, Flannery BP (1992). Numerical Recipes in FORTRAN. The Art of Scientic Computing. Cambridge University Press, 2nd edition. R Development Core Team (2008). R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing, Vienna, Austria. ISBN 3-900051-07-0, URL http: //www.R-project.org. Shampine L, Thompson S (2000). Solving Delay Dierential Equations with dde23. URL http://www.runet.edu/~thompson/webddes/tutorial.pdf.
49
Soetaert K, Cash JR, Mazzia F (2010a). bvpSolve: Solvers for Boundary Value Problems of Ordinary Dierential Equations. R package version 1.2, URL http://CRAN.R-project. org/package=bvpSolve. Soetaert K, Herman PMJ (2009). A Practical Guide to Ecological Modelling. Using R as a Simulation Platform. Springer. ISBN 978-1-4020-8623-6. Soetaert K, Meysman F (2010). ReacTran: Reactive transport modelling in 1D, 2D and 3D. R package version 1.3. Soetaert K, Petzoldt T, Setzer R (2010b). Solving Dierential Equations in R: Package deSolve. Journal of Statistical Software, 33(9), 125. ISSN 1548-7660. URL http://www. jstatsoft.org/v33/i09. Soetaert K, Petzoldt T, Setzer RW (2008). R package deSolve: Writing Code in Compiled Languages. deSolve vignette - R package version 1.8. Soetaert K, Petzoldt T, Setzer RW (2010c). deSolve: General solvers for initial value problems of ordinary dierential equations (ODE), partial dierential equations (PDE), dierential algebraic equations (DAE) and delay dierential equations (DDE). R package version 1.8.
Aliation:
Karline Soetaert Centre for Estuarine and Marine Ecology (CEME) Royal Netherlands Institute of Sea Research (NIOZ) 4401 NT Yerseke, Netherlands E-mail: karline.soetaert@nioz.nl URL: http://www.nioz.nl Thomas Petzoldt Institut f ur Hydrobiologie Technische Universit at Dresden 01062 Dresden, Germany E-mail: thomas.petzoldt@tu-dresden.de URL: http://tu-dresden.de/Members/thomas.petzoldt/ R. Woodrow Setzer National Center for Computational Toxicology US Environmental Protection Agency URL: http://www.epa.gov/comptox
50
Table 1: Summary of the functions that solve dierential equations Function ode ode.1D ode.2D ode.3D ode.band dede daspk radau lsoda lsodar lsode or vode Description integrates systems of ordinary dierential equations, assumes a full, banded or arbitrary sparse Jacobian integrates systems of ODEs resulting from 1-dimensional reactiontransport problems integrates systems of ODEs resulting from 2-dimensional reactiontransport problems integrates systems of ODEs resulting from 3-dimensional reactiontransport problems integrates systems of ODEs resulting from unicomponent 1dimensional reaction-transport problems integrates systems of delay dierential equations solves systems of dierential algebraic equations, assumes a full or banded Jacobian solves systems of ordinary or dierential algebraic equations, assumes a full or banded Jacobian; includes a root solving procedure integrates ODEs, automatically chooses method for sti or non-sti problems, assumes a full or banded Jacobian same as lsoda, but includes a root-solving procedure integrates ODEs, user must specify if sti or non-sti assumes a full or banded Jacobian; Note that, as from version 1.7, lsode includes a root nding procedure, similar to lsodar. integrates ODEs, using sti method and assuming an arbitrary sparse Jacobian. Note that, as from version 1.7, lsodes includes a root nding procedure, similar to lsodar integrates ODEs, using Runge-Kutta methods (includes Runge-Kutta 4 and Euler as special cases) integrates ODEs, using the classical Runge-Kutta 4th order method (special code with less options than rk) integrates ODEs, using Eulers method (special code with less options than rk) integrates ODEs composed of complex numbers, full, banded, sti or nonsti
lsodes
51
Table 2: Meaning of the integer return parameters in the dierent integration routines. If out is the output matrix, then this vector can be retrieved by function attributes(out)$istate; its contents is displayed by function diagnostics(out). Note that the number of function evaluations, is without the extra evaluations needed to generate the output for the ordinary variables. Nr Description 1 the return ag; the conditions under which the last call to the solver returned. For lsoda, lsodar, lsode, lsodes, vode, rk, rk4, euler these are: 2: the solver was successful, -1: excess work done, -2: excess accuracy requested, -3: illegal input detected, -4: repeated error test failures, -5: repeated convergence failures, -6: error weight became zero the number of steps taken for the problem so far the number of function evaluations for the problem so far the number of Jacobian evaluations so far the method order last used (successfully) the order of the method to be attempted on the next step If return ag = -4,-5: the largest component in the error vector the length of the real work array actually required. (FORTRAN code) the length of the integer work array actually required. (FORTRAN code) the number of matrix LU decompositions so far the number of nonlinear (Newton) iterations so far the number of convergence failures of the solver so far the number of error test failures of the integrator so far the number of Jacobian evaluations and LU decompositions so far the method indicator for the last succesful step, 1 = adams (nonsti), 2 = bdf (sti) the number of nonzero elements in the sparse Jacobian the current method indicator to be attempted on the next step, 1 = adams (nonsti), 2 = bdf (sti) the number of convergence failures of the linear iteration so far
2 3 4 5 6 7 8 9 10 11 12 13 14 15 17 18 19
52
Table 3: Meaning of the double precision return parameters in the dierent integration routines. If out is the output matrix, then this vector can be retrieved by function attributes(out)$rstate; its contents is displayed by function diagnostics(out) Nr 1 2 3 4 5 Description the step size in t last used (successfully) the step size to be attempted on the next step the current value of the independent variable which the solver has actually reached a tolerance scale factor, greater than 1.0, computed when a request for too much accuracy was detected the value of t at the time of the last method switch, if any (only lsoda, lsodar)