An ordinary Differential Equation (of first order) is an expression
\[ y' = f(t, y) \]
where \(y = y(t)\) is a function of the independent variable \(t\) (or \(x\)) defined on an interval/a time span \([t_0, t_1]\) and satisfies an initial condition \(y(t_0) = y_0\). Numerically the result shall be a vector (or matrix) of function values \(y\) at certain time points \(t\) in the interval.
The standard package for solving (ordinary) differential equations with R is deSolve. Alternatives are the sundialsr package or my own pracma package that encompasses several ODE solvers (written in pure R). We will use ‘pracma’, but the syntax is quite similar for all three packages.
A spark flaring up to a flame (of some size \(y\)) follows the equation \(y' = y^2 - y^3\), where \(y^3\) is representing the volume where oxygen gets burnt, and \(y^2\) the surface through which new oxygen can flow in.
f in RJust write the right hand side \(f\) as a function in R.
flame <- function(t, y) y^2 - y^3
NOTE: The argument t is necessary though not used in the
calculation.
Solvers in ‘pracma’ have names relating to corresponding solvers in
MATLAB, such as ode45, ode23, or
ode23s.
sol <- pracma::ode45(flame, 0, 500, 1/250) # ode23, ode23s
str(sol)
## List of 2
## $ t: num [1:112] 0 5 61.8 119.4 157.3 ...
## $ y: num [1:112] 0.004 0.00408 0.0053 0.00762 0.01067 ...
cbind(sol$t[1:5], sol$y[1:5])
## [,1] [,2]
## [1,] 0.00000 0.004000000
## [2,] 5.00000 0.004081296
## [3,] 61.75143 0.005304131
## [4,] 119.43906 0.007621370
## [5,] 157.27441 0.010670698
The numerical solution is stored in sol in list
components t and y.These solvers are
adaptive, that is they decide at which (time) points the
solution values are calculated.
plot(sol$t, sol$y, type = 'l', lwd = 2, col = "red",
main = "Solution to the 'flame' problem")
points(sol$t, sol$y, pch = 20, col = "gray50")
grid()
A system of differential equations is a set of such equations.
\[ \begin{align} y_1' & = f_1(t, y_1, y_2, ...)\\ y_2' & = f_2(t, y_1, y_2, ...)\\ ... \end{align} \]
The relation between predator and prey species in a relatively isolated habitat has been modeled according to the Lottka-Volterra equations (e.g. \(y_1\) hares, \(y_2\) foxes):
\[ \begin{align} y_1' = p_1\, y_1 + p_2\, y_1 y_2\\ y_2' = p_3\, y_2 + p_4\, y_1 y_2 \end{align} \]
We will try to solve this with parameters \(p_1 = 0.1; p_2 = -0.01; p3 = -0.05; p_4 = 0.001\), initial conditions \(y_1(t_0) 50; y_2(t_0) = 15\), and a certain time span:
p0 <- c(0.1, -0.01, -0.05, 0.001) # parameter
y0 <- c(50, 15) # initial conditions
tspan <- seq(0, 240, by = 2) # time span (months?)
LVmod = function(t, y, p = p0) {
dy1 = p[1]*y[1] + p[2]*y[1]*y[2]
dy2 = p[3]*y[2] + p[4]*y[1]*y[2]
c(dy1, dy2)
}
sol <- pracma::ode23(LVmod, 0, 250, c(50, 16))
str(sol)
## List of 2
## $ t: num [1:51] 0 1.33 6.77 11.71 15.84 ...
## $ y: num [1:51, 1:2] 50 46.2 34.1 27.4 24.2 ...
plot(sol$t, sol$y[, 1], type = 'l',
ylim = c(0, 100), col = "darkgreen")
lines(sol$t, sol$y[, 2], col = "darkred")
grid()
The solution is oscillating for both species, but is not elementary in the sense that it cannot be represented by trigonometric functions.
Second order differential equations involve second derivatives, depending on the function itself and possible its first derivatives.
\[ y'' = f(t, y, y') \]
By setting \(y_1 = y; y_2 = y'\) this is transformed into a system of two differential equations:
\[ \begin{align} y_1' & = y_2\\ y_2' & = f(t, y_1, y_2) \end{align} \]
A mathematical pendulum (massless thread) is described by the differential equation (where \(y\) is the elongation):
\[ y'' = -g/L \cdot \sin(y) \]
g <- 9.81 # gravitational acceleration [m/s^2]
L <- 1.0 # pendulum length [m]
pend <- function(t, y) {
dy1 <- y[2]
dy2 <- -g/L * sin(y[1])
return( c(dy1, dy2) )
}
sol <- pracma::ode45(pend, 0, 10, c(pi/4, 0))
plot(sol$t, sol$y[, 1], type = 'l', col = "darkblue", ylim = c(-2.5, 2.5),
xlab = "time", ylab = "", main = "Pendulum: elongation and acceleration")
lines(sol$t, sol$y[, 2], col = "darkred"); grid()
We see the vibrations of the pendulum (in blue) and of its speed (in red).
\[ y'' = -g + c_w \cdot \rho/2 \cdot A \cdot v^2 \]
with \(c_w \cdot \rho/2 \cdot A\) also called the drag force.
The differential equation for a freely falling object with air
resistance and drag force Fd is given as
g = 9.81 # m/s^2
ffall <- function(t, y, Fd) {
dy1 <- y[2]
dy2 <- -g + Fd * y[2]^2
return( c(dy1, dy2) )
}
Plotting a fall without air resistance (Fd = 0) shows
that the object falling from 50 meters will take about 3.2 seconds to
arrive at the ground:
sol <- pracma::ode23(ffall, 0, 5, c(50, 0), Fd = 0)
plot(sol$t, sol$y[, 1], type = 'l', col = "darkblue")
grid()
To determine the exact time to ground, we interpolate the solution
and determine the root (where y = 0).
w = function(t)
pracma::interp1(sol$t, sol$y[, 1], t, method = "cubic")
pracma::ridders(w, 0, 5)$root # uniroot
## [1] 3.192708
Because of \(v = g t\) and therefore \(s = \frac{1}{2} gt^2\) we have \(t = \sqrt{\frac{2s}{g}}\), that is
( t = sqrt(2*50/g) ) # free fall time from 50 m
## [1] 3.192754
We assume a test series of dropping a basketball from different heights (e.g., from the tower of Pisa) and measuring the time until it hits the ground. The data are as follows:
xs <- c(50, 45, 40, 35, 30, 25, 20, 15, 10, 50, 45, 40, 35, 30, 25, 20, 15, 10)
ys <- c(3.72, 3.27, 3.05, 2.60, 2.56, 2.35, 2.20, 1.74, 1.36,
3.34, 3.32, 3.23, 2.79, 2.66, 2.51, 2.11, 1.99, 1.41)
plot(xs, ys); grid()
Define a function that for a given drag force calculates the fall times for different heights.
fall_time <- function(h, Fd) {
# Solve the DE for height h and drag force Fd
sol <- pracma::ode23(ffall, 0, 10, c(h, 0), Fd = Fd)
# Define an interpolated function
w = function(t)
pracma::interp1(sol$t, sol$y[, 1], t, method = "cubic")
# Calculate the falling time
t0 <- pracma::ridders(w, 0, 10)$root
return( t0 )
}
rms_fall_time <- function(Fd) {
n <- length(xs)
s <- numeric(n)
for (i in 1:n) {
s[i] <- fall_time(xs[i], Fd)
}
# cbind(xs, ys, s)
sum((ys - s)^2)
}
Fd = seq(0, 0.02, length.out = 101)
ts = numeric(101)
for (i in 1:101) {
ts[i] <- rms_fall_time(Fd[i])
}
plot(Fd, ts, type = 'l', col = "darkblue"); grid()
Now compute the optimal drag force for the data.
( ccmin <- pracma::fminbnd(rms_fall_time, 0, 0.02)$xmin ) # optimize
## [1] 0.01160236
The ‘drag force’ \(F_d\) and the ‘drag coefficient’ \(c_w\) are connected through
\[ F_d = {cw} \cdot \rho/2 \cdot A \]
where \(\rho = 1.225 \, [kg/m^3]\) is the air density on ocean level, \(A\) the area of projection onto a plane (of a ball of circumference \(U = 0.75 [m]\)), i.e. \(A = \pi \cdot 0.12^2 = 0.045 \, [m^2]\).
So we calculate the \(c_w\) value as
rho = 1.225
A = 0.045
cw = ccmin / (rho/2 * A)
cw
## [1] 0.4209475
which corresponds nicely with the cited \(c_w\) value for balls (and spheres in general) of values between 0.40 and 0.45 !
Contrary to “inital value problems” (IVP), “boundary value problems” fix the values of the solution at begin and end of the solution interval.
A beam that is fixed at its ends bends according to its elasticity. Technical Mechanics tells us it follows the formula
\[ y'' - \frac{T}{E\ I}\ y = \frac{wx(x-L)}{2\ E\ I} \]
Let the beam be of uniform thickness so that the product \(E\,I\) is constant, As an example we will assume the following parameters:
T <- 500 # axial tension
E <- 10e07 # modulus of elasticity
I <- 500 # central moment of inertia
w <- 100 # uniform transverse load
L <- 100 # beam length
beam <- function(x, y) {
dy1 <- y[2]
dy2 <- T/(E*I)*y[1] + w*x*(x-L)/(2*E*I)
c(dy1, dy2)
}
This diffeential equation is of the form
\[ y'' = f(x)\ y'' + g(x)\ y + h(x) \]
with boundary value conditions \(y(0) = y(100) = 0\). Such differential
equations (BVP) can be directly solved with the bvp
function in ‘pracma’.
f <- function(x) rep(0, length(x))
g <- function(x) rep(T/(E*I), length(x))
h <- function(x) w*x*(x-L)/(2*E*I)
sol <- pracma::bvp(f, g, h, c(0, L), c(0, 0))
cat("Maximal deflection is:", max(sol$ys), "[m].")
## Maximal deflection is: 0.002603739 [m].
The height of the surface of a water droplet satisfies
\[ y'' = (y-1)*(1+y'^2)^{1.5} \]
with \(y(1.0) = y(-1.0) = 0\).
We try to solve it with a shooting method from the ‘bvpSolve’ package.
droplet <- function(t, y, params) {
dy1 <- y[2]
dy2 <- (y[1] - 1) * (1 + y[2]^2)^(3/2)
return( list(c(dy1, dy2)) )
}
init <- c(0, NA) # no assumtion about the slope at end points
end <- c(0, NA)
sol <- bvpSolve::bvptwp(yini = init, x = seq(-1, 1, length.out = 101),
fun = droplet, yend = end)
xs <- sol[, 1]
ys <- sol[, 2]
plot(xs, ys, type = 'l', col = 4, asp = 1, bty = 'n',
xlab = "", ylab = "", main = "Form of a droplet")
grid()
polygon(c(xs, -1), c(ys, 0), col = 4, border = "gray50")
One can verify that the form of the surface does not follow a parabola.
In 2012, Felix Baumgartner broke the highest altitude and longest-distance free fall record when he had himself transported in a helium balloon and pressurized capsule to a height of 39 km altitude to jump from there with a protective suit and parachute. He reached with 1343 km/h a and opened the parachute after about three and a half minutes in free fall at a height of 2500~m above the earth.
The flow resistance in a gas depends on its density and pressure. Therefore, we first calculate the air pressure of the earth’s atmosphere depending on the altitude.
Let \(p(h)\) and \(\rho(h)\) describe air pressure and air density of our atmosphere at \(h\) meter above sea level. Pressure \(p_0\) and density \(ρ_0\) at \(0\) meter (and 0°C, we neglect lower temperatures in the stratosphere) are:
g <- 9.80665 # [m/s^2] Erdbeschleunigung
p_0 <- 101325 # [Pa] Luftdruck in Meereshoehe
rho_0 <- 1.293 # [kg/m^3] Luftdichte in Meereshöhe
According to Boyle-Mariotte’s law (for ideal gases), pressure and density of a gas are are proportional to each other, i.e. \(\rho(h)/\rho(0) = p(h)/p(0)\) or
\[ \rho(h) = \frac{p(h)}{p_0} \rho_0 \]
For (compressible) ideal gases, the barometric altitude formula also applies, according to which pressure varies proportionally with density and the acceleration due to gravity g (which we we assume to be constant at these altitudes); i.e. \(\mathrm{d}p / \mathrm{d}h = -g \cdot \rho(h)\) or
\[ p(h) = p_0 \ e^{-g \ \frac{\rho_0}{p_0} \ h} \]
air_pressure <- function(h) p_0 * exp(-g*rho_0/p_0*h) # in [Pa]
air_density <- function(h) air_pressure(h)/p_0*rho_0 # in [kg/m^3]
The differential equation for free fall is
\[ y'' = -g + c_w \frac{\rho}{2}A\ v^2 \]
and therefore the right hand side function is
fn <- function(t, y, p) {
cw <- 0.09; A <- 0.06
dy1 <- y[2]
dy2 <- -g + cw * air_density(y[1])/2 * A * y[2]^2
return( list(c(dy1, dy2)) )
}
the differential equation is solved with
y0 <- c(39000, 0)
tseq <- seq(0, 300, by = 1.0)
sol <- deSolve::ode(y = y0, times = tseq, func = fn, p = NULL)
and the solution is plotted – the red line indication “speed of sound”.
plot(sol[, 1], sol[, 3], type = 'l', col = "darkblue",
main = "Velocity during space dive",
xlab = "Time (in seconds)", ylab = "velocity (in m/s)")
abline(h = -330, col = "red", lty = 2)
grid()
We can see that for a short time span the speed of sound was reached.
Imagine a pendulum whose mounting suspension is moved back and forth with its own frequency, independent of the pendulum. This is an example of a differential equation where the right hand side also depends on the independent variable of time.
\[ z'' = -g/L\ \sin(z) + \frac{\omega^2}{L}\ Y\cos(z)\sin(\omega\ t) \]
As before, we convert this function into an R function, now also
containing the argument t.
fun <- function(t, z) {
g <- 9.81
L <- 1.0
Y <- 0.25
w <- 2.5
dz1 <- z[2]
dz2 <- -g/L*sin(z[1]) + w^2/L*Y*cos(z[1])*sin(w*t)
c(dz1, dz2)
}
sol <- pracma::ode45(fun, 0, 60, c(0, 0))
plot(sol$t, sol$y[, 1], type = 'l', lwd = 1.5, col = "darkblue",
ylim = c(-1, 1), xlab = "time", ylab = "elongation",
main = "Force acting on a pendulum")
grid()
We find an almost chaotic solution, the oscillations being interrupted by somewhat non-predictable exceptions.
We can enhance the Lotka-Volterra model by assuming that the number of prey cannot grow indefinitely, but is confined by the resources of the habitat. This restriction is often expressed with the logistic equation.
\[ \begin{align} dx/dt &= a\ (1 - \frac{x(t)}{x_{max}})\ x(t) - b\ x(t)\ y(t)\\ dy/dt &= -c\ y(t) + d\ x(t)\ y(t) \end{align} \]
Define two right hand side functions, one without such a limitation, the other one relying on it.
fn1 <- function(t, y, p) {
a <- p[1]; b <- p[2]
c <- p[3]; d <- p[4]
dx <- a*(1 - y[1]/3.0)*y[1] - b*y[1]*y[2]
# dx <- a*y[1] - b*y[1]*y[2]
dy <- -c*y[2] + d*y[2]*y[1]
c(dx, dy)
}
fn2 <- function(t, y, p) {
a <- p[1]; b <- p[2]
c <- p[3]; d <- p[4]
# dx <- a*(1 - y[1]/2.0)*y[1] - b*y[1]*y[2]
dx <- a*y[1] - b*y[1]*y[2]
dy <- -c*y[2] + d*y[2]*y[1]
c(dx, dy)
}
p0 <- c(2/3, 4/3, 1, 1)
ic <- c(1.0, 1.0)
sol1 <- pracma::ode45(fn1, 0, 50, ic, p = p0)
sol2 <- pracma::ode45(fn2, 0, 50, ic, p = p0)
plot(c(0, 50), c(0, 2.0), type = 'n',
xlab = "time", ylab = "", main = "Lotka-Volterra equations")
lines(sol1$t, sol1$y[, 1], lty = 2, col = 1)
lines(sol1$t, sol1$y[, 2], lty = 2, col = 2)
lines(sol2$t, sol2$y[, 1], lwd = 2, col = 1)
lines(sol2$t, sol2$y[, 2], lwd = 2, col = 2)
grid()
The dashed lines show an example of a finite habitat. In this case the solution converges to a stable and constant situation, as is seen quite often in real biological studies.
The trajectory of a ball has to be modeled in an \((x, y)\)-plane (leaving out the third dimension). Gravitation pulls it down in the negative \(y\) direction, but air resistance acts in both directions. Considering the actual velocity \(v\) and its projection in \(x\)- and \(y\)-directions, reasonable model equations are:
\[ \begin{align} x''(t) &= -r\ v\ x'(t)\\ y''(t) &= -r\ v\ y'(t) - g \end{align} \]
When the second derivatives are eliminated, we come up with the following system of differential equations:
\[ \begin{align} y_1' &= y_2\\ y_2' &= -r\ v\ y_2\\ y_3' &= y_4\\ y_4' &= -r\ v\ y_4 - g \end{align} \]
where \(v = v(t) = \sqrt{x'^2 + y'^2} = \sqrt{y_2^2 + y_4^2}\) is the speed of the ball. We assume a drag coefficient of \(cw = 0.09\) and a cross-sectional area of \(A = 0.0365\) [m^2] for a diameter of \(d = 21.5\) cm for a standardized soccer ball.
We estimate the drag force from the geometry of a soccer ball.
g <- 9.81
cw <- 0.4
rho <- 1.2
A <- 0.0365
( r <- cw * rho/2 * A )
## [1] 0.00876
r <- 0.009
sball <- function(t, y) {
v <- sqrt(y[2]^2+y[4]^2)
dy1 <- y[2]
dy2 <- -r*v*y[2]
dy3 <- y[4]
dy4 <- -r*v*y[4] - g
c(dy1, dy2, dy3, dy4)
}
Next assume the ball is shot with a velocity of 100 km/h or 28 m/s (not extremely hard). We handle the launching angel (between 35 and 50 degrees) as an argument to the initial conditions.
v0 <- 28 # velocity in m/s
w0 <- 45 # angle in degrees
y0 <- c(0, v0*pracma::cosd(w0), 0, v0*pracma::sind(w0))
sol <- pracma::ode45(sball, 0, 5, y0, atol = 1e-8)
traj <- function(t) pracma::interp1(sol$t, sol$y[, 3], t)
t1 <- pracma::ridders(traj, 1, 5)$root
sol <- pracma::ode45(sball, 0, t1, y0, atol = 1e-8)
plot(sol$y[, 1], sol$y[, 3], type = 'l', col = "darkblue",
xlab = "x", ylab = "y", main = "Trajectory of a soccer ball")
grid()
We see how the ball appears to “drop down” during the second part of its flight, unlike it would fly on a parabola without air resistance.
Kicked with an angle of 45 degrees it lands at 52.7 m, while for instance with an angle of 40 degrees it lands at 53 m. Without air resistance the maximum width will be achieved with exactly 45 degrees!
Epidemics are often model through so-called SIR models with
Here we treat one of the simplest SIR models, the Kermack-McKendrick model as a system of differential equations.
\[ \begin{align} S'(t) &= - \alpha\ I(t)\ S(t)\\ I'(t) &= \alpha\ I(t)\ S(t) - \beta\ I(t)\\ R'(t) &= \beta\ I(t) \end{align} \]
The corresponding R function is
SIRmod <- function(t, y, p) {
a <- p[1]; b <- p[2]
dy1 <- -a*y[2]*y[1]
dy2 <- a*y[2]*y[1] - b*y[2]
dy3 <- b*y[2]
c(dy1, dy2, dy3)
}
and for instance we solve it with the following parameters and initial conditions.
a <- 0.6; b = 0.35
y0 <- c(0.99999, 0.00001, 0)
tspan <- c(0, 100)
sol <- pracma::ode45(SIRmod, 0, 100, y0, p = c(a, b), atol = 1e-10)
plot(c(0, 100), c(0, 1), type = 'n',
xlab = "days", ylab = "number of ... persons", main = "SIR model")
grid()
lines(sol$t, sol$y[, 1], col = 1, lwd = 1.5)
lines(sol$t, sol$y[, 2], col = 2, lwd = 1.5)
lines(sol$t, sol$y[, 3], col = 3, lwd = 1.5)
legend(0, 0.8
, legend = c("susceptible", "infected", "recovered"),
, col = c(1,2,3), lwd = 1.5, bty = 'n')
Find more realistic parameters by solving the system of equations numerically, for a variety of initial conditions.
See the ‘odin’ package for more advanced SIR models (deterministic and stochastic ones) and plots of their solutions.
Solve the following Lorentz equation system of
differential equations with the ode solver of the ‘deSolve’
package.
\[ \begin{align} \dot{x} & = -\sigma (x-y)\\ \dot{y} & = r x - y - x z\\ \dot{z} & = x y - b z \end{align} \]
For the ‘deSolve’ package the function must be defined as `function(t, y, parameters, …), parameters required, and must return a list!
fun <- function(t, y, p) {
s <- p[1]; r <- p[2]; b <- p[3];
dx <- -s*(y[1] - y[2])
dy <- r*y[1] - y[2] - y[1]*y[3]
dz <- y[1]*y[2] - b*y[3]
return( list(c(dx, dy, dz)) )
}
We will apply the original parameters used by Lorentz (\(\sigma=10; r=28; b=8/3\)) and his initial conditions (\(x=30; y=10; z=40\)).
p0 <- c(s=10, r=28, b=8/3)
y0 <- c(x = 30, y = 10, z = 40)
tseq <- seq(0, 20, length.out = 1001)
sol <- deSolve::ode(y = y0, times = tseq, func = fun, p = p0, # or: = NULL
method = "lsoda")
‘lsoda’ is the default method; for other available methods see below.
scatterplot3d::scatterplot3d(sol[, 'x'], sol[, 'y'], sol[, 'z'],
type = 'l', color = "navy",
xlab = 'x', ylab = 'y', zlab = 'z',
main = "Lorentz Equation: Chaotic behavior")
Other solvers available in ‘deSolve’ are:
| rkMethod | Description |
|---|---|
| “euler” | Euler’s Method |
| “rk2” | 2nd order Runge-Kutta, fixed time step (Heun’s method) |
| “rk4” | classical 4th order Runge-Kutta, fixed time step |
| “rk23” | Runge-Kutta, order 2(3); Octave: ode23 |
| “rk23bs”, “ode23” | Bogacki-Shampine, order 2(3); Matlab: ode23 |
| “rk34f” | Runge-Kutta-Fehlberg, order 3(4) |
| “rk45ck” | Runge-Kutta Cash-Karp, order 4(5) |
| “rk45f” | Runge-Kutta-Fehlberg, order 4(5); Octave: ode45, pair=1 |
| “rk45e” | Runge-Kutta-England, order 4(5) |
| “rk45dp6” | Dormand-Prince, order 4(5), local order 6 |
| “rk45dp7”, “ode45” | Dormand-Prince 4(5), local order 7 (also known as dopri5; MATLAB: ode45; Octave: ode45, pair=0) |
| “rk78f” | Runge-Kutta-Fehlberg, order 7(8) |
| “rk78dp” | Dormand-Prince, order 7(8) |
TO BE DONE.
The van-der-Pol equation describes stable oscillations in electrical circuits and is given by the following formula:
\[ y'' - \mu (1 - y^2)\ y' + y = 0 \]
I wanted to solve it with the help of the ‘sundialr’ package, but did
not immediately succeed. Here is a solution with the ode45
solver.
vdP <- function(t, y, mu) {
dy1 <- y[2]
dy2 <- mu*(1-y[1]^2)*y[2] - y[1]
c(dy1, dy2)
}
mu1 <- 0.4; y1 <- c(0.1, 0.0)
mu2 <- 2.0; y2 <- c(1.0, 1.0)
sol1 = pracma::ode45(vdP, 0, 30, y1, mu = mu1)
sol2 = pracma::ode45(vdP, 0, 30, y2, mu = mu2)
plot(c(0, 30), c(-3, 3), type = 'n',
xlab = "time", ylab = "", main = "van-der-Pol equation")
lines(sol1$t, sol1$y[, 1], lwd = 1.5, col = "navy")
lines(sol2$t, sol2$y[, 1], lwd = 1.5, col = "firebrick")
grid()
Demonstration of how to calculate the solution with the solvers
cvode(s) (for stiff ODEs) in the ‘sundialr’ package will
follow later.
See “Notes on JuliaCall” how to solve differential equations in R utilizing the ‘DifferentialEquations.jl’ package in Julia. These Julia solvers can also be called directly through the ‘diffeqr’ R package.
TO BE DONE
Another example: There are three chemical substances in a reactor with initial concentrations of 0.6, 0.2, and 0.2. The chemical engineer tells us that the concentrations of the substances change in the presence of the other substances according to the following laws:
\[ \begin{align} c_1' &= -k1c1 + k3c3\\ c_2' &= +k1c1 - k2c2\\ c_3' &= +k2c2 - k3c3 \end{align} \]
The inital concentrations are \(k1=0.3, k2=0.2, k3=0.5\).The concentrations after a suitable reaction time are to be determined.
# library(diffeqr)
de <- diffeqr::diffeq_setup()
## Julia version 1.7.2 at location /home/hwb/Programs/julia-1.7.2/bin will be used.
## Loading setup script for JuliaCall...
## Finish loading setup script for JuliaCall.
conc <- function(u, p, t) {
du1 <- -p[1]*u[1] + p[3]*u[3]
du2 <- p[1]*u[1] - p[2]*u[2]
du3 <- p[2]*u[2] - p[3]*u[3]
c(du1, du2, du3)
}
u0 <- c(0.3, 0.2, 0.5) # initial concentrations
p0 <- c(0.55, 0.15, 0.30) # reaction constants
tspan <- c(0, 20) # 20 minutes
problem <- de$ODEProblem(conc, u0, tspan, p0)
solution <- de$solve(problem)
It can be convenient to turn this solution structure into an R matrix with sapply, having each row as a time series. The transpose of the matrix can be turned into a data frame.
mat <- sapply(solution$u, identity)
udf <- as.data.frame(t(mat))
matplot(solution$t, udf, type = 'l', col = c(1, 2, 4),
lwd = 1.5, lty = 1,
xlab = "time", ylab = "concentration", main = "Reaction profile")
grid()
K. Soetaert and Th. Petzoldt. ‘Differential Equations’. CRAN Task
View 2021
URL: https://CRAN.R-project.org/view=DifferentialEquations
K. Soetaert and P. M. J. Herman. A Practical Guide to Ecological
Modelling
Using R as a Simulation Platform. Springer Science+Business Media,
2009
K. Soetaert, J. Cash, and F. Mazzia. Solving Differential
Equations in R.
UseR! series, Springer Verlag, Berlin Heidelberg 2012.
H. W. Borchers. Notes on JuliaCall (section on Differential
Equations).
URL: https://hwborchers.github.io/, Github.com 2021.
G. Teschl. Ordinary Differential Equations and Dynamical
Systems.
American Mathematical Society, Providence, Rhode Island, 2011.