Mathsy Coding

Solving Initial Value Problems in R Using deSolve

How to solve initial value problems (ODEs) using the deSolve package for R.
Edits:
  • : Changing image and file paths
  • : Changing picture and file paths

This post is a repeat of my earlier post on solving initial value problems, but now using the deSolve package (documentation).

library(deSolve)
library(ggplot2)

options(repr.plot.width=12, repr.plot.height=8)

Solving Linear Growth

The first, and simplest, system that we’ll solve is linear growth. This system is given by

dydt=ay\frac{dy}{dt} = a*y

For simplicity, we’ll choose a=1a=1. Of course, we can easily solve this exactly - the solution is y(t)=y0eaty(t) = y_0 e^{at}. Now let’s see how we can solve it numerically.

For ordinary differently equations, the deSolve package provides the ode function. This function takes in the initial state (y=), the times (times=), any parameters to be passed into the system itself (parms=), and a function func representing the system. The function func has signature func(t, state, parameters), where

parameters <- c(a=1) # here the growth rate is 1
state <- c(y=1) # initial state is 1
times <- seq(0, 5, by=0.1)
# Function which takes the current state and returns a list of derivatives
linearGrowth <- function(t, state, parameters) {
    with(as.list(c(state, parameters)),{
        dydt <- a * y
        list(dydt)
    })
}

While most of this is relatively straightforward, there is one wrinkle - the with function. What this does is make the variables and parameters stored in state and parameters respectively available using just their names, rather than having to access them using parameters[['a']] or whatnot. Although confusing at first, the R documentation does a good job of explaining how it works.

out <- ode(y=state, times=times, func=linearGrowth, parms=parameters)
head(out)
A matrix: 6 × 2 of type dbl
timey
0.01.000000
0.11.105171
0.21.221403
0.31.349861
0.41.491827
0.51.648723
# convert to a dataframe for graphing
df <- data.frame(time=out[,"time"], y=out[,'y'])
head(df)
A data.frame: 6 × 2
timey
<dbl><dbl>
10.01.000000
20.11.105171
30.21.221403
40.31.349861
50.41.491827
60.51.648723
ggplot(df, aes(time, y)) + geom_point() + geom_line()

Numerical solution to linear growth

From this, it seems like our numerical solution to this system is pretty good. But how good is it? Since we can solve this system exactly, we can actually compute the maximum deviation that the numerical solution produced:

df['exact'] <- exp(df['time'])
max(abs(df['y'] - df['exact']))

0.000236225065123108

So our numerical approximation is pretty good!

Two Variables - Lotka-Volterra

Now let’s make this a little more complicated by moving to the two-variable Lotka-Volterra system. This system is given by

dxdt=x(bpy)dydt=y(rxd)\begin{align*} \frac{dx}{dt} &= x(b - py) \\ \frac{dy}{dt} &= y(rx - d) \\ \end{align*}

Where

And the parameter

Luckily, our approach to solving this hardly has to change at all.

lotkaVolterra <- function(t, state, parameters) {
    with(as.list(c(state, parameters)), {
        dxdt <- x * (b - p * y)
        dydt <- y * (r * x - d)
        list(c(dxdt, dydt))
    })
}
parameters <- c(b=1, p=1, r=1, d=1)
state <- c(x=0.5, y=0.5)
times <- seq(0, 15, by=0.01)

out <- ode(y=state, times=times, parms=parameters, func=lotkaVolterra)
# again, converting to a dataframe for graphing
df <- data.frame(time=out[,'time'], x=out[,'x'], y=out[,'y'])
head(df)
A data.frame: 6 × 3
timexy
<dbl><dbl><dbl>
10.000.50000000.5000000
20.010.50251250.4975125
30.020.50505020.4950498
40.030.50761310.4926119
50.040.51020140.4901987
60.050.51281520.4878099

In order to see what is going on with our solution, we’ll first plot both the predator and prey populations against the time.

