Getting Started¶
This guide walks through flepimop2's more advanced features. We describe the system-engine architecture, then show how to use that for model specification and solver configuration. We show those elements flow into simulation setup and then post-processing. It is intended for users who have already worked through the quickstart guide and are ready to explore the framework in more depth.
Prerequisites¶
Make sure the following are available on your system:
Installation¶
For details on installation please refer to the installation guide.
Create a Project¶
Download full-feature-project.zip, unzip it in the directory you want to conduct your analysis in, then enter the project:
The bundle already includes the walkthrough config, post-processing files, and an environment.yaml with the additional dependencies required for this guide. Then create and activate the environment:
The System-Engine Architecture¶
flepimop2 separates the specification of a model from the computation that runs it. A system defines what happens - the equations governing state transitions. An engine defines how those equations are solved - the numerical method and its configuration. These two components are independently specified in the config file and can be swapped in any combination.
This separation matters in practice. A disease modeler can publish a system - the compartmental structure and transmission logic - without prescribing a solver. Collaborators can then run that same system with a different engine suited to their performance requirements or numerical preferences, without ever touching the model code.
The Wrapper Pattern¶
The quickstart guide demonstrates the simplest case: both the system and engine are user-provided Python scripts loaded at runtime.
system:
- module: wrapper
state_change: flow
script: model_input/plugins/SIR.py
engine:
- module: wrapper
state_change: flow
script: model_input/plugins/solve_ivp.py
The wrapper module dynamically imports the script (path defined in the script argument) and looks for a required entry point function: stepper() for systems, runner() for engines. The state_change field tells flepimop2 what the stepper returns - flow means dY/dt (derivatives suitable for ODE integration), delta means ΔY (increments), and state means the full new state vector.
Swapping Solvers Without Changing the Model¶
Because the system and engine are decoupled, changing the solver requires only a change to the config file. The following two configurations run the same SIR.py model but use different engines:
With the wrapper engine (scipy):
system:
- module: wrapper
state_change: flow
script: model_input/plugins/SIR.py
engine:
- module: wrapper
state_change: flow
script: model_input/plugins/solve_ivp.py
With op_engine (swapping only the engine block):
system:
- module: wrapper
state_change: flow
script: model_input/plugins/SIR.py
engine:
- module: op_engine
config:
method: heun
adaptive: false
rtol: 1.0e-6
atol: 1.0e-9
SIR.py is untouched. The only difference is the engine block. This makes it straightforward to benchmark solvers against each other, validate results across implementations, or switch to a faster solver once a model has been validated.
Defining Models Directly in YAML (op_system)¶
For users who prefer not to write Python, the op_system module (from the flepimop2-op-system package) allows compartmental models to be specified entirely within the config file using symbolic expressions.
system:
- module: op_system
spec:
kind: expr
state: [S, I, R]
equations:
S: -beta * S * I / sum_state()
I: beta * S * I / sum_state() - gamma * I
R: gamma * I
initial_state:
S: s0
I: i0
R: r0
The example above specifies a basic SIR model. kind: expr indicates dY/dt expressions will be provided for each compartment. The compartments are listed in the state field, and their corresponding dY/dt expressions are defined in equations. The sum_state() helper evaluates to the total population at each time step - equivalent to S + I + R - and is provided to make density-dependent transmission terms concise and readable. Symbolic representations for the initial states of each of these compartments are defined in initial_state; each of these symbolic representations are then assigned a numeric value in parameters later in the file.
If a user wants to defines disease system by specifying the transitions between states instead of the expression for each state, they can do so as follows:
system:
- module: op_system
spec:
kind: transitions
state: [S, I, R]
transitions:
- from: S
to: I
rate: beta * I / sum_state()
- from: I
to: R
rate: gamma
When using op_system, no state_change field is required; the module infers it from the symbolic specification. op_system requires op_engine - the symbolic expression compiler and solver are designed to work together.
Simulation Targets and Time Specifications¶
Defining Multiple Targets¶
A single configuration file can define multiple named simulation targets under the simulate block. Each target can have its own time grid, and can independently reference any named system, engine, or backend defined elsewhere in the config:
flepimop2 defaults to the first defined target when none is specified. To run a specific target:
Multiple targets in a single config are useful for running quick sanity checks at coarse resolution alongside production runs at fine resolution, without maintaining separate config files.
Time Specification Syntax¶
The times field supports two formats:
Explicit list: Time points are specified directly. Useful when evaluation times are irregular or correspond to observation times:
Range shorthand: A compact start:step:stop notation generates a uniformly spaced grid.
times: 0.0:0.1:100.0 # 1001 time points from 0 to 100, inclusive
times: 0.0:1.0:365.0 # daily steps over one year
The range syntax is particularly convenient for high-resolution runs where listing every time point by hand would be impractical.
Post-Processing¶
Post-processing steps are defined in the process block and are executed by flepimop2 process. Each named entry under process specifies a module type and the arguments needed to run it. Like simulation targets, process targets can be run selectively with --target. These post-processing steps will often require accompanying scripts; ensure these scripts have been saved in the file paths specified in the command argument.
Shell Processes¶
The shell module runs an arbitrary command-line program. This is the standard way to invoke R scripts or standalone Python scripts:
process:
plot_demo:
module: shell
command: python postprocessing/SIR_plot_op_engine.py
args:
- configs/config.yml
- model_output/SIR_plot_op_engine.png
The command field is the executable, and args is a list of positional arguments passed to it. The working directory is the project root.
R scripts work the same way:
process:
r_plot:
module: shell
command: Rscript postprocessing/SIR_plot.R
args:
- configs/config.yml
- model_output/SIR_plot.png
Any shell command is valid here - the module imposes no restrictions on what runs. Exit codes are checked; a non-zero exit will raise an error and halt the pipeline.
Jupyter Notebook Rendering (ipynbrender)¶
The ipynbrender module executes a Jupyter notebook and renders the output to HTML:
process:
notebook:
module: ipynbrender
file: postprocessing/SirPlot.ipynb
output: model_output/SirPlot.html
ipynbrender requires the flepimop2-ipynbrender adapter package (included in the full-feature environment.yaml). The notebook is executed in place and a self-contained HTML file is written to the output path. This is useful for producing reports that include inline figures and narrative text alongside simulation outputs.
Multiple Process Targets¶
Multiple named process targets can coexist in a single config:
process:
r_plot:
module: shell
command: Rscript postprocessing/SIR_plot.R
args:
- configs/config.yml
- model_output/SIR_plot.png
python_plot:
module: shell
command: python postprocessing/SIR_plot_op_engine.py
args:
- configs/config.yml
- model_output/SIR_plot_py.png
notebook:
module: ipynbrender
file: postprocessing/SirPlot.ipynb
output: model_output/SirPlot.html
Running flepimop2 process configs/config.yml executes the first defined target. To run a specific one:
Accessing Config Programmatically in Post-Processing Scripts¶
Python post-processing scripts can use flepimop2's public configuration API to locate output files without hardcoding paths. ConfigurationModel.from_yaml() returns a fully validated, typed representation of the config, from which the backend's output directory can be resolved:
from flepimop2.configuration import ConfigurationModel
from pathlib import Path
config_model = ConfigurationModel.from_yaml(Path("configs/config.yml"))
# Get the backend used by the first simulate target
first_sim = next(iter(config_model.simulate.values()))
backend_name = getattr(first_sim, "backend", None) or "default"
backend_cfg = config_model.backends[backend_name].model_dump()
results_dir = Path(backend_cfg.get("root") or "model_output")
# Find the most recently modified CSV
latest_csv = sorted(results_dir.glob("*.csv"), key=lambda p: p.stat().st_mtime)[-1]
This pattern means post-processing scripts adapt automatically when the backend or output directory changes in the config - no manual path updates required.
Full Project Setup¶
The files for this walkthrough are bundled in full-feature-project.zip and are also available individually in assets/full-feature-project/ in the flepimop2 repository.
File Layout¶
The bundle includes the following project files:
- config.yml →
configs/config.yml - SIR_plot.R →
postprocessing/SIR_plot.R - SirPlot.ipynb →
postprocessing/SirPlot.ipynb - SIR_plot_op_engine.py →
postprocessing/SIR_plot_op_engine.py
Your project layout should look like this:
full-feature-project/
├── configs/
│ ├── built/
│ ├── config.yml
│ └── EDITME.yaml
├── environment.yaml
├── justfile
├── model_input/
│ ├── data/
│ └── plugins/
├── model_output/
├── postprocessing/
│ ├── SIR_plot.R
│ ├── SIR_plot_op_engine.py
│ └── SirPlot.ipynb
└── README.md
The Configuration File¶
config.yml
---
name: SIR_Script
system:
- module: op_system
spec:
kind: expr
state: [S, I, R]
equations:
S: -beta * S * I / sum_state()
I: beta * S * I / sum_state() - gamma * I
R: gamma * I
initial_state:
S: s0
I: i0
R: r0
engine:
- module: op_engine
state_change: flow
config:
method: heun
adaptive: false
strict: true
rtol: 1.0e-6
atol: 1.0e-9
simulate:
demo:
times: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
hires:
times: 0.0:0.1:100.0
backend:
- module: csv
process:
plot_demo:
module: shell
command: python postprocessing/SIR_plot_op_engine.py
args:
- configs/config.yml
- model_output/SIR_plot_op_engine.png
r_plot:
module: shell
command: Rscript postprocessing/SIR_plot.R
args:
- configs/config.yml
- model_output/SIR_plot.png
notebook:
module: ipynbrender
file: postprocessing/SirPlot.ipynb
output: model_output/SirPlot.html
parameter:
beta: 0.3
gamma: 0.1
s0: 999
i0: 1
r0: 0
Note that this config uses op_system and op_engine - no Python plugin scripts are needed for the model or solver. The model_input/plugins directory can remain empty.
Running the Pipeline¶
# Run the default (demo) simulation target
flepimop2 simulate configs/config.yml
# Run the high-resolution target
flepimop2 simulate --target hires configs/config.yml
# Run post-processing for the default target
flepimop2 process configs/config.yml
# Run post-processing for the r_plot target
flepimop2 process --target r_plot configs/config.yml
# Run post-processing for the notebook target using `flepimop2-ipynbrender`
flepimop2 process --target notebook configs/config.yml
Simulation outputs are written to model_output as CSV files. Post-processing outputs (plots, rendered notebooks) are also written there, with filenames as specified in the process block.