Nothing
## Test basic system building and solving using the harmonic oscillator modules.
##
## Bugs: The tests may fail for large values of ω (that is, when the spring
## constant is very large compared to the mass). This might be remedied by
## screening out these cases as not suitable for the model or possibly by
## adjusting the error tolerance in such cases.
DEBUG_TEST <- FALSE # Change this to TRUE to get useful output for debugging these tests.
NUMBER_OF_TRIALS <- 10 # number of different sets of parameters and initial conditions to test
MAX_INDEX <- 100 # how long to run the simulation
SAMPLE_SIZE <- 5 # number of time points to test in each simulation result
# The default tolerance factor used in the third edition of testthat for
# expect_equal is very tight, so we need to modify it here
TOLERANCE <- testthat_tolerance() * 1e4
## Equations for the position x and velocity v of an undamped oscillating mass are
##
## x = A sin(ωt + φ)
## v = ωA cos(ωt + φ)
##
## where A is the amplitude, ω is the angular frequency, and φ is the phase.
##
## A, φ, and ω may in turn be computed from the mass m, the spring constant k, the
## initial position x0, and the initial velocity v0 using the equations
##
## A = sqrt(x0^2 + m/k * v0^2)
## φ = arctan(x0 * ω / v0) (if v0 > 0)
## = arctan(x0 * ω / v0) + π (if v0 < 0)
## = π/2 (if v0 = 0 and x0 > 0)
## = -π/2 (if v0 = 0 and x0 < 0)
## ω = sqrt(k/m)
##
## Note that φ is undefined if both x0 and v0 are zero, but in this case A will
## also be zero, so the value of φ is immaterial and may be arbitrarily assigned
## a value of zero. m and k are assumed to be non-zero.
##
## The total energy of the system is constant over time and is given by
##
## E = 1/2 * kA^2
##
##
## A note on units
##
## In the equations above, we assume a consistent set of units so that no
## conversion factors are required. Moreover, φ is assumed to be in radians and
## ω in radians per unit time.
##
## Although it is a goal of BioCro to work exclusively in SI units, currently,
## the base time unit for BioCro is always an hour. This means that even if we
## choose meters for the unit of length and kilogram for the unit of mass, the
## derived units in which the quantities v (velocity), k (the spring constant),
## and E (the system energy) will not be expressed in SI units. v, for example,
## will be in meters per hour, and k will be in kilograms per hour squared, or
## in other words, a unit equivalent to 7.716 x 10^-8 newtons per meter. E will
## be in units of kilogram-meters squared per hour squared, a unit equivalent to
## 7.716 x 10^-8 joules.
## helper functions:
position <- function(time, amplitude, angular_frequency, phase) {
amplitude * sin(angular_frequency * time + phase)
}
velocity <- function(time, amplitude, angular_frequency, phase) {
angular_frequency * amplitude * cos(angular_frequency * time + phase)
}
debug_print <- function(ob) {
if (DEBUG_TEST) print(ob)
}
debug_view <- function(ob) {
if (DEBUG_TEST) View(ob)
}
differential_modules <- "BioCro:harmonic_oscillator"
direct_modules <- "BioCro:harmonic_energy"
drivers <- data.frame(doy=rep(0, MAX_INDEX), hour=seq(from=0, by=1, length=MAX_INDEX))
default_ode_solver <- list(type='boost_rkck54', output_step_size=1, adaptive_rel_error_tol=1e-7, adaptive_abs_error_tol=1e-7, adaptive_max_steps=200)
## Given system parameters and initial conditions, run a simulation of harmonic
## motion and test that the values from the simulation match those predicted
## from harmonic motion equations.
run_trial <- function(initial_position, initial_velocity, mass, spring_constant, ode_solver, trial_description) {
initial_values <- list(position=initial_position, velocity=initial_velocity)
parameters <- list(mass=mass, spring_constant=spring_constant, timestep=1)
debug_print(initial_values)
debug_print(parameters)
## compute equation of motion parameters:
amplitude <- sqrt(initial_position^2 + initial_velocity^2 * mass / spring_constant)
angular_frequency <- sqrt(spring_constant / mass)
# arbitrarily set the phase to zero if the mass isn't moving:
phase <- if (initial_velocity == 0) {
if (initial_position > 0) pi/2
else if (initial_position < 0) -pi/2
else 0
}
else atan(initial_position * angular_frequency / initial_velocity) + (if (initial_velocity < 0) pi else 0)
## compute the total energy, which doesn't depend on time:
total_energy <- 0.5 * spring_constant * amplitude^2
debug_print(list(amplitude = amplitude, phase = phase, angular_frequency = angular_frequency))
## calculate the derivative corresponding to the initial conditions and
## compare against the expected values
oscillator_system_derivative_fcn <- system_derivatives(
parameters,
drivers,
direct_modules,
differential_modules
)
iv <- unlist(initial_values)
initial_derivative <- oscillator_system_derivative_fcn(0, iv, NULL)
expected_position_deriv <- initial_velocity
expected_velocity_deriv <- -spring_constant * initial_position / mass
expect_equal(initial_derivative[[1]][['position']], expected_position_deriv, tolerance = TOLERANCE)
expect_equal(initial_derivative[[1]][['velocity']], expected_velocity_deriv, tolerance = TOLERANCE)
## try out the ode_solver
result <- run_biocro(initial_values, parameters, drivers, direct_modules, differential_modules, ode_solver)
## add useful columns to the resulting data frame:
result$time <- result$time * 24 # time is in hours
result$expected_position <- position(result$time, amplitude, angular_frequency, phase)
result$expected_velocity <- velocity(result$time, amplitude, angular_frequency, phase)
## select and order the columns we wish to keep (for debug display):
result <- result[, c("time", "expected_position", "position", "expected_velocity", "velocity", "kinetic_energy", "spring_energy", "total_energy")]
debug_view(result)
if (DEBUG_TEST) {
## something is seriously wrong if the initial values doesn't conform to the equations of motion:
if (abs(result$position[1] - result$expected_position[1]) > .01) break
if (abs(result$velocity[1] - result$expected_velocity[1]) > .01) break
}
overall_description <- paste("Harmonic oscillator position and velocity values match the expected values (", trial_description, ")", sep="")
test_that(overall_description, {
expect_true(class(result) == "data.frame") # sanity check
sample <- sample(1:MAX_INDEX, SAMPLE_SIZE) # randomly choose a number of points in the evolution of the system to test
for (index in sample) {
time <- result$time[index] # for convenience
## in these tests, we set the tolerance based on the maximum posible value for each quantity:
expect_equal(result$position[index], result$expected_position[index], tolerance = TOLERANCE)
expect_equal(result$velocity[index], result$expected_velocity[index], tolerance = TOLERANCE)
expect_equal(result$total_energy[index], total_energy, tolerance = TOLERANCE)
}
})
}
## some special cases
run_trial(initial_position = 0, initial_velocity = 26.18, mass = 14.59, spring_constant = 1, default_ode_solver, "initial position 0, amplitude 100, and period 24")
run_trial(initial_position = 0, initial_velocity = 0, mass = 100, spring_constant = 100, default_ode_solver, "a mass at rest")
run_trial(initial_position = 10, initial_velocity = 0, mass = 100, spring_constant = 100, default_ode_solver, "a mass with no initial velocity with initial positive displacement")
run_trial(initial_position = -10, initial_velocity = 0, mass = 100, spring_constant = 100, default_ode_solver, "a mass with no initial velocity and with initial negative displacement")
## run a number of randomly-chosen cases. set a seed first to ensure
## repeatability, since this test sometimes picks particularly nasty parameters
set.seed(1234)
for (trial_number in seq(length=NUMBER_OF_TRIALS)) {
## randomly select parameter values and initial values:
initial_position <- runif(1, -100, 100)[1]
initial_velocity <- runif(1, -100, 100)[1]
mass <- runif(1, 0, 100)[1]
spring_constant <- runif(1, 0, 100)[1]
run_trial(initial_position, initial_velocity, mass, spring_constant, default_ode_solver, "random parameters and initial values")
}
## test each ode_solver method using a really weak spring (so the Euler methods still work)
all_ode_solver_types <- get_all_ode_solvers()
for (ode_solver_type in all_ode_solver_types) {
ode_solver <- default_ode_solver
ode_solver$type <- ode_solver_type
description <- paste("using the ", ode_solver_type, " method", sep="")
run_trial(initial_position = 1, initial_velocity = 0, mass = 1, spring_constant = 1e-6, ode_solver, description)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.