Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 100 additions & 8 deletions R/MCSim_model.R
Original file line number Diff line number Diff line change
Expand Up @@ -41,50 +41,134 @@ Model <- setRefClass("Model",
initialize = function(...) {
"Initialize the Model object using an MCSim model specification file (mName) or an MCSim model specification string (mString)."
callSuper(...)

# Validate input arguments first
if (length(mName) == 0 & length(mString) == 0) {
stop("To create a Model object, supply either a file name (mName) or a model specification string (mString).")
}
if (length(mName) > 0 & length(mString) > 0) {
stop("Cannot create a Model object using both a file name (mName) and a model specification string (mString). Provide only one of these arguments.")
}

# Set intelligent defaults based on input type
if (length(writeTemp) == 0) {
if (length(mString) > 0) {
writeTemp <<- TRUE # mString always requires temp files
} else {
writeTemp <<- FALSE # mName defaults to local file handling
}
}
if (length(verboseOutput) == 0) {
verboseOutput <<- FALSE
}
# Track user's model file for proper change detection
model_file_path <- NULL
if (length(mString) > 0) {
if (writeTemp == FALSE) {
stop("The value of writeTemp must be TRUE when creating a Model object using a model specification string (mstring).")
}
file <- tempfile(pattern = "mcsimmod_", fileext = ".model")
writeLines(mString, file)

# Write model string to file with error handling and ensure it's flushed
tryCatch(
{
# Validate mString before writing
if (length(mString) == 0 || all(nchar(mString) == 0)) {
stop("mString is empty or contains no content")
}

# Use cat for more reliable file writing in all contexts
cat(paste(mString, collapse = "\n"), "\n", file = file, sep = "")

# On Windows, ensure file is fully written to disk
if (.Platform$OS.type == "windows") {
Sys.sleep(0.1) # Increased delay for package build context
}

# Verify file was written correctly
if (!file.exists(file)) {
stop("Model file was not created: ", file)
}

# Basic validation that file has content
file_size <- file.size(file)
if (is.na(file_size) || file_size == 0) {
stop("Model file is empty after writing: ", file, " (size: ", file_size, ")")
}
},
error = function(e) {
# Enhanced error message with context
stop(
"Failed to create model file from mString: ", e$message,
" (mString length: ", length(mString),
", mString chars: ", sum(nchar(mString)), ")"
)
}
)

# For mString, model and working files are the same
model_file_path <- file
} else {
if (writeTemp == TRUE) {
source_file <- normalizePath(paste0(mName, ".model"))
model_file <- normalizePath(paste0(mName, ".model"))
model_file_path <- model_file # Store user's model file path
temp_directory <- tempdir()
file <- file.path(temp_directory, basename(source_file))
file_copied <- file.copy(from = source_file, to = file)
file <- file.path(temp_directory, basename(model_file))
file_copied <- file.copy(from = model_file, to = file)
} else {
file <- normalizePath(paste0(mName, ".model"))
model_file_path <- file # writeTemp=FALSE: model and working are same
}
}
mList <- .fixPath(file)
mName <<- mList$mName
mPath <- mList$mPath

# Determine hash file location based on model file
if (writeTemp == TRUE && length(mString) == 0) {
# For writeTemp=TRUE with mName, store hash alongside user's model file
model_mList <- .fixPath(model_file_path)
hash_file_path <- file.path(model_mList$mPath, paste0(model_mList$mName, "_model.md5"))
} else {
# For writeTemp=FALSE or mString cases, store hash with working files
hash_file_path <- file.path(mPath, paste0(mName, "_model.md5"))
}

paths <<- list(
dll_name = paste0(mName, "_model"),
c_file = file.path(mPath, paste0(mName, "_model.c")),
o_file = file.path(mPath, paste0(mName, "_model.o")),
dll_file = file.path(mPath, paste0(mName, "_model", .Platform$dynlib.ext)),
inits_file = file.path(mPath, paste0(mName, "_model_inits.R")),
model_file = file.path(mPath, paste0(mName, ".model")),
hash_file = file.path(mPath, paste0(mName, "_model.md5"))
source_file = file, # Use the actual file path (tempfile for mString, or copied file for mName)
model_file = model_file_path,
hash_file = hash_file_path
)
},
loadModel = function(force = FALSE) {
"Translate (if necessary) the model specification text to C, compile (if necessary) the resulting C file to create a dynamic link library (DLL) file (on Windows) or a shared object (SO) file (on Unix), and then load all essential information about the Model object into memory (for use in the current R session)."
hash_exists <- file.exists(paths$hash_file)
if (hash_exists) {
# Check changes against user's model file, not working copy
hash_has_changed <- .fileHasChanged(paths$model_file, paths$hash_file)

# If model file changed and we're using temp directory, update working copy
if (hash_has_changed && writeTemp == TRUE && length(mString) == 0) {
file_copied <- file.copy(from = paths$model_file, to = paths$source_file, overwrite = TRUE)
if (!file_copied) {
stop("Failed to update working file from model file: ", paths$model_file)
}
}
} else {
hash_has_changed <- TRUE

# If no hash exists and we're using temp directory, ensure working copy is current
if (writeTemp == TRUE && length(mString) == 0 && !identical(paths$model_file, paths$source_file)) {
file_copied <- file.copy(from = paths$model_file, to = paths$source_file, overwrite = TRUE)
if (!file_copied) {
stop("Failed to update working file from model file: ", paths$model_file)
}
}
}

# Conditions for compiling a model:
Expand All @@ -97,8 +181,16 @@ Model <- setRefClass("Model",
# match the previously saved hash, indicating that the model
# specification file has been changed since the last translation and
# compiling.
if (!file.exists(paths$dll_file) | (force) | (!hash_exists) | (hash_exists & hash_has_changed)) {
compileModel(paths$model_file, paths$c_file, paths$dll_name, paths$dll_file, hash_file = paths$hash_file, verbose_output = verboseOutput)
if (!file.exists(paths$dll_file) | (force) | (!hash_exists) | (hash_has_changed)) {
# When writeTemp = TRUE and model file has changed, update the working copy
if (writeTemp && hash_has_changed && !identical(paths$model_file, paths$source_file)) {
file.copy(from = paths$model_file, to = paths$source_file, overwrite = TRUE)
}

# Call compileModel - always compile and hash the working copy (source_file)
compileModel(paths$source_file, paths$c_file, paths$dll_name, paths$dll_file,
hash_file = paths$hash_file, verbose_output = verboseOutput
)
}

# Load the compiled model (DLL).
Expand Down
98 changes: 90 additions & 8 deletions R/compileModel.R
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@
#' @useDynLib MCSimMod, .registration=TRUE
#' @export
compileModel <- function(model_file, c_file, dll_name, dll_file, hash_file = NULL, verbose_output = FALSE) {
# Normalize paths for Windows compatibility
if (.Platform$OS.type == "windows") {
model_file <- normalizePath(model_file, winslash = "/", mustWork = TRUE)
# For c_file, ensure proper path normalization
c_file <- normalizePath(c_file, winslash = "/", mustWork = FALSE)
dll_file <- normalizePath(dll_file, winslash = "/", mustWork = FALSE)
if (!is.null(hash_file)) {
hash_file <- normalizePath(hash_file, winslash = "/", mustWork = FALSE)
}
}

# Unload DLL if it has been loaded.
mList <- .fixPath(model_file)
model_name <- mList$mName
Expand All @@ -33,11 +44,25 @@ compileModel <- function(model_file, c_file, dll_name, dll_file, hash_file = NUL
# specification file (ending with ".model"). Write translator output to the
# text connection.
sink(text_conn)
.C("c_mod", model_file, c_file)
tryCatch(
{
.C("c_mod", model_file, c_file)
},
error = function(e) {
sink()
close(text_conn)
stop("MCSim translator failed: ", e$message)
}
)
sink()
close(text_conn)
mod_output <- paste(mod_output, collapse = "\n")

# Add a small delay on Windows to ensure files are fully written
if (.Platform$OS.type == "windows") {
Sys.sleep(0.1)
}

# Save the translator output to a file.
if (!verbose_output) {
temp_directory <- tempdir()
Expand Down Expand Up @@ -98,8 +123,48 @@ compileModel <- function(model_file, c_file, dll_name, dll_file, hash_file = NUL

# Code to update C source file using model-specific names for objects.

# Read the original C source file.
lines <- readLines(c_file)
# Check if C file was created successfully
if (!file.exists(c_file)) {
# Provide diagnostic information for debugging
inits_file <- sub("\\.c$", "_inits.R", c_file)

# Try to read model file content for diagnosis
model_content <- "Could not read model file"
if (file.exists(model_file)) {
tryCatch(
{
model_content <- paste(readLines(model_file), collapse = "\n")
if (nchar(model_content) == 0) {
model_content <- "[Model file is empty]"
}
},
error = function(e) {
model_content <- paste("Error reading model file:", e$message)
}
)
}

diagnostics <- paste0(
"C file was not created: ", c_file, "\n",
"Model file: ", model_file, " (exists: ", file.exists(model_file),
", size: ", ifelse(file.exists(model_file), file.size(model_file), "N/A"), " bytes)\n",
"Expected inits file: ", inits_file, " (exists: ", file.exists(inits_file), ")\n",
"Working directory: ", getwd(), "\n",
"Model file content:\n", model_content, "\n",
"MCSim output:\n", mod_output
)
stop(diagnostics)
}

# Read the original C source file with error handling
tryCatch(
{
lines <- readLines(c_file)
},
error = function(e) {
stop("Failed to read C file '", c_file, "': ", e$message)
}
)

# Find and replace C object names with model-specific names.
item_to_replace <- c(
Expand All @@ -120,7 +185,7 @@ compileModel <- function(model_file, c_file, dll_name, dll_file, hash_file = NUL
"event",
"root"
)
for (idx in seq(length(item_to_replace))) {
for (idx in seq_along(item_to_replace)) {
lines <- gsub(
paste0("\\b", item_to_replace[idx], "\\b"),
paste0(item_to_replace[idx], "_", model_name),
Expand All @@ -140,8 +205,20 @@ compileModel <- function(model_file, c_file, dll_name, dll_file, hash_file = NUL
paste0(model_name, "_model_inits.R")
)

# Read the original inits R source file.
lines <- readLines(inits_file)
# Check if inits file was created successfully
if (!file.exists(inits_file)) {
stop("Inits R file was not created: ", inits_file)
}

# Read the original inits R source file with error handling
tryCatch(
{
lines <- readLines(inits_file)
},
error = function(e) {
stop("Failed to read inits R file '", inits_file, "': ", e$message)
}
)

# Find and replace R and C object names with model-specific names.
item_to_replace <- c(
Expand All @@ -151,7 +228,7 @@ compileModel <- function(model_file, c_file, dll_name, dll_file, hash_file = NUL
"initStates",
"initState"
)
for (idx in seq(length(item_to_replace))) {
for (idx in seq_along(item_to_replace)) {
lines <- gsub(
paste0("\\b", item_to_replace[idx], "\\b"),
paste0(item_to_replace[idx], "_", model_name),
Expand All @@ -167,6 +244,10 @@ compileModel <- function(model_file, c_file, dll_name, dll_file, hash_file = NUL
# machine code file (ending with ".dll" or ".so"). Write compiler output
# to a character string.
r_path <- file.path(R.home("bin"), "R")
if (.Platform$OS.type == "windows") {
r_path <- normalizePath(r_path, winslash = "/")
c_file <- normalizePath(c_file, winslash = "/")
}
compiler_output <- system(paste(
shQuote(r_path), "CMD SHLIB",
shQuote(c_file)
Expand All @@ -184,7 +265,8 @@ compileModel <- function(model_file, c_file, dll_name, dll_file, hash_file = NUL
# If hash file name was provided, create a hash (md5 sum) for the model file
# and print a message about its location.
if (!is.null(hash_file)) {
file_hash <- as.character(md5sum(model_file))
# Always hash the model_file (the file that gets compiled)
file_hash <- as.character(tools::md5sum(model_file))
write(file_hash, file = hash_file)
message(
"Hash created and saved in the file ", normalizePath(hash_file),
Expand Down
2 changes: 1 addition & 1 deletion R/fileHasChanged.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

.fileHasChanged <- function(model_file, hash_file) {
# Calculate hash for current model file
current_hash <- as.character(md5sum(model_file))
current_hash <- as.character(tools::md5sum(model_file))

# Read saved hash
saved_hash <- readLines(hash_file, n = 1)
Expand Down
22 changes: 19 additions & 3 deletions R/fixPath.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,28 @@
new.mName <- strsplit(basename(file), "[.]")[[1]][1]
new.mPath <- dirname(file)
if (.Platform$OS.type == "windows") {
new.mPath <- gsub("\\\\", "/", utils::shortPathName(new.mPath))
# Normalize path and convert backslashes to forward slashes
new.mPath <- normalizePath(new.mPath, winslash = "/", mustWork = FALSE)
# Only use shortPathName if there are spaces and it's needed
if (grepl(" ", new.mPath)) {
tryCatch(
{
short_path <- utils::shortPathName(new.mPath)
if (short_path != "" && short_path != new.mPath) {
new.mPath <- gsub("\\\\", "/", short_path)
}
},
error = function(e) {
# If shortPathName fails, keep the original path
# The compilation might still work with quoted paths
}
)
}
}

has_space <- grepl(" ", new.mPath)
if (has_space == T) {
stop("Error: User-defined directory has space which will throw error for .dll/.so compilation")
if (has_space == TRUE) {
warning("Directory path contains spaces which may cause compilation issues: ", new.mPath)
}

return(list("mPath" = new.mPath, "mName" = new.mName))
Expand Down
2 changes: 1 addition & 1 deletion tests/testthat/test-compareHash.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ testthat::test_that("test_compareHash", {
# Test to make sure changing the file returns a changed path

dir.create(file.path(tempdir(), "testDir"))
mName <- tempfile(pattern = "mcsimmod_", tmpdir = file.path(tempdir(), "testDir"))
mName <- normalizePath(tempfile(pattern = "mcsimmod_", tmpdir = file.path(tempdir(), "testDir")), mustWork = FALSE)
mString <- readLines(file.path(testthat::test_path(), "data", "exponential.model"))
writeLines(mString, paste0(mName, ".model"))

Expand Down
6 changes: 3 additions & 3 deletions tests/testthat/test-sim.R
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ testthat::test_that("Model$localModel", {
})

testthat::test_that("Model$relativeModel", {
mName <- file.path(testthat::test_path(), "data", "exponential")
mName <- normalizePath(file.path(testthat::test_path(), "data", "exponential"), mustWork = FALSE)
testthat::expect_true(file.exists(paste0(mName, ".model")))

model <- createModel(mName)
Expand All @@ -45,8 +45,8 @@ testthat::test_that("Model$absoluteModel", {
# Use absolute path of temp directory,
# Test to make sure changing the file returns a changed path

dir.create(file.path(tempdir(), "testDir"))
mName <- tempfile(pattern = "mcsimmod_", tmpdir = file.path(tempdir(), "testDir"))
dir.create(file.path(tempdir(), "testDir"), showWarnings = FALSE)
mName <- normalizePath(tempfile(pattern = "mcsimmod_", tmpdir = file.path(tempdir(), "testDir")), mustWork = FALSE)
mString <- readLines(file.path(testthat::test_path(), "data", "exponential.model"))
writeLines(mString, paste0(mName, ".model"))

Expand Down
Loading
Loading