R Code structure

Introduction

This document describes the OSPSuite-R package structure. The package provides OSPSuite functionality in the R programming language. This document covers the package elements, code structure, and components that interface between the OSPSuite .NET codebase and R.

OSPSuite-R communication with .NET

The OSPSuite-R package provides access to OSPSuite functionality implemented in .NET. The {rsharp} package enables communication between R and .NET using C++ as an intermediate layer. .NET communicates with C++ through a custom native host. C++ then communicates with R through the R .C interface. Use {rsharp} to load libraries compiled from the .NET code.

On the .NET side, the OSPSuite.R project in OSPSuite.Core serves as the main entry point for R. This entry point provides access to required Core libraries, including OSPSuite.Core and OSPSuite.Infrastructure. To access PK-Sim functionality, use the separate entry point in the PK-Sim codebase: PKSim.R.

OSPSuite-R code structure

The package follows R package best practices for file and code structure. Unlike most R packages, OSPSuite-R uses an object-oriented design. This design reflects the object-oriented structure of PK-Sim and OSPSuite.Core in .NET.

Initializing the package

R loads package files alphabetically, so zzz.R is evaluated last. This file calls .onLoad() to ensure all functions in other files are evaluated first. The zzz.R file checks that R is running the x64 version, then calls .initPackage(). The init-package.R file uses {rsharp} to call the OSPSuite-R package entry point in OSPSuite.Core.

Object oriented design and {rsharp} encapsulation

OSPSuite-R uses an object-oriented design. The package uses the R6 framework to create and work with objects. Calls through {rsharp} create objects in the .NET environment. Access these objects through getters, setters, and methods. Encapsulate objects passed from .NET to R in wrapper classes. The base wrapper class is DotNetWrapper. All specific wrapper classes (for example, for simulations) inherit from DotNetWrapper.

