Tabular model definition
The model’s state variables comprise the concentrations of 12
components listed in the table below. Each component in the water column
(suffix _w
) has its counterpart in the sediment (suffix
_s
). With regard to bacteria, two strains of E. coli are
distinguished: a Tetracycline resistant strain and a susceptible
one.
Declaration of state variables (file ‘vars.txt’).
S_w |
g C/m3 |
susceptible bacteria in water |
0.180 |
S_s |
g C/m3 |
susceptible bacteria in sediment |
1.800 |
R_w |
g C/m3 |
resistant bacteria in water |
0.020 |
R_s |
g C/m3 |
resistant bacteria in sediment |
8.000 |
POM_w |
g C/m3 |
particulate OM in water |
10.000 |
POM_s |
g C/m3 |
particulate OM in sediment |
75.000 |
TSS_w |
g DW/m3 |
total suspended solids in water |
16.000 |
TSS_s |
g DW/m3 |
total suspended solids in sediment |
160.000 |
A_w |
g/m3 |
total antibiotic concentration in water |
0.002 |
A_s |
g/m3 |
total antibiotic concentration in sediment |
0.020 |
DOM_w |
g C/m3 |
dissolved OM in water |
3.200 |
DOM_s |
g C/m3 |
dissolved OM in sediment |
32.000 |
The values displayed in the rightmost column of the above table are
used as initial values in the model application. Concentrations are
generally specified as mass per bulk volume in units of mg/L. The mass
of total suspended solids is quantified as dry weight; masses of
biogenic organic matter, including bacteria, are expressed on a Carbon
basis. For E. coli, biomass can be converted into cell numbers using a
factor of roughly 1 ⋅ 10−13
g Carbon / cell.
This conversion is based on information from the bionumbers
data base reporting a value of 7e+9 Carbon atoms per E. coli cell.
Taking into account the Avogadro constant (about 6e23 atoms/mol) and the
molar mass of Carbon (12 g/mol), one obtains 7e+9 / 6e23 * 12 = 1.4e-13
g Carbon / cell. An alternative estimate (Hellweger, Ruan, and Sanchez (2011), supplement
page S12) is based on dry weight of E. coli (180e-15 g/cell) and an
approximate ratio of 0.5 g Carbon /g dry weight. The corresponding
estimate is 180e-15 * 0.5 = 0.9e-13 g Carbon / cell.
The state variables’ dynamics are controlled by a variety of
processes specified in tabular form below.
Besides the physical phenomena of advection and dispersion, the list
of processes includes growth and respiration losses of bacteria, as well
as gene transfer between the two strains of E. coli. Particularly, the
model describes the horizontal transfer of Tetracycline resistance
through the exchange of plasmids (conjugation) as well as the potential
loss of the plasmid during cell division (segregational loss). While
carriage of the resistance genes is clearly advantageous under exposure
to the antibiotic, the synthesis of plasmids also has side effects to
the cell, e. g. in the form of physiological costs. This phenomenon is
described as a reduction of the growth rate of the
Tetracycline-resistant E. coli strain but evolutionary reduction of
plasmid costs (Lenski 1998) is
neglected.
Tetracycline undergoes photolytic decay (Wammer et al. 2011) which is modeled as a
first-order process. The antibiotic is also known to bind to organic and
inorganic matter in relevant percentages (Tolls
2001). Hence, sorption effects must be included in the model to
obtain realistic estimates of the freely available Tetracycline
concentration to which the bacteriostatic effect is attributed (Chen et al. 2015). As in many bio-geo-chemical
codes, it is assumed that sorption equilibria arise instantaneously. As
this leads to a set of algebraic equations rather than ODE, sorption is
not explicitly listed in the above table of processes.
The stoichiometric factors linking the processes with the state
variables’ derivatives are provided in matrix format below.
The model specification is completed by the declaration of parameters
and functions that appear in the process rate expressions (Tables
below). Table numbers appearing in column ‘references’ refer to the
supplement of the Hellweger, Ruan, and Sanchez
(2011) paper.
Declaration of parameters (file ‘pars.txt’).
dw |
m |
depth of water column |
6.0e-01 |
guess |
ds |
m |
depth of sediment layer |
3.0e-02 |
guess |
por |
- |
porosity of sediments |
3.0e-01 |
guess |
ka_w |
1/d |
rate constant of antibiotic decay in water |
1.0e-01 |
Table S4 |
ka_s |
1/d |
rate constant of antibiotic decay in sediments |
0.0e+00 |
Table S4 |
kd_DOM |
m3/g C |
coefficient for antibiotic sorption to DOM |
1.6e-02 |
Table S4 |
kd_TSS |
m3/g DW |
coefficient for antibiotic sorption to TSS |
2.0e-04 |
Table S4 |
us |
m/d |
settling velocity |
NA |
Table S6 |
ur |
m/d |
resuspension velocity |
NA |
Table S6 |
ud |
m/d |
velocity describing turbulent diffusion |
5.0e-03 |
Table S6 |
P |
g C/m2/d |
rate of POM production |
2.3e+00 |
Table S6 |
kh_w |
1/d |
rate constant of POM hydrolysis in water |
5.0e-02 |
Table S6 |
kh_s |
1/d |
rate constant of POM hydrolysis in sediments |
5.0e-02 |
Table S6 |
fp |
- |
fraction of water-column bacteria being bound to
particles |
1.0e-01 |
Table S6 |
kr |
1/d |
bacteria respiration rate constant |
1.1e-01 |
Table S4 |
kg |
1/d |
max. growth rate of sensitive bacteria |
2.6e+00 |
Table S4 |
h_DOM |
g C/m3 |
half saturation concentration of DOM for bacteria
growth |
9.1e+00 |
Table S6 |
Y |
- |
yield; bacterial carbon / DOM carbon |
3.6e-01 |
Table S7 |
Amic |
g A/m3 |
minimum inhibiting conc. of freely dissolved
antibiotic |
1.3e-02 |
Table S6 |
ks |
1/d |
rate constant of segregational loss |
4.0e-02 |
Table S6 |
kc |
m3/g C/d |
constant to control conjugation |
1.0e-05 |
Table S6 |
alpha |
- |
reduction of growth rate due to resistance (plasmid
cost) |
1.0e-01 |
Table S6 |
S_in |
g C/m3 |
concentrations in inflow |
1.8e-02 |
Table S7 |
R_in |
g C/m3 |
concentrations in inflow |
2.0e-03 |
Table S7 |
A_in |
g C/m3 |
concentrations in inflow |
2.0e-03 |
guess |
DOM_in |
g C/m3 |
concentrations in inflow |
3.2e+00 |
Table S7 |
POM_in |
g C/m3 |
concentrations in inflow |
4.0e-01 |
Table S7 |
TSS_in |
g TSS/m3 |
concentrations in inflow |
1.6e+01 |
Table S6 |
V |
m3 |
reactor volume |
NA |
- |
Q_in |
m3/d |
external inflow (not from upstream reactor) |
NA |
- |
Q |
m3/d |
inflow from upstream reactor |
NA |
- |
Declaration of functions (file ‘funs.txt’).
A_part |
mol/m3 |
concentration of antibiotic; particulate fraction |
A_diss |
mol/m3 |
concentration of antibiotic; freely dissolved and
sorbed to dissolved matter |
A_free |
mol/m3 |
concentration of antibiotic; freely dissolved |
max |
- |
maximum of arguments |
Implementation of functions
For computational efficiency, the source code is generated in
Fortran, hence, the functions need to be implemented in Fortran as well
(source code shown below). They return the different fractions of the
antibiotic (particulate, dissolved, and freely dissolved) in the
presence of two sorbents (DOM, TSS) assuming linear sorption isothermes.
The underlying equations can be found in the supplement of Hellweger, Ruan, and Sanchez (2011), number
S2-S4.
module functions
implicit none
contains
! Consult the functions' declaration table to see what these functions do
double precision function A_part(A_tot, kd_DOM, kd_TSS, DOM, TSS) result (r)
double precision, intent(in):: A_tot, kd_DOM, kd_TSS, DOM, TSS
r= A_tot * kd_TSS * TSS / (1d0 + kd_DOM * DOM + kd_TSS * TSS)
end function
double precision function A_diss(A_tot, kd_DOM, kd_TSS, DOM, TSS) result (r)
double precision, intent(in):: A_tot, kd_DOM, kd_TSS, DOM, TSS
r= A_tot - A_part(A_tot, kd_DOM, kd_TSS, DOM, TSS)
end function
double precision function A_free(A_tot, kd_DOM, kd_TSS, DOM, TSS) result (r)
double precision, intent(in):: A_tot, kd_DOM, kd_TSS, DOM, TSS
r= A_tot / (1d0 + kd_DOM * DOM + kd_TSS * TSS)
end function
end module
R code with corresponding graphical outputs
The following code section instantiates a rodeo
object
using the tabular data from above and it produces the required Fortran
library. Note that basic properties of the simulated system, namely the
number of tanks, are defined at the top of the listing.
After object creation and code generation, the state variables are
initialized using the numbers from the table displayed above (column
‘initial’). Likewise, the values of parameters are taken column
‘default’ of the respective table. Note that some parameters are set to
NA
, i.e. they are initially undefined. Those values are
either computed from the system’s physical properties or from mass
balance considerations according to supplement S 3.3.c of Hellweger, Ruan, and Sanchez (2011).
# Adjustable settings ##########################################################
# Properties of the reach not being parameters of the core model
len <- 125000 # reach length (m)
uL <- 0.5 * 86400 # flow velocity (m/d)
dL <- 300 * 86400 # longitudinal dispersion coefficient (m2/d)
xsArea <- 0.6 * 15 # wet cross-section area (m2)
# End of settings ##############################################################
# Computational parameters
nTanks <- trunc(uL * len / dL / 2) + 1 # number of tanks; see Elgeti (1996)
dt_max <- 0.5 * len / nTanks / uL # max. time step (d); Courant criterion
# Load packages
library("rodeo")
library("deSolve")
library("rootSolve")
# Initialize rodeo object
rd <- function(f, ...) { read.table(file=f, header=TRUE, sep="\t", ...) }
model <- rodeo$new(vars=rd("vars.txt"), pars=rd("pars.txt"), funs=rd("funs.txt"),
pros=rd("pros.txt"), stoi=as.matrix(rd("stoi.txt", row.names="process")),
asMatrix=TRUE, dim=c(nTanks))
# Generate code, compile into shared library, load library
model$compile(sources="functions.f95", fortran=TRUE)
# Assign initial values
vars <- matrix(rep(as.numeric(model$getVarsTable()$initial), each=nTanks),
ncol=model$lenVars(), nrow=nTanks, dimnames=list(NULL, model$namesVars()))
model$setVars(vars)
# Assign / update values of parameters; River flow is assumed to be steady
# and uniform (i.e. constant in space and time); Settling and resuspension
# velocities are computed from steady-state mass balance as in Hellweger (2011)
pars <- matrix(
rep(suppressWarnings(as.numeric(model$getParsTable()$default)), each=nTanks),
ncol=model$lenPars(), nrow=nTanks, dimnames=list(NULL, model$namesPars()))
pars[,"V"] <- xsArea * len/nTanks # tank volumes
pars[,"Q"] <- c(0, rep(uL * xsArea, nTanks-1)) # inflow from upstr.
pars[,"Q_in"] <- c(uL * xsArea, rep(0, nTanks-1)) # inflow to tank 1
pars[,"us"] <- pars[,"kh_s"] * pars[,"ds"] / # settling velocity
((vars[,"POM_w"] / vars[,"POM_s"]) -
(vars[,"TSS_w"] / vars[,"TSS_s"]))
pars[,"ur"] <- pars[,"us"] * vars[,"TSS_w"] / # resuspension velo.
vars[,"TSS_s"]
model$setPars(pars)
The following code section creates a graphical representation of the
stochiometry matrix. Positive (negative) stoichiometric factors are
indicated by upward (downward) oriented triangles. The stoichiometric
factors for transport processes are displayed as circles since their
sign is generally variable as it depends on spatial gradients.
# Plot stoichiometry matrix using symbols
m <- model$stoichiometry(box=1)
clr <- function(x, ignoreSign=FALSE) {
res <- rep("transparent", length(x))
if (ignoreSign) {
res[x != 0] <- "black"
} else {
res[x < 0] <- "lightgrey"
res[x > 0] <- "white"
}
return(res)
}
sym <- function(x, ignoreSign=FALSE) {
res <- rep(NA, length(x))
if (ignoreSign) {
res[x != 0] <- 21
} else {
res[x < 0] <- 25
res[x > 0] <- 24
}
return(res)
}
omar <- par("mar")
par(mar=c(1,6,6,1))
plot(c(1,ncol(m)), c(1,nrow(m)), bty="n", type="n", xaxt="n", yaxt="n",
xlab="", ylab="")
abline(h=1:nrow(m), v=1:ncol(m), col="grey")
for (ir in 1:nrow(m)) {
ignoreSign <- grepl(pattern="^transport.*", x=rownames(m)[ir]) ||
grepl(pattern="^diffusion.*", x=rownames(m)[ir])
points(1:ncol(m), rep(ir,ncol(m)), pch=sym(m[ir,1:ncol(m)], ignoreSign),
bg=clr(m[ir,1:ncol(m)], ignoreSign))
}
mtext(side=2, at=1:nrow(m), rownames(m), las=2, line=0.5, cex=0.8)
mtext(side=3, at=1:ncol(m), colnames(m), las=2, line=0.5, cex=0.8)
par(mar=omar)
rm(m)

