Skip to content

Commit b006ec8

Browse files
simonbeyer1claude
andcommitted
fix(compile): keep loadDLL from unloading live shared objects
dyn.unload() nulls every native symbol pointer resolved so far, and nothing resolves them again, so a second loadDLL() killed the objective it was meant to restore. loadDLL() now skips what is already loaded. The batch handle cached by Xs() holds a pointer of its own that no symbol-cache flush reached; it keys on the flush count now. The README example profiles and reports confidence intervals on the data column; regenerated output and figures come with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lkpck7VrrYcCWPYWiFXQTS
1 parent 86fb184 commit b006ec8

12 files changed

Lines changed: 443 additions & 298 deletions

File tree

‎DESCRIPTION‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
Package: dMod2
22
Type: Package
33
Title: Dynamic Modeling and Parameter Estimation in ODE Models
4-
Version: 0.6.0
5-
Date: 2026-07-18
4+
Version: 0.6.1
5+
Date: 2026-08-31
66
Authors@R: c(
77
person("Simon", "Beyer", email = "simon.beyer@fdm.uni-freiburg.de",
88
role = c("aut", "cre")),

‎NEWS.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
# dMod2 0.6.1
2+
3+
* `loadDLL()` skips shared objects that are already loaded in the current
4+
process. Unloading them nulled the native symbol pointers held by the
5+
prediction, observation and parameter functions built from them, leaving
6+
every later call without a way to resolve them again.
7+
* `Xs()` caches a prepared batch handle that carries such a pointer of its
8+
own, which no symbol-cache flush reached. It is now rebuilt whenever the
9+
cache is flushed.
10+
111
# dMod2 0.6.0
212

313
* The core is complete: equations, compiled prediction, observation and

‎R/compile.R‎

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,19 @@
100100
hdr
101101
}
102102

