Note: sometimes, the embedded shiny apps don’t show up on Github. Please use this link as a temporary work around in such cases.
The following examples show how to control dynamic models implemented in the R language interactively from a web interface. We use a technically simple ordinary differential equation (ODE) model as an example, then we will see how to add GUI elements and finally how we can make it faster by using compiled code.
This tutorial was originally presented at the useR!2017 conference in Brussels.
The HTML version embeds the shiny apps from a remote server and needs no local installation. The source code of this HTML file is in Rmarkdown (.Rmd) format that can be run locally from within RStudio when the shiny apps locally installed.
Note: this is an interactive document. Please use the Code
buttons to unfold the code of individual code cunks or for the complete document. The content is best viewed with Chrome or Firefox.
The Brusselator (Brussels Oscillator) a mathematical model of an autocatalytic reaction of two substances A and B, two intermediate products X and Y and two final products C and D. The latter are continuesly removed.
Implementation of the model in R is straightforward. We need the package deSolve, translate the differential equations of the Brusselator into an R function, define initial values (\(y_0\)), parameters and time steps and then use ode for the numerical solution. Finally we plot the results.
library("deSolve")
brusselator <- function(t, y, p) {
with(as.list(c(y, p)), {
dX <- k1*A - k2*B*X + k3*X^2*Y - k4*X
dY <- k2*B*X - k3*X^2*Y
list(c(X=dX, Y=dY))
})
}
y0 <- c(X=1, Y=1)
parms <- c(A=1, B=3, k1=1, k2=1, k3=1, k4=1)
out <- ode(y = y0, times=seq(0, 100, .1), brusselator, parms)
plot(out)
library("shiny")
server <- function(input, output) {
output$distPlot <- renderPlot({
hist(rnorm(input$obs))
})
}
ui <- fluidPage(
sliderInput("obs", "Number of observations:", min = 10, max = 500, value = 100),
plotOutput("distPlot")
)
shinyApp(ui = ui, server = server)
The second part creates the web interface. The app consists of a server element doing the computations and plotting, and a user interface (ui), that is responsible for handling user inputs and for sending the outputs back to the user. Both parts, the server and the ui can be saved in a single file ‘app.R’. It can then be run directly from RStudio or placed on a shiny-enabled web server.
Exercise: Copy the app to Rstudio, save it to the disc (fille app.R) and try it out
More about this: https://shiny.rstudio.com
Now let’s combine the shiny ui/server environment with our Brusselator model. We start with a very simple application that allows to change two parameters A
and B
(essentially the start concentrations of substances A
and B
) and then plots the time series of the model.
The code is stunningly simple, consisting of three parts:
input
and output
. The two parameters A
and B are taken from the input
and the plot is then directed to an element output$brussels
of the output
. Note also that we use deSolve’s function matplot.0d
instead of plot
as before, so that both state variables appear in a single figure.fluidpage
with two numericInput
calls and one call to plotOutput
.library("deSolve")
brusselator <- function(t, y, p) {
with(as.list(c(y, p)), {
dX <- k1*A - k2*B*X + k3*X^2*Y - k4*X
dY <- k2*B*X - k3*X^2*Y
list(c(X=dX, Y=dY))
})
}
server <- function(input, output) {
output$brussels <- renderPlot({
parms <- c(A=input$A, B=input$B, k1=1, k2=1, k3=1, k4=1)
out <- ode(y = c(X=1, Y=1), times=seq(0, 100, .1), brusselator, parms)
matplot.0D(out)
})
}
ui <- fluidPage(
numericInput("A", label = "A", value = 1),
numericInput("B", label = "B", value = 3),
plotOutput("brussels")
)
shinyApp(ui=ui, server=server)
Now, we can start to adapt the app to our needs: change the layout, unlock more parameters of the model, add a second figure and so on. An especially convenient layout is the sidebarLayout
that separates the control elements (sidebarPane
left or top) and output of the app (mainPanel
right or bottom).
HTML tags like h2
, h3
, or p
can be used to annotate the app, see help pages for more.
library("deSolve")
brusselator <- function(t, y, p) {
with(as.list(c(y, p)), {
dX <- k1*A - k2*B*X + k3*X^2*Y - k4*X
dY <- k2*B*X - k3*X^2*Y
list(c(X=dX, Y=dY))
})
}
server <- function(input, output) {
output$brussels <- renderPlot({
y0 <- c(X=input$X, Y=input$Y)
parms <- c(A=input$A, B=input$B,
k1=input$k1, k2=input$k2, k3=input$k3, k4=input$k4)
times <- seq(0, 100, .1)
out <- ode(y0, times, brusselator, parms)
par(mfrow=c(1, 2))
matplot.0D(out, main="time series")
plot(out[,2:3], type="l", xlab="A", ylab="B", main="state diagram")
})
}
ui <- fluidPage(
headerPanel("Brusselator"),
sidebarLayout(
sidebarPanel(
h3("Init values"),
numericInput("X", label = "X",
min = 0.0, max = 5, value = 1, step = 0.2, width=100),
numericInput("Y", label = "Y",
min = 0.0, max = 5, value = 1, step = 0.2, width=100),
h3("Parameters"),
numericInput("A", label = "A",
min = 0.0, max = 5, value = 1, step = 0.2, width=100),
numericInput("B", label = "B",
min = 0.0, max = 5, value = 3, step = 0.2, width=100),
numericInput("k1", label = "k1",
min = 0.0, max = 10, value = 1, step = 0.2, width=100),
numericInput("k2", label = "k2",
min = 0.0, max = 10, value = 1, step = 0.2, width=100),
numericInput("k3", label = "k3",
min = 0.0, max = 10, value = 1, step = 0.2, width=100),
numericInput("k4", label = "k4",
min = 0.0, max = 10, value = 1, step = 0.2, width=100)
),
mainPanel(
h3("Simulation results"),
plotOutput("brussels")
)
)
)
shinyApp(ui = ui, server = server)
The code above should work as intended, but there are of course further options of improvement.
Here, we want to focus on options 2 and 3.
R is a programming language, so why not creating the user interface directly from information found already in the initial values and parameters of the model? This would allow us to save stupid typing (and copy and paste!) and to adapt the user interface ``automagically’’ if we change something in the model.
The solution is, to construct lists of lists beforehand, in our case one for the parameters L_parms
and one for the initial values L_y0
. Each element of the top level list contains all the parameters needed by a call to numericInput
, including some constant optional values like the witdh
of the input field. The templates stored in these lists are then expanded with lapply
and do.call
. The result is exacly the same as before, compare: https://weblab.hydro.tu-dresden.de/apps/bruss03
library("deSolve")
brusselator <- function(t, y, p) {
with(as.list(c(y, p)), {
dX <- k1*A - k2*B*X + k3*X^2*Y - k4*X
dY <- k2*B*X - k3*X^2*Y
list(c(X=dX, Y=dY))
})
}
## prepare data structures to create UI programmatically
y0 <- c(X=1, Y=1)
parms <- c(A=1, B=3, k1=1, k2=1, k3=1, k4=1)
makelist <- function(i, obj, min=NA, max=NA, step=NA, width=NULL) {
list(inputId=names(obj[i]), label=names(obj[i]),
value=unname(obj[i]), min=min, max=max, step=step,
width=width)
}
## two lists of lists
L_parms <- lapply(1:length(parms), makelist, obj=parms, min=0, max=10, step=0.2, width=100)
L_y0 <- lapply(1:length(y0), makelist, obj=y0, min=0, max=10, step=0.2, width=100)
server <- function(input, output) {
output$brussels <- renderPlot({
L_input <- reactiveValuesToList(input) # to enable width
y0 <- with(L_input, c(X=X, Y=Y))
parms <- with(L_input, c(A=A, B=B, k1=k1, k2=k2, k3=k3, k4=k4))
times <- seq(0, 100, .1)
out <- ode(y0, times, brusselator, parms)
par(mfrow=c(1, 2))
matplot.0D(out, main="time series")
plot(out[,2:3], type="l", xlab="A", ylab="B", main="state diagram")
})
}
ui <- fluidPage(
headerPanel("Brusselator: generic UI creation"),
sidebarLayout(
sidebarPanel(
## generic creation of UI elements
h3("Init values"),
lapply(L_y0, function(x) do.call("numericInput", x)), # <--------
h3("Parameters"),
lapply(L_parms, function(x) do.call("numericInput", x)) # <--------
),
mainPanel(
h3("Simulation results"),
plotOutput("brussels")
)
)
)
shinyApp(ui = ui, server = server)
As a second improvement of our app, we want to make our model faster. Quick response of a server based app is crucial when models become bigger, because our users may think that the connection is broken otherwise.
This is fortunately possible, given that some development tools (e.g. the Rtools on Windows) are installed.
Package deSolve supports models in compiled code provided by a .dll
/ .so
directly. It allows the solver to communicate directly with the model, and without any overhead of a back-call to R. Our experience shows, that this can then be 2 … 200 times faster, depending on the type of model.
Several approaches were developed, depending on model complexity and your skills:
We use cOde here, because it is especially convenient for small models, while for more complex models, the table based notation of rodeo can be more advantageous.
Package cOde allows to compile our (technically!) simple Brusselator without knowledge of Fortran or C. The only prerequisite is, that we have the development tools (the compiler etc. …) installed, that the system PATH correctly set, and that write access to the working directory is granted.
Then it is sufficient, to write the differential equations in a character vector and then to compile it with funC
. This creates a C file and compiles it into a shared library (.dll or .so), which then can be numerically solved with odeC
.
library("deSolve")
library("cOde") # 3rd party package by Daniel Kaschek
y0 <- c(X=1.1, Y=1)
times <- seq(0, 100, .1)
parms <- c(A=1, B=3, k1=1, k2=1, k3=1, k4=1)
brussC <- funC(c(
X = "k1*A - k2*B*X + k3*X^2*Y - k4*X",
Y = "k2*B*X - k3*X^2*Y"
), modelname="brusselator", compile=TRUE)
out <- odeC(y0, times, brussC, parms)
matplot(out[,1], out[,2:3], type="l")
As a final exercise, we modify our app from before, to use the compiled C version of the model instead of the R version.
library("deSolve")
library("cOde") # 3rd party package by Daniel Kaschek
## helper function, adapted from package cOde v.2.2 (GPL >= 2.0)
loadDLL <- function(func, cfunction="derivs") {
.so <- .Platform$dynlib.ext
checkDLL <- try(getNativeSymbolInfo(cfunction), silent=TRUE)
if(inherits(checkDLL, "try-error")) {
dyn.load(paste0(func, .so))
#cat("Shared object is loaded and ready to use\n")
} else if ((is.null(checkDLL$package))) {
# We are on Windows (always overload)
dyn.load(paste0(func, .so))
} else if((checkDLL$package)[[1]] != func) {
# We are on Unix
dyn.load(paste0(func, .so))
}
}
compile <- !file.exists(paste0("brusselator", .Platform$dynlib.ext))
brussC <- funC(c(
X = "k1*A - k2*B*X + k3*X^2*Y - k4*X",
Y = "k2*B*X - k3*X^2*Y"
), modelname="brusselator", compile=compile)
if (!compile) loadDLL("brusselator")
y0 <- c(X=1, Y=1)
parms <- c(A=1, B=3, k1=1, k2=1, k3=1, k4=1)
makelist <- function(i, obj, min=NA, max=NA, step=NA, width=NULL) {
list(inputId=names(obj[i]), label=names(obj[i]),
value=unname(obj[i]), min=min, max=max, step=step,
width=width)
}
L_y0 <- lapply(1:length(y0), makelist, obj=y0, min=0, max=10, step=0.2, width=100)
L_parms <- lapply(1:length(parms), makelist, obj=parms, min=0, max=10, step=0.2, width=100)
server <- function(input, output) {
output$brussels <- renderPlot({
L_input <- reactiveValuesToList(input)
y0 <- with(L_input, c(X=X, Y=Y))
parms <- with(L_input, c(A=A, B=B, k1=k1, k2=k2, k3=k3, k4=k4))
times <- seq(0, 100, .1)
#out <- ode(y0, times, brusselator, parms) # R version
out <- odeC(y0, times, brussC, parms) # C version
par(mfrow=c(1, 2))
matplot.0D(out, main="time series")
plot(out[,2:3], type="l", xlab="A", ylab="B", main="state diagram")
})
}
ui <- fluidPage(
headerPanel("Brusselator"),
sidebarLayout(
sidebarPanel(
h3("Init values"),
lapply(L_y0, function(x) do.call("numericInput", x)),
h3("Parameters"),
lapply(L_parms, function(x) do.call("numericInput", x))
),
mainPanel(
h3("Simulation results"),
plotOutput("brussels")
)
)
)
shinyApp(ui = ui, server = server)
The result looks identical as before, just a little bit faster.
If you want to see the C code: here is it
Original author: tpetzoldt