Skip to content

Commit b7d4317

Browse files
committed
sanity check in *.fn + compile bugfix
1 parent 16f2668 commit b7d4317

4 files changed

Lines changed: 182 additions & 134 deletions

File tree

NAMESPACE

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,6 @@ export(vcov)
284284
export(wide2long)
285285
export(write.eqnlist)
286286
export(x2logx)
287-
import(BH)
288287
import(data.table)
289288
import(ggplot2)
290289
import(parallel)

R/classes.R

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,47 @@ test_conditions <- function(c1, c2) {
11181118
#' @example inst/examples/prediction.R
11191119
#' @export
11201120
"*.fn" <- function(p1, p2) {
1121+
1122+
# ============================================================
1123+
# Global consistency check for condition handling
1124+
#
1125+
# Rules:
1126+
# - A condition-unspecific function (conditions = NULL) may be
1127+
# combined with any other function.
1128+
# - Two condition-specific functions must cover the same set
1129+
# of conditions.
1130+
# - It is NOT allowed to combine a single-condition function
1131+
# with a multi-condition function.
1132+
# ============================================================
1133+
1134+
conditions.p1 <- attr(p1, "conditions")
1135+
conditions.p2 <- attr(p2, "conditions")
1136+
1137+
is_unspecific <- function(x) is.null(x)
1138+
is_specific <- function(x) !is.null(x) && length(x) == 1
1139+
is_multiple <- function(x) !is.null(x) && length(x) > 1
1140+
1141+
if (!is_unspecific(conditions.p1) &&
1142+
!is_unspecific(conditions.p2)) {
1143+
1144+
# one specific, one multiple -> forbidden
1145+
if ((is_specific(conditions.p1) && is_multiple(conditions.p2)) ||
1146+
(is_specific(conditions.p2) && is_multiple(conditions.p1))) {
1147+
1148+
stop(
1149+
"Invalid composition of functions:\n",
1150+
"Incompatible condition sets.\n\n",
1151+
"Left-hand function conditions: ",
1152+
paste(conditions.p1, collapse = ", "), "\n",
1153+
"Right-hand function conditions: ",
1154+
paste(conditions.p2, collapse = ", "), "\n\n",
1155+
"A function defined for a single condition cannot be\n",
1156+
"combined with a function defined for multiple conditions.\n",
1157+
"Either both functions must cover all conditions,\n",
1158+
"or one function must be condition-unspecific."
1159+
)
1160+
}
1161+
}
11211162

11221163
# obsfn * obsfn -> obsfn
11231164
if (inherits(p1, "obsfn") & inherits(p2, "obsfn")) {

R/tools.R

Lines changed: 109 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -416,164 +416,147 @@ expand.grid.alt <- function(seq1, seq2) {
416416
#' Compiles one or more objects of class `parfn`, `obsfn`, or `prdfn` into
417417
#' shared libraries (`.so` or `.dll`).
418418
#'
419+
#' If `output` is `NULL`, each detected C/C++ source file is compiled and
420+
#' linked into its own shared object.
421+
#'
422+
#' If `output` is provided, all detected source files are compiled
423+
#' (optionally in parallel if multiple files are present and `cores > 1`)
424+
#' into object files (`.o`) and then linked together into a single shared
425+
#' library with the specified name.
426+
#'
427+
#' If no additional compiler flags are supplied via `args`, the compilation
428+
#' defaults to using `-O3` for optimization.
429+
#'
419430
#' @param ... One or more objects of class `parfn`, `obsfn`, or `prdfn`.
420-
#' The corresponding C/C++ source files are automatically detected.
421-
#' @param output Optional base name for the combined shared object.
422-
#' @param args Optional compiler or linker arguments (e.g. `"-lm"`).
423-
#' @param verbose Logical; if `TRUE`, show compiler output.
424-
#' @param cores Number of parallel compilation jobs (ignored on Windows).
431+
#' The corresponding C/C++ source files (e.g., `model.c`, `model.cpp`,
432+
#' `model_deriv.c`) are automatically detected based on the model name.
433+
#'
434+
#' @param output Optional character string. If supplied, all compiled object
435+
#' files are linked into a single shared library named
436+
#' `paste0(output, .Platform$dynlib.ext)`. If omitted, each source file is
437+
#' built into a separate shared library.
438+
#'
439+
#' @param args Optional character string containing additional flags passed
440+
#' to `R CMD SHLIB` during compilation and linking (e.g., `"-leinspline"`).
441+
#' If `NULL` or empty, the compiler is invoked with `-O3`.
442+
#'
443+
#' @param verbose Logical. If `TRUE`, compiler and linker output is printed
444+
#' to the R console.
445+
#'
446+
#' @param cores Integer. Number of CPU cores used for parallel compilation
447+
#' of individual source files into object files. Parallel compilation is
448+
#' supported on all major operating systems.
425449
#'
426450
#' @details
427-
#' Boost headers are provided via the BH package (declared in
428-
#' `LinkingTo: BH`) and no system-installed Boost libraries are required.
451+
#' Compilation proceeds in two stages. First, each C/C++ source file is
452+
#' compiled into an object file (`.o`), using parallel processing if enabled.
453+
#' Second, the object files are linked into one or multiple shared libraries,
454+
#' depending on whether `output` is specified. Any previously loaded shared
455+
#' objects with matching names are automatically unloaded before linking.
456+
#' The resulting shared libraries are loaded into the current R session upon
457+
#' successful compilation.
429458
#'
430-
#' @return Invisibly returns `TRUE` if compilation succeeds.
459+
#' @return
460+
#' Invisibly returns `TRUE` if compilation succeeds.
431461
#'
432-
#' @import BH
433462
#' @export
434463
compile <- function(..., output = NULL, args = NULL, cores = 1, verbose = FALSE) {
435-
objects <- list(...)
464+
objects <- list(...)
436465
obj.names <- as.character(substitute(list(...)))[-1]
437466

438-
439-
# --- collect all source files ---
440-
files <- character()
467+
# --- collect all source files ------------------------------------------------
468+
files <- NULL
441469
for (i in seq_along(objects)) {
442470
if (inherits(objects[[i]], c("obsfn", "parfn", "prdfn"))) {
443-
filename <- modelname(objects[[i]])
444-
filename <- outer(filename, c("", "_deriv", "_s", "_s2", "_sdcv", "_dfdx", "_dfdp"), paste0)
445-
candidates <- c(paste0(filename, ".c"), paste0(filename, ".cpp"))
446-
candidates <- candidates[file.exists(candidates)]
447-
files <- union(files, candidates)
471+
filename <- modelname(objects[[i]])
472+
filename <- outer(filename, c("", "_deriv", "_s", "_s2", "_sdcv", "_dfdx", "_dfdp"), paste0)
473+
files.obj <- c(paste0(filename, ".c"), paste0(filename, ".cpp"))
474+
files.obj <- files.obj[file.exists(files.obj)]
475+
files <- union(files, files.obj)
448476
}
449477
}
450478

451-
.so <- .Platform$dynlib.ext
452-
453479
if (length(files) == 0)
454480
stop("No source files found for compilation (no .c or .cpp files).")
455481

456-
roots <- vapply(files, function(f) sub("\\.[^.]+$", "", f), character(1))
482+
.so <- .Platform$dynlib.ext
457483

458-
# --- Clean up old compiled files ---
459-
for (root in roots) {
460-
so_file <- paste0(root, .so)
461-
o_file <- paste0(root, ".o")
462-
try(dyn.unload(so_file), silent = TRUE)
463-
if (file.exists(so_file)) {
464-
if (verbose) message("Removing old: ", so_file)
465-
unlink(so_file)
466-
}
467-
if (file.exists(o_file)) {
468-
if (verbose) message("Removing old: ", o_file)
469-
unlink(o_file)
470-
}
484+
# -- include and compiler flags -----------------------------------------------
485+
include_flags <- c(paste0("-I", shQuote(system.file("include", package = "CppODE"))))
486+
cxxflags <- if (Sys.info()[["sysname"]] == "Windows") {
487+
"-std=c++20 -O3 -DNDEBUG"
488+
} else {
489+
"-std=c++20 -O3 -DNDEBUG -fPIC"
471490
}
472491

473-
# --- Compiler flags ---
474-
if (Sys.info()[["sysname"]] == "Windows") cores <- 1
475-
476-
include_flags <- paste(
477-
paste0("-I", system.file("include", package = "CppODE")),
478-
paste0("-I", system.file("include", package = "BH"))
492+
Sys.setenv(
493+
PKG_CPPFLAGS = paste(include_flags, collapse = " "),
494+
PKG_CXXFLAGS = cxxflags
479495
)
480496

481-
cxxflags <- if (Sys.info()[["sysname"]] == "Windows") {
482-
"-std=c++20 -O2 -DNDEBUG -w"
497+
# -- set automatic optimization flags -----------------------------------------
498+
optflags <- if (is.null(args) || !nzchar(args)) "-O3" else args
499+
500+
# --- set up parallel backend if needed ---------------------------------------
501+
if (cores > 1) {
502+
if (Sys.info()[["sysname"]] == "Windows") {
503+
cl <- parallel::makeCluster(cores)
504+
doParallel::registerDoParallel(cl)
505+
parallel::clusterCall(cl, function(x) .libPaths(x), .libPaths())
506+
} else {
507+
doParallel::registerDoParallel(cores = cores)
508+
}
509+
`%mydo%` <- foreach::`%dopar%`
483510
} else {
484-
"-std=c++20 -O2 -DNDEBUG -fPIC -fno-var-tracking-assignments -w"
511+
`%mydo%` <- foreach::`%do%`
485512
}
486513

487-
# --- Helper: compile without loading ---
488-
compile_one <- function(file, root) {
489-
old_cppflags <- Sys.getenv("PKG_CPPFLAGS", unset = NA)
490-
old_cxxflags <- Sys.getenv("PKG_CXXFLAGS", unset = NA)
491-
492-
Sys.setenv(
493-
PKG_CPPFLAGS = include_flags,
494-
PKG_CXXFLAGS = cxxflags
495-
)
496-
497-
on.exit({
498-
if (is.na(old_cppflags)) Sys.unsetenv("PKG_CPPFLAGS") else Sys.setenv(PKG_CPPFLAGS = old_cppflags)
499-
if (is.na(old_cxxflags)) Sys.unsetenv("PKG_CXXFLAGS") else Sys.setenv(PKG_CXXFLAGS = old_cxxflags)
500-
})
501-
502-
cmd <- paste0(R.home("bin"), "/R CMD SHLIB ", shQuote(file), " ", args)
503-
result <- system(cmd, intern = !verbose)
504-
505-
if (!file.exists(paste0(root, .so))) {
506-
stop("Compilation failed for ", file)
507-
}
508-
509-
invisible(root)
514+
# --- unload previously loaded libs -------------------------------------------
515+
all_roots <- unique(sub("\\.(c|cpp)$", "", basename(files), ignore.case = TRUE))
516+
for (r in all_roots) {
517+
try(dyn.unload(paste0(r, .so)), silent = TRUE)
518+
}
519+
if (!is.null(output)) {
520+
try(dyn.unload(paste0(output, .so)), silent = TRUE)
521+
}
522+
523+
Rbin <- shQuote(file.path(R.home("bin"), "R"))
524+
obj_files <- sub("\\.(c|cpp)$", ".o", files, ignore.case = TRUE)
525+
526+
# --- compile to object files in parallel -------------------------------------
527+
foreach::foreach(i = seq_along(files)) %mydo% {
528+
if (file.exists(obj_files[i])) file.remove(obj_files[i])
529+
cmd <- paste(Rbin, "CMD SHLIB -c", shQuote(files[i]), optflags)
530+
system(cmd, intern = !verbose)
510531
}
511532

533+
if (cores > 1 && Sys.info()[["sysname"]] == "Windows") {
534+
parallel::stopCluster(cl)
535+
}
536+
537+
# --- link --------------------------------------------------------------------
512538
if (is.null(output)) {
513-
if (verbose) message("Compiling ", length(files), " model(s)...")
514-
515-
# Compile in parallel (without loading)
516-
parallel::mclapply(seq_along(files), function(i) {
517-
compile_one(files[i], roots[i])
518-
}, mc.cores = cores, mc.silent = !verbose)
519-
520-
# Load sequentially in correct order: base models first, then derived
521-
# This ensures initmod etc. are available when sensitivity models load
522-
base_roots <- roots[!grepl("_(s2?|deriv|sdcv|dfdx|dfdp)$", roots)]
523-
derived_roots <- roots[grepl("_(s2?|deriv|sdcv|dfdx|dfdp)$", roots)]
524-
525-
for (root in c(base_roots, derived_roots)) {
526-
dyn.load(paste0(root, .so))
527-
if (verbose) message("\u2713 Loaded ", root, .so)
539+
# Separate shared libs
540+
for (i in seq_along(files)) {
541+
ofile <- paste0(all_roots[i], .so)
542+
cmd <- paste(Rbin, "CMD SHLIB", shQuote(obj_files[i]), "-o", shQuote(ofile), optflags)
543+
system(cmd, intern = !verbose)
544+
dyn.load(ofile)
528545
}
529-
530546
} else {
531-
# --- Combine all into one shared object ---
532-
output <- sub("\\.so$", "", output)
533-
output_so <- paste0(output, .so)
534-
output_o <- paste0(output, ".o")
535-
536-
try(dyn.unload(output_so), silent = TRUE)
537-
if (file.exists(output_so)) {
538-
if (verbose) message("Removing old: ", output_so)
539-
unlink(output_so)
540-
}
541-
if (file.exists(output_o)) {
542-
if (verbose) message("Removing old: ", output_o)
543-
unlink(output_o)
544-
}
545-
546-
old_cppflags <- Sys.getenv("PKG_CPPFLAGS", unset = NA)
547-
old_cxxflags <- Sys.getenv("PKG_CXXFLAGS", unset = NA)
548-
549-
Sys.setenv(
550-
PKG_CPPFLAGS = include_flags,
551-
PKG_CXXFLAGS = cxxflags
552-
)
553-
554-
on.exit({
555-
if (is.na(old_cppflags)) Sys.unsetenv("PKG_CPPFLAGS") else Sys.setenv(PKG_CPPFLAGS = old_cppflags)
556-
if (is.na(old_cxxflags)) Sys.unsetenv("PKG_CXXFLAGS") else Sys.setenv(PKG_CXXFLAGS = old_cxxflags)
557-
})
558-
559-
cmd <- paste0(
560-
R.home("bin"), "/R CMD SHLIB ",
561-
paste(shQuote(files), collapse = " "),
562-
" -o ", shQuote(output_so), " ",
563-
args
564-
)
565-
if (verbose)
566-
message("Linking into shared library: ", output, .so)
567-
568-
result <- system(cmd, intern = !verbose)
569-
570-
if (!file.exists(output_so)) {
571-
stop("Compilation failed for combined output")
547+
# Combined shared lib
548+
for (i in seq_along(objects)) {
549+
eval(parse(text = sprintf("modelname(%s) <<- '%s'", obj.names[i], output)))
550+
# Get only the .o files that belong to this object
551+
obj_modelname <- modelname(objects[[i]])
552+
obj_o <- obj_files[grepl(paste0("^", obj_modelname), basename(obj_files))]
553+
eval(parse(text = sprintf("attr(%s, 'objfiles') <<- obj_o", obj.names[i])))
572554
}
573555

574-
dyn.load(output_so)
575-
if (verbose)
576-
message("\u2713 Loaded ", output, .so)
556+
cmd <- paste(Rbin, "CMD SHLIB", paste(shQuote(obj_files), collapse = " "),
557+
"-o", shQuote(paste0(output, .so)), optflags)
558+
system(cmd, intern = !verbose)
559+
dyn.load(paste0(output, .so))
577560
}
578561

579562
invisible(TRUE)

man/compile.Rd

Lines changed: 32 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)