Skip to content

Welcome to flepimop2

The next generation of the flexible epidemic modeling pipeline.

What is flepimop2?

flepimop2 is a Python package and command-line tool for running dynamic system simulations. It works with configuration files to define and execute analysis pipelines. Because it is also a library, you can write custom analyses that read and write data in a way that works seamlessly with the rest of the pipeline, and advanced users can develop shareable modules that plug directly into it.

At the core of flepimop2 is a modular design that separates three concerns:

  • System: the mathematical model describing how the dynamical system evolves (e.g., disease spreads with a compartmental SIR model)
  • Engine: the numerical solver that runs the model forward in time
  • Backend: the output format for saving results

Each of these is defined independently and referenced in a single YAML configuration file. Because the full pipeline - model, solver, parameters, time grid, and post-processing - lives in one file, workflows are reproducible, version-controllable, and easy to share with collaborators.

Installation

flepimop2 is published on PyPI and can be installed with pip:

pip install flepimop2

This makes the flepimop2 command available globally so you can use it from any project directory. If you want to work from a local clone instead, see the installation guide for a source install and dependency setup.

Create a Project

Download quickstart-project.zip, unzip it wherever you want your project to live, then run:

unzip quickstart-project.zip
cd quickstart-project

The bundle already contains the standard project structure created by flepimop2 skeleton:

quickstart-project/
├── configs/
│   ├── built/
│   ├── config.yaml
│   └── EDITME.yaml
├── environment.yaml
├── justfile
├── model_input/
│   ├── data/
│   └── plugins/
├── model_output/
├── postprocessing/
│   └── SIR_plot.R
└── README.md

Every flepimop2 project needs at least three things to run: a configuration file, a system, and an engine. The configuration file (saved in configs) is a YAML file that specifies your model parameters, which system and engine to use, where to write outputs, and optionally what post-processing steps to run after a simulation. The system and engine are backends that implement the model dynamics and the numerical solver, respectively. In this quickstart, we will use Python scripts (saved in model_input/plugins) for both the system and the engine. The ZIP bundle above already places those files in the correct locations, includes the dependencies required for this page, and includes the post-processing script used later in the guide.

Configuration File
---
name: Postprocessing_Sample_Model
system:
  - module: wrapper
    state_change: flow
    script: model_input/plugins/SIR.py
    model_state:
      parameter_names: [s0, i0, r0]
      labels: [S, I, R]
engine:
  - module: wrapper
    state_change: flow
    script: model_input/plugins/solve_ivp.py
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:
  demo:
    module: shell
    command: Rscript postprocessing/SIR_plot.R
    args:
      - configs/config.yaml
      - model_output/SIR_plot.png
  hires:
    module: shell
    command: Rscript postprocessing/SIR_plot.R
    args:
      - configs/config.yaml
      - model_output/SIR_plot_hires.png
parameter:
  beta: 0.3
  gamma: 0.1
  s0: 999
  i0: 1
  r0: 0
SIR System
"""SIR model plugin for flepimop2 demo."""

import numpy as np

from flepimop2.typing import Float64NDArray


def stepper(
    time: float,  # noqa: ARG001
    state: Float64NDArray,
    beta: float,
    gamma: float,
) -> Float64NDArray:
    """
    Compute dY/dt for the SIR model.

    Args:
        time: The current time (not used in this model, but included for compatibility).
        state: A numpy array containing the current values [S, I, R].
        beta: The infection rate.
        gamma: The recovery rate.

    Returns:
        A numpy array containing the derivatives [dS/dt, dI/dt, dR/dt].

    """
    y_s, y_i, _ = np.asarray(state, dtype=float)
    infection = (beta * y_s * y_i) / np.sum(state)
    recovery = gamma * y_i
    dydt = [-infection, infection - recovery, recovery]
    return np.array(dydt, dtype=float)
scipy ODE Engine
"""ODE solver plugin that wraps `scipy.integrate.solve_ivp` for flepimop2 demo."""

from collections.abc import Mapping
from typing import Any

import numpy as np
from scipy.integrate import solve_ivp

from flepimop2.parameter.abc import ModelStateSpecification, ParameterValue
from flepimop2.system.abc import SystemProtocol
from flepimop2.typing import Float64NDArray


