Wageningen University & Research | FEM-31806 | Models for Ecological Systems | FEM | PPS | WEC
Compare different methods of integration for different time
steps. Note that you can simply set these as arguments for your model
calling function ode. Compare the Euler method with a
variable time-step 4th order Runga-Kutta method (“ode45”). Run the model
with Euler for a time step of 1, 1.5 and 1.75 years. Save the output
with computed states for these runs with Euler and save also one output
for a run with ode45 as method. For a direct comparison with output at
exactly the same moments in time for both integration methods, you can
define the variable delt and adjust the sequence that you
use to define the output for time (e.g.:
seq(0, 1000, by=delt)). Note that the actual time-steps
taken by when using the ode45 integration method will be smaller as the
time step is in this method adjusted when required.
Plot the state variables of the 4 simulations against time in the
same plot. Alternatively, plot the state variables of each of the three
Euler simulations with those computed with the ode45 integration method
in a plot. Limit the plotting ranges for the x- and y by setting
xlim = c(... , ...) and ylim = c(... , ...) to
zoom in to times when the model outputs quickly change. What can you
roughly conclude for the appropriate time step using Euler?
You can also calculate the relative deviation of Euler when compared to the ode45 results (by computing |Euler state-RK state|/RK state , for example at time = 50 years). Within what range of time-step values do state variables not differ more than 1% after a reasonable time period (50 years)?
Develop an equation to calculate the time coefficients for each of the state variables. Implement them and plot time coefficients against time.
Download here the code as shown on this page in a separate .r file.
To check for the difference between Euler and Runge-Kutta
integration, let’s solve the model numerically for 50 time-units in
time-steps of 1, 1.5 and 1.75 (the fixed time-step of the
euler method), as well as with a variable time-step
Runge-Kutta method (ode45, here with output every
time-step):
states_rk <- ode(y = InitCO2fix(),
times = seq(from = 0, to = 50, by = 1),
parms = ParmsCO2fix(),
func = RatesCO2fix,
method = "ode45")
delt <- 1
states_euler1 <- ode(y = InitCO2fix(),
times = seq(from = 0, to = 50, by = delt),
parms = ParmsCO2fix(),
func = RatesCO2fix,
method = "euler")
delt <- 1.5
states_euler2 <- ode(y = InitCO2fix(),
times = seq(from = 0, to = 50, by = delt),
parms = ParmsCO2fix(),
func = RatesCO2fix,
method = "euler")
delt <- 1.75
states_euler3 <- ode(y = InitCO2fix(),
times = seq(from = 0, to = 50, by = delt),
parms = ParmsCO2fix(),
func = RatesCO2fix,
method = "euler")
We can have a look at the output of the last time-steps (for all 4 solutions):
tail(states_rk)
## time WF WR WS WH WP WT WW DWFDT DWRDT DWSDT DWHDT DWPDT DWTDT
## [46,] 45 0.2978606 1.092157 3.931755 2.615400 0.2617871 14.16006 9.657869 -2.135739e-08 -5.291068e-07 1.403259e-06 0.0008653870 4.954577e-05 0.2405713
## [47,] 46 0.2978606 1.092157 3.931756 2.616204 0.2618331 14.39827 9.673938 -1.584266e-08 -4.046576e-07 1.053836e-06 0.0007449584 4.264028e-05 0.2358714
## [48,] 47 0.2978606 1.092157 3.931757 2.616896 0.2618727 14.63183 9.688513 -1.176274e-08 -3.092030e-07 7.917345e-07 0.0006412765 3.669775e-05 0.2312557
## [49,] 48 0.2978606 1.092156 3.931757 2.617491 0.2619068 14.86081 9.701731 -8.741324e-09 -2.360721e-07 5.950428e-07 0.0005520155 3.158377e-05 0.2267237
## [50,] 49 0.2978606 1.092156 3.931758 2.618004 0.2619361 15.08530 9.713718 -6.501606e-09 -1.801030e-07 4.473731e-07 0.0004751721 2.718272e-05 0.2222749
## [51,] 50 0.2978606 1.092156 3.931758 2.618445 0.2619614 15.30539 9.724587 -4.839785e-09 -1.373092e-07 3.364623e-07 0.0004090204 2.339513e-05 0.2179086
## DWWDT
## [46,] 0.01686474
## [47,] 0.01529795
## [48,] 0.01387496
## [49,] 0.01258283
## [50,] 0.01140973
## [51,] 0.01034489
tail(states_euler1)
## time WF WR WS WH WP WT WW DWFDT DWRDT DWSDT DWHDT DWPDT DWTDT
## [46,] 45 0.2978606 1.092156 3.931759 2.617694 0.2619185 14.18879 9.688503 -2.153625e-09 -7.654115e-08 1.661031e-07 0.0005218025 2.982819e-05 0.2401804
## [47,] 46 0.2978606 1.092156 3.931759 2.618216 0.2619483 14.42897 9.702419 -1.537631e-09 -5.533180e-08 1.193183e-07 0.0004435487 2.535347e-05 0.2354186
## [48,] 47 0.2978606 1.092156 3.931759 2.618659 0.2619736 14.66439 9.714969 -1.098662e-09 -3.998469e-08 8.573146e-08 0.0003770284 2.155010e-05 0.2307457
## [49,] 48 0.2978606 1.092155 3.931759 2.619036 0.2619952 14.89514 9.726286 -7.855592e-10 -2.888471e-08 6.161232e-08 0.0003204827 1.831733e-05 0.2261609
## [50,] 49 0.2978606 1.092155 3.931759 2.619357 0.2620135 15.12130 9.736491 -5.620460e-10 -2.085990e-08 4.428743e-08 0.0002724164 1.556955e-05 0.2216634
## [51,] 50 0.2978606 1.092155 3.931759 2.619629 0.2620291 15.34296 9.745691 -4.023655e-10 -1.506050e-08 3.183986e-08 0.0002315584 1.323399e-05 0.2172519
## DWWDT
## [46,] 0.013915631
## [47,] 0.012550141
## [48,] 0.011317292
## [49,] 0.010204405
## [50,] 0.009199983
## [51,] 0.008293600
tail(states_euler2)
## time WF WR WS WH WP WT WW DWFDT DWRDT DWSDT DWHDT DWPDT DWTDT
## [29,] 42.0 0.2978599 1.092156 3.931759 2.616865 0.2618711 13.44663 9.655125 7.886133e-07 -3.533391e-08 1.664864e-07 0.0006461048 3.692642e-05 0.2549574
## [30,] 43.5 0.2978610 1.092156 3.931759 2.617834 0.2619265 13.82906 9.680941 -5.375396e-07 -4.437463e-08 2.923109e-08 0.0005007562 2.861975e-05 0.2473862
## [31,] 45.0 0.2978602 1.092155 3.931759 2.618586 0.2619694 14.20014 9.702960 3.641736e-07 -9.633215e-09 6.258793e-08 0.0003880905 2.217850e-05 0.2400247
## [32,] 46.5 0.2978608 1.092155 3.931759 2.619168 0.2620027 14.56018 9.721732 -2.480020e-07 -1.658745e-08 5.249897e-09 0.0003007795 1.718922e-05 0.2328706
## [33,] 48.0 0.2978604 1.092155 3.931759 2.619619 0.2620285 14.90949 9.737732 1.681475e-07 -2.176906e-09 2.411977e-08 0.0002331049 1.332085e-05 0.2259206
## [34,] 49.5 0.2978607 1.092155 3.931759 2.619969 0.2620485 15.24837 9.751365 -1.144331e-07 -6.334554e-09 -3.369122e-10 0.0001806599 1.032408e-05 0.2191709
## DWWDT
## [29,] 0.017211279
## [30,] 0.014679342
## [31,] 0.012514093
## [32,] 0.010666687
## [33,] 0.009088826
## [34,] 0.007743263
tail(states_euler3)
## time WF WR WS WH WP WT WW DWFDT DWRDT DWSDT DWHDT DWPDT DWTDT
## [24,] 40.25 0.3350770 1.092934 3.933870 2.615526 0.2617257 12.98520 9.587361 -0.04168243 -0.0009779823 -0.002760613 1.058181e-03 1.250792e-04 0.2642477
## [25,] 42.00 0.2621328 1.091223 3.929039 2.617377 0.2619446 13.44763 9.701458 0.03996850 0.0009477095 0.002683908 2.973015e-04 -4.395022e-05 0.2547607
## [26,] 43.75 0.3320777 1.092881 3.933736 2.617898 0.2618676 13.89346 9.654012 -0.03832255 -0.0009015249 -0.002545362 6.889438e-04 9.884857e-05 0.2462614
## [27,] 45.50 0.2650132 1.091304 3.929282 2.619103 0.2620406 14.32442 9.750840 0.03674931 0.0008727345 0.002471110 6.265765e-05 -5.261858e-05 0.2373824
## [28,] 47.25 0.3293245 1.092831 3.933606 2.619213 0.2619485 14.73984 9.700512 -0.03523849 -0.0008308293 -0.002346135 4.786543e-04 8.210455e-05 0.2294287
## [29,] 49.00 0.2676571 1.091377 3.929500 2.620051 0.2620922 15.14134 9.783980 0.03379382 0.0008035668 0.002274931 -5.756612e-05 -5.509876e-05 0.2211373
## DWWDT
## [24,] 0.06519874
## [25,] -0.02711211
## [26,] 0.05533027
## [27,] -0.02875917
## [28,] 0.04769601
## [29,] -0.02908449
Now, you can plot all solutions together in a single figure using:
plot(states_rk, states_euler1,states_euler2, states_euler3)


