Running a Simulation Interactively

In this tutorial, we’ll walk through getting a simulation up and running in an interactive setting such as a Python interpreter or Jupyter notebook.

Running a simulation in this way is useful for a variety of reasons, foremost for debugging and validation work. It allows for changing simulation configuration programmatically, stepping through a simulation in a controlled fashion, and examining the state of the simulation itself.

For the following tutorial, we will assume you have set up an environment and installed vivarium. If you have not, please see the Getting Started section. We’ll be using the disease model constructed in a separate tutorial here, though no background knowledge of population health is necessary to follow along. The components constructed in that tutorial are available in the vivarium package, so you don’t need to build them yourself before starting this tutorial.

Setting up a Simulation

To run a simulation interactively, we will need to create a simulation context. At a bare minimum, we need to provide the context with a set of components that encode all the behavior of the simulation model. Frequently, we’ll also provide some configuration data that is used to parameterize those components.

Note

We can also optionally provide a set of plugins to the simulation framework. Plugins are special components that add new functionality to the framework itself. This is an advanced feature for building tools to adapt vivarium to models in a particular problem domain and not important for most users.

The combination of components, configuration, and plugins forms a model specification, a complete description of a vivarium model.

The InteractiveContext can be generated from several different kinds of data and may be generated at two separate lifecycle stages. We’ll explore several examples of generating simulation objects here.

With a Model Specification File - The Automatic Way

A model specification file contains all the information needed to prepare and run a simulation, so to get up and running quickly, we need only provide this file. You typically find yourself in this use case if you already have a well-developed model and you’re looking to explore its behavior in more detail than you’d be able to using the command line utility simulate.

In this example, we will use the model specification from our disease model tutorial:

File: disease_model.yaml
components:
    vivarium.examples.disease_model:
        population:
            - BasePopulation()
        mortality:
            - Mortality()
        disease:
            - SISDiseaseModel('lower_respiratory_infections')
        risk:
            - Risk('child_wasting')
            - RiskEffect('child_wasting', 'infected_with_lower_respiratory_infections.incidence_rate')
        intervention:
            - TreatmentIntervention('sqlns', 'child_wasting.proportion_exposed')
        observer:
            - Observer()

configuration:
    randomness:
        key_columns: ['entrance_time', 'age']
    time:
        start:
            year: 2022
            month: 1
            day: 1
        end:
            year: 2026
            month: 12
            day: 31
        step_size: 0.5  # Days
    population:
        population_size: 100_000
        age_start: 0
        age_end: 5
    mortality:
        mortality_rate: 0.0114
        life_expectancy: 88.9
    lower_respiratory_infections:
        incidence_rate: 0.871
        remission_rate: 45.1
        excess_mortality_rate: 0.634
    child_wasting:
        proportion_exposed: 0.0914
    effect_of_child_wasting_on_infected_with_lower_respiratory_infections.incidence_rate:
        relative_risk: 4.63
    sqlns:
        effect_size: 0.18

Generating a simulation from a model specification is very straightforward, as it is the primary use case.

from vivarium import InteractiveContext
p = "/path/to/disease_model.yaml"
sim = InteractiveContext(p)

In order to make it easier to follow along with this tutorial, we’ve provided a convenience function to get the path to the disease model specification distributed with vivarium.

from vivarium import InteractiveContext
from vivarium.examples.disease_model import get_model_specification_path

p = get_model_specification_path()
sim = InteractiveContext(p)

The sim object produced here is all set up and ready to run if you want to jump directly to the running the simulation section.

Without a Model Specification File - The Manual Way

It is possible to prepare a simulation by explicitly passing in the instantiated objects you wish to use rather than getting them from a model specification file. This method requires initializing all the model components and building the simulation configuration by hand. This requires a lot of boilerplate code but is frequently very useful during model development and debugging.

To demonstrate this, we will recreate the simulation from the disease_model.yaml specification without using the actual file itself.

Components

We will first instantiate the components necessary for the simulation. In this case, we will get them directly from the disease model example and we will place them in a normal Python list.

from vivarium.examples.disease_model import (BasePopulation, Mortality, Observer,
                                             SISDiseaseModel, Risk, RiskEffect,
                                             TreatmentIntervention)

components = [BasePopulation(),
              Mortality(),
              SISDiseaseModel('diarrhea'),
              Risk('child_growth_failure'),
              RiskEffect('child_growth_failure', 'infected_with_diarrhea.incidence_rate'),
              RiskEffect('child_growth_failure', 'infected_with_diarrhea.excess_mortality_rate'),
              TreatmentIntervention('breastfeeding_promotion', 'child_growth_failure.proportion_exposed'), ]

Configurations

We also need to create a dictionary for the configuration information for the simulation. Components will typically have defaults for many of these parameters, so this dictionary will contain all the parameters we want to change (or that we want to show are available to change).