103+
## Counts the symbol-cache flushes of this session. Callers that hold a
104+
## resolved address of their own key on it, since resolving again per call
105+
## costs more than the check is worth.
106+
.dmodSymbols <- new.env(parent = emptyenv())
107+
.dmodSymbols$generation <- 0L
108+
109+
.symbolGeneration <- function() .dmodSymbols$generation
110+
103111
## cppDE caches the addresses of a model's compiled entry points, so loading a
104112
## shared object invalidates them. Guarded, to stay loadable against a cppDE
105113
## that predates the cache.
106114
.clearSymbols <- function() {
115+
.dmodSymbols$generation <- .dmodSymbols$generation + 1L
107116
f <- get0("clearNativeSymbols", envir = asNamespace("cppDE"), inherits = FALSE)
108117
if (is.function(f)) f()
109118
}
@@ -677,30 +686,47 @@ getLocalDLLs <- function() {
677686

678687

679688

689+
## Absolute paths of the shared objects loaded in this process.
690+
.loadedDLLPaths <- function() {
691+
dlls <- getLoadedDLLs()
692+
if (!length(dlls)) return(character(0))
693+
paths <- vapply(dlls, function(d) unclass(d)$path, character(1))
694+
normalizePath(paths, winslash = "/", mustWork = FALSE)
695+
}
696+
697+
680698
#' Load shared object for a dMod object
681-
#'
699+
#'
682700
#' Usually when restarting the R session, although all objects are saved in
683701
#' the workspace, the dynamic libraries are not linked any more. `loadDLL`
684702
#' is a wrapper for `dyn.load` that uses the "modelname" attribute of
685703
#' dMod objects like prediction functions, observation functions, etc. to
686704
#' load the corresponding shared object.
687-
#'
705+
#'
706+
#' Shared objects already loaded in the current process are skipped, so
707+
#' calling `loadDLL` repeatedly is a no-op. Unloading them would null the
708+
#' native symbol pointers that the already built prediction, observation and
709+
#' parameter functions hold, and nothing resolves those again.
710+
#'
688711
#' @param ... objects of class prdfn, obsfn, parfn, objfn, ...
689-
#'
712+
#'
713+
#' @return Invisibly, the character vector of files loaded by this call.
714+
#'
690715
#' @export
691716
loadDLL <- function(...) {
692-
717+
693718
.so <- .Platform$dynlib.ext
694719
models <- modelname(...)
695720
files <- paste0(outer(models, c("", "_s", "_s2", "_sdcv", "_deriv", "_dfdx", "_dfdp"), paste0), .so)
696721
files <- files[file.exists(files)]
697-
698-
for (f in files) {
699-
try(dyn.unload(f), silent = TRUE)
700-
dyn.load(f)
701-
}
722+
files <- files[!normalizePath(files, winslash = "/", mustWork = FALSE) %in%
723+
.loadedDLLPaths()]
724+
if (!length(files)) return(invisible(character(0)))
725+
726+
for (f in files) dyn.load(f)
702727
.clearSymbols()
703728
message("The following local files were dynamically loaded: ", paste(files, collapse = ", "))
729+
invisible(files)
704730
}
705731

706732

‎R/prediction.R‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,11 +352,14 @@ Xs.cppDE <- function(odemodel, forcings = NULL, events = NULL, names = NULL, con
352352
fixed = NULL, forcings = controls$forcings), o)))
353353
} else if (!is.null(prepFn) && !is.null(solveFn)) {
354354
# The handle bakes in everything but the numbers, so it is only valid
355-
# while shapes and labels stay put.
355+
# while shapes and labels stay put. It also holds the resolved native
356+
# symbol, which unloading the shared object nulls in place, so the
357+
# symbol generation is part of the signature.
356358
sig <- list(model = as.character(model), times = timesL,
357359
deriv = deriv, deriv2 = deriv2,
358360
sens = lapply(preps, function(pr) dimnames(pr$sens1ini)),
359-
forcings = controls$forcings, opts = obatch)
361+
forcings = controls$forcings, opts = obatch,
362+
symbols = .symbolGeneration())
360363
if (!identical(bcache$sig, sig)) {
361364
bcache$handle <- do.call(prepFn, c(list(model, conditions = mkConds()),
362365
obatch))

‎README.Rmd‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,15 +238,15 @@ thr_90 <- profileThreshold(0.90, method = "F", n = nd, p = remlfit$rank)
238238
profiles <- profile(
239239
obj, best, names(best), limits = c(-5, 5), cores = length(best), delta = thr_95,
240240
stepControl = list(stepsize = 1e-4, min = 1e-4, max = Inf,
241-
atol = 1e-3, rtol = 1e-3, limit = 200, stop = "value"),
241+
atol = 1e-3, rtol = 1e-3, limit = 200, stop = "data"),
242242
algoControl = list(reoptimize = TRUE),
243243
optControl = list(rinit = 0.1, rmax = 10, iterlim = 20))
244244
245245
plotProfile(profiles, mode %in% c("data", "prior"), threshold = c("90%" = thr_90, "95%" = thr_95))
246246
```
247247

248248
```{r confint}
249-
confint(profiles, level = 0.95, val.column = "value",
249+
confint(profiles, level = 0.95, val.column = "data",
250250
method = "F", n = nd, p = remlfit$rank)
251251
```
252252

‎README.md‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -259,15 +259,15 @@ rbind(ML = best[remlfit$errpars], REML = remlfit$argument[remlfit$errpars])
259259
```
260260

261261
## sd_pSTAT5A_rel sd_pSTAT5B_rel sd_rSTAT5A_rel
262-
## ML 0.5878611 0.8200770 0.4987482
263-
## REML 0.6253905 0.8442257 0.5248830
262+
## ML 0.5876483 0.8200724 0.4987661
263+
## REML 0.6254742 0.8442083 0.5248422
264264

265265
``` r
266266
remlfit$dof # n_g minus the leverage each observable spends
267267
```
268268

269269
## pSTAT5A_rel pSTAT5B_rel rSTAT5A_rel
270-
## 13.67911 14.22154 14.09935
270+
## 13.67813 14.22147 14.10040
271271

272272
``` r
273273
remlfit$rank # effective number of mean parameters
@@ -284,7 +284,7 @@ tapply(lev$leverage, lev$name, sum)
284284
```
285285

286286
## pSTAT5A_rel pSTAT5B_rel rSTAT5A_rel
287-
## 2.370236 1.743215 1.886549
287+
## 2.372391 1.742590 1.885019
288288

289289
### Fit and uncertainty band
290290

@@ -349,7 +349,7 @@ thr_90 <- profileThreshold(0.90, method = "F", n = nd, p = remlfit$rank)
349349
profiles <- profile(
350350
obj, best, names(best), limits = c(-5, 5), cores = length(best), delta = thr_95,
351351
stepControl = list(stepsize = 1e-4, min = 1e-4, max = Inf,
352-
atol = 1e-3, rtol = 1e-3, limit = 200, stop = "value"),
352+
atol = 1e-3, rtol = 1e-3, limit = 200, stop = "data"),
353353
algoControl = list(reoptimize = TRUE),
354354
optControl = list(rinit = 0.1, rmax = 10, iterlim = 20))
355355

