The goal of rODE
is to explore R
and its
S4
classes and its differences with Java and Python classes
while exploring physics simulations by solving ordinary differential
equations (ODE
).
This is not your typical black-box ODE solver. You really have to develop your ODE algorithm using any of the ODE solvers available in the package. The objective is learning while coding R, understanding the physics and using the math.
rODE
has been inspired on the extraordinary physics
library for computer simulations OpenSourcePhyisics.
Take a look at it at http://opensourcephysics.org. I highly recommend the
book An
Introduction to Computer Simulation Methods: Applications To Physical
Systems, (Gould, Tobochnik, and Christian, 2017)
. It
has helped me a lot in understanding the physics behind ordinary
differential equations. The book briliantly combines code, algorithms,
math and physics.
Additionally I have consulted these sources during the developing of
the package rODE
:
(Nobles, 1974)
(Atkinson, Han, and Stewart, 2009)
.(Sincovec, 1975)
.(Ritschel, 2013)
.(Kimura, 2009)
.(Dormand and Prince, 1980)
, where you can see the
derivation of the ODE solver RK-45
.(Soetaert, Petzoldt, and Setzer, 2010)
.(Ashino, Nagase, and Vaillancourt, 2000)
.The ODE solvers implemented in R so far:
You can install the latest development version of rODE
from github with:
# install from the *develop* branch
::install_github("f0nzie/rODE", ref = "develop") devtools
Or the stable
version from CRAN
:
install.packages("rODE")
Example scripts are located under the folder examples
inside the package.
These examples make use of a parent class containing a customized rate calculation as well as the step and startup method. The methods that you would commonly find in the base script or parent class are:
getRate()
getState()
step()
or doStep()
setStepSize()
init()
, which is not the same as the S4
class initialize
methodinitialize()
, andThese methods are defined in the virtual classes ODE
and
ODESolver
.
Two other classes that serve as definition classes for the ODE
solvers are: AbstractODESolver
and
ODEAdaptiveSolver
.
For instance, the application KeplerApp.R
needs the
class Kepler
located in the Kepler.R
script,
which is called with planet <- Kepler(r, v)
, an
ODE
object. The solver for the same application is
RK45
called with solver <- RK45(planet)
,
where planet
is a previuously declared ODE
object. Since RK45
is an ODE solver, the script
RK45.R
will be located in the folder ./R
in
the package.
The vignettes contain examples of the use of the various ODE solvers.
For instance, the notebook Comparison
and
Kepler
use the ODE solver RK45
;
FallingParticle
and Planet
use the
Euler
solver; Pendulum
makes use of
EulerRichardson
; Planet
of Euler
,
Projectile
; Reaction
of RK4
, and
KeplerEnergy
uses the ODE solver Verlet
.
There are tests for the core ODE solver classes under tests/testthat, as well as additional tests for the examples themselves.
The tests for the examples are two: one for the base/parent classes
such as Kepler
or Planet
or
Projectile
; this test runner is called
run_tests_this_folder.R
.
For the applications there is another runner
(run_test_applications.R
) that opens each of the
applications as request for a return value. If the hard coded value is
not returned, the test will fail. This ensures that any minor change in
the core solver classes do not have any impact on the application
solutions, and if there is, it must be explained.
You can test all applications under the examples
folder
by running the script run_test_applications.R
. The way it
works is by getting the list of all applications by filtering those
ending with App
. Then removes the extension .R
from each app and starts looping to call each of the applications with
do.call
. A list contains the expected
results
that are compared against the result coming out from the call to the R
application.
library(rODE)
#>
#> Attaching package: 'rODE'
#> The following object is masked from 'package:stats':
#>
#> step
importFromExamples("AdaptiveStep.R")
# running function
<- function(verbose = FALSE) {
AdaptiveStepApp <- new("Impulse")
ode <- RK45(ode)
ode_solver <- init(ode_solver, 0.1)
ode_solver <- setTolerance(ode_solver, 1.0e-4)
ode_solver <- 1; rowVector <- vector("list")
i while (getState(ode)[1] < 12) {
<- list(s1 = getState(ode)[1],
rowVector[[i]] s2 = getState(ode)[2],
t = getState(ode)[3])
<- step(ode_solver)
ode_solver <- ode_solver@ode
ode <- i + 1
i
}return(data.table::rbindlist(rowVector))
}# run application
<- AdaptiveStepApp()
solution plot(solution)
# ++++++++++++++++++++++++++++++++++++++++++++++++ example: ComparisonRK45App.R
# Compares the solution by the RK45 ODE solver versus the analytical solution
# Example file: ComparisonRK45App.R
# ODE Solver: Runge-Kutta 45
# Class: RK45
library(rODE)
importFromExamples("ODETest.R")
<- function(verbose = FALSE) {
ComparisonRK45App <- new("ODETest") # create an `ODETest` object
ode <- RK45(ode) # select the ODE solver
ode_solver <- setStepSize(ode_solver, 1) # set the step
ode_solver <- setTolerance(ode_solver, 1e-8) # set the tolerance
ode_solver <- vector("list")
rowVector <- 0
time <- 1
i while (time < 50) {
<- list(t = ode_solver@ode@state[2],
rowVector[[i]] s1 = getState(ode_solver@ode)[1],
s2 = getState(ode_solver@ode)[2],
xs = getExactSolution(ode_solver@ode, time),
rc = getRateCounts(ode),
time = time)
<- step(ode_solver) # advance one step
ode_solver <- ode_solver@stepSize # update the step size
stepSize <- time + stepSize
time <- getState(ode_solver@ode) # get the `state` vector
state <- i + 1
i
}return(data.table::rbindlist(rowVector)) # a data table with the results
}# show solution
<- ComparisonRK45App() # run the example
solution plot(solution)
# +++++++++++++++++++++++++++++++++++++++++++++++ example: FallingParticleApp.R
# Application that simulates the free fall of a ball using Euler ODE solver
library(rODE)
importFromExamples("FallingParticleODE.R") # source the class
<- function(verbose = FALSE) {
FallingParticleODEApp # initial values
<- 10
initial_y <- 0
initial_v <- 0.01
dt <- FallingParticleODE(initial_y, initial_v)
ball <- Euler(ball) # set the ODE solver
solver <- setStepSize(solver, dt) # set the step
solver <- vector("list")
rowVector <- 1
i # stop loop when the ball hits the ground, state[1] is the vertical position
while (ball@state[1] > 0) {
<- list(t = ball@state[3],
rowVector[[i]] y = ball@state[1],
vy = ball@state[2])
<- step(solver) # move one step at a time
solver <- solver@ode # update the ball state
ball <- i + 1
i
}<- data.table::rbindlist(rowVector)
DT return(DT)
}# show solution
<- FallingParticleODEApp()
solution plot(solution)
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ example KeplerApp.R
# KeplerApp solves an inverse-square law model (Kepler model) using an adaptive
# stepsize algorithm.
# Application showing two planet orbiting
# File in examples: KeplerApp.R
library(rODE)
importFromExamples("Kepler.R") # source the class Kepler
<- function(verbose = FALSE) {
KeplerApp # set the orbit into a predefined state.
<- c(2, 0) # orbit radius
r <- c(0, 0.25) # velocity
v <- 0.1
dt <- Kepler(r, v)
planet <- RK45(planet)
solver <- vector("list")
rowVector <- 1
i while (planet@state[5] <= 10) {
<- list(t = planet@state[5],
rowVector[[i]] planet1.r = planet@state[1],
p1anet1.v = planet@state[2],
planet2.r = planet@state[3],
p1anet2.v = planet@state[4])
<- step(solver)
solver <- solver@ode
planet <- i + 1
i
}<- data.table::rbindlist(rowVector)
DT return(DT)
}
<- KeplerApp()
solution plot(solution)
# ++++++++++++++++++++++++++++++++++++++++++++++++++ example: KeplerEnergyApp.R
# Demostration of the use of the Verlet ODE solver
#
library(rODE)
importFromExamples("KeplerEnergy.R") # source the class Kepler
<- function(verbose = FALSE) {
KeplerEnergyApp # initial values
<- 1
x <- 0
vx <- 0
y <- 2 * pi
vy <- 0.01
dt <- 1e-3
tol <- KeplerEnergy()
particle <- init(particle, c(x, vx, y, vy, 0))
particle <- Verlet(particle)
odeSolver <- init(odeSolver, dt)
odeSolver @odeSolver <- odeSolver
particle<- getEnergy(particle)
initialEnergy <- vector("list")
rowVector <- 1
i while (getTime(particle) <= 1.20) {
<- list(t = particle@state[5],
rowVector[[i]] x = particle@state[1],
vx = particle@state[2],
y = particle@state[3],
vy = particle@state[4],
E = getEnergy(particle))
<- doStep(particle)
particle <- getEnergy(particle)
energy <- i + 1
i
}<- data.table::rbindlist(rowVector)
DT return(DT)
}
<- KeplerEnergyApp()
solution plot(solution)
library(rODE)
importFromExamples("Logistic.R") # source the class Logistic
# Run the application
<- function(verbose = FALSE) {
LogisticApp <- 0.1
x <- 0
vx <- 2 # Malthusian parameter (rate of maximum population growth)
r <- 10.0 # carrying capacity of the environment
K <- 0.01; tol <- 1e-3; tmax <- 10
dt <- Logistic()
population <- init(population, c(x, vx, 0), r, K)
population <- Verlet(population)
odeSolver <- init(odeSolver, dt)
odeSolver @odeSolver <- odeSolver
population<- vector("list")
rowVector <- 1
i while (getTime(population) <= tmax) {
<- list(t = getTime(population),
rowVector[[i]] s1 = population@state[1],
s2 = population@state[2])
<- doStep(population)
population <- i + 1
i
}<- data.table::rbindlist(rowVector)
DT return(DT)
}# show solution
<- LogisticApp()
solution plot(solution)
# ++++++++++++++++++++++++++++++++++++++++++++++++++ example: PendulumApp.R
# Simulation of a pendulum using the EulerRichardson ODE solver
library(rODE)
suppressPackageStartupMessages(library(ggplot2))
importFromExamples("Pendulum.R") # source the class
<- function(verbose = FALSE) {
PendulumApp # initial values
<- 0.2
theta <- 0
thetaDot <- 0.1
dt <- new("ODE")
ode <- Pendulum()
pendulum @state[3] <- 0 # set time to zero, t = 0
pendulum<- setState(pendulum, theta, thetaDot)
pendulum <- setStepSize(pendulum, dt = dt) # using stepSize in RK4
pendulum @odeSolver <- setStepSize(pendulum@odeSolver, dt) # set new step size
pendulum<- vector("list")
rowvec <- 1
i while (pendulum@state[3] <= 40) {
<- list(t = pendulum@state[3], # time
rowvec[[i]] theta = pendulum@state[1], # angle
thetadot = pendulum@state[2]) # derivative of angle
<- step(pendulum)
pendulum <- i + 1
i
}<- data.table::rbindlist(rowvec)
DT return(DT)
}# show solution
<- PendulumApp()
solution plot(solution)
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ example: PlanetApp.R
# Simulation of Earth orbiting around the SUn using the Euler ODE solver
library(rODE)
importFromExamples("Planet.R") # source the class
<- function(verbose = FALSE) {
PlanetApp # x = 1, AU or Astronomical Units. Length of semimajor axis or the orbit
# of the Earth around the Sun.
<- 1; vx <- 0; y <- 0; vy <- 6.28; t <- 0
x <- c(x, vx, y, vy, t)
state <- 0.01
dt <- Planet()
planet @odeSolver <- setStepSize(planet@odeSolver, dt)
planet<- init(planet, initState = state)
planet <- vector("list")
rowvec <- 1
i # run infinite loop. stop with ESCAPE.
while (planet@state[5] <= 90) { # Earth orbit is 365 days around the sun
<- list(t = planet@state[5], # just doing 3 months
rowvec[[i]] x = planet@state[1], # to speed up for CRAN
vx = planet@state[2],
y = planet@state[3],
vy = planet@state[4])
for (j in 1:5) { # advances time
<- doStep(planet)
planet
}<- i + 1
i
}<- data.table::rbindlist(rowvec)
DT return(DT)
}# run the application
<- PlanetApp()
solution <- seq(1, nrow(solution), 10) # do not overplot
select_rows <- solution[select_rows,]
solution plot(solution)
# +++++++++++++++++++++++++++++++++++++++++++++++++ application: ProjectileApp.R
# test Projectile with RK4
# originally uses Euler
library(rODE)
importFromExamples("Projectile.R") # source the class
<- function(verbose = FALSE) {
ProjectileApp # initial values
<- 0; vx <- 10; y <- 0; vy <- 10
x <- c(x, vx, y, vy, 0) # state vector
state <- 0.01
dt <- Projectile()
projectile <- setState(projectile, x, vx, y, vy)
projectile @odeSolver <- init(projectile@odeSolver, 0.123)
projectile@odeSolver <- setStepSize(projectile@odeSolver, dt)
projectile<- vector("list")
rowV <- 1
i while (projectile@state[3] >= 0) {
<- list(t = projectile@state[5],
rowV[[i]] x = projectile@state[1],
vx = projectile@state[2],
y = projectile@state[3], # vertical position
vy = projectile@state[4])
<- step(projectile)
projectile <- i + 1
i
}<- data.table::rbindlist(rowV)
DT return(DT)
}
<- ProjectileApp()
solution plot(solution)
# +++++++++++++++++++++++++++++++++++++++++++++++++++ application: ReactionApp.R
# ReactionApp solves an autocatalytic oscillating chemical
# reaction (Brusselator model) using
# a fourth-order Runge-Kutta algorithm.
library(rODE)
importFromExamples("Reaction.R") # source the class
<- function(verbose = FALSE) {
ReactionApp <- 1; Y <- 5;
X <- 0.1
dt <- Reaction(c(X, Y, 0))
reaction <- RK4(reaction)
solver <- vector("list")
rowvec <- 1
i while (solver@ode@state[3] < 100) { # stop at t = 100
<- list(t = solver@ode@state[3],
rowvec[[i]] X = solver@ode@state[1],
Y = solver@ode@state[2])
<- step(solver)
solver <- i + 1
i
}<- data.table::rbindlist(rowvec)
DT return(DT)
}
<- ReactionApp()
solution plot(solution)
# +++++++++++++++++++++++++++++++++++++++++++++++ application: RigidBodyNXFApp.R
# example of a nonstiff system is the system of equations describing
# the motion of a rigid body without external forces.
library(rODE)
importFromExamples("RigidBody.R")
# run the application
<- function(verbose = FALSE) {
RigidBodyNXFApp # load the R class that sets up the solver for this application
<- 0 # initial y1 value
y1 <- 1 # initial y2 value
y2 <- 1 # initial y3 value
y3 <- 0.01 # delta time for step
dt <- RigidBodyNXF(y1, y2, y3)
body <- Euler(body)
solver <- setStepSize(solver, dt)
solver <- vector("list")
rowVector <- 1
i # stop loop when the body hits the ground
while (body@state[4] <= 12) {
<- list(t = body@state[4],
rowVector[[i]] y1 = body@state[1],
y2 = body@state[2],
y3 = body@state[3])
<- step(solver)
solver <- solver@ode
body <- i + 1
i
}<- data.table::rbindlist(rowVector)
DT return(DT)
}
# get the data table from the app
<- RigidBodyNXFApp()
solution plot(solution)
library(rODE)
importFromExamples("SHO.R")
# SHOApp.R
<- function(...) {
SHOApp <- 1.0; v <- 0; k <- 1.0; dt <- 0.01; tolerance <- 1e-3
x <- SHO(x, v, k)
sho <- ODESolverFactory()
solver_factory <- createODESolver(solver_factory, sho, "DormandPrince45")
solver # solver <- DormandPrince45(sho) # this can also be used
<- setTolerance(solver, tolerance)
solver <- init(solver, dt)
solver <- 1; rowVector <- vector("list")
i while (sho@state[3] <= 500) {
<- list(x = sho@state[1],
rowVector[[i]] v = sho@state[2],
t = sho@state[3])
<- step(solver)
solver <- solver@ode
sho <- i + 1
i
}return(data.table::rbindlist(rowVector))
}
<- SHOApp()
solution plot(solution)
# ++++++++++++++++++++++++++++++++++++++++++++++++++application: SpringRK4App.R
# Simulation of a spring considering no friction
library(rODE)
importFromExamples("SpringRK4.R")
# run application
<- function(verbose = FALSE) {
SpringRK4App <- 0
theta <- -0.2
thetaDot <- 22; dt <- 0.1
tmax <- new("ODE")
ode <- SpringRK4()
spring @state[3] <- 0 # set time to zero, t = 0
spring<- setState(spring, theta, thetaDot)
spring <- setStepSize(spring, dt = dt) # using stepSize in RK4
spring @odeSolver <- setStepSize(spring@odeSolver, dt) # set new step size
spring<- vector("list")
rowvec <- 1
i while (spring@state[3] <= tmax) {
<- list(t = spring@state[3], # angle
rowvec[[i]] y1 = spring@state[1], # derivative of the angle
y2 = spring@state[2]) # time
<- i + 1
i <- step(spring)
spring
}<- data.table::rbindlist(rowvec)
DT return(DT)
}
# show solution
<- SpringRK4App()
solution plot(solution)
# ++++++++++++++++++++++++++++++++++++++++++++++++ application: VanderPolApp.R
# Solution of the Van der Pol equation
#
library(rODE)
importFromExamples("VanderPol.R")
# run the application
<- function(verbose = FALSE) {
VanderpolApp # set the orbit into a predefined state.
<- 2; y2 <- 0; dt <- 0.1;
y1 <- VanderPol(y1, y2)
rigid_body <- RK45(rigid_body)
solver <- vector("list")
rowVector <- 1
i while (rigid_body@state[3] <= 20) {
<- list(t = rigid_body@state[3],
rowVector[[i]] y1 = rigid_body@state[1],
y2 = rigid_body@state[2])
<- step(solver)
solver <- solver@ode
rigid_body <- i + 1
i
}<- data.table::rbindlist(rowVector)
DT return(DT)
}# show solution
<- VanderpolApp()
solution plot(solution)
# +++++++++++++++++++++++++++++++++++++ example: VanderpolMuTimeControlApp.R
# This is a modification of the original Vanderpol.R script
# In this version, we will add tha ability of setting mu and time lapse.
# This example is also shown in the Matlab help guide
library(rODE)
importFromExamples("VanderpolMuTimeControl.R")
# run the application
<- function(verbose = FALSE) {
VanderpolMuTimeControlApp # set the orbit into a predefined state.
<- 2; y2 <- 0; mu <- 10; tmax <- mu * 3; dt <- 0.01
y1 <- VanderPol(y1, y2, mu)
rigid_body <- RK45(rigid_body)
solver <- vector("list")
rowVector <- 1
i while (rigid_body@state[3] <= tmax) {
<- list(t = rigid_body@state[3],
rowVector[[i]] y1 = rigid_body@state[1],
y2 = rigid_body@state[2]
)<- step(solver)
solver <- solver@ode
rigid_body <- i + 1
i
}<- data.table::rbindlist(rowVector)
DT return(DT)
}
# show solution
<- VanderpolMuTimeControlApp()
solution plot(solution)
The following books and papers were consulted during the development of this package:
[1] R. Ashino, M. Nagase and R. Vaillancourt. “Behind and beyond the Matlab ODE suite”. In: Computers & Mathematics with Applications 40.4-5 (Aug. 2000), pp. 491-512. DOI: 10.1016/s0898-1221(00)00175-9.
[2] K. Atkinson, W. Han and D. E. Stewart. Numerical Solution of Ordinary Differential Equations. Wiley, 2009. ISBN: 978-0-470-04294-6.
[3] J. R. Dormand and P. J. Prince. “A family of embedded Runge-Kutta formulae”. In: Journal of computational and applied mathematics 6.1 (Mar. 1980), pp. 19-26. DOI: 10.1016/0771-050x(80)90013-3.
[4] H. Gould, J. Tobochnik and W. Christian. An Introduction to Computer Simulation Methods: Applications To Physical Systems. CreateSpace Independent Publishing Platform, 2017. ISBN: 978-1974427475.
[5] T. Kimura. “On dormand-prince method”. In: Retrieved April 27 (2009), p. 2014. <URL: http://depa.fquim.unam.mx/amyd/archivero/DormandPrince_19856.pdf>.
[6] M. A. Nobles. Using the Computer to Solve Petroleum Engineering Problems. Gulf Publishing Co, 1974. ISBN: 978-0872018860.
[7] T. Ritschel. “Numerical Methods For Solution of Di<U+FB00>erential Equations”. Cand. thesis. DTU supervisor: John Bagterp Jørgensen, jbjo@dtu.dk, DTU Compute. Technical University of Denmark, Department of Applied Mathematics and Computer Science, 2013, p. 224. <URL: http://www.compute.dtu.dk/English.aspx>.
[8] R. Sincovec. “Numerical Reservoir Simulation Using an Ordinary-Differential-Equations Integrator”. In: Society of Petroleum Engineers Journal 15.03 (Jun. 1975), pp. 255-264. DOI: 10.2118/5104-pa.
[9] K. Soetaert, T. Petzoldt and R. W. Setzer. “Solving differential equations in R: package deSolve”. In: Journal of Statistical Software 33 (2010).