The class handles basic object initialization. The initialize method (the R6 equivalent of a C# constructor) saves a reference to the .NET object internally:

DotNetWrapper:

#' Initialize a new instance of the class
#' @param ref Instance of the `.NET` object to wrap.
#' @return A new `DotNetWrapper` object.
initialize = function(ref) {
    private$.ref <- ref
}

Wrapper classes encapsulate {rsharp} calls that work on objects. Users should never call {rsharp} directly. All {rsharp} calls are encapsulated in wrapper classes or their utility functions (see utilities files below).

Wrap each .NET class with a corresponding wrapper class. Define wrapper classes in separate files named after the R class. For example, the R Simulation class wraps an OSPSuite simulation and is defined in simulation.R.

The Simulation class derives from ObjectBase. ObjectBase extends DotNetWrapper by adding Name and Id properties:

simulation.R:

Simulation <- R6::R6Class(
  "Simulation",
  cloneable = FALSE,
  inherit = ObjectBase,
  ...

object-base.R:

#' @title ObjectBase
#' @docType class
#' @description  Abstract wrapper for an OSPSuite.Core ObjectBase.
#'
#' @format NULL
#' @keywords internal
ObjectBase <- R6::R6Class(
  "ObjectBase",
  cloneable = FALSE,
  inherit = DotNetWrapper,
  active = list(
    #' @field name The name of the object. (read-only)
    name = function(value) {
      private$.wrapReadOnlyProperty("Name", value)
    },
    #' @field id The id of the .NET wrapped object. (read-only)
    id = function(value) {
      private$.wrapReadOnlyProperty("Id", value)
    }
  )
)

Access simulation properties (for example, the Output Schema) through DotNetWrapper functionality:

simulation.R

#' @field outputSchema outputSchema object for the simulation (read-only)
outputSchema = function(value) {
  private$.readOnlyProperty(
    "outputSchema",
    value,
    private$.settings$outputSchema
  )
}

R wrapper classes must implement a meaningful print function. Example:

simulation.R

#' @description
#' Print the object to the console
#' @param ... Rest arguments.
print = function(...) {
  ospsuite.utils::ospPrintClass(self)
  ospsuite.utils::ospPrintItems(list(
    "Name" = self$name,
    "Source file" = self$sourceFile
  ))
}

Basic access to object methods and properties is often insufficient. Create utility functions for additional functionality and place them in separate utilities files. For example, see utilities-simulation.R. Utilities files contain R code that works on class objects. These files can also include {rsharp} calls to .NET functions that operate on objects. Don't place {rsharp} calls that only expose object properties or methods in utilities files. Put those in the R wrapper class.

By convention, prefix internal package functions with a dot. For example, use .runSingleSimulation instead of runSingleSimulation:

utilities-simulation.R

.runSingleSimulation <- function(
  simulation,
  simulationRunOptions,
  population = NULL,
  agingData = NULL
) { ... }

Communication between R and .NET has performance overhead. Minimize cross-language calls when possible.

Tasks and task caching

Tasks are reusable objects defined on the .NET side that provide functionality for other objects. Access tasks through Api.cs in OSPSuite.Core. The OSPSuite side creates tasks through the IoC container.

The following example shows how to use the hasDimension utility function to check if a dimension (provided as a string) is supported:

utilities-units.R:

#' Dimension existence
#'
#' @param dimension String name of the dimension.
#' @details Returns `TRUE` if the provided dimension is supported otherwise `FALSE`
#' @export
hasDimension <- function(dimension) {
  validateIsString(dimension)
  dimensionTask <- .getNetTaskFromCache("DimensionTask")
  dimensionTask$call("HasDimension", dimension)
}

The example calls the internal function .getNetTaskFromCache to retrieve the Dimension Task. To avoid repeated retrieval from .NET, tasks are cached on the R side. See get-net-task.R:

#' @title .getNetTaskFromCache
#' @description Get an instance of the specified `.NET` Task that is retrieved
#' from cache if already initiated. Otherwise a new task will be initiated and
#' cached in the `tasksEnv`.
#'
#' @param taskName The name of the task to retrieve (**without** `Get` prefix).
#'
#' @return returns an instance of of the specified `.NET` task.
#'
#' @keywords internal
.getNetTaskFromCache <- function(taskName) {
  if (is.null(tasksEnv[[taskName]])) {
    tasksEnv[[taskName]] <- .getNetTask(taskName)
  }
  return(tasksEnv[[taskName]])
}

Tasks are cached in the tasksEnv[] list. If a task is not found in the cache, retrieve it from .NET through an {rsharp} call and add it to the cache for future use.

Tests

The OSPSuite-R package includes comprehensive tests. Find test code in testthat. Tests ensure correct and consistent package functioning. They also serve as examples for understanding how objects are created and used.

Updating Core DLLs

The R package stores local copies of DLLs from OSPSuite.Core and PK-Sim in inst/lib/. Update these DLLs when a newer version of the .NET codebase is released.

Run the update workflow

Use the update-core-files.yaml GitHub Actions workflow to update the Core DLLs. The workflow automates downloading files and creating a Pull Request.

To run the workflow:

  1. Open the workflow page on GitHub.

  2. Click Run workflow.

  3. Select the branch to run the workflow on (typically main).

  4. (Optional) Configure workflow inputs:

    • Branch name: Specify a custom branch name (default: update-core-files-YYYYMMDD-HHMMSS)

    • PR title: Specify the Pull Request title (default: "Update Core Files")

    • PR body: Specify the Pull Request description (default: auto-generated)

  5. Click Run workflow.

What the workflow does

The workflow performs these steps:

  1. Runs the R script .github/scripts/update_core_files.R to download core files from the latest PK-Sim build artifacts.

  2. Checks for changes in the inst/lib/ directory.

  3. If changes are detected:

    • Creates a new branch.

    • Commits the updated files.

    • Creates a Pull Request.

    • Builds SQLite libraries for macOS (arm64).

The workflow file is located at .github/workflows/update-core-files.yaml.

Last updated