diff --git a/.Rbuildignore b/.Rbuildignore new file mode 100644 index 0000000..0107785 --- /dev/null +++ b/.Rbuildignore @@ -0,0 +1,12 @@ +^.*\.Rproj$ +^\.Rproj\.user$ +^\.travis\.yml$ +^codecov\.yml$ +^CRAN-RELEASE$ +^cran-comments\.md$ +^\.github$ +^doc$ +^Meta$ +^windows\\i386\* +^i386\.*$ +.lintr \ No newline at end of file diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml index 41633c1..63d525f 100644 --- a/.github/workflows/pkgdown.yaml +++ b/.github/workflows/pkgdown.yaml @@ -2,7 +2,11 @@ # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help on: push: - branches: [main, master] + branches: + - '**' + pull_request: + branches: + - '**' release: types: [published] workflow_dispatch: @@ -28,9 +32,16 @@ jobs: extra-packages: any::pkgdown, local::., any::XML needs: website - - name: Build site - run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) + - uses: actions/setup-python@v2 + with: + python-version: '3.9' + + - name: Setup r-reticulate env + run: reticulate::virtualenv_create("r-reticulate", Sys.which("python")) shell: Rscript {0} + + - name: Build site + run: Rscript -e "pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE)" - name: Deploy to GitHub pages if: github.event_name != 'pull_request' diff --git a/.github/workflows/test_macos.yml b/.github/workflows/test_macos.yml index 40cab78..3816760 100644 --- a/.github/workflows/test_macos.yml +++ b/.github/workflows/test_macos.yml @@ -3,7 +3,7 @@ name: "Unit tests: macOS" on: push: branches: - - main + - '**' pull_request: branches: - '**' @@ -22,6 +22,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 + + - name: Install build dependencies + run: | + brew install curl - name: Setup R uses: r-lib/actions/setup-r@v2 @@ -49,6 +53,7 @@ jobs: install.packages(c("devtools")) remotes::install_deps(dependencies = TRUE) remotes::install_cran("rcmdcheck") + reticulate::install_python(version = "3.9:latest") shell: Rscript {0} - name: Check @@ -62,4 +67,4 @@ jobs: - name: Show testthat output if: always() run: find check -name 'testthat.Rout*' -exec cat '{}' \; || true - shell: bash + shell: bash \ No newline at end of file diff --git a/.github/workflows/test_ubuntu.yml b/.github/workflows/test_ubuntu.yml index cc6209c..b8d90be 100644 --- a/.github/workflows/test_ubuntu.yml +++ b/.github/workflows/test_ubuntu.yml @@ -3,7 +3,7 @@ name: "Unit tests: Ubuntu" on: push: branches: - - main + - '**' pull_request: branches: - '**' diff --git a/.github/workflows/test_windows.yml b/.github/workflows/test_windows.yml index 3656a2c..c2ad3d6 100644 --- a/.github/workflows/test_windows.yml +++ b/.github/workflows/test_windows.yml @@ -3,13 +3,12 @@ name: "Unit tests: Windows" on: push: branches: - - main + - '**' pull_request: branches: - '**' jobs: - test: name: tests on Windows with R ${{ matrix.R }} runs-on: windows-latest @@ -19,6 +18,13 @@ jobs: R: [ '4.1.0' ] steps: + - name: Check Windows architecture + run: | + if (-not [Environment]::Is64BitOperatingSystem) { throw "Not 64-bit Windows" } + Write-Host "OS Architecture: $([Environment]::Is64BitOperatingSystem)" + Write-Host "Process Architecture: $([Environment]::Is64BitProcess)" + systeminfo | findstr /B /C:"OS Name" /C:"OS Version" /C:"System Type" + shell: pwsh - name: Checkout repository uses: actions/checkout@v2 @@ -28,14 +34,23 @@ jobs: with: r-version: ${{ matrix.R }} Ncpus: 2 - + r-arch: 'x64' + + - name: Verify R architecture + run: | + Rscript -e "if(R.version$arch != 'x64') stop('R architecture is not 64-bit!')" + Rscript -e "cat('R Architecture:', R.version$arch, '\n')" + Rscript -e "cat('.Machine$sizeof.pointer:', .Machine$sizeof.pointer, '\n')" + Rscript -e "cat('Using R at:', R.home(), '\n')" + shell: cmd + - name: Query dependencies run: | install.packages('remotes') saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2) shell: Rscript {0} - - name: cache R installed packages + - name: Cache R packages uses: actions/cache@v2.1.7 id: cache with: @@ -43,7 +58,7 @@ jobs: ${{ env.R_LIBS_USER }} key: ${{ runner.os }}-R${{ matrix.R }}-2-${{ hashFiles('.github/depends.Rds') }} restore-keys: ${{ runner.os }}-R${{ matrix.R }}-2- - + - name: Install packages run: | install.packages(c("devtools")) @@ -56,10 +71,10 @@ jobs: _R_CHECK_CRAN_INCOMING_REMOTE_: false run: | options(crayon.enabled = TRUE) - rcmdcheck::rcmdcheck(args = c("--no-manual", "--as-cran"), error_on = "error", check_dir = "check") + rcmdcheck::rcmdcheck(args = c("--no-manual", "--as-cran" ,'--no-multiarch'), error_on = "error", check_dir = "check") shell: Rscript {0} - name: Show testthat output if: always() run: find check -name 'testthat.Rout*' -exec cat '{}' \; || true - shell: bash + shell: bash \ No newline at end of file diff --git a/DESCRIPTION b/DESCRIPTION index 8f4073c..a84cef2 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -3,7 +3,7 @@ Type: Package Title: R-Wrapper of Epiabm Version: 0.0.2 Authors@R: c( - person('Anita', 'Applegarth', role = c('aut', 'cre', 'cph'), email = 'anita.applegarth@reub.ox.ac.uk'), + person('Anita', 'Applegarth', role = c('aut', 'cre', 'cph'), email = 'anita.applegarth@reuben.ox.ac.uk'), person('Kingsley', 'Oguma', role = 'aut', email = "kemukperuo@gmail.com") ) Description: Wraps the epiabm Python module in R. @@ -12,20 +12,23 @@ Encoding: UTF-8 Archs: x64 SystemRequirements: Python (>= 2.7.0) Suggests: tinytest -Imports: ggplot2, here -Depends: - reticulate (>= 1.14), - R (>= 3.3.0) +Imports: + ggplot2, + here, + tidyr, + reticulate (>= 1.14) +Depends: R (>= 3.3.0) Config/reticulate: list( packages = list( list(package = "numpy", pip = TRUE), - list(package = "pandas", pip = TRUE), + list(package = "pandas", pip = TRUE), list(package = "matplotlib", pip = TRUE), list( package = "pyEpiabm", pip = TRUE, - pip_options = "--index-url git+https://github.com/SABS-R3-Epidemiology/epiabm.git@main#egg=pyEpiabm&subdirectory=pyEpiabm" + pip_options = "--extra-index-url https://github.com/SABS-R3-Epidemiology/epiabm.git@main#egg=pyEpiabm&subdirectory=pyEpiabm" ) ) ) +RoxygenNote: 7.3.2 diff --git a/NAMESPACE b/NAMESPACE index c950c17..d9e4281 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1 +1,4 @@ -import(reticulate) +# Generated by roxygen2: do not edit by hand + +export(check_python_env) +export(initialize_python_env) diff --git a/R/simulation_flow_check.R b/R/simulation_flow_check.R index e0ee6a1..4990237 100644 --- a/R/simulation_flow_check.R +++ b/R/simulation_flow_check.R @@ -2,15 +2,20 @@ # Example simulation script with data output # # Import dependencies +source("R/zzz.R") +initialize_python_env() +check_python_env() library(reticulate) +#library(rEpiabm) library(here) +library(tidyr) -os <- import("os") -logging <- import("logging") -pd <- import("pandas") -plt <- import("matplotlib.pyplot") -pe <- import("pyEpiabm") +os <- import("os", delay_load = TRUE) +logging <- import("logging", delay_load = TRUE) +pd <- import("pandas", delay_load = TRUE) +plt <- import("matplotlib.pyplot", delay_load = TRUE) +pe <- import("pyEpiabm", delay_load = TRUE) # Set working directory for relative directory references base_dir <- here() @@ -91,12 +96,8 @@ sim$compress_csv() # Create dataframe for plots filename <- here("simulation_outputs", "output.csv") -df <- pd$read_csv(filename) +df <- read.csv(filename) -# Convert pandas dataframe to R dataframe -df_r <- as.data.frame(df) - -# Load library for plotting library(ggplot2) # Reshape the data from wide to long format using base R @@ -105,13 +106,15 @@ status_columns <- c("InfectionStatus.Susceptible", "InfectionStatus.Recovered", "InfectionStatus.Dead") -df_long <- data.frame( - time = rep(df_r$time, length(status_columns)), - Status = factor(rep(status_columns, each = nrow(df_r)), - levels = status_columns, - labels = c("Susceptible", "Infected", "Recovered", "Dead")), - Count = unlist(df_r[status_columns]) +df_long <- pivot_longer( + df, + cols = all_of(status_columns), + names_to = "Status", + values_to = "Count" ) +df_long$Status <- factor(df_long$Status, + levels = status_columns, + labels = c("Susceptible", "Infected", "Recovered", "Dead")) # Create the plot p <- ggplot(df_long, aes(x = time, y = Count, color = Status)) + diff --git a/R/zzz.R b/R/zzz.R new file mode 100644 index 0000000..bd741c7 --- /dev/null +++ b/R/zzz.R @@ -0,0 +1,189 @@ +# Environment setup and management functions for R package +local <- new.env() + +#' Create and configure Python environment +#' @param env_name Character. Name of the virtual environment to create +#' @param python_version Character. Python version to use (e.g., "3.8") +#' @return Logical indicating success +#' @keywords internal +create_python_env <- function(env_name = "r-reticulate", python_version = "3.9") { + tryCatch({ + # Check if virtualenv package is available + if (!reticulate::virtualenv_exists(env_name)) { + message(sprintf("Creating new Python virtual environment: %s", env_name)) + + # Create virtual environment with system site packages to help with SSL + reticulate::virtualenv_create( + envname = env_name, + version = python_version, + packages = "pip", + system_site_packages = TRUE + ) + } + + # Activate the environment + reticulate::use_virtualenv(env_name, required = TRUE) + return(TRUE) + }, error = function(e) { + warning(sprintf("Failed to create/activate Python environment: %s", e$message)) + return(FALSE) + }) +} + +.onLoad <- function(libname, pkgname) { + # Create and activate Python environment + env_success <- create_python_env() + + if (!env_success) { + warning("Failed to set up Python environment. Some functionality may be limited.") + return() + } + + # Configure reticulate to use the package's environment + reticulate::configure_environment(pkgname) + + # Install required packages if not present + ensure_python_dependencies() + + # Import dependencies with error handling + tryCatch({ + load_python_modules() + }, error = function(e) { + warning(sprintf("Error loading Python dependencies: %s\nPlease run check_python_env() to diagnose issues.", e$message)) + }) +} + +#' Ensure all required Python dependencies are installed +#' @keywords internal +ensure_python_dependencies <- function() { + # Upgrade pip + reticulate::py_run_string(" +import sys +import subprocess +subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'pip']) + ") + + # Install required packages + reticulate::py_run_string(" +import sys +import subprocess + +# Install basic packages +packages = ['numpy', 'pandas', 'matplotlib'] +for package in packages: + subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--upgrade', package]) + +# Install pyEpiabm from GitHub +subprocess.check_call([ + sys.executable, '-m', 'pip', 'install', '--upgrade', + 'git+https://github.com/SABS-R3-Epidemiology/epiabm.git@main#egg=pyEpiabm&subdirectory=pyEpiabm' +]) + ") + + # Verify installations + result <- reticulate::py_run_string(" +import importlib.util + +packages = ['numpy', 'pandas', 'matplotlib', 'pyEpiabm'] +missing = [] + +for package in packages: + if importlib.util.find_spec(package) is None: + missing.append(package) + +print('Missing packages:', missing if missing else 'None') + ") + + if (length(result$missing) > 0) { + stop("Failed to install required packages: ", paste(result$missing, collapse = ", ")) + } + + message("All required packages installed successfully") +} + +#' Load Python modules into package environment +#' @keywords internal +load_python_modules <- function() { + # Define modules to load + modules <- list( + os = "os", + logging = "logging", + pd = "pandas", + plt = "matplotlib.pyplot", + pe = "pyEpiabm" + ) + + # Import each module + for (var_name in names(modules)) { + module_name <- modules[[var_name]] + tryCatch({ + module <- reticulate::import(module_name, delay_load = TRUE) + assign(var_name, module, envir = parent.env(local)) + }, error = function(e) { + warning(sprintf("Failed to load module %s: %s", module_name, e$message)) + }) + } +} + +#' Check Python environment and package availability +#' @export +check_python_env <- function() { + # Get Python configuration + python_config <- reticulate::py_config() + + # Print Python information + cat(sprintf("Python version: %s\n", reticulate::py_version())) + cat(sprintf("Python path: %s\n", python_config$python)) + cat(sprintf("virtualenv: %s\n", if(is.null(python_config$virtualenv)) "None" else python_config$virtualenv)) + + # Check required packages + required_packages <- c("numpy", "pandas", "matplotlib", "pyEpiabm") + + cat("\nPackage Status:\n") + pkg_status <- list() + + for (pkg in required_packages) { + if (reticulate::py_module_available(pkg)) { + # Try to get version information + tryCatch({ + version <- reticulate::py_eval(sprintf("__import__('%s').__version__", pkg)) + cat(sprintf("✓ %s (version %s)\n", pkg, version)) + pkg_status[[pkg]] <- TRUE + }, error = function(e) { + cat(sprintf("✓ %s (version unknown)\n", pkg)) + pkg_status[[pkg]] <- TRUE + }) + } else { + cat(sprintf("✗ %s is not available\n", pkg)) + pkg_status[[pkg]] <- FALSE + } + } + + # Return invisibly whether all packages are available + invisible(all(unlist(pkg_status))) +} + +#' Initialize or repair Python environment +#' @param force Logical. If TRUE, recreates the environment even if it exists +#' @export +initialize_python_env <- function(force = FALSE) { + env_name <- "r-py-env" + + if (force && reticulate::virtualenv_exists(env_name)) { + message("Removing existing Python environment...") + reticulate::virtualenv_remove(env_name) + } + + # Create and activate environment + if (create_python_env(env_name)) { + message("Python environment successfully created/activated") + + # Install dependencies + ensure_python_dependencies() + + # Check environment + check_python_env() + } else { + stop("Failed to initialize Python environment") + } +} \ No newline at end of file diff --git a/man/check_python_env.Rd b/man/check_python_env.Rd new file mode 100644 index 0000000..abb0efc --- /dev/null +++ b/man/check_python_env.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/zzz.R +\name{check_python_env} +\alias{check_python_env} +\title{Check Python environment and package availability} +\usage{ +check_python_env() +} +\description{ +Check Python environment and package availability +} diff --git a/man/create_python_env.Rd b/man/create_python_env.Rd new file mode 100644 index 0000000..d8c0399 --- /dev/null +++ b/man/create_python_env.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/zzz.R +\name{create_python_env} +\alias{create_python_env} +\title{Create and configure Python environment} +\usage{ +create_python_env(env_name = "r-py-env", python_version = "3.9") +} +\arguments{ +\item{env_name}{Character. Name of the virtual environment to create} + +\item{python_version}{Character. Python version to use (e.g., "3.8")} +} +\value{ +Logical indicating success +} +\description{ +Create and configure Python environment +} +\keyword{internal} diff --git a/man/ensure_python_dependencies.Rd b/man/ensure_python_dependencies.Rd new file mode 100644 index 0000000..20f0563 --- /dev/null +++ b/man/ensure_python_dependencies.Rd @@ -0,0 +1,12 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/zzz.R +\name{ensure_python_dependencies} +\alias{ensure_python_dependencies} +\title{Ensure all required Python dependencies are installed} +\usage{ +ensure_python_dependencies() +} +\description{ +Ensure all required Python dependencies are installed +} +\keyword{internal} diff --git a/man/initialize_python_env.Rd b/man/initialize_python_env.Rd new file mode 100644 index 0000000..55918ab --- /dev/null +++ b/man/initialize_python_env.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/zzz.R +\name{initialize_python_env} +\alias{initialize_python_env} +\title{Initialize or repair Python environment} +\usage{ +initialize_python_env(force = FALSE) +} +\arguments{ +\item{force}{Logical. If TRUE, recreates the environment even if it exists} +} +\description{ +Initialize or repair Python environment +} diff --git a/man/load_python_modules.Rd b/man/load_python_modules.Rd new file mode 100644 index 0000000..ec34d09 --- /dev/null +++ b/man/load_python_modules.Rd @@ -0,0 +1,12 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/zzz.R +\name{load_python_modules} +\alias{load_python_modules} +\title{Load Python modules into package environment} +\usage{ +load_python_modules() +} +\description{ +Load Python modules into package environment +} +\keyword{internal} diff --git a/rEpiabm.Rproj b/rEpiabm.Rproj new file mode 100644 index 0000000..21a4da0 --- /dev/null +++ b/rEpiabm.Rproj @@ -0,0 +1,17 @@ +Version: 1.0 + +RestoreWorkspace: Default +SaveWorkspace: Default +AlwaysSaveHistory: Default + +EnableCodeIndexing: Yes +UseSpacesForTab: Yes +NumSpacesForTab: 2 +Encoding: UTF-8 + +RnwWeave: Sweave +LaTeX: pdfLaTeX + +BuildType: Package +PackageUseDevtools: Yes +PackageInstallArgs: --no-multiarch --with-keep.source diff --git a/simulation_outputs/simulation_flow_SIR_plot.png b/simulation_outputs/simulation_flow_SIR_plot.png index 9975830..5f43b63 100644 Binary files a/simulation_outputs/simulation_flow_SIR_plot.png and b/simulation_outputs/simulation_flow_SIR_plot.png differ