Skip to content

Commit

Permalink
Merge pull request #249 from mrc-ide/i248
Browse files Browse the repository at this point in the history
Generate js code directly from odin code
  • Loading branch information
weshinsley authored Nov 15, 2021
2 parents fb72af9 + 4b30326 commit 713ee8e
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 108 deletions.
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: odin
Title: ODE Generation and Integration
Version: 1.3.0
Version: 1.3.1
Authors@R: c(person("Rich", "FitzJohn", role = c("aut", "cre"),
email = "rich.fitzjohn@gmail.com"),
person("Thibaut", "Jombart", role = "ctb"),
Expand Down
73 changes: 29 additions & 44 deletions R/js_bundle.R
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
##' Create a JavaScript bundle of odin models
##' Create a JavaScript bundle of an odin model
##'
##' @title Create a bundle of odin models
##' @section Warning:
##'
##' @param filenames Filenames with odin source code
##' The interface and generated code here are subject to change.
##'
##' @param dest Destination file for the generated javascript
##' @title Create a bundle of an odin model
##'
##' @param code An expression, string or path to a file containing
##' odin code (as for [odin::odin_parse_]
##'
##' @param include Optional vector of paths of filenames to include
##' into the javascript bundle
Expand All @@ -13,57 +16,37 @@
##' should be included as well.
##'
##' @export
odin_js_bundle <- function(filenames, dest = tempfile(),
##' @examples
##' js <- odin::odin_js_bundle(quote({
##' deriv(x) <- 1
##' initial(x) <- 1
##' }), include_dopri = FALSE)
##' head(js)
odin_js_bundle <- function(code,
include = NULL,
include_dopri = TRUE) {
## The two options here seem to be: use a vector of paths to source
## files or use a path to a directory. The rest of the interface is
## also totally subject to change because we might want to move
## those options into the general options interface. The option to
## directly minify from R via V8 would be nice too but probably is
## not possible because of the way that the minification process
## involves the disk.
err <- !file.exists(filenames)
if (any(err)) {
stop(sprintf("%s not exist: %s",
ngettext(sum(err), "File does", "Files do"),
paste(squote(filenames[err]), collapse = ", ")))
}

options <- odin_options(target = "js")
ir <- odin_parse_(code, options)
dat <- generate_js(ir, options)

f <- function(file) {
ir <- odin_parse_(file, options)
generate_js(ir, options)
}
dat <- lapply(filenames, f)

nms <- vcapply(dat, "[[", "name")
err <- duplicated(nms)
if (any(err)) {
stop(sprintf("Duplicate model names: %s",
paste(squote(unique(nms[err])), collapse = ", ")))
}

needed <- apply(do.call(cbind, lapply(dat, "[[", "include")), 1, any)
support <- c(
if (include_dopri) "dopri.js",
"support.js",
names(which(needed)))
names(which(dat$include)))
support_js <- lapply(odin_file(file.path("js", support)),
readLines, warn = FALSE)

if (!is.null(include)) {
include <- js_flatten_eqs(lapply(include, readLines))
}
support_js <- lapply(odin_file(file.path("js", support)),
readLines, warn = FALSE)

code <- c(js_flatten_eqs(support_js),
sprintf("var %s = {};", JS_GENERATORS),
js_flatten_eqs(lapply(dat, "[[", "code")),
include)

writeLines(code, dest)
dest
## TODO: Revisit this now that there's only one generator and work
## out what a sensible return type would be. This depends to a
## degree on how things want to be pushed around in wodin.
c(js_flatten_eqs(support_js),
sprintf("var %s = {};", JS_GENERATORS),
dat$code,
include)
}


Expand All @@ -84,7 +67,9 @@ odin_js_example <- function(filename, dest = tempfile()) {
html <- dir(path, pattern = "\\.html$", full.names = TRUE)

dir.create(dest, FALSE, TRUE)
odin_js_bundle(filename, file.path(dest, "odin.js"), include = include)
writeLines(
odin_js_bundle(filename, include = include),
file.path(dest, "odin.js"))
file.copy(html, dest, overwrite = TRUE)
dest
}
25 changes: 18 additions & 7 deletions man/odin_js_bundle.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 13 additions & 56 deletions tests/testthat/test-js-bundle.R
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,14 @@ test_that("bundle works", {
"N0 <- user(1)",
"K <- 100",
"r <- user()")
p <- tempfile()
dir.create(p)
filename <- file.path(p, "odin.R")
writeLines(code, filename)

res <- odin_js_bundle(filename)
bundle <- odin_js_bundle(code)

## Keep unwanted bits out:
txt <- readLines(res)
expect_false(any(grepl("odinSum2", txt)))
expect_false(any(grepl("interpolateCheckY", txt)))
expect_false(any(grepl("odinSum2", bundle)))
expect_false(any(grepl("interpolateCheckY", bundle)))

ct <- V8::v8()
invisible(ct$source(res))
invisible(ct$eval(bundle))

t <- 0:10
res <- call_odin_bundle(ct, "odin", list(r = 0.5), t)
Expand All @@ -31,27 +25,6 @@ test_that("bundle works", {
})


test_that("bundle fails with missing files", {
path1 <- file.path(tempdir(), "myfile1.R")
path2 <- file.path(tempdir(), "myfile2.R")
expect_error(odin_js_bundle(path1), "File does not exist:")
expect_error(odin_js_bundle(c(path1, path2)), "Files do not exist:")
})


test_that("unique model names", {
path1 <- tempfile()
path2 <- tempfile()

code <- c("deriv(y) <- 1", "initial(y) <- 1", 'config(base) <- "test"')
writeLines(code, path1)
writeLines(code, path2)

expect_error(odin_js_bundle(c(path1, path2)),
"Duplicate model names: 'test'")
})


test_that("include interpolate", {
code <- c("deriv(y) <- pulse",
"initial(y) <- 0",
Expand All @@ -62,15 +35,10 @@ test_that("include interpolate", {
"dim(zp) <- user()",
"output(p) <- pulse")

p <- tempfile()
dir.create(p)
filename <- file.path(p, "odin.R")
writeLines(code, filename)

res <- odin_js_bundle(filename)
res <- odin_js_bundle(code)

ct <- V8::v8()
invisible(ct$source(res))
invisible(ct$eval(res))

tt <- seq(0, 3, length.out = 301)
tp <- c(0, 1, 2)
Expand All @@ -93,14 +61,11 @@ test_that("include sum", {
"output(y2[]) <- y[i] * 2",
"dim(y2) <- length(y)")

p <- tempfile()
dir.create(p)
filename <- file.path(p, "odin.R")
writeLines(code, filename)
res <- odin_js_bundle(filename)

res <- odin_js_bundle(code)

ct <- V8::v8()
invisible(ct$source(res))
invisible(ct$eval(res))
tt <- seq(0, 10, length.out = 101)
yy <- call_odin_bundle(ct, "odin", NULL, tt)
res <- list(y = unname(yy[, 2:4]),
Expand Down Expand Up @@ -130,11 +95,7 @@ test_that("include fancy sum", {
"dim(a) <- c(n_spp, n_spp)",
"dim(ay) <- c(n_spp, n_spp)")

p <- tempfile()
dir.create(p)
filename <- file.path(p, "odin.R")
writeLines(code, filename)
res <- odin_js_bundle(filename)
res <- odin_js_bundle(code)

user <- list(r = c(1.00, 0.72, 1.53, 1.27),
a = rbind(c(1.00, 1.09, 1.52, 0.00),
Expand All @@ -144,7 +105,7 @@ test_that("include fancy sum", {
y0 = c(0.3013, 0.4586, 0.1307, 0.3557))

ct <- V8::v8()
invisible(ct$source(res))
invisible(ct$eval(res))

tt <- seq(0, 50, length.out = 101)
yy <- call_odin_bundle(ct, "odin", user, tt)
Expand All @@ -159,14 +120,10 @@ test_that("simple stochastic model in a bundle", {
"initial(x) <- 0",
"update(x) <- x + norm_rand()")

p <- tempfile()
dir.create(p)
filename <- file.path(p, "odin.R")
writeLines(code, filename)
res <- odin_js_bundle(filename)
res <- odin_js_bundle(code)

ct <- V8::v8()
invisible(ct$source(res))
invisible(ct$eval(res))
ct$call("setSeed", 1)
tt <- 0:20
yy <- call_odin_bundle(ct, "odin", NULL, tt)
Expand Down

0 comments on commit 713ee8e

Please sign in to comment.