tests/testthat/test-difeq.R

context("discrete")

## Solve some basic discrete time equations
test_that("increase", {
  rhs <- function(i, y, p) {
    y + p
  }
  y0 <- as.numeric(1:5)
  p <- 1
  i <- 0:10
  res <- difeq(y0, i, rhs, p)

  expect_equal(res[, 1], i)
  expect_equal(unname(res[, -1]), outer(i, y0, "+"))

  y0 <- runif(5)
  p <- runif(5)
  res <- difeq(y0, i, rhs, p)
  cmp <- t(y0 + outer(p, i))
  expect_equal(unname(res[, -1]), cmp)

  res <- difeq(y0, i, rhs, p, n_history = length(i))

  ## Check the history buffer:
  h <- attr(res, "history")
  expect_equal(h[1, ], i)
  expect_equal(h[-1, ], t(cmp))

  ## Check the returned values:
  attr(res, "history") <- NULL
  expect_equal(unname(res[, -1]), cmp)

  ## And again, but using the shortcut:
  res2 <- difeq(y0, max(i), rhs, p,
                return_initial = TRUE,
                n_history = length(i))
  expect_equal(attr(res2, "history"), h)
  attr(res2, "history") <- NULL
  expect_equal(res2, res)

  ## And again, but with only a few history times:
  i2 <- seq(0, 10, by = 2)
  res <- difeq(y0, i2, rhs, p,
               return_initial = TRUE,
               n_history = length(i))
  ## The history length should not change here.
  expect_identical(attr(res, "history"), h)
  attr(res, "history") <- NULL
  expect_equal(unname(res[, -1]), cmp[i2 + 1, ])
})