@@ -359,20 +359,20 @@ plotProfile(profiles, mode %in% c("data", "prior"), threshold = c("90%" = thr_90
359359
<img src="man/figures/README-profiles-1.svg" alt="" width="100%" />
360360

361361
``` r
362-
confint(profiles, level = 0.95, val.column = "value",
362+
confint(profiles, level = 0.95, val.column = "data",
363363
method = "F", n = nd, p = remlfit$rank)
364364
```
365365

366-
## name value lower upper
367-
## Epo_degradation_BaF3 Epo_degradation_BaF3 -1.5678382 -1.73695985 -1.3902979
368-
## k_exp_hetero k_exp_hetero -4.4194494 -10.34823306 -2.8946270
369-
## k_exp_homo k_exp_homo -2.2024766 -2.55028643 -1.8657436
370-
## k_imp_hetero k_imp_hetero -1.7874266 -1.91294412 -1.6603002
371-
## k_imp_homo k_imp_homo 1.4794785 0.07366071 8.1411569
372-
## k_phos k_phos 4.1977304 4.10556883 4.2980496
373-
## sd_pSTAT5A_rel sd_pSTAT5A_rel 0.5878611 0.41152546 0.7985545
374-
## sd_pSTAT5B_rel sd_pSTAT5B_rel 0.8200770 0.66585246 1.0147002
375-
## sd_rSTAT5A_rel sd_rSTAT5A_rel 0.4987482 0.34832821 0.6924861
366+
## name value lower upper
367+
## Epo_degradation_BaF3 Epo_degradation_BaF3 -1.5680325 -1.7398858 -1.3903480
368+
## k_exp_hetero k_exp_hetero -4.4562477 -Inf -2.9546299
369+
## k_exp_homo k_exp_homo -2.2032225 -2.5569151 -1.8895645
370+
## k_imp_hetero k_imp_hetero -1.7872354 -1.9108026 -1.6583018
371+
## k_imp_homo k_imp_homo 1.4788926 0.1002196 Inf
372+
## k_phos k_phos 4.1976370 4.1064208 4.2964734
373+
## sd_pSTAT5A_rel sd_pSTAT5A_rel 0.5876483 0.4098707 0.7938722
374+
## sd_pSTAT5B_rel sd_pSTAT5B_rel 0.8200724 0.6648408 1.0126894
375+
## sd_rSTAT5A_rel sd_rSTAT5A_rel 0.4987661 0.3488114 0.6910599
376376

377377
The plot separates the two terms, and that is where the two
378378
non-identifiable directions become visible: for `k_imp_homo` and

‎dMod2.Rproj‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
Version: 1.0
22
ProjectId: dmod2
3+
34
RestoreWorkspace: No
45
SaveWorkspace: No
6+
AlwaysSaveHistory: Default
7+
58
EnableCodeIndexing: Yes
69
UseSpacesForTab: Yes
710
NumSpacesForTab: 2
811
Encoding: UTF-8
12+
13+
RnwWeave: Sweave
14+
LaTeX: pdfLaTeX
15+
916
BuildType: Package
1017
PackageUseDevtools: Yes

0 commit comments

Comments
 (0)