The black line now is the Runge-Kutta solution (with variable
time-step), the dashed red line is the first Euler solution (with
time-step of 1), the dashed green line is the second Euler solution
(with time-step of 1.5), and the dashed blue line is the third Euler
solution (with time-step of 1.75). To focus a bit more on the first time
steps, we can limit the x-axis to a range between 0 and 10 using the
xlim argument:
plot(states_rk, states_euler1,states_euler2, states_euler3, xlim = c(0,10))


To get the relative difference between the two integration method at time 50 (here as a fraction of the reference ode45, rounded off to 4 digits):
round(100 * tail(states_euler1, 1)[,-1] / tail(states_rk,1)[,-1], 4)
## WF WR WS WH WP WT WW DWFDT DWRDT DWSDT DWHDT DWPDT DWTDT DWWDT
## 100.0000 100.0000 100.0000 100.0452 100.0258 100.2455 100.2170 8.3137 10.9683 9.4631 56.6129 56.5673 99.6987 80.1710
round(100 * tail(states_euler2, 1)[,-1] / tail(states_rk,1)[,-1], 4)
## WF WR WS WH WP WT WW DWFDT DWRDT DWSDT DWHDT DWPDT DWTDT DWWDT
## 100.0000 100.0000 100.0000 100.0582 100.0333 99.6274 100.2754 2364.4260 4.6134 -0.1001 44.1689 44.1292 100.5793 74.8511
round(100 * tail(states_euler3, 1)[,-1] / tail(states_rk,1)[,-1], 4)
## WF WR WS WH WP WT WW DWFDT DWRDT DWSDT DWHDT
## 8.985990e+01 9.992870e+01 9.994260e+01 1.000613e+02 1.000500e+02 9.892820e+01 1.006107e+02 -6.982504e+08 -5.852244e+05 6.761325e+05 -1.407410e+01
## DWPDT DWTDT DWWDT
## -2.355138e+02 1.014817e+02 -2.811483e+02
and here as a relative difference:
round(100 * abs ( (tail(states_euler1, 1)[,-1] - tail(states_rk,1)[,-1]) / tail(states_rk,1)[,-1] ), 4)
## WF WR WS WH WP WT WW DWFDT DWRDT DWSDT DWHDT DWPDT DWTDT DWWDT
## 0.0000 0.0000 0.0000 0.0452 0.0258 0.2455 0.2170 91.6863 89.0317 90.5369 43.3871 43.4327 0.3013 19.8290
round(100 * abs ( (tail(states_euler2, 1)[,-1] - tail(states_rk,1)[,-1]) / tail(states_rk,1)[,-1] ), 4)
## WF WR WS WH WP WT WW DWFDT DWRDT DWSDT DWHDT DWPDT DWTDT DWWDT
## 0.0000 0.0000 0.0000 0.0582 0.0333 0.3726 0.2754 2264.4260 95.3866 100.1001 55.8311 55.8708 0.5793 25.1489
round(100 * abs ( (tail(states_euler3, 1)[,-1] - tail(states_rk,1)[,-1]) / tail(states_rk,1)[,-1] ), 4)
## WF WR WS WH WP WT WW DWFDT DWRDT DWSDT DWHDT DWPDT
## 1.014010e+01 7.130000e-02 5.740000e-02 6.130000e-02 5.000000e-02 1.071800e+00 6.107000e-01 6.982505e+08 5.853244e+05 6.760325e+05 1.140741e+02 3.355138e+02
## DWTDT DWWDT
## 1.481700e+00 3.811483e+02
As you can see in these values, as well as in the figures above: the Euler integration method with smaller time-step better approaches the variable time-step Runge-Kutta method. Using the Euler integration method, when the time-step increases, the deviation from the Runge-Kutta solution increases. Thus, this pattern demonstrates the expected pattern of time-step convergence.
To get the information needed to compute the time coefficients (the
value of the state variables when the system is in equilibrium, but also
the rates of change of the state variables over time), we have to change
AUX <- NULL in function RatesCO2fix to
AUX <- RATES.
## time WF WR WS WH WP WT WW DWFDT DWRDT DWSDT DWHDT DWPDT DWTDT
## [1,] 0 0.0200000 0.0200000 0.0600000 0.00000000 0.00000000 0.00000000 0.0000000 0.257495098 0.27349510 0.8234853 0.00600000 0.00120000 0.00480000
## [2,] 1 0.2774951 0.2934951 0.8834853 0.00600000 0.00120000 0.00480000 0.0310000 0.099690951 0.31688703 0.9939354 0.08744853 0.01718971 0.07106282
## [3,] 2 0.3771860 0.6103821 1.8774206 0.09344853 0.01838971 0.07586282 0.4656674 -0.038035918 0.19375410 0.6612510 0.17372479 0.03022253 0.15615228
## [4,] 3 0.3391501 0.8041362 2.5386717 0.26717331 0.04861224 0.23201510 1.1156634 -0.012560376 0.11926390 0.4468464 0.21379117 0.03181078 0.21982730
## [5,] 4 0.3265898 0.9234001 2.9855181 0.48096449 0.08042302 0.45184240 1.7586953 -0.009828462 0.07240023 0.3018813 0.22640713 0.02911814 0.26828176
## [6,] 5 0.3167613 0.9958004 3.2873994 0.70737162 0.10954116 0.72012416 2.3924187 -0.006319573 0.04337774 0.2045037 0.22263419 0.02512484 0.30517920
## DWWDT
## [1,] 0.0310000
## [2,] 0.4346674
## [3,] 0.6499960
## [4,] 0.6430319
## [5,] 0.6337234
## [6,] 0.6076742