test_that("output (R)", {
  rhs <- function(i, y, p) {
    ret <- y + p
    attr(ret, "output") <- sum(y)
    ret
  }
  y0 <- runif(5)
  p <- runif(5)
  i <- 0:10
  i2 <- seq(0, 10, by = 2)

  cmp <- y0 + outer(p, i)
  cmp2 <- cmp[, i2 + 1L]

  ## We run this three times:
  ##   * res_o: coming off the output storage
  ##   * res_h: coming off history
  ##   * res_n: coming off of short history
  res_o <- difeq(y0, i, rhs, p, return_by_column = FALSE, return_step = FALSE,
                 return_output_with_y = FALSE,
                 n_out = 1)
  res_h <- difeq(y0, i, rhs, p, return_by_column = FALSE, return_step = FALSE,
                 return_output_with_y = FALSE,
                 n_out = 1, n_history = length(i))
  res_n <- difeq(y0, i, rhs, p, return_by_column = FALSE, return_step = FALSE,
                 return_output_with_y = FALSE,
                 n_out = 1, n_history = 2L)

  expect_equal(res_o, cmp, check.attributes = FALSE)
  expect_equal(res_h, cmp, check.attributes = FALSE)
  expect_equal(res_n, cmp, check.attributes = FALSE)

  output <- attr(res_o, "output")
  expect_is(output, "matrix")
  expect_equal(dim(output), c(1, length(i)))
  ## This checks off-by-one errors
  expect_equal(drop(output), colSums(res_o))
  expect_equal(drop(attr(res_h, "output")), colSums(res_h))
  expect_equal(drop(attr(res_n, "output")), colSums(res_n))

  ## Also check with no initial condition; this changes things around
  ## a bit.
  res_o <- difeq(y0, i, rhs, p, return_initial = FALSE,
                 return_by_column = FALSE, return_step = FALSE,
                 return_output_with_y = FALSE, n_out = 1)
  res_h <- difeq(y0, i, rhs, p, return_initial = FALSE,
                 return_by_column = FALSE, return_step = FALSE,
                 return_output_with_y = FALSE, n_out = 1,
                 n_history = length(i))
  res_n <- difeq(y0, i, rhs, p, return_initial = FALSE,
                 return_by_column = FALSE, return_step = FALSE,
                 return_output_with_y = FALSE, n_out = 1,
                 n_history = 2L)

  expect_equal(res_o, cmp[, -1], check.attributes = FALSE)
  expect_equal(res_h, cmp[, -1], check.attributes = FALSE)
  expect_equal(res_n, cmp[, -1], check.attributes = FALSE)

  expect_equal(drop(attr(res_o, "output")), colSums(res_o))
  expect_equal(drop(attr(res_h, "output")), colSums(res_h))
  expect_equal(drop(attr(res_n, "output")), colSums(res_n))

  ## There's another huge class of bugs that turns up when output is
  ## filtered, so try that too.
  res_o <- difeq(y0, i2, rhs, p, return_by_column = FALSE, return_step = FALSE,
                 return_output_with_y = FALSE, n_out = 1)
  res_h <- difeq(y0, i2, rhs, p, return_by_column = FALSE, return_step = FALSE,
                 return_output_with_y = FALSE, n_out = 1,
                 n_history = length(i))
  res_n <- difeq(y0, i2, rhs, p, return_by_column = FALSE, return_step = FALSE,
                 return_output_with_y = FALSE, n_out = 1,
                 n_history = 2L)

  expect_equal(res_o, cmp2, check.attributes = FALSE)
  expect_equal(res_h, cmp2, check.attributes = FALSE)
  expect_equal(res_n, cmp2, check.attributes = FALSE)

  output <- attr(res_o, "output")
  expect_is(output, "matrix")
  expect_equal(dim(output), c(1, length(i2)))
  expect_equal(drop(output), colSums(res_o))
  expect_equal(drop(output), colSums(res_o))
  expect_equal(drop(attr(res_h, "output")), colSums(res_h))
  expect_equal(drop(attr(res_n, "output")), colSums(res_n))

  ## And while dropping initial conditions:
  res_o <- difeq(y0, i2, rhs, p, return_initial = FALSE,
                 return_by_column = FALSE, return_step = FALSE,
                 return_output_with_y = FALSE, n_out = 1)
  res_h <- difeq(y0, i2, rhs, p, return_initial = FALSE,
                 return_by_column = FALSE, return_step = FALSE,
                 return_output_with_y = FALSE, n_out = 1,
                 n_history = length(i))
  res_n <- difeq(y0, i2, rhs, p, return_initial = FALSE,
                 return_by_column = FALSE, return_step = FALSE,
                 return_output_with_y = FALSE, n_out = 1,
                 n_history = 2L)

  expect_equal(res_o, cmp2[, -1], check.attributes = FALSE)
  expect_equal(res_h, cmp2[, -1], check.attributes = FALSE)
  expect_equal(res_n, cmp2[, -1], check.attributes = FALSE)

  expect_equal(drop(attr(res_o, "output")), colSums(res_h))
  expect_equal(drop(attr(res_h, "output")), colSums(res_h))
  expect_equal(drop(attr(res_n, "output")), colSums(res_n))
})

test_that("transpose output", {
  rhs <- function(i, y, p) {
    ret <- y + p
    attr(ret, "output") <- sum(y)
    ret
  }
  y0 <- runif(5)
  p <- runif(5)
  i <- 0:10
  i2 <- seq(0, 10, by = 2)

  cmp <- y0 + outer(p, i)
  cmp2 <- cmp[, i2 + 1L]

  res_mo <- difeq(y0, i, rhs, p, return_minimal = TRUE, n_out = 1)
  res_mh <- difeq(y0, i, rhs, p, return_minimal = TRUE, n_out = 1,
                 n_history = length(i), return_history = FALSE)
  res_fo <- difeq(y0, i, rhs, p, n_out = 1, ynames = TRUE)
  res_fh <- difeq(y0, i, rhs, p, n_out = 1, ynames = TRUE,
                 n_history = length(i), return_history = FALSE)

  expect_equal(colnames(res_fo), c("step", as.character(seq_along(y0)), ""))
  expect_null(dimnames(res_mo))

  expect_equal(res_fo[, 1], i)
  expect_equal(dim(res_fo), c(ncol(res_mo) + 1, nrow(res_mo) + 2))

  expect_equal(res_mo, cmp[, -1], check.attributes = FALSE)
  expect_equal(res_fo[, -c(1, ncol(res_fo))], t(cmp), check.attributes = FALSE)

  expect_equal(res_fo[, ncol(res_fo)], colSums(cmp))
  expect_equal(attr(res_mo, "output")[1, ], colSums(cmp)[-1])

  expect_null(attr(res_fo, "output"))
  expect_equal(res_fo, res_fh)
  expect_equal(res_mo, res_mh)
})

