## 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) }