The next code section demonstrates how a steady-state solution can be
obtained by a call to method steady.1D
from package rootSolve
.
This specific solver accounts for the banded structure of the Jacobian
matrix resulting from the tanks-in-series approach taking into account
the layout of the vector of state variables (explained in the section on
multi-box models). Plotting was restricted to
the bacteria concentrations in the water column and sediment,
respectively.
# Estimate steady-state
std <- rootSolve::steady.1D(y=model$getVars(), time=NULL, func=model$libFunc(),
parms=model$getPars(), nspec=model$lenVars(), dimens=nTanks, positive=TRUE,
dllname=model$libName(), nout=model$lenPros()*nTanks)
if (!attr(std, which="steady", exact=TRUE))
stop("Steady-state run failed.")
names(std$y) <- names(model$getVars())
# Plot bacterial densities
stations= ((1:nTanks) * len/nTanks - len/nTanks/2) / 1000 # stations (km)
domains= c(Water="_w", Sediment="_s") # domain suffixes
layout(matrix(1:length(domains), ncol=length(domains)))
for (i in 1:length(domains)) {
R= match(paste0("R",domains[i],".",1:nTanks), names(std$y)) # resistant bac.
S= match(paste0("S",domains[i],".",1:nTanks), names(std$y)) # susceptibles
plot(x=range(stations), y=range(std$y[c(S,R)]), type="n",
xlab=ifelse(i==1,"Station (km)",""), ylab=ifelse(i==1,"mg/l",""))
lines(stations, std$y[R], lty=1)
lines(stations, std$y[S], lty=2)
if (i==1) legend("topleft", bty="n", lty=1:2, legend=c("Resistant","Suscept."))
mtext(side=3, names(domains)[i])
}
The above code outputs the steady-state longitudinal profiles of E.
Coli displayed below. The graphs indicate elevated concentrations in the
sediment as compared to the water column. Predominance of the resistant
strain in sediments is explained by increased antibiotic levels in the
dark. Growth conditions for the susceptible strain improve along the
flow path due to photolysis of Tetracycline in the water column and
subsequent dilution of pore water concentrations.