test_that("incorrect output", {
  f <- function(i, y, p) {
    structure(y, output = p)
  }
  y0 <- runif(5)
  i <- 0:10

  ## Check that things generally work:
  expect_equal(difeq(y0, i, f, NULL, return_minimal = TRUE),
               matrix(y0, length(y0), length(i) - 1L))

  ## These are going to cause leaks for now, but I need to handle
  ## errors in the calling function too.
  ##
  ## The options for doing this are going to be either wrapping every
  ## call to an R objective function in tryCatch (which is quite slow;
  ## being ~3us rather than a few seconds) or smart pointers.
  expect_error(difeq(y0, i, f, NULL, n_out = 1),
               "Missing output")
  expect_error(difeq(y0, i, f, c(1, 2), n_out = 1),
               "Incorrect length")
  expect_error(difeq(y0, i, f, 1L, n_out = 1),
               "Incorrect type")
})

test_that("logistic", {
  logistic <- function(i, y, p) {
    r * y * (1 - y)
  }
  y0 <- 0.1
  r <- 1.5
  i <- 0:20

  cmp <- difeq(y0, i, logistic, r)
  res <- difeq(y0, i, "logistic", r, dllname = "dde_logistic")
  expect_equal(res, cmp)
})

test_that("vector output (R)", {
  growth <- function(i, y, p) {
    structure(y + p, output = y + 1)
  }

  y0 <- runif(5)
  p <- runif(5)
  i <- 0:10
  i2 <- seq(0, 10, by = 2)

  cmp <- y0 + outer(p, i)
  cmp2 <- y0 + outer(p, i2)

  ## TODO: The NA in history here looks very fixable to me.
  res <- difeq(y0, i, growth, p, return_initial = TRUE, n_out = 5L,
               return_by_column = FALSE, return_step = FALSE,
               return_output_with_y = FALSE,
               n_history = length(i))
  expect_equal(res, cmp, check.attributes = FALSE)
  expect_equal(attr(res, "output"), cmp + 1)
  h <- attr(res, "history")
  expect_equal(h[1, ], i)
  expect_equal(h[seq_along(y0) + 1, ], cmp)
  expect_equal(h[seq_along(y0) + 1 + length(y0), ],
               cbind(NA, cmp[, -1], deparse.level = 0) + 1)

  res2 <- difeq(y0, i2, growth, p, return_initial = TRUE, n_out = 5L,
               return_by_column = FALSE, return_step = FALSE,
               return_output_with_y = FALSE,
                n_history = length(i))
  j <- seq_len(length(y0) + 1)
  expect_equal(attr(res2, "history")[j, ], h[j, ])

  expect_equal(res2, cmp2, check.attributes = FALSE)
  expect_equal(attr(res2, "output"), cmp2 + 1)

  ## NOTE: At the moment the status of output within history is a bit
  ## of a mess; nobody should rely on it being there, or use it for
  ## anything.
})