ggplot(df, aes(x=time)) +
    geom_line(aes(y=x, colour="x"), linewidth=2) +
    geom_line(aes(y=y, colour="y"), linewidth=2) +
    guides(colour=guide_legend(title="Animal")) +
    scale_colour_manual(labels=c("Prey", "Predator"), values=c("red", "blue")) +
    ylab("Population") +
    ggtitle("Lotka-Volterra Predatory-Prey Dynamics") +
    theme(plot.title=element_text(hjust=0.5))

Solution to Lotka-Volterra - populations against time

From this, is seems like the system demonstrates some periodic behaviour. Just by eyeballing this graph, it seems like the period is roughly seven years. To see this more clearly, let’s now plot the prey population against the predator one for about one cycle.

ggplot(df[df['time'] < 7,], aes(x, y, colour=time)) +
	scale_colour_gradient(low='blue', high='red', aesthetics='colour') +
	geom_path(linewidth=4) +
	labs(title="Lotka-Volterra Phase Space Diagram") +
	theme(plot.title = element_text(hjust=0.5)) +
	xlab("Prey") +
	ylab("Predator")

Lotka-Volterra Phase Space Diagram

This make it far more clear that the system is periodic.

3 Variables - Lorenz System

Let’s take this up another dimension by seeing the chaotic behaviour of the Lorenz system. This set of differential equations is given by

dxdt=σ(yx)dydt=x(ρz)ydzdt=xyβz\begin{align*} \frac{dx}{dt} &= \sigma(y - x) \\ \frac{dy}{dt} &= x(\rho - z) - y \\ \frac{dz}{dt} &= xy - \beta z \\ \end{align*}

with σ=10\sigma=10, β=83\beta=\frac{8}{3}, and ρ=28\rho = 28 (as originally used by Lorenz).

parameters <- c(sigma=10, beta=8/3, rho=28)
state <- c(x=1, y=1, z=1)

lorenz <- function(t, state, parameters) {
    with(as.list(c(state, parameters)), {
        dxdt <- sigma * (y - x)
        dydt <- x * (rho - z)
        dzdt <- x * y - beta * z
        list(c(dxdt, dydt, dzdt))
    })
}

out <- ode(y=state, times=times, parms=parameters, func=lorenz)
head(out)
A matrix: 6 × 4 of type dbl
timexyz
0.001.0000001.0000001.0000000
0.011.0130941.2712640.9849476
0.021.0510451.5496000.9733735
0.031.1124751.8414900.9658361
0.041.1967452.1531380.9631516
0.051.3038552.4906480.9664250
# convert to dataframe
df <- data.frame(
    time=out[,'time'],
    x=out[,'x'],
    y=out[,'y'],
    z=out[,'z']
)
head(df)
A data.frame: 6 × 4
timexyz
<dbl><dbl><dbl><dbl>
10.001.0000001.0000001.0000000
20.011.0130941.2712640.9849476
30.021.0510451.5496000.9733735
40.031.1124751.8414900.9658361
50.041.1967452.1531380.9631516
60.051.3038552.4906480.9664250
# 'melt' the dataframe for graphing
melted_df <- cbind(df['time'], stack(df[c('x', 'y', 'z')]))
head(melted_df)
A data.frame: 6 × 3
timevaluesind
<dbl><dbl><fct>
10.001.000000x
20.011.013094x
30.021.051045x
40.031.112475x
50.041.196745x
60.051.303855x

The first way that we’ll examine this is through plots of each individual variable x, y, and z against the time.

ggplot(melted_df, aes(x=time, y=values)) +
	geom_line() +
	facet_wrap(~ind)

Solution to Lorenz system - against time

It’s a little hard to tell anything from these! To better see the behaviour, we should plot this in 3D. To do so, we’ll use plotly for R to see th results in three dimensions.

library(plotly)
plot_ly(df, x=~x, y=~y, z=~z, type='scatter3d', mode='lines')

3D View of the Lorenz System

Conclusion

There we have it - several examples of solving initial value problem using the deSolve library! The API for this library is quite nice, and as you can see, the set ups for each different system are remarkably similar to each other. The documentation for this library is also very nicely laid out with clear examples and good explanations of not only what they are doing, but why they’re doing it.

Sources / Further Reading