In the next code section, a dynamic solution is obtained using the
default integration method from deSolve
.
As in the steady-state computation above, the solver is informed on the
structure of the Jacobian to speed up numerical integration.
Graphical output is generated for a single state variable only,
displayed right after the code listing. The plot just illustrates how
the computed concentration of susceptibe bacteria in the water column
(state variable S_w
) drifts away from the guessed initial
state.
# Dynamic simulation
times <- seq(0, 7, 1/48) # requested output times
dyn <- deSolve::ode(y=model$getVars(), times=times, func=model$libFunc(),
parms=model$getPars(), NLVL=nTanks, dllname=model$libName(),
hmax=dt_max, nout=model$lenPros()*nTanks,
jactype="bandint", bandup=1, banddown=1)
if (attr(dyn, which="istate", exact=TRUE)[1] != 2)
stop("Dynamic run failed.")
# Plot dynamic solution
stations= (1:nTanks) * len/nTanks - len/nTanks/2
name <- "S_w"
m <- dyn[ ,match(paste0(name,".",1:nTanks), colnames(dyn))]
filled.contour(x=stations, y=dyn[,"time"], z=t(m), xlab="Station", ylab="Days",
color.palette=colorRampPalette(c("lightskyblue3","khaki","peru")),
main=name, cex.main=1, font.main=1)