test_that("error conditions", {
  rhs <- function(i, y, p) {
    y + p
  }
  y0 <- as.numeric(1:5)
  p <- 1
  i <- 0:10

  ## Beginning and end times are the same:
  expect_error(difeq(y0, 0, rhs, p),
               "Beginning and end steps are the same")
  expect_error(difeq(y0, c(0, 0), rhs, p),
               "Beginning and end steps are the same")
  expect_error(difeq(y0, c(i, 0), rhs, p),
               "Beginning and end steps are the same")

  ## Incorrect order:
  expect_error(difeq(y0, rev(i), rhs, p),
               "Steps not strictly increasing")
  expect_error(difeq(y0, c(0, 1, 1, 2), rhs, p),
               "Steps not strictly increasing")
  expect_error(difeq(y0, c(0, 2, 1), rhs, p),
               "Steps not strictly increasing")

  expect_error(difeq(y0, i, rhs, p, unknown = TRUE),
               "Invalid dot arguments")

  res <- difeq(y0, i, rhs, p, restartable = TRUE)
  expect_error(difeq_continue(res, i + 1, unknown = TRUE),
               "Invalid dot arguments")

  expect_error(difeq(y0, i, rhs, p, dllname = "dde_logistic"),
               "dllname must not be given")

  expect_error(difeq(y0, (-5):(-1), rhs, p),
               "steps must be positive")

  expect_error(difeq(y0, i, rhs, p, n_history = 1L),
               "n_history must be at least 2")
  expect_error(difeq(y0, i, rhs, p, n_history = -1L),
               "n_history must be nonnegative")
})

test_that("names", {
  p <- c(10, 28, 8 / 3)
  y0 <- c(10, 1, 1)

  rhs <- function(i, y, p) {
    structure(y + p$x, output = p$output)
  }

  y0 <- as.numeric(1:5)
  p <- list(x = 1, output = NULL)
  i <- 0:10

  nms <- letters[seq_along(y0)]
  cmp <- list(NULL, c("step", nms))

  ## These are duplicated from ode:
  expect_null(dimnames(difeq(y0, i, rhs, p, ynames = FALSE)))
  expect_equal(dimnames(difeq(y0, i, rhs, p, ynames = nms)), cmp)
  expect_equal(dimnames(difeq(setNames(y0, nms), i, rhs, p)), cmp)
  expect_null(dimnames(difeq(setNames(y0, nms), i, rhs, p, ynames = FALSE)),
              cmp)

  p$output <- runif(3)
  onms <- LETTERS[seq_along(p$output)]
  ocmp <- list(NULL, onms)

  f <- function(..., return_output_with_y = FALSE) {
    difeq(y0, i, rhs, p, n_out = length(p$output),
          return_output_with_y = return_output_with_y, ...)
  }
  expect_equal(dim(attr(f(), "output")), c(length(i), length(p$output)))
  expect_null(dimnames(attr(f(), "output")))
  expect_null(dimnames(attr(f(outnames = NULL), "output")))
  expect_equal(dimnames(attr(f(outnames = onms), "output")), ocmp)
  expect_error(f(outnames = nms), "outnames must have length n_out")
  expect_error(f(outnames = 1), "Invalid value for outnames")

  ## Check both together:
  res <- f(ynames = nms, outnames = onms)
  expect_equal(dimnames(res), cmp)
  expect_equal(dimnames(attr(res, "output")), ocmp)
})

test_that("don't call yprev improperly", {
  expect_error(yprev(10), "Can't call this without being in an integration")
})

test_that("externalptr input", {
  y0 <- runif(5)
  r <- runif(length(y0))
  i <- 0:11

  cmp <- difeq(y0, i, "logistic", r, dllname = "dde_logistic")

  ptr <- .Call("logistic_init",  r, PACKAGE = "dde_logistic2")
  expect_is(ptr, "externalptr")
  res <- difeq(y0, i, "logistic", ptr, parms_are_real = FALSE,
               dllname = "dde_logistic2")

  expect_identical(res, cmp)
})

