Unit of OM | Unit of DO | Value of parameter s |
---|---|---|
mol C mL-1 | mol O2 mL-1 | 1 |
g C mL-1 | g O2 mL-1 | 32/12 = 2.67 |
mol C6H12O6 mL-1 | mol O2 mL-1 | 6 |
g C6H12O6 mL-1 | g O2 mL-1 | 192/180 = 1.067 |
System analysis: Simultaneous ODE
\(\leftarrow\) BACK TO TEACHING SECTION OF MY HOMEPAGE
1 Introduction
1.1 The meaning of “simultaneous”
The description of environmental systems generally requires multiple state variables, including, e.g., water levels, concentrations, species abundances. The state of a system is thus expressed as a vector \(\{ y_1, y_2, \dots y_n \}\).
Since all state variables potentially undergo dynamics, their derivatives with respect to time form a corresponding vector \(\{ \frac{d}{dt}y_1, \frac{d}{dt}y_2, \dots \frac{d}{dt}y_n \}\) often shortly written as \(\{ \dot{y_1}, \dot{y_2}, \dots \dot{y_n} \}\).
Very typically, the derivative of one variable is dependent on the value of another variable, resulting in relations like, e.g., \(\frac{d}{dt}y_1 = f(y_2)\). The following examples illustrate such a situation:
The growth of algae results in a change of population density. Since growth requires the uptake of dissolved phosphorus, the change in population density depends on ambient P, but it also alters the concentration.
In a predator-prey system, the growth of the predator population is dependent on prey density. At the same time, the decline in prey population is controlled by the abundance of predators.
If we like to understand the dynamics of those systems, we need to deal with simultaneous ODE. The term simultaneous is used to express coupling between equations. It is impossible to solve just one of the ODE without simultaneously solving the other ones too, carefully keeping track of the dependencies.
1.2 Example: Degradation of organic matter
We consider the aerobic decay of organic matter (\(OM\)) according to a first order kinetics and the corresponding impact on dissolved oxygen (\(DO\)). The pool of \(DO\) is simultaneously depleted by microbial consumption and replenished by atmospheric aeration (Figure 1).
For simplicity, we consider a system without external input or outputs, e.g. a bottle. The perspective would also be applicable to a travelling control volume in an idealized river with negligible longitudinal mixing (see the plug-flow reactor concept).
The dynamics of the two state variables \(OM\) and \(DO\) is expressed by the simultaneous ODE from Equation 1
\[ \begin{aligned} \tfrac{d}{dt}OM &= -k_d \cdot OM \\ \tfrac{d}{dt}DO &= -k_d \cdot OM \cdot s + k_a \cdot (DO_{sat} - DO) \end{aligned} \tag{1}\]
where
\(k_d\) is a rate constant to describe the efficiency of degradation (unit: 1/time). The value of that constant must account for the (constant) density of the bacterial population and the respiration activity of the cells.
\(k_a\) is the rate constant of re-aeration (unit: 1/time). The value certainly depends on turbulence.
\(DO_{sat}\) represents the oxygen saturation level in equilibrium with the atmosphere. The value depends on temperature and salinity.
\(s\) is a stoichiometric coefficient specifying the amount of oxygen consumed per amount of degraded \(OM\) (units discussed below in Table 1).
Like the classical model of Streeter & Phelps published in 1925, Equation 1 is obviously overly simplistic in several aspects:
The rate constant of degradation, \(k_d\), does not vary over time. Thus, in the model, microbes do not reproduce. If reproduction was to be considered, one would need to add bacteria as another state variable!
The equations do not account for oxygen limitation or a possible adaptation of the microbial community to anaerobic conditions. Hence, the model returns non-sense if \(DO\) concentrations fall below about 2 mg/L. In fact, \(DO\) can even go negative.
Further, the model does not acount for the settling of organic particles, the possible role of surface-attached bacterial biofilms, a possible accumulation of recalcitrant \(OM\), and many other details …
Let’s spent some thoughts on the value of the stoichiometric parameter \(s\) appearing in Equation 1. As it specifies how much oxygen is consumed per amount of degraded substrate, we should be able to infer a reasonable value from the corresponding chemical reaction. Let us adopt the reaction formula
\[CH_2O + O_2 \rightarrow CO_2 + H_2O \]
where the organic matter has the same stoichiometry as glucose (\(C_6H_{12}O_6\)) but, for simplicity, all quantities have been divided by six. Apparently, one mole of \(O_2\) is consumed per mole of organic carbon (\(C\)) which results in \(s=1\).
However, unlike marine scientists, freshwater biologists tend to use mass concentrations instead of molar concentrations which necessarily affects the value of \(s\). In addition, the concentration of organic matter is not necessarily specificed in units of carbon but it could also be, for example, glucose equivalents. A few typical cases are illustrated in Table 1.
To find the values of \(s\) listed in Table 1, you need to know
the stoichiometry of the reaction (given by the reaction formula)
the molar masses of the involved elements as specified in the periodic table
the rule for converting between molar and mass concentrations typically written as \(n = m / M\) with \(n\): amount of moles, \(m\): mass in grams, \(M\): molar mass of the substance in grams per mole.
2 Approaches to solving simultaneous ODE
2.1 General features
Besides a few special cases, systems of simultaneous ODE cannot be solved by analytical methods. Consequently, we need to adopt numerical algorithms to find approximate solutions in an iterative manner. Obviously, at this point, computers become really essential.
Like in the case of individual ODE, there are two possible goals when solving simultaneous ODE: We can be interested in either the system’s dynamics or a possible steady-state.
2.2 Dynamic solutions
2.2.1 Theory
Dynamic solutions of the initial value problem require integration. To get a basic idea of how numerical integration works, consider Figure 2. The latter illustrates the idea behind Euler’s method applied to a single ODE. The method is equivalent to linear extrapolation into the future using the known derivative(s). Actual numerical algorithms are still rooted in this basic idea but they employ sophisticated iteration schemes to achieve convergence towards the true solution. The Runge-Kutta methods are good examples of such algorithms with moderate complexity.
2.2.2 Computing dynamic solutions in R
A well-tested and widely used add-on package for numerical integration in R is deSolve
. You can find the full package documentation on this CRAN page and the corresponding package vignettes. We will use this package heavily in the future.
My favorite solver from the deSolve
package is called lsoda
as it is very efficient and tackles even difficult problems where simpler solvers may struggle. You may want to inspect the respective help page in detail later.
install.packages("deSolve") # install package (once)
library("deSolve") # load package
# help for recommended solver ?lsoda
We will apply this solver to Equation 1 shortly in Section 3.
2.3 Steady-state solutions
2.3.1 Theory
A steady-state solution of a system of simultaneous ODE can be obtained by setting all left-hand sides to zero, indicating no change in any of the variables (Equation 2).
\[ \begin{aligned} \frac{d}{dt}y_1 &= 0 \\ \frac{d}{dt}y_2 &= 0 \\ \dots \\ \frac{d}{dt}y_n &= 0 \end{aligned} \tag{2}\]
The resulting algebraic system of equations is often non-linear and can only be tackled with numerical root-finding algorithms. An initial guess of the solution is generally needed to start such algorithms.
Remember that a non-trivial steady-state solution does not necessarily exist. For example, Equation 1 apparently has only a trivial steady state at \(OM = 0\) and \(DO = DO_{sat}\).
2.3.2 Computing steady-state solutions using R
To directly obtain steady-steady solutions, one can use the rootSolve
package available through this CRAN page. It is in fact a companion-package to deSolve
. Although the direct approach is elegant and often fast, the algorithm may be “fragile”. Specifically, the root-solving algorithm often requires a good initial guess to converge towards a solution. A good guess may not be easy to obtain.
As an alternative, one can simply use the same approach as for initial value problems (i.e., deSolve
) and run the integration over a very long period of time while keeping all boundary conditions at constant values. For this, one only needs a guessed initial state but not a guess of the solution. In fact, a very crude approximation of the initial state is sufficient (unless the system has multiple steady states). The only drawback of this very robust approach may be the computational overhead of a very-long-time integration.
3 Dynamic solution of the degradation example
Let’s integrate the simultaneous ODE from Equation 1 using the deSolve
package. To do so, we have to provide four elements of code:
- A vector with the initial values of all state variables with elements being named. A suitable line of code for the example might read like:
<- c(OM=1, DO=8/32) initial
- A vector holding the values of any parameters appearing at the right hand side of the ODE system. A suitable line of code might be:
<- c(kd=0.07, ka=0.1, DO_sat=8/32, s=1) parameters
- A vector of times in the future at which the values of the state variables are to be predicted. Of course, time units should be consistent with the units of the parameters.
<- 0:72 times
- Finally, we must provide a function to compute the right hand sides of the simultaneous ODE. As can be seen in Equation 1, the values depend on the current values of the state variables, the parameters, and (possibly but not here) on the time. Hence, this function’s interface must provide formal arguments for theses three inputs. Consequently, the interface must look like
function (time, vars, pars)
. When the function is called to actually compute derivatives, the time is passed astime
, the state variables’ values asvars
, and the parameters aspars
.
Next, we need to understand what goes into the body of the function with the characteristic interface function (time, vars, pars)
. Here is a brief summary:
The function must compute the vector of derivatives. Let’s call it
dydt
. Under all circumstances, the elements of elementsdydt
must be in the same order as the elements in the input vectorvars
. A safe way to achieve this is a statement likedydt <- dydt[names(vars)]
.The vector
dydt
must be wrapped into a list before it is being returned from the function. This is technically necessary to separate the computed derivatives from any (optional) auxiliary information to be returned.
Consequently, a function to return the state variables’ derivatives would look like this:
<- function(time, vars, pars) {
derivatives
# compute the vector of derivatives using 'vars', 'pars' ('time' not used)
<- c(
dydt OM = unname(-pars["kd"] * vars["OM"]),
DO = unname(-pars["kd"] * vars["OM"] * pars["s"] +
"ka"] * (pars["DO_sat"] - vars["DO"]))
pars[
)
<- dydt[names(vars)] # sort derivatives like the values in 'vars'
dydt list(dydt) # return derivatives wrapped into a list
}
Note the use of unname
in the statements above. This function avoids an undesired carry-over of element names from the right hand side (one of the few annoying “features” of R).
If the right hand sides are rather complex, the with
statement of R can simplify the access to the values in the vectors vars
and pars
. If, for example, the vector vars
has a named element “OM”, the following code would make it accessible just as OM
(instead of vars["OM"]
). Although the use of with
is very convenient, it is much easier to make bad mistakes.
# function as before, coded differently
<- function(time, vars, pars) {
derivatives
# compute the vector of derivatives using 'vars', 'pars' ('time' not used)
<- with( as.list(c(vars, pars)), { # "dissolves" vectors into scalars
dydt c(
OM = kd * OM, # much better to read ...
DO = kd * OM * s + ka * (DO_sat - DO) # ... but more error-prone
)
})
<- dydt[names(vars)] # sort derivatives like the values in 'vars'
dydt list(dydt) # return derivatives wrapped into a list
}
Now that all required elements of code are ready, let’s call the solver and inspect the predicted dynamics visually (Figure 3).
# call the solver providing it with the initial state, the target times,
# our function to compute the derivatives, and the parameters
<- lsoda(y=initial, times=times, func=derivatives, parms=parameters)
x
# outputs from the solver can be plotted instantly
plot(x)
The predicted dynamics of \(OM\) in Figure 3 is in good agreement with expectation. A first order decay processes should result in convergence towards zero as we have seen earlier.
The predicted dynamics of \(DO\) is a bit more tricky. Apparently, larger amounts of oxygen are consumed initially as the degradation process is very active. Later, as most of \(OM\) has already been degraded, the rate of oxygen consumption declines and concentrations recover due to replenishment from the atmosphere. However, the fact that \(DO\) temporarily goes negative is certainly problematic and reflects a conceptual issue with Equation 1. In fact, the problem is a missing limitation term that prevents the degradation process from continuing once \(DO\) is fully depleted. We will look at approaches to modeling resource limitation in a subsequent session.