Then, we can define a function that computes the time coefficient; it needs as input a solved model, as well as the time at which the system has reached equilibrium. With this information, we can compute the time coefficients:
tcCO2fix <- function(states, time_equil) {
# Find index for row with states that are in equilibrium
ii <- which(states[,"time"] == time_equil)
# Compute TC for all states using general TC formula
TC <- data.frame(time = states[,"time"],
TC_WF = abs( (states[,"WF"] - states[ii,"WF"]) / states[,"DWFDT"] ),
TC_WR = abs( (states[,"WR"] - states[ii,"WR"]) / states[,"DWRDT"] ),
TC_WS = abs( (states[,"WS"] - states[ii,"WS"]) / states[,"DWSDT"] ),
TC_WP = abs( (states[,"WP"] - states[ii,"WP"]) / states[,"DWPDT"] ))
return(TC)
}
With a solved model, we can now thus compute the time coefficients, and plot how they vary over time:
states_rk <- ode(y = InitCO2fix(),
times = seq(from = 0, to = 50, by = 1),
parms = ParmsCO2fix(),
func = RatesCO2fix,
method = "ode45")
TC <- tcCO2fix(states=states_rk, time_equil = 30)
par(mfrow = c(2,4))
plot(states_rk[,"time"], states_rk[,"WF"], ylab = "WF", xlab = "time", type = "l", col = "black")
plot(states_rk[,"time"], states_rk[,"WR"], ylab = "WR", xlab = "time", type = "l", col = "black")
plot(states_rk[,"time"], states_rk[,"WS"], ylab = "WS", xlab = "time", type = "l", col = "black")
plot(states_rk[,"time"], states_rk[,"WP"], ylab = "WP", xlab = "time", type = "l", col = "black")
plot(TC[,"time"], TC[,"TC_WF"], ylab = "TC WF", xlab = "time", type = "l", col = "black")
plot(TC[,"time"], TC[,"TC_WR"], ylab = "TC WR", xlab = "time", type = "l", col = "black")
plot(TC[,"time"], TC[,"TC_WS"], ylab = "TC WS", xlab = "time", type = "l", col = "black")
plot(TC[,"time"], TC[,"TC_WP"], ylab = "TC WP", xlab = "time", type = "l", col = "black")

