From b0b710ee9f9bb9bbe9708676d0c5822465e02203 Mon Sep 17 00:00:00 2001
From: Johannes Ranke <jranke@uni-bremen.de>
Date: Sat, 15 Apr 2023 15:35:06 +0200
Subject: Make predefined symbols safer

We still need to create a parallel processing cluster _after_ creating a
compiled model that is moved to a user defined location, at least I did
not find another way to make it work. This is not a problem with
parallel processing without a cluster, which is not available on
Windows.
---
 R/mkinfit.R                                        | 14 ++++--
 R/mkinmod.R                                        |  2 +-
 R/mkinpredict.R                                    |  6 +--
 R/saem.R                                           |  8 +++-
 .../hierarchical_kinetics/skeleton/skeleton.Rmd    | 24 ++++++++--
 log/test.log                                       | 29 ++++++------
 man/mkinpredict.Rd                                 |  4 ++
 tests/testthat/print_mmkin_sfo_sfo_dmta.txt        |  0
 tests/testthat/test_compiled_symbols.R             | 54 ++++++++++++++++++++++
 9 files changed, 113 insertions(+), 28 deletions(-)
 create mode 100644 tests/testthat/print_mmkin_sfo_sfo_dmta.txt
 create mode 100644 tests/testthat/test_compiled_symbols.R

diff --git a/R/mkinfit.R b/R/mkinfit.R
index b97bc7e2..c851fddb 100644
--- a/R/mkinfit.R
+++ b/R/mkinfit.R
@@ -501,10 +501,15 @@ mkinfit <- function(mkinmod, observed,
   }
 
   # Get native symbol before iterations info for speed
+  use_symbols = FALSE
   if (solution_type == "deSolve" & use_compiled[1] != FALSE) {
-    mkinmod[["symbols"]] <- deSolve::checkDLL(dllname = mkinmod$dll_info[["name"]],
-      func = "diffs", initfunc = "initpar",
-      jacfunc = NULL, nout = 0, outnames = NULL)
+    mkinmod[["symbols"]] <- try(
+      deSolve::checkDLL(dllname = mkinmod$dll_info[["name"]],
+        func = "diffs", initfunc = "initpar",
+        jacfunc = NULL, nout = 0, outnames = NULL))
+    if (!inherits(mkinmod[["symbols"]], "try-error")) {
+      use_symbols = TRUE
+    }
   }
 
   # Get the error model and the algorithm for fitting
@@ -616,8 +621,9 @@ mkinfit <- function(mkinmod, observed,
                          odeini, outtimes,
                          solution_type = solution_type,
                          use_compiled = use_compiled,
+                         use_symbols = use_symbols,
                          method.ode = method.ode,
-                         atol = atol, rtol = rtol, 
+                         atol = atol, rtol = rtol,
                          ...)
 
       observed_index <- cbind(as.character(observed$time), as.character(observed$name))
diff --git a/R/mkinmod.R b/R/mkinmod.R
index 29542b71..2f930adb 100644
--- a/R/mkinmod.R
+++ b/R/mkinmod.R
@@ -464,11 +464,11 @@ mkinmod <- function(..., use_of_ff = "max", name = NULL,
       silent = TRUE)
 
     if (!inherits(model$cf, "try-error")) {
+      if (!quiet) message("Temporary DLL for differentials generated and loaded")
       if (!is.null(dll_dir)) {
         model$dll_info <- inline::moveDLL(model$cf, name, dll_dir,
           unload = unload, overwrite = overwrite, verbose = !quiet)
       }
-      if (!quiet) message("Temporary DLL for differentials generated and loaded")
       model$dll_info <- inline::getDynLib(model$cf)
     }
   }
diff --git a/R/mkinpredict.R b/R/mkinpredict.R
index 8fa41217..60456fb2 100644
--- a/R/mkinpredict.R
+++ b/R/mkinpredict.R
@@ -27,8 +27,8 @@
 #' When using compiled code, only lsoda is supported.
 #' @param use_compiled If set to \code{FALSE}, no compiled version of the
 #' [mkinmod] model is used, even if is present.