config = {
   'randomness': {
       'key_columns': ['entrance_time', 'age'],
   },
   'population': {
       'population_size': 10_000,
   },
   'diarrhea': {
       'incidence_rate': 2.5,        # Approximately 2.5 cases per person per year.
       'remission_rate': 42,         # Approximately 6 day median recovery time
       'excess_mortality_rate': 12,  # Approximately 22 % of cases result in death
   },
   'child_growth_failure': {
       'proportion_exposed': 0.5,
   },
   'effect_of_child_growth_failure_on_infected_with_diarrhea.incidence_rate': {
       'relative_risk': 5,
   },
   'effect_of_child_growth_failure_on_infected_with_diarrhea.excess_mortality_rate': {
       'relative_risk': 5,
   },
   'breastfeeding_promotion': {
       'effect_size': 0.5,
   },
}

Setting up

With our components and configuration in hand, we can then set up the simulation in a very similar manner as before.

from vivarium import InteractiveContext
sim = InteractiveContext(components=components, configuration=config)

Typically when you’re working this way, you’re not trying to load in and parameterize so many components, so it’s usually not this bad. You typically only want to do this if you’re building a new simulation from scratch.

With this final step, you can proceed directly to running the simulation, or stick around to see one last way to set up the simulation in an interactive setting.

Modifying an Existing Simulation

Another frequent use case is when you’re trying to modify an already existing simulation.

Changing configuration

Here you’ll want to grab a prebuilt simulation before the setup phase so you can add extra components or modify the configuration data. You then have to call setup on the simulation yourself.

To do this we’ll set the setup flag in the InteractiveContext to False.

from vivarium import InteractiveContext
from vivarium.examples.disease_model import get_model_specification_path

p = get_model_specification_path()
sim = InteractiveContext(p, setup=False)

This function returns a simulation object that has not been setup yet so we can alter the configuration programmatically, if we wish. Let’s alter the population size to be smaller so the simulation takes less time to run.

# Setting attributes ensures you are updating existing keys, rather than
# creating new ones
sim.configuration.population.population_size = 1_000
# .update can be a more concise alternative -- but note that this will not
# warn you if you are adding new keys, e.g. due to a typo!
# sim.configuration.update({'population': {'population_size': 1_000}})

We then need to call the vivarium.framework.engine.SimulationContext.setup() method on the simulation context to prepare it to run.

sim.setup()

After this step, we are ready to run the simulation.

Note

While this is a kind of trivial example, this last use case is extremely important. Practically speaking, the utility of initializing the simulation without setting it up is that it allows you to alter the configuration data and components in the simulation before it is run or examined. This is frequently useful for setting up specific configurations for validating the simulation from a notebook or for reproducing a particular set of configuration parameters that produce unexpected outputs.

from vivarium import InteractiveContext
from vivarium.examples.disease_model import get_model_specification_path

p = get_model_specification_path()
sim = InteractiveContext(p, setup=False)
sim.configuration.population.population_size = 1_000
sim.setup()

Adding additional components

Another use case for creating the InteractiveContext in its pre-setup state is to extend existing models.

For example, say we wanted to add another risk for unsafe water sources into our disease model. We could do the following.

from vivarium import InteractiveContext
from vivarium.examples.disease_model import get_model_specification_path, Risk, RiskEffect

p = get_model_specification_path()
sim = InteractiveContext(p, setup=False)

sim.add_components([Risk('unsafe_water_source'),
                    RiskEffect('unsafe_water_source', 'infected_with_lower_respiratory_infections.incidence_rate')])

sim.configuration.update({
    'unsafe_water_source': {
        'proportion_exposed': 0.3
    },
    'effect_of_unsafe_water_source_on_infected_with_lower_respiratory_infections.incidence_rate': {
        'relative_risk': 8,
    },
})

sim.setup()

This is an easy way to take an old model and toy around with new components to immediately see their effects.

Removing components

Currently, there isn’t a way to use the above approach to remove components from an existing model (without using private attributes).

Removing components should be done with care: if you remove a component without removing other components that depend on it, the simulation will break!

You can remove components by creating a model specification and editing it before creating the InteractiveContext:

from vivarium import InteractiveContext
from vivarium.framework.configuration import build_model_specification
from vivarium.examples.disease_model import get_model_specification_path

p = get_model_specification_path()
model_spec = build_model_specification(p)

# Remove all observer components
del model_spec.components['vivarium.examples.disease_model'].observer
# Remove all RiskEffect components
model_spec.components['vivarium.examples.disease_model'].risk = [
    c for c in model_spec.components['vivarium.examples.disease_model'].risk
    if 'RiskEffect' not in c
]
# This approach can also be used to change configuration
model_spec.configuration.time.start.year = 2021
sim = InteractiveContext(model_spec)

Running the Simulation

A simulation can be run in several ways once it is set up. The simplest way to advance a simulation is to call sim.run() on it, which will advance it from its current time to the end time specified in the simulation configuration. If you need finer control, there are a set of methods on the context that allow you to run the simulation for specified spans of time or numbers of simulation steps. Below is a table of the functions that can be called on an InteractiveContext to advance a simulation in different ways.

InteractiveContext methods for advancing simulations

Method

Description

Run the simulation for its entire duration, from its current time
to its end time. The start time and end time are specified in the
time block of the configuration.
Advance the simulation one step. The step size is taken from the
time block of the configuration.
Advance the simulation n steps.
Advance the simulation to a specific time. This time should make
sense given the simulation’s clock type.
Advance the simulation for a duration. This duration should make
sense given the simulation’s clock type.