--- title: "Getting started with rjsf" description: "Set up Python JSF, define compartmental models, and run Jump-Switch-Flow simulations from R." format: html: toc: true toc-depth: 3 vignette: > %\VignetteIndexEntry{Getting started with rjsf} %\VignetteEngine{quarto::html} %\VignetteEncoding{UTF-8} execute: eval: false echo: true warning: false message: false --- ## Introduction `rjsf` provides an R interface to Jump-Switch-Flow (JSF), a hybrid simulation framework for compartmental models that combines stochastic jump dynamics at low populations with deterministic flow dynamics at high populations. Running simulations requires the Python `jsf` backend, which `rjsf` accesses through `reticulate`. The main user-facing function is `jsf_simulate()`. It lets users: 1. Define a compartmental reaction model in R. 2. Pass the model to the Python `jsf` backend through `reticulate`. 3. Return the simulation output as either: * a regular R `data.frame`; or * a structured `JSFResult` object. A `JSFResult` object supports standard methods such as: ```{r} print(result) summary(result) plot(result) ``` ## Set up Python `jsf` `rjsf` uses `reticulate` to communicate with Python. The Python `jsf` package must therefore already be installed in the Python environment used by `reticulate`. Installation instructions for the Python dependencies are provided in the package README. You can inspect the Python environment selected by `reticulate` with: ```{r} reticulate::py_config() ``` ## Check Python `jsf` Load `rjsf` and check whether the Python `jsf` module is available: ```{r} library(rjsf) jsf_available() ``` A correctly configured environment should return: ```text [1] TRUE ``` If it returns `FALSE`, check that: * Python `jsf` is installed; * it is installed in the environment currently used by `reticulate`; and * `reticulate::py_config()` reports the expected Python executable. ## Lotka-Volterra predator-prey model The Lotka-Volterra model describes interactions between two species: * `prey`; * `predator`. The model contains three reactions: 1. Prey birth: `Prey -> 2 Prey` 2. Predator death: `Predator -> NULL` 3. Predation: `Prey + Predator -> 2 Predator` The state vector is: ```{r} x <- c(prey, predator) ``` ### Define the rate function The Python `jsf` backend expects the rate function to be a Python callable. We create one using `reticulate::py_eval()`: ```{r} rates_lv <- reticulate::py_eval( "lambda x, t: [2.0 * x[0], 1.5 * x[1], 0.05 * x[0] * x[1]]" ) ``` The three rates correspond to: ```text 2.0 * prey # prey birth 1.5 * predator # predator death 0.05 * prey * predator # predation ``` ### Define the stoichiometry The reactant matrix describes what each reaction consumes: ```{r} reactant_lv <- matrix( c( 1, 0, 0, 1, 1, 1 ), ncol = 2, byrow = TRUE ) reactant_lv ``` The product matrix describes what each reaction produces: ```{r} product_lv <- matrix( c( 2, 0, 0, 0, 0, 2 ), ncol = 2, byrow = TRUE ) product_lv ``` Internally, `jsf_simulate()` calculates the net stoichiometry matrix as: ```text nu = product - reactant ``` For this model, the net state changes are: ```text Prey -> 2 Prey [+1, 0] Predator -> NULL [ 0, -1] Prey + Predator -> 2 Predator [-1, +1] ``` ### Run the simulation Run the model and request a `JSFResult` object: ```{r} result_lv <- jsf_simulate( x0 = c(prey = 50, predator = 10), rates = rates_lv, reactant = reactant_lv, product = product_lv, do_disc = c(1, 1), t_max = 10, dt = 0.01, switching_threshold = c(30, 30), species_names = c("prey", "predator"), return_type = "JSFResult" ) ``` The important arguments are: * `x0` defines the initial number of prey and predators. * `do_disc = c(1, 1)` allows both species to be treated discretely when required. * `switching_threshold = c(30, 30)` defines the switching threshold for each species. * `return_type = "JSFResult"` returns a structured result containing the simulation trajectory and associated metadata. ### Inspect the result Print the result: ```{r} result_lv ``` Calculate a summary: ```{r} summary(result_lv) ``` Plot the simulated trajectories: ```{r} plot(result_lv) ``` ## SIR epidemic model The SIR model describes the spread of an infectious disease through three compartments: * `S`: susceptible; * `I`: infectious; * `R`: recovered. The model contains two reactions: 1. Infection: `S + I -> 2 I` 2. Recovery: `I -> R` The state vector is: ```{r} x <- c(S, I, R) ``` ### Define the model parameters Set the total population, infection rate, and recovery rate: ```{r} N <- 1000 beta <- 0.5 gamma <- 0.1 ``` Here: ```text N = total population beta = infection rate gamma = recovery rate ``` ### Define the rate function Create a Python callable containing the infection and recovery rates: ```{r} rates_sir <- reticulate::py_eval(sprintf( "lambda x, t: [%f * x[0] * x[1] / %f, %f * x[1]]", beta, N, gamma )) ``` The rates correspond to: ```text beta * S * I / N # infection gamma * I # recovery ``` ### Define the stoichiometry Define the reactant matrix: ```{r} reactant_sir <- matrix( c( 1, 1, 0, 0, 1, 0 ), ncol = 3, byrow = TRUE ) reactant_sir ``` Define the product matrix: ```{r} product_sir <- matrix( c( 0, 2, 0, 0, 0, 1 ), ncol = 3, byrow = TRUE ) product_sir ``` The net state changes are: ```text S + I -> 2 I [-1, +1, 0] I -> R [ 0, -1, +1] ``` ### Run the simulation ```{r} result_sir <- jsf_simulate( x0 = c(S = N - 10, I = 10, R = 0), rates = rates_sir, reactant = reactant_sir, product = product_sir, do_disc = c(0, 1, 0), t_max = 60, dt = 0.1, switching_threshold = c(N, 10, N), species_names = c("S", "I", "R"), return_type = "JSFResult" ) ``` In this example, `do_disc = c(0, 1, 0)` allows the infectious compartment to be treated discretely when required. This is useful because stochastic effects can be particularly important when the number of infectious individuals is small. ### Inspect the result Print the result: ```{r} result_sir ``` Calculate a summary: ```{r} summary(result_sir) ``` Plot the simulated trajectories: ```{r} plot(result_sir) ``` ## Return a data frame The default output from `jsf_simulate()` is a regular R `data.frame`. ```{r} df_lv <- jsf_simulate( x0 = c(prey = 50, predator = 10), rates = rates_lv, reactant = reactant_lv, product = product_lv, do_disc = c(1, 1), t_max = 10, dt = 0.01, switching_threshold = c(30, 30), species_names = c("prey", "predator"), return_type = "data.frame" ) head(df_lv) ``` A data frame returned by `jsf_simulate()` contains: * a `time` column; and * one column for each model species. A data frame can later be converted into a `JSFResult` object: ```{r} result_from_df <- as_jsf_result( df_lv, method = "operator-splitting", config = list( model = "Lotka-Volterra", dt = 0.01, switching_threshold = c(30, 30) ) ) result_from_df ``` ## Summary The main `rjsf` workflow is: ```text Define the initial state -> define the rate function -> define the reactant and product matrices -> call jsf_simulate() -> return a data.frame or JSFResult -> inspect the output with print(), summary(), and plot() ``` Use a `data.frame` when you primarily need the simulation values for further analysis. Use a `JSFResult` when you want a structured object containing both the simulation trajectories and their associated metadata.