par(mfrow = c(1,1))
As we can see, the time coefficients vary quite a bit over time. We may be interested in the minimum time coefficient during times where the system is not yet in equilibrium. Let’s say from the plots that this is up to time 20 We can thus compute the minimum time coefficient up to that time:
TC_sub = subset(TC, time <= 20)
min(TC_sub$TC_WF)
## [1] 0.3755233
min(TC_sub$TC_WR)
## [1] 0.2652315
min(TC_sub$TC_WS)
## [1] 3.063995
min(TC_sub$TC_WP)
## [1] 5.102101
Thus, the minimum time coefficient seems to be ca 0.105.
To analyse the mass balance of the model, let’s copy-paste the “InitCO2Fix” and “RatesCO2Fix” functions as defined earlier, and add 2 states (IN and OUT), at initial value 0:
InitCO2fix <- function(WF = 0.02, # WF, kgC in Foliage/m2
WR = 0.02, # WR, kgC in Roots/m2
WS = 0.06, # WS, kgC in Stems/m2
WH = 0.0, # WH, kgC in Stems/m2
WP = 0.0, # WP kgC in paper/ m2
WT = 0.0, # WT, kgC in timber/m2
WW = 0.0 # WW, kgC in waste/m2
){
# Gather initial conditions in a named vector; given names are names for the state variables in the model
y <- c(WF = WF,
WR = WR,
WS = WS,
WH = WH,
WP = WP,
WT = WT,
WW = WW,
IN = 0.0,
OUT = 0.0)
# Return
return(y)
}
In the function computing the rates of change of the state variables with respect to time, we now have to include the rates of change of “IN” and “OUT” as well:
### SAME as earlier, but with specification of mass balance terms MB
RatesCO2fix <- function(t, y, parms) {
# use the with() function to be able to access the parameters and states easily
# remember that the with() function is with(data, expr), where
# - 'data' is ideally a list with named elements
# (thus combine 'y' and 'parms' first, using the function c(), then convert to list using as.list())
# - 'expr' is an expression (i.e. code) that is evaluated
# this can span multiple lines of code when embraced with curly brackets {}
with(
as.list(c(y, parms)),{
### Optional: forcing functions: get value of the driving variables at time t (if any)
### Optional: auxiliary equations
LAI <- WF * SLA / FCL
IA <- IE *(1 - exp(-K * LAI ))
PGS <- (1 / (2 * CF)) * (LUE * IA + PC -
sqrt((LUE * IA + PC)^2 -4 * CF * LUE * IA* PC) )
PG <- PGS * CSPH * CDLH * CDGS * (MWC / (MWC + 2 * MWO))
PN <- PG - (RF * WF + RR * WR + RS * WS)
### Rate equations
DWFDT <- PN * NF - LRF * WF - NH * WF
DWRDT <- PN * NR - LRR * WR - NH * WR
DWSDT <- PN * NS - LRS * WS - NH * WS - TSH*WS
DWHDT <- TSH* WS - LRS * WH - NH * WH
HT <- NH * (WF + WR + WS + WH) # total harvest
HU <- NH * (WS + WH) # useful harvest
# The useful harvest, HU, is only the stem, so: (NH*DWS).
# This is distributed over the two states paper and timber. These are
# fractions of the useful harvest: paper NP; timber NT=(1-NP).
DWPDT <- HU * NP - LRP * WP # paper
DWTDT <- HU * NT - LRT * WT # Timber
DWWDT <- HT - HU + LRF * WF + LRR * WR +LRS *(WS+WH) - LRW * WW # Waste
### Gather all rates of change in a vector
# - the rates should be in the same order as the states (as specified in 'y')
# - it can be a named vector, but does not need to be
RATES <- c(DWFDT = DWFDT,
DWRDT = DWRDT,
DWSDT = DWSDT,
DWHDT = DWHDT,
DWPDT = DWPDT,
DWTDT = DWTDT,
DWWDT = DWWDT)
### Optional: get in/out flow used to compute mass balances (or set MB <- NULL)
RIN = PN
ROUT = LRP *WP + LRT * WT + LRW * WW
MB <- c(IN = RIN, OUT = ROUT)
### Optional: gather auxiliary variables that should be returned (or set AUX <- NULL)
# - this should be a named vector or list!
AUX <- NULL
# Return result as a list
# - the first element is a vector with the rates of change (in the same order as 'y')
# - all other elements are (optional) extra output, which should be named
outList <- list(c(RATES, # the rates of change of the state variables (same order as 'y'!)
MB), # the rates of change of the mass balance terms (or NULL)
AUX) # optional additional output per time step
return(outList)
})
}
Now, we can solve the model with these extra states using:
states <- ode(y = InitCO2fix(),
times = seq(from = 0, to = 50, by = 1),
parms = ParmsCO2fix(),
func = RatesCO2fix,
method = "euler")
and plot the result:
plot(states)