test_that("externalptr input safety", {
  y0 <- runif(5)
  r <- runif(length(y0))
  i <- 0:11
  ptr <- .Call("logistic_init",  r, PACKAGE = "dde_logistic2")
  expect_error(difeq(y0, i, "logistic", make_null_pointer(ptr),
                     parms_are_real = FALSE,
                     dllname = "dde_logistic2"),
               "Was passed null pointer for 'parms'")
})

test_that("externalptr target", {
  y0 <- runif(5)
  r <- runif(length(y0))
  i <- 0:11
  cmp <- difeq(y0, i, "logistic", r, dllname = "dde_logistic")
  ptr <- getNativeSymbolInfo("logistic", PACKAGE = "dde_logistic")
  expect_identical(difeq(y0, i, ptr, r), cmp)
})

test_that("externalptr target safety", {
  y0 <- runif(5)
  r <- runif(length(y0))
  i <- 0:11
  ptr <- getNativeSymbolInfo("logistic", PACKAGE = "dde_logistic")
  expect_error(difeq(y0, i, make_null_pointer(ptr), r),
               "Was passed null pointer for 'target'")
})

test_that("grow history", {
  rhs <- function(i, y, p) {
    y + p
  }
  y0 <- runif(5)
  p <- 1
  i <- 0:50

  res <- difeq(y0, i, rhs, p, return_initial = FALSE,
               n_history = 5, grow_history = TRUE)
  h <- attr(res, "history")
  expect_equal(ncol(h), length(i))

  cmp <- difeq(y0, i, rhs, p, return_initial = FALSE, n_history = 100)
  hc <- attr(cmp, "history")

  expect_equal(h, hc)
  expect_equal(cmp, res)
})


test_that("restart and copy", {
  growth <- function(i, y, p) {
    y + p
  }

  n <- 5L
  y0 <- runif(n)
  p <- runif(n)

  tt <- 0:50
  tc <- 20
  tt1 <- tt[tt <= tc]
  tt2 <- tt[tt >= tc]

  cmp <- difeq(y0, tt, growth, p)
  cmp2 <- cmp[tt >= tc, ]

  res1 <- difeq(y0, tt1, growth, p, restartable = TRUE)

  res2 <- difeq_continue(res1, tt2, copy = TRUE)
  res3 <- difeq_continue(res1, tt2, copy = TRUE)

  expect_equal(res2, cmp2, check.attributes = FALSE)
  expect_equal(res2, res3)

  res4 <- difeq_continue(res1, tt2, copy = FALSE)
  expect_equal(res4, cmp2, check.attributes = FALSE)

  ## Can't step forward now:
  expect_error(difeq_continue(res1, tt2),
               "Incorrect initial step on simulation restart")
})

test_that("restart error handling", {
  growth <- function(i, y, p) {
    y + p
  }
  n <- 5L
  y0 <- runif(n)
  p <- runif(n)
  tt1 <- 0:10
  tt2 <- 10:20
  res1 <- difeq(y0, tt1, growth, p, restartable = TRUE)

  y1 <- res1[nrow(res1), seq_len(n) + 1]
  expect_error(difeq_continue(res1, tt2, y1[-1], copy = FALSE),
               "Incorrect size 'y' on simulation restart", fixed = TRUE)
  expect_error(difeq_continue(res1, tt2[1], y1, copy = FALSE),
               "At least two steps must be given", fixed = TRUE)
  expect_error(difeq_continue(res1, numeric(0), y1, copy = FALSE),
               "At least two steps must be given", fixed = TRUE)
  expect_error(difeq_continue(res1, tt2[-1], y1, copy = FALSE),
               "Incorrect initial step on simulation restart", fixed = TRUE)
})

test_that("incorrect length output", {
  f <- function(i, y, p) {
    1
  }
  expect_error(difeq(c(1, 2), 0:5, f, NULL),
               "Incorrect length variable output")
})
mrc-ide/dde documentation built on Sept. 28, 2024, 10:54 p.m.