def runner(
    fun: SystemProtocol,
    times: Float64NDArray,
    initial_state: dict[str, ParameterValue],
    params: Mapping[str, ParameterValue] | None = None,
    *,
    model_state: ModelStateSpecification | None = None,
    **solver_options: Any,
) -> Float64NDArray:
    """Solve an initial value problem using scipy.solve_ivp.

    Args:
        fun: A function that computes derivatives.
        times: sequence of time points where we evaluate the solution. Must have
            length >= 1.
        initial_state: Structured initial-state entries.
        params: Optional dict of keyword parameters forwarded to fun.
        model_state: Specification describing how to order the initial-state
          entries into a numeric state array.
        **solver_options: Additional keyword options forwarded to
          scipy.integrate.solve_ivp.

    Returns:
        FloatArray: Array with time and state values evaluated at `times`.
        Each row is [t, y...].

    Raises:
        ValueError: If `times` is not a 1D sequence of time points with length >= 1, or
            if the first time point is negative.

    """
    if not (times.ndim == 1 and times.size >= 1):
        msg = "times must be a 1D sequence of time points"
        raise ValueError(msg)
    if model_state is None:
        msg = "model_state must be provided to assemble the initial condition."
        raise ValueError(msg)

    times.sort()

    t0, tf = 0.0, times[-1]
    if times[0] < t0:
        msg = f"times[0] must be >= 0; got times[0]={times[0]}"
        raise ValueError(msg)

    y0 = np.stack([
        np.asarray(initial_state[name].value) for name in model_state.parameter_names
    ]).astype(np.float64)
    args = tuple(val.item() for val in params.values()) if params is not None else None
    result = solve_ivp(
        fun,
        (t0, tf),
        y0,
        t_eval=times,
        args=args,  # type: ignore[arg-type]
        **solver_options,
    )
    return np.transpose(np.vstack((result.t, result.y)))

Next, set up the project's virtual environment. The bundled environment.yaml already includes the dependencies required for this guide.

Environment YAML file
---
channels:
  - 'conda-forge'
  - 'defaults'
  - 'r'
  - 'dnachun'
dependencies:
  - 'python==3.13'
  - 'pip'
  - pip:
      - 'flepimop2'
  - 'scipy'
  - 'r-base>=4.3'
  - 'r-data.table>=1.17'
  - 'r-ggplot2>=3.4'
  - 'r-yaml>=2.3'

Then create the environment:

just venv
conda activate ./venv

As flepimop2 will only work when it is run in an appropriate environment, you will need to run conda activate ./venv each time you open a new terminal session before using flepimop2.

Simulate an Outbreak

Activate the project environment and run the simulation:

flepimop2 simulate configs/config.yaml

Results are saved automatically to the model_output directory as a CSV file.

Adding Post-Processing

flepimop2 supports post-processing steps that run after a simulation - useful for generating plots, rendering notebooks, or producing summary tables. Post-processing steps are defined in the process block of your configuration file and can invoke R scripts, Python scripts, or Jupyter notebooks.

The same quickstart-project.zip bundle already includes the post-processing config, script, and dependencies needed for this section.

The bundled project includes this post-processing file layout:

quickstart-project/
├── configs/
│   ├── built/
│   ├── config.yaml
│   └── EDITME.yaml
├── environment.yaml
├── justfile
├── model_input/
│   ├── data/
│   └── plugins/
│       ├── SIR.py
│       └── solve_ivp.py
├── model_output/
├── postprocessing/
│   └── SIR_plot.R
└── README.md

Take a look at configs/config.yaml. It defines two simulation targets - demo and hires - that share the same model parameters but use different time resolutions. A separate post-processing pipeline is defined for each target under the process block. flepimop2 defaults to the first defined target, but you can select a specific one with --target:

# Run the default (demo) target
flepimop2 simulate configs/config.yaml

# Run the high-resolution target instead
flepimop2 simulate --target hires configs/config.yaml

To run post-processing after a simulation, call process with the same config:

flepimop2 simulate configs/config.yaml
flepimop2 process configs/config.yaml

This runs the post-processing steps defined for the demo target, producing a plot. You can call post-processing for a specific plot with the same --target argument as you use for simulations.

flepimop2 simulate --target hires configs/config.yaml
flepimop2 process --target hires configs/config.yaml