-#' @param use_symbols If set to \code{TRUE}, symbol info present in the 
-#' [mkinmod] object is used if available for accessing compiled code
+#' @param use_symbols If set to \code{TRUE} (default), symbol info present in
+#' the [mkinmod] object is used if available for accessing compiled code
 #' @param atol Absolute error tolerance, passed to the ode solver.
 #' @param rtol Absolute error tolerance, passed to the ode solver.
 #' @param maxsteps Maximum number of steps, passed to the ode solver.
@@ -174,7 +174,7 @@ mkinpredict.mkinmod <- function(x,
   if (solution_type == "deSolve") {
     if (!is.null(x$cf) & use_compiled[1] != FALSE) {
 
-      if (!is.null(x$symbols) & use_symbols[1] == TRUE) {
+      if (!is.null(x$symbols) & use_symbols) {
         lsoda_func <- x$symbols
       } else {
         lsoda_func <- "diffs"
diff --git a/R/saem.R b/R/saem.R
index 2fa770bb..83de97b0 100644
--- a/R/saem.R
+++ b/R/saem.R
@@ -583,11 +583,15 @@ saemix_model <- function(object, solution_type = "auto",
     transform_fractions <- object[[1]]$transform_fractions
 
     # Get native symbol info for speed
+    use_symbols = FALSE
     if (solution_type == "deSolve" & !is.null(mkin_model$cf)) {
-      mkin_model$symbols <- deSolve::checkDLL(
+      mkin_model$symbols <- try(deSolve::checkDLL(
         dllname = mkin_model$dll_info[["name"]],
         func = "diffs", initfunc = "initpar",
-        jacfunc = NULL, nout = 0, outnames = NULL)
+        jacfunc = NULL, nout = 0, outnames = NULL))
+      if (!inherits(mkin_model$symbols, "try-error")) {
+        use_symbols = TRUE
+      }
     }
 
     # Define the model function
diff --git a/inst/rmarkdown/templates/hierarchical_kinetics/skeleton/skeleton.Rmd b/inst/rmarkdown/templates/hierarchical_kinetics/skeleton/skeleton.Rmd
index 38a6bd20..cb328308 100644
--- a/inst/rmarkdown/templates/hierarchical_kinetics/skeleton/skeleton.Rmd
+++ b/inst/rmarkdown/templates/hierarchical_kinetics/skeleton/skeleton.Rmd
@@ -21,11 +21,18 @@ library(readxl)
 ```{r n_cores, cache = FALSE}
 n_cores <- detectCores()
 
-if (Sys.info()["sysname"] == "Windows") {
-  cl <- makePSOCKcluster(n_cores)
-} else {
-  cl <- makeForkCluster(n_cores)
+# We need to start a new cluster after defining a compiled model that is
+# saved as a DLL to the user directory, therefore we define a function
+# This is used again after defining the pathway model
+start_cluster <- function(n_cores) {
+  if (Sys.info()["sysname"] == "Windows") {
+    ret <- makePSOCKcluster(n_cores)
+  } else {
+    ret <- makeForkCluster(n_cores)
+  }
+  return(ret)
 }
+cl <- start_cluster(n_cores)
 ```
 
 \clearpage
@@ -179,6 +186,10 @@ illparms(parent_best_pH_2)
 parms(parent_best_pH_2, ci = TRUE) |> kable(digits = 3)
 ```
 
+```{r}
+stopCluster(cl)
+```
+
 \clearpage
 
 # Pathway fits
@@ -203,6 +214,7 @@ Separate evaluations of all datasets are performed with constant variance
 and using two-component error.
 
 ```{r path-1-sep, dependson = c("path-1-degmod", "ds")}
+cl <- start_cluster(n_cores)
 sforb_sep_const <- mmkin(list(sforb_path = m_sforb_sfo2), ds,
   cluster = cl, quiet = TRUE)
 sforb_sep_tc <- update(sforb_sep_const, error_model = "tc")
@@ -268,6 +280,10 @@ plot(path_1_refined)
 parms(path_1_refined, ci = TRUE) |> kable(digits = 3)
 ```
 
+```{r}
+stopCluster(cl)
+```
+
 \clearpage
 
 # Appendix
diff --git a/log/test.log b/log/test.log
index c89cf02e..fa808f22 100644
--- a/log/test.log
+++ b/log/test.log
@@ -4,20 +4,21 @@
 ✔ |         5 | Analytical solutions for coupled models [1.6s]
 ✔ |         5 | Calculation of Akaike weights
 ✔ |         3 | Export dataset for reading into CAKE
+✔ |         7 | Use of precompiled symbols in mkinpredict [3.3s]
 ✔ |        12 | Confidence intervals and p-values [0.4s]
-✔ |     1  12 | Dimethenamid data from 2018 [12.2s]
+✔ |     1  12 | Dimethenamid data from 2018 [13.4s]
 ────────────────────────────────────────────────────────────────────────────────
-Skip ('test_dmta.R:99'): Different backends get consistent results for SFO-SFO3+, dimethenamid data
+Skip ('test_dmta.R:88'): Different backends get consistent results for SFO-SFO3+, dimethenamid data
 Reason: Fitting this ODE model with saemix takes about 15 minutes on my system
 ────────────────────────────────────────────────────────────────────────────────
-✔ |        14 | Error model fitting [2.4s]
+✔ |        14 | Error model fitting [2.5s]
 ✔ |         5 | Time step normalisation
 ✔ |         4 | Calculation of FOCUS chi2 error levels [0.3s]
-✔ |        14 | Results for FOCUS D established in expertise for UBA (Ranke 2014) [0.4s]
-✔ |         4 | Test fitting the decline of metabolites from their maximum [0.2s]
+✔ |        14 | Results for FOCUS D established in expertise for UBA (Ranke 2014) [0.5s]
+✔ |         4 | Test fitting the decline of metabolites from their maximum [0.3s]
 ✔ |         1 | Fitting the logistic model [0.1s]
-✔ |        10 | Batch fitting and diagnosing hierarchical kinetic models [19.5s]
-✔ |     1  11 | Nonlinear mixed-effects models [5.8s]
+✔ |        10 | Batch fitting and diagnosing hierarchical kinetic models [19.8s]
+✔ |     1  11 | Nonlinear mixed-effects models [6.1s]
 ────────────────────────────────────────────────────────────────────────────────
 Skip ('test_mixed.R:78'): saemix results are reproducible for biphasic fits
 Reason: Fitting with saemix takes around 10 minutes when using deSolve
@@ -27,12 +28,12 @@ Reason: Fitting with saemix takes around 10 minutes when using deSolve
 ✔ |         3 | mkinfit features [0.5s]
 ✔ |         8 | mkinmod model generation and printing
 ✔ |         3 | Model predictions with mkinpredict [0.1s]
-✔ |        12 | Multistart method for saem.mmkin models [22.0s]
-✔ |        16 | Evaluations according to 2015 NAFTA guidance [1.6s]
-✔ |         9 | Nonlinear mixed-effects models with nlme [3.9s]
-✔ |        15 | Plotting [5.1s]
+✔ |        12 | Multistart method for saem.mmkin models [21.7s]
+✔ |        16 | Evaluations according to 2015 NAFTA guidance [1.5s]
+✔ |         9 | Nonlinear mixed-effects models with nlme [3.8s]
+✔ |        15 | Plotting [4.7s]
 ✔ |         4 | Residuals extracted from mkinfit models
-✔ |     1  36 | saemix parent models [31.4s]
+✔ |     1  36 | saemix parent models [32.0s]
 ────────────────────────────────────────────────────────────────────────────────
 Skip ('test_saemix_parent.R:143'): We can also use mkin solution methods for saem
 Reason: This still takes almost 2.5 minutes although we do not solve ODEs
@@ -47,11 +48,11 @@ Reason: This still takes almost 2.5 minutes although we do not solve ODEs
 ✔ |         4 | Calculation of maximum time weighted average concentrations (TWAs) [0.7s]
 
 ══ Results ═════════════════════════════════════════════════════════════════════
-Duration: 115.0 s
+Duration: 120.2 s
 
 ── Skipped tests  ──────────────────────────────────────────────────────────────
 • Fitting this ODE model with saemix takes about 15 minutes on my system (1)
 • Fitting with saemix takes around 10 minutes when using deSolve (1)
 • This still takes almost 2.5 minutes although we do not solve ODEs (1)
 
-[ FAIL 0 | WARN 0 | SKIP 3 | PASS 270 ]
+[ FAIL 0 | WARN 0 | SKIP 3 | PASS 277 ]
diff --git a/man/mkinpredict.Rd b/man/mkinpredict.Rd
index ad749fa4..792d0e47 100644
--- a/man/mkinpredict.Rd
+++ b/man/mkinpredict.Rd
@@ -15,6 +15,7 @@ mkinpredict(x, odeparms, odeini, outtimes, ...)
   outtimes = seq(0, 120, by = 0.1),
   solution_type = "deSolve",
   use_compiled = "auto",
+  use_symbols = FALSE,
   method.ode = "lsoda",
   atol = 1e-08,
   rtol = 1e-10,
@@ -67,6 +68,9 @@ parent compound.}
 \item{use_compiled}{If set to \code{FALSE}, no compiled version of the
 \link{mkinmod} model is used, even if is present.}
 
+\item{use_symbols}{If set to \code{TRUE} (default), symbol info present in
+the \link{mkinmod} object is used if available for accessing compiled code}
+
 \item{method.ode}{The solution method passed via \link{mkinpredict} to \link{ode}] in
 case the solution type is "deSolve" and we are not using compiled code.
 When using compiled code, only lsoda is supported.}
diff --git a/tests/testthat/print_mmkin_sfo_sfo_dmta.txt b/tests/testthat/print_mmkin_sfo_sfo_dmta.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/testthat/test_compiled_symbols.R b/tests/testthat/test_compiled_symbols.R
new file mode 100644
index 00000000..e5f151eb
--- /dev/null
+++ b/tests/testthat/test_compiled_symbols.R
@@ -0,0 +1,54 @@
+context("Use of precompiled symbols in mkinpredict")
+
+test_that("We can safely use compiled code", {
+
+  # Generate temporary DLL
+  sfo_sfo_tmp <- mkinmod(DMTA = mkinsub("SFO", to = "M23"),
+    M23 = mkinsub("SFO"))
+
+  # Generate temporary DLL and move to user specified location
+  if (!dir.exists("test_dlls")) dir.create("test_dlls")
+  sfo_sfo_dll <- mkinmod(DMTA = mkinsub("SFO", to = "M23"),
+    M23 = mkinsub("SFO"),
+    dll_dir = "test_dlls",
+    name = "sfo_sfo",
+    unload = TRUE, overwrite = TRUE
+  )
+
+  if (Sys.info()["sysname"] != "Windows") {
+    # mclapply using forks
+    expect_known_output(
+      mmkin(list(sfo_sfo_dll), dmta_ds, cores = n_cores, quiet = TRUE),
+      "print_mmkin_sfo_sfo_dmta.txt"
+    )
+
+    # cluster describing itself as socket cluster
+    cl_fork <- parallel::makeForkCluster(n_cores)
+    expect_known_output(
+      mmkin(list(sfo_sfo_tmp), dmta_ds, cluster = cl_fork, quiet = TRUE),
+      "print_mmkin_sfo_sfo_dmta.txt"
+    )
+    expect_known_output(
+      mmkin(list(sfo_sfo_dll), dmta_ds, cluster = cl_fork, quiet = TRUE),
+      "print_mmkin_sfo_sfo_dmta.txt"
+    )
+    parallel::stopCluster(cl_fork)
+  }
+
+  # PSOCK cluster
+  cl_psock <- parallel::makePSOCKcluster(n_cores)
+  expect_known_output(
+    mmkin(list(sfo_sfo_tmp), dmta_ds, cluster = cl_psock, quiet = TRUE),
+    "print_mmkin_sfo_sfo_dmta.txt"
+  )
+  expect_known_output(
+    mmkin(list(sfo_sfo_dll), dmta_ds, cluster = cl_psock, quiet = TRUE),
+    "print_mmkin_sfo_sfo_dmta.txt"
+  )
+  parallel::stopCluster(cl_psock)
+
+  # Clean up
+  expect_true(file.remove("test_dlls/sfo_sfo.so"))
+  expect_true(file.remove("test_dlls"))
+})
+
-- 
cgit v1.2.1