The code below performs a global sensitivity analysis to test the
impact of parameter values on a specific quantity of interest, namely
the proportion of resistant bacteria after passage of the reach. The
four selected parameters were:
the constant to control gene transfer by conjugation,
kc
,
the rate constant of segregational loss,
ks
,
the plasmid costs, alpha
,
the upstream concentration of Tetracycline,
A_in
.
# Define parameter values for sensitivity analysis
testList <- list(
A_in= c(0.002, 0.005), # input of antibiotic
alpha= c(0, 0.25), # cost of resistance
ks= seq(0, 0.02, 0.002), # loss of resistance
kc= 10^seq(from=-4, to=-2, by=0.5)) # transfer of resistance
# Set up parameter sets
testSets <- expand.grid(testList)
# Function to return the steady-state solution for specific parameters
f <- function(set, y0) {
p <- model$getPars(asArray=TRUE)
p[,names(set)] <- rep(as.numeric(set), each=nTanks) # update parameters
out <- rootSolve::steady.1D(y=y0, time=NULL, func=model$libFunc(),
parms=p, nspec=model$lenVars(), dimens=nTanks, positive=TRUE,
dllname=model$libName(), nout=model$lenPros()*nTanks)
if (attr(out, which="steady", exact=TRUE)) { # solution found?
names(out$y) <- names(model$getVars())
down_S_w <- out$y[paste0("S_w",".",nTanks)] # bacteria concentrations
down_R_w <- out$y[paste0("R_w",".",nTanks)] # at lower end of reach
return(unname(down_R_w / (down_R_w + down_S_w))) # fraction of resistant b.
} else {
return(NA) # if solver failed
}
}
# Use already computed steady state solution as initial guess
y0 <- array(std$y, dim=c(nTanks, model$lenVars()),
dimnames=list(NULL, model$namesVars()))
# Apply model to all sets and store results as 4-dimensional array
res <- array(apply(X=testSets, MARGIN=1, FUN=f, y0=y0),
dim=lapply(testList, length), dimnames=testList)
# Plot results of the analysis
omar <- par("mar")
par(mar=c(4,4,1.5,1))
breaks <- pretty(res, 8)
colors <- colorRampPalette(c("steelblue2","khaki2","brown"))(length(breaks)-1)
nr <- length(testList$A_in)
nc <- length(testList$alpha)
layout(cbind(matrix(1:(nr*nc), nrow=nr), rep(nr*nc+1, nr)))
for (alpha in testList$alpha) {
for (A_in in testList$A_in) {
labs <- (A_in == tail(testList$A_in, n=1)) && (alpha == testList$alpha[1])
image(x=log10(as.numeric(dimnames(res)$kc)), y=as.numeric(dimnames(res)$ks),
z=t(res[as.character(A_in), as.character(alpha),,]),
zlim=range(res), breaks=breaks, col=colors,
xlab=ifelse(labs, "log10(kc)", ""), ylab=ifelse(labs, "ks", ""))
if (A_in == testList$A_in[1])
mtext(side=3, paste0("alpha = ",alpha), cex=par("cex"), line=.2)
if (alpha == tail(testList$alpha, n=1))
mtext(side=4, paste0("A_in = ",A_in), cex=par("cex"), las=3, line=.2)
}
}
plot.new()
legend("left", bty="n", title="% resistant", fill=colors,
legend=paste0(breaks[-length(breaks)]*100," - ", breaks[-1]*100))
layout(1)
par(mar=omar)
Thanks to the Fortran-based model implementation, a reasonable
section of the parameter space can be explored within acceptable times.
On a recent machine (3 GHz CPU, 8 GB memory) a single steady-state run
takes less than 50 ms. This opens up the possibility for more demanding
analysis like, for example, Bayesian parameter estimation or more
exhaustive sensitivity analyses.
The graphics created by the above listing (see below) illustrate the
effect of the four varied parameters on the model output. The analyzed
output variable is the percentage of suspended E. coli at the downstream
end of the reach being resistant to Tetracycline. The sensitivity with
respect to the rate constants of conjugation (kc
, x-axis)
and segregational loss (ks
, y-axis) is displayed in the
individual plots. The input concentration of Tetracycline
(A_in
) increases from the top to the bottom panel; plasmid
costs (alpha
) increase from left to right column. The
figure clearly illustrates that Tetracycline resistance is promoted by
high conjugation rates, low segregational losses, marginal plasmid
costs, and, of course, increased levels of the antibiotic.