We can now define a function computing the mass balances, one that
needs 2 inputs: state and init, which
respectively are an object as returned by the ode function,
and a vector with initial conditions for the state variables:
MassBalanceCO2fix <- function(y, ini) {
# Add "0" to the state variables' names to indicate initial condition
names(ini) = paste0(names(ini), "0")
# Using the with() function we can access elements directly by there name
# here: 'y' is a deSolve matrix
# so it is best to first convert it to a data.frame using function as.data.frame()
# and then combine it with 'ini' using the function c()
# and convert to a list using as.list()
with(as.list(c(as.data.frame(y), ini)), {
states <- ode(y = InitCO2fix(),
times = seq(from = 0, to = 50, by = 1),
parms = ParmsCO2fix(),
func = RatesCO2fix,
method = "euler")
# Compute mass balance terms
# All that was already in the system at the start
INI = sum(ini) # Initial amount of C in the systems, sum of all states, kgC/m2
# All that is in the system at the end and what left the system
FIN = y[,"WF"] + y[,"WR"] + y[,"WS"] + y[,"WH"] + y[,"WP"] + y[,"WT"] + y[,"WW"]
# Balance: difference of all what comes in and goes out
BAL <- FIN - INI + y[,"OUT"] - y[,"IN"]
# Return output BAL
return(BAL)
})
}
After defining this function, we can check the mass balances per state variable and plot them:
balance <- MassBalanceCO2fix(states, ini = InitCO2fix())
plot(states[,"time"], balance, type = "l", xlab = "time", ylab = "Balance C")

Note that the balances are sufficiently small
(e.g. 1e-14) to consider it effectively zero!