Welcome to the documentation for Vivarium Core!

The Vivarium Core library provides the Vivarium interface and engine for composing and simulating integrative, multiscale models.

_images/hierarchy1.png

Getting Started

Download and Installation

Getting Organized

Creating Enclosing Directory

Create a vivarium_work folder anywhere you like. But for installing some third-party software, everything we do will occur inside this folder.

Setting PYTHONPATH

Vivarium Core needs the root of the repository to be in your PYTHONPATH environment variable so that Python can find Vivarium Core. To make this easy to set, we suggest adding this line to your shell startup file:

alias pycd='export PYTHONPATH="$PWD"'

Now when you are about to work on Vivarium Core, navigate to the root of the Vivarium Core repository (vivarium_work/vivarium-template) and run pycd in your terminal. You will need to do this for each terminal window you use.

Installing Dependencies

Below, we list the dependencies Vivarium Core requires, how to check whether you have them, how to install them, and in some cases, how to set them up for Vivarium Core. Make sure you have each of them installed.

Python 3

Vivarium Core requires Python 3.8, 3.9, or 3.10.

Check Installation

$ python --version
Python <version>

Make sure you see a version starting with 3.8, 3.9, or 3.10.

Install

Download the latest installer from the Python download page

MongoDB

We use a MongoDB database to store the data collected from running simulations. This can be a remote server, but for this guide we will run a MongoDB server locally.

Note: MongoDB is only required if you want to store data in MongoDB or want to run experiments that do so. You don’t need MongoDB to work through this guide.

Check Installation

$ mongod --version
db version v4.2.3
...

Make sure you see a version at least 3.2.

Install

If you are on macOS, you can install MongoDB using Homebrew. You will need to add the MongoDB tap following the instructions here.

If you are on Linux, see the MongoDB documentation’s instructions.

Setup

You can get a MongoDB server up and running locally any number of ways. Here is one:

  1. Create a folder vivarium_work/mongodb. This is where MongoDB will store the database We store the database here instead of at the default location in /usr/local/var/mongodb to avoid permissions issues if you are not running as an administrator.

  2. Make a copy of the mongod configuration file so we can make changes:

    $ cp /usr/local/etc/mongod.conf vivarium_work/mongod.conf
    

    Note that your configuration file may be somewhere slightly different. Check the MongoDB documentation for your system.

  3. In vivarium_work/mongod.conf change the path after dbPath: to point to vivarium_work/mongodb.

  4. Create a shell script vivarium_work/mongo.sh with the following content:

    #!/bin/bash
    
    mongod --config mongod.conf
    
  5. Make the script executable:

    $ chmod 700 vivarium_work/mongo.sh
    

    Now you can launch MongoDB by running this script:

    $ vivarium_work/mongo.sh
    

Download and Setup Template Project

Download the Code

The template code is available on GitHub. Move into your vivarium_work directory and clone the repository to download the code

$ cd vivarium_work
$ git clone https://github.com/vivarium-collective/vivarium-template.git

This will create a vivarium-template folder inside vivarium_work. All the code for your model will be inside this vivarium-template folder.

Repository Structure

The repository you downloaded should look like this:

.
├── README.md
├── pytest.ini
├── release.sh
├── requirements.txt
├── setup.py
└── template
    ├── __init__.py
    ├── compartments
    ├── composites
    │   ├── __init__.py
    │   └── injected_glc_phosphorylation.py
    ├── experiments
    │   ├── __init__.py
    │   └── glucose_phosphorylation.py
    ├── library
    │   └── __init__.py
    └── processes
        ├── __init__.py
        ├── glucose_phosphorylation.py
        └── template_process.py

We suggest you use the structure laid out here, but you don’t have to. The template repository has TODO notes where you’ll need to make changes. Before you publish your project, make sure you have removed all the TODO notes!

The template directory is where your package will live. Under it, we have the following sub-folders:

  • library: This is for utility functions like those shared across processes, composers, and/or experiments.

  • processes: This is where you’ll write your processes. We’ve provided a template_process.py file to get you started. Generally you’ll want to have one process per file.

  • composites: This folder will hold your composers, which generate composites of multiple processes.

  • experiments: This folder will hold your experiments. These are the files you’ll probably be executing to run your simulations.

To show how you can build models with Vivarium, we’ve included some examples around modeling glucose phosphorylation.

Installing Python Packages

Above we installed all the non-Python dependencies, but we still have to install the Python packages Vivarium Core uses.

  1. Move into the vivarium-template folder created when you cloned the repository.

  2. (optional) Create and activate a virtual environment using venv or pyenv virtualenv, e.g.:

    $ python3 -m venv venv --prompt "vivarium-template"
    ...
    $ source venv/bin/activate
    
  3. Install packages

    $ pip install -r requirements.txt
    

Now you are all set to create models and run simulations with Vivarium Core!

Run Simulations

Some Terminology: Processes and Composites

We break our cell models into processes. Each process models part of the cell’s function. For example, you might have processes for metabolism, transcription, and translation. We can combine these processes into composites that model a system with all the functionality modeled by the included processes. For example, we could compose transcription and translation to create a fuller gene expression model.

We store individual processes in vivarium-template/template/processes and the composers that generate composites of processes in vivarium-template/template/composites. We recommend you use a similar structure when creating your own processes and composers.

Running Experiments

Running experiments is as easy as executing their files. For example, this repository comes with an example experiment in vivarium-template/template/experiments/glucose_phosphorylation.py. Try running it like this:

$ python template/experiments/glucose_phosphorylation.py

In out/experiments/glucose_phosphorylation you should see a file simulation.png that looks like this:

Two columns of plots. The first has one plot of mass increasing linearly. The second has 4 plots, the first 3 of which show ADP, ATP, and G6P increasing linearly. The last plot shows GLC decreasing linearly.

Run Tests

We strongly encourage you to write tests for your code. It will make development much easier for you. The template repository comes with some tests already. To run them, just execute pytest.

Getting Started for Developers

Note

This guide is for developers who want to contribute to Vivarium Core. If you just want to use Vivarium to build models, see the getting started guide.

Download and Installation

This guide assumes that you have access to an administrator or sudoer account on a macOS or Linux system.

Getting Organized

Creating Enclosing Directory

Create a vivarium_work folder anywhere you like. But for installing some third-party software, everything we do will occur inside this folder.

Setting PYTHONPATH

Vivarium Core needs the root of the repository to be in your PYTHONPATH environment variable so that Python can find Vivarium Core. To make this easy to set, we suggest adding this line to your shell startup file:

alias pycd='export PYTHONPATH="$PWD"'

Now when you are about to work on Vivarium Core, navigate to the root of the Vivarium Core repository (vivarium_work/vivarium-core) and run pycd in your terminal. You will need to do this for each terminal window you use.

Installing Dependencies

Below, we list the dependencies Vivarium Core requires, how to check whether you have them, how to install them, and in some cases, how to set them up for Vivarium Core. Make sure you have each of them installed.

Python 3

Vivarium Core requires Python 3.8, 3.9, or 3.10.

Check Installation

$ python --version
Python <version>

Make sure you see a version beginning with 3.8, 3.9, or 3.10.

Install

Download the latest installer from the Python download page

MongoDB

We use a MongoDB database to store the data collected from running simulations. This can be a remote server, but for this guide we will run a MongoDB server locally.

Note: MongoDB is only required if you want to store data in MongoDB or want to run experiments that do so. You don’t need MongoDB to work through this guide.

Check Installation

$ mongod --version
db version v4.2.3
...

Make sure you see a version at least 4.2.

Install

If you are on macOS, you can install MongoDB using Homebrew. You will need to add the MongoDB tap following the instructions here.

If you are on Linux, see the MongoDB documentation’s instructions.

Setup

You can get a MongoDB server up and running locally any number of ways. Here is one:

  1. Create a folder vivarium_work/mongodb. This is where MongoDB will store the database We store the database here instead of at the default location in /usr/local/var/mongodb to avoid permissions issues if you are not running as an administrator.

  2. Make a copy of the mongod configuration file so we can make changes:

    $ cp /usr/local/etc/mongod.conf vivarium_work/mongod.conf
    

    Note that your configuration file may be somewhere slightly different. Check the MongoDB documentation for your system.

  3. In vivarium_work/mongod.conf change the path after dbPath: to point to vivarium_work/mongodb.

  4. Create a shell script vivarium_work/mongo.sh with the following content:

    #!/bin/bash
    
    mongod --config mongodb.conf
    
  5. Make the script executable:

    $ chmod 700 vivarium_work/mongo.sh
    

    Now you can launch MongoDB by running this script:

    $ vivarium_work/mongo.sh
    

Download and Setup Vivarium Core

Download the Code

Note: These instructions give you the latest development version of Vivarium Core. If you want to use the most recent release, which is more stable, you can instead run pip install vivarium-core in the Installing Python Packages section below.

The Vivarium Core code is available on GitHub. Move into your vivarium_work directory and clone the repository to download the code

$ cd vivarium_work
$ git clone https://github.com/vivarium-collective/vivarium-core.git

This will create a vivarium-core folder inside vivarium_work. All the code for Vivarium Core is inside this vivarium-core folder.

Installing Python Packages

Above we installed all the non-Python dependencies, but we still have to install the Python packages Vivarium Core uses.

  1. Move into the vivarium-core folder created when you cloned the repository.

  2. (optional) Create and activate a virtual environment using venv or pyenv virtualenv, e.g.:

    $ python3 -m venv venv --prompt "vivarium-core"
    ...
    $ source venv/bin/activate
    
  3. Install packages

    $ pip install -r requirements.txt
    

Now you are all set to begin developing! Be sure to review our contribution instructions before you get started.

Topic Guides

Each of these guides dives into the technical details of an important concept in Vivarium. If you’re looking to use Vivarium, you should work through the getting started guide first.

Processes

You should interpret words and phrases that appear fully capitalized in this document as described in RFC 2119. Here is a brief summary of the RFC:

  • “MUST” indicates absolute requirements. Vivarium may not work correctly if you don’t follow these.

  • “SHOULD” indicates strong suggestions. You might have a valid reason for deviating from them, but be careful that you understand the ramifications.

  • “MAY” indicates truly optional features that you can include or exclude as you wish.

Models in Vivarium are built by combining processes, each of which models a mechanism in the system being studied. These processes can be combined in a composite to build more complicated models. Process models are defined in classes that inherit from vivarium.core.process.Process, and these process classes can be instantiated to create individual processes. During instantiation, the process class may accept configuration options.

Note

Processes are the foundational building blocks of models in Vivarium, and they should be as simple to define and compose as possible.

Process Interface Protocol

Each process class MUST implement the application programming interface (API) that we describe below.

Class Variables

Each process class SHOULD define default configurations in a defaults class variable. The constructor SHOULD read these defaults. For example:

class MyProcess:
    defaults = {
        'growth_rate': 0.0006,
    }
Constructor

The constructor of a process class MUST accept as its first positional argument an optional dictionary of configurations. If the process class is configurable, it SHOULD accept configuration options through this dictionary.

In the constructor, the process class MUST call its superclass constructor with a dictionary of parameters.

Passing Parameters to Superclass Constructor

The dictionary of parameters SHOULD include any configuration options not used by the process class. Any information needed by the process class MAY also be included in these parameters. Once the object has been instantiated, these parameters are available as self.parameters, where they have been stored by the vivarium.core.process.Process constructor.

Example Constructor

Let’s examine an example constructor from a growth process class.

def __init__(self, initial_parameters=None):
    if initial_parameters == None:
        initial_parameters = {}
    parameters = {'growth_rate': self.defaults['growth_rate']}
    parameters.update(initial_parameters)
    super().__init__(parameters)

Note that Vivarium Core actually handles combining the provided parameters with the default parameters, so a constructor as simple as the one above can actually be dropped. The superclass constructor makes it redundant, but we show it here for clarity.

Warning

Python creates only one instance of both class variables and function argument defaults. This means that you MUST not change the default parameters object. Make a copy instead. This also means that you SHOULD avoid using a mutable object as a default argument. This is why we use None as the default for initial_parameters instead of {}.

While the default growth rate is 0.0006, this can be overridden by including a growth_rate key in the configuration dictionary passed to initial_parameters.

These special parameters get handled by the superclass constructor:

  • name: The value of the name parameter gets assigned to the process’s name attribute (e.g. my_process.name). If no name is specified in the parameters or as a class variable, we use self.__class__.__name__ as the name.

  • time_step: If not specified, the time_step parameter is set to 1. This parameter determines how frequently the simulation engine runs this process’s next_update function.

  • _condition: The value of this parameter should be a path in the states dictionary passed to next_update() to a variable. The variable should hold a boolean specifying whether the process’s next_update function should run.

Ports Schema

Each process declares what stores it expects by specifying a port for each store it accepts. Note that if two processes are to be combined in a model and share variables through a shared store, the processes MUST use the same variable names for the shared variables.

The process class MUST implement a ports_schema method with no required arguments. This method MUST return nested dictionaries of the following form:

{
    'port_name': {
        'variable_name': {
            'schema_key': 'schema_value',
            ...
        },
        ...
    },
    ...
}
Schema keys

schema_key MUST be a schema key and have an appropriate value. Any applicable and omitted schema keys will take on their default values. Note that every variable SHOULD specify _default. If the cell will be dividing, every variable also MUST specify _divider. Variables in the ports schema SHOULD NOT specify _value.

Available schema keys include:

  • _default: The default value of the state variable if no initial value is provided. This also sets the data type of the variable, including units.

  • _updater: How to apply state variable updates. Available updaters are listed in below

  • _divider: How to divide the state variable’s values between daughter cells. Available dividers are listed below.

  • _emit: A Boolean value that sets whether to log this variable to the simulation database for later analysis.

  • _properties: User-defined properties such as molecular weight. These can be used for calculating variables such as total system mass.

Updaters

Updaters are methods by which an update from a process is applied to a variable’s value.

Updaters provided by vivarium-core include:

  • accumulate: The default updater. Add the update value to the current value.

  • set: The update value becomes the new current value.

  • merge: Update an existing dictionary with new values, and add any newly declared keys.

  • null: Do not apply the update.

  • nonnegative_accumulate: Add the update value to the current value, and set to 0 if the result is negative.

  • dict_value: translates _add and _delete -style updates to operations on a dictionary.

New updaters can be easily defined and passed into a port schema:

# updater that returns a random value
def random_updater(current_value, update_value):
    return random.random()

def port_schema(self):
    ports = {
        'port1': {
            'variable1': {
                '_default': 1.0
                '_updater': {
                    'updater': random_updater
                    }
            }
        }
    }
    return ports
Dividers

Dividers are methods by which a variable’s value is divided when division is triggered.

Dividers available in vivarium-core include:

  • set: The default divider. Daughters get the same value as the mother.

  • binomial: Sample the first daughter’s value from a binomial distribution of the mother’s value, and the second daughter gets the remainder.

  • split: Divide the mother’s value in two. Odd integers will make one daughter receive 1 more than the other daughter.

  • split_dict: Splits a dictionary of {key: value} pairs, with each daughter receiving a dictionary with the same keys, but with each value split.

  • zero: Daughter values are both set to 0.

  • no_divide: Asserts that this value should not be divided.

New dividers can be easily defined and passed into a port schema:

# divider that returns a random value for each daughter
def random_divider(mother_value, state):
    return [
        random.random(),
        random.random()]

def port_schema(self):
    ports = {
        'port1': {
            'variable1': {
                '_default': 1.0
                '_divider': {
                    'divider': random_divider
                    }
            }
        }
    }
    return ports
Example Ports Schema
def ports_schema(self):
    return {
        'global': {
            'mass': {
                '_emit': True,
                '_default': 1339 * units.fg,
                '_updater': 'set',
                '_divider': 'split'},
            'volume': {
                '_updater': 'set',
                '_divider': 'split'},
            'divide': {
                '_default': False,
                '_updater': 'set'
            }
        }
    }

Here we specify that only mass should be emitted. We assign a default value of 1339 fg to mass, and we declare that the mass and volume variables should be split in half on division. Further, we specify that all the three variables should have their updates set, not accumulated.

Views

When the process is asked to provide an update to the model state, it is only provided the variables it specifies. For example, it might get a model state like this:

{
    'global': {
        'mass': 1339 <Unit('femtogram')>,
        'volume': 1.2,
        'divide': False,
    },
}

This would happen even if the store linked to the global port contained more variables. We call this stripping-out of variables the process doesn’t need masking.

Advanced Ports Schema

Use the glob * schema to declare expected sub-store structure, and view all child values of the store:

schema = {
    'port1': {
        '*': {
            '_default': 1.0
        }
    }
}

Use the glob ** schema to connect to an entire sub-branch, including child nodes, grandchild nodes, etc:

schema = {
    'port1': '**'
}

Ports flagged as output-only won’t be viewed through the next_update’s states, which can save some overhead time:

schema = {
    'port1': {
        '_output': True,
        'A': {'_default': 1.0},
    }
}
Next Updates

Each process class MUST implement a next_update method that accepts two positional arguments: the timestep and the current state of the model. The timestep describes, in units of seconds, the length of time for which the update should be computed.

State Format

The next_update method MUST accept the simulation state as a dictionary of the same form as the ports schema dictionary, but with the dictionary of schema keys replaced with the current (i.e. pre-update) value of the variable.

Note

In the code, you may see the simulation state referred to as states. This is left over from when stores were called states, and so the simulation state was a collection of these states. As you may already notice, this naming was confusing, which is why we now use the name “stores.”

Because of masking, each port will contain only the variables specified in the ports schema, even if the linked store contains more variables.

Warning

The next_update method MUST NOT modify the states it is passed in any way. The state’s variables are not copied before they are passed to next_update, so changes to any objects in the state will affect the simulation state before the update is applied.

Update Format

next_update MUST return a single dictionary, the update that describes how the modeled mechanism would change the simulation state over the specified time. The update dictionary MUST be of the same form as the ports schema dictionary, though with the dictionaries of schema keys replaced with update values. Also, variables that do not need to be updated can be excluded.

Example Next Update Method

Here is an example next_update method for our growth process:

def next_update(self, timestep, states):
    mass = states['global']['mass']
    new_mass = mass * np.exp(self.parameters['growth_rate'] * timestep)
    return {'global': {'mass': new_mass}}

Recall from our example schema that we use the set updater for the mass variable. Thus, we compute the new mass of the cell and include it in our update. Notice that we access the growth rate specified in the constructor by using the self.parameters attribute.

Note

Notice that this function works regardless of what timestep we use. This is important because different simulations may need different timesteps based on what they are modeling.

Process Class Examples

Many of our process classes have examples in the form of test functions at the bottom. These are great resources if you are trying to figure out how to use a process.

If you are writing your own process, please include these examples! Also, executing the process class Python file should execute one of these examples and save the output as demonstrated in vivarium.processes.glucose_phosphorylation. Lastly, any top-level functions you include that are prefixed with test_ will be executed by pytest. Please add these tests to help future developers make sure they haven’t broken your process!

Steps

Processes have one major drawback: you cannot specify when or in what order they run. Processes can request timesteps, but the Vivarium engine may not honor that request. This behavior can be problematic when you have operations that need to run in a particular order. For example, imagine that you want to model transcription and chromosome replication in a bacterium. It seems natural to have a transcription process and another replication process, but then how do you handle collisions between the replisome and the RNA Polymerase (RNAP)? You might want to say something like “If a replisome and RNAP collide, remove the RNAP from the chromosome.” To support this kind of statement, you can create a step.

vivarium.core.process.Step is a subclass of vivarium.core.process.Process that is not time-dependent. Steps run before the first timestep and after the dynamic processes during simulation. They run according to a dependency graph called a flow (like a workflow) – see our guide to flows. These can serve many different roles, including translating states between different modeling formats, implementing lift or restriction operators to translate states between scales, and as auxiliary processes that offload complexity. As an example of offloading complexity, a step might recalculate concentrations after counts have been updated.

To create a step, you follow the same steps as you would to create a process except that your class should inherit from vivarium.core.process.Step. For example, we could create a replisome-RNAP collision reconciler like this:

class CollisionReconciler(Step):

    def ports_schema(self):
        return {
            'replisomes': {
                '*': {
                    'position': {'_default': 0},
                },
            },
            'RNAPs': {
                '*': {
                    'position': {'_default': 0},
                },
            },
        }

    def next_update(self, timestep, states):
        # We can ignore the timestep since it will always be 0.
        replisome_positions
            replisome['position']
            for replisome in states['replisomes'].values()
        ])
        rnap_positions = np.array([
            rnap['position']
            for rnap in states['RNAPs'].values()
        ])
        # Assume that our timestep is small enough that we can
        # ignore RNAPs and replisomes that move past each other
        # (instead of to the same position) in one timestep.
        collision_mask = replisome_positions == rnap_positions
        rnap_keys = np.array(list(states['RNAPs'].keys()))
        to_remove = rnap_keys[collision_mask]
        return {
            'RNAPs': {
                '_delete': to_remove.tolist(),
            },
        }

Note

Steps are always given a timestep of 0 by the simulation engine.

Step Implementation Details

Steps are technically identified by whether their vivarium.core.process.Process.is_step() methods return True. This means that you can make a process that determines whether it should be a Step based on its configuration. Note however that we do not support changing whether a process is a step mid-simulation.

Advanced Features

Adaptive Timesteps

You can set process timesteps for the duration of a simulation using the time_step parameter, but you can also override the vivarium.core.process.Process.calculate_timestep() method to compute timesteps dynamically based on the same view into the simulation state that next_update() sees.

Conditional Updates

Sometimes you might want the simulation engine to skip a process when generating updates. You can implement this by overriding vivarium.core.process.Process.update_condition() to return False whenever you don’t want the process to run. This method takes as a parameter the same view into the simulation state that next_update() sees.

Using Process Objects

Your use of process objects will likely be limited to instantiating them and passing them to other functions in Vivarium that handle running the simulation. Still, you may find that in some instances, using process objects directly is helpful. For example, for simple processes, the clearest way to write a test may be to run your own simulation loop.

Simulating a process can be sketched by the following pseudocode:

# Create the process
configuration = {...}
process = ProcessClass(configuration)

# Get the initial state from the process's schema
# This means the stores and ports are the same
state = {}
schema = process.ports_schema()
for port, port_dict in schema.items():
    for variable, variable_schema in port_dict.items():
        state[port][variable] = variable_schema["_default"]

# Run the simulation in a loop for 10 seconds
time = 0
while time < 10:
    # We are using a timestep of 1 second
    update = process.next_update(1, state)
    # This is a simplified way to apply the update that assumes all
    # all variables are numbers and all updaters are "accumulate"
    for port in update:
        for variable_name, value in port.items():
            state[port][variable_name] += value
# Now that the loop is finished, the predicted state after 10
# seconds is in "state"

The above pseudocode is simplified, and for all but the most simple processes you will be better off using Vivarium’s built-in simulation capabilities. We hope though that this helps you understand how processes are simulated and the purpose of the API we defined.

Parallel Processing

Process Commands

When a process is run in parallel, we can’t interact with it in the normal Python way. Instead, we can only exchange messages with it through a pipe. Vivarium structures these exchanges using process commands.

Vivarium provides some built-in commands, which are documented in vivarium.core.process.Process.send_command(). Also see that method’s documentation for instructions on how to add support for your own commands.

Process commands are designed to be used asynchronously, so to retrieve the result of running a command, you need to call vivarium.core.process.Process.get_command_result(). As a convenience, you can also call vivarium.core.process.Process.run_command() to send a command and get its result as a return value in one function call.

Running Processes in Parallel

In normal situations though, you shouldn’t have to worry about process commands. Instead, just pass '_parallel': True in a process’s configuration dictionary, and the Vivarium Engine will handle the parallelization for you. Just remember that parallelization requires that processes be serialized and deserialized at the start of the simulation, and this serialization only preserves the process parameters. This means that if you instantiate a process and then change its instance variables, those changes won’t be preserved when the process gets parallelized.

Composites

Once you start building models with Vivarium, you will probably discover groups of processes that you want to reuse. For example, if you have a group of processes that model a cell, you might want to create many instances of those processes to a collection of cells. In Vivarium, we support grouping processes using composites.

Note

The terminology here can be tricky because “process” can refer both to the process object and to the process class. However, for composites, the analogous ideas have different names: “composer” and “composite.” You can use a composer object to generate different composite objects depending on what configuration you provide. Therefore, composers are like process classes while composites are like process objects.

Processes and Stores

A model in Vivarium consists of state variables, which are grouped into collections called stores, and processes which mutate those variables at each timestep. A topology defines which processes operate on which stores.

For example, consider a variable tracking ATP concentrations. We might assign a “cytoplasm” store to two processes: one for metabolism and one for sodium pumps. Now when we run a simulation the metabolism and sodium pump processes will be changing the same variable, the ATP concentration in the cytoplasm store. This means that if the rate of metabolism decreases, the cytoplasmic ATP concentration variable will drop, so the sodium pump will export less sodium. Thus, shared stores in composites let us simulate interacting concurrent processes.

Process and Store Implementation
Processes

We write processes as classes that inherit from vivarium.core.process.Process. To create a composite, we create instances of these classes to make the processes we want to compose. If a process is configurable, we might provide a dictionary of configuration options. For information on configuring a process, see the process’s documentation, e.g. vivarium.processes.tree_mass.TreeMass.

We uniquely name each process in the composite. This lets us include instances of the same process class.

Stores

We represent stores with the vivarium.core.store.Store class; see its documentation for further details. Note that when constructing a composite, you don’t need to create the stores. Instead, the Vivarium engine automatically creates the stores based on the topology you specify.

Tip

To see the data held by a store, you can use the vivarium.core.store.Store.get_config() function. This returns a dictionary representation of the store’s data. To show this dictionary more readably, use vivarium.library.pretty.format_dict().

Topologies

Each process has a list of named ports, one for each store it expects. The process can perform all its computations in terms of these ports, and the process also provides its update using port names. This means that a composite can apply each process to any collection of stores, making processes modular.

This modularity is analogous to the modularity of Python functions. Think of each process as a function like this:

def sodium_pump(cytoplasm, extracellularSpace):
    ...
    return "Update: Decrease ATP concentration in cytoplasm by x mM"

A function’s modularity comes from the fact that we can pass in different objects for the cytoplasm parameter, even objects the function authors hadn’t thought of. cytoplasm is like the port, to which we can provide any store we like.

How do we specify which store goes with which port? To continue the function analogy from above, we need something analogous to this:

cell = Cell()
bloodVessel = BloodVessel()
# We need something like the line below
update = sodium_pump(cytoplasm=cell, extracellularSpace=bloodVessel)

When we call sodium_pump, we specify which objects go with which parameters. Analogously, we specify the mapping between ports and stores using a topology.

Defining Topologies

We define topologies as dictionaries with process names as keys and dictionaries (termed “sub-dictionaries”) as values. These sub-dictionaries have port names as keys and paths to stores as values. For example, the topology for the ATP example we have been considering might look like this:

{
    'sodium_pump': {
        'cytoplasm': ('cell',),
        'extracellularSpace': ('bloodVessel',),
    },
    'metabolism': {
        'cytoplasm': ('cell',),
    },
}
Advanced Topologies

The syntax used for declaring paths is a Unix-style tuple, with every element in the tuple going further down the path from the root compartment, and .. moving up a level to an outer compartment.

topology = {
    'process': {
        'port1': ('path','to','store'),  # connect port1 to inner compartment
        'port2': ('..','outer_store')  # connect port2 to outer compartment
    }
}

You can splitting a port into multiple stores. Variables read through the same port can come from different stores. To do this, the port is mapped to a dictionary with a _path key that specifies the path to the default store. Variables that need to be read from different stores each get their own path in that same dictionary. This same approach can be used to remap variable names, so different processes can use the same variable but see it with different names.

topology = {
    # split a port into multiple stores
    'process1': {
        'port': {
            '_path': ('path_to','default_store'),
            'rewired_variable': ('path_to','alternate_store')
        }
    }
    # mapping variable names in process to different name in store
    'process2': {
        'port': {
            '_path': ('path to','default_store'),
            'variable_name': 'new_variable_name'
        }
    }
}

Flows for Ordered Step Operations

When constructing a composite of many steps, you may find that some steps depend on other steps. For example, you might have one step that calculates the cell’s mass and another step that calculates the cell’s volume based on that mass. Vivarium supports these dependencies, which you can specify in a flow. Flows have the same structure as topologies, but instead of their leaf values being paths, they are lists of paths where each path specifies a dependency step. For example, this flow would represent our mass-volume dependency:

{
    'mass_calculator': [],
    'volume_calculator': [('mass_calculator',)],
}

The simulation engine will automatically figure out what order to run the steps in such that the dependencies in the flow are respected. Note that if two orderings both respect the flow, you should not assume that the engine will pick one of the two orderings.

Note

Step updates are applied immediately after the step executes, which is unlike process updates.

Composers

Most of the time, you won’t need to create composites directly. Instead, you’ll create composers that know how to generate composites. To create a composer, you need to define a composer class that inherits from vivarium.core.composer.Composer and implements the vivarium.core.composer.Composer.generate_processes() and vivarium.core.composer.Composer.generate_topology() methods. generate_processes should return a mapping from process names to instantiated process objects, while generate_topology should return a topology.

Example Composer

To put all this information together, let’s take a look at an example composer that combines the glucose phosphorylation process from the process-writing tutorial with an injector, which lets us “inject” molecules into a store.

class InjectedGlcPhosphorylation(Composer):

        defaults = {
                'glucose_phosphorylation': {
                        'k_cat': 1e-2,
                },
                'injector': {
                        'substrate_rate_map': {
                                'GLC': 1e-4,
                                'ATP': 1e-3,
                        },
                },
        }

        def generate_processes(self, config):
                injector = Injector(self.config['injector'])
                glucose_phosphorylation = GlucosePhosphorylation(
                        self.config['glucose_phosphorylation'])

                return {
                        'injector': injector,
                        'glucose_phosphorylation': glucose_phosphorylation,
                }

        def generate_topology(self, config):
                return {
                        'injector': {
                                'internal': ('internal', ),
                        },
                        'glucose_phosphorylation': {
                                'cytoplasm': ('cell', ),
                                'nucleoside_phosphates': ('cell', ),
                                'global': ('global', ),
                        },
                }

Notice how we use the generate_processes function to create a dictionary that maps process names to instantiated and configured process objects. Similarly, we use generate_topology to create a dictionary that maps port names to stores. To create steps and flows, use the generate_steps and generate_flow methods.

You may wonder why we identify stores with tuples. In more complex compartments, these tuples could contain many elements that specify a kind of file path. We represent the total model state as a tree, and we can create a store at any node to represent the sub-tree rooted at that node. This tree is analogous to directory trees on a filesystem, and we use tuples of store names to specify a path through this tree. We call this tree the hierarchy, and we discuss it in more detail in the hierarchy guide.

Experiments

Once you have created a model using processes and compartments, you might want to run simulations of your model and save simulation parameters so you and others can reproduce your results. Vivarium uses experiments to define these simulations, which can contain arbitrarily nested compartments. For example, you could create an experiment with an environment compartment that contains several cell compartments. Then you could run the experiment to see how these cells might interact in a shared environment.

Defining Experiments

To create an experiment, you need only instantiate the vivarium.core.engine.Engine class. To help others reproduce your experiment, create a file in the vivarium-core/experiments directory that defines a function that generates your experiment. For example, here is the function from vivarium.experiments.glucose_phosphorylation to create a toy experiment that simulates the phosphorylation of injected glucose:

def glucose_phosphorylation_experiment(config=None):
    if config is None:
        config = {}
    default_config = {
        'injected_glc_phosphorylation': {},
        'emitter': {
            'type': 'timeseries',
        },
        'initial_state': {},
    }
    default_config.update(config)
    config = default_config
    compartment = InjectedGlcPhosphorylation(
        config['injected_glc_phosphorylation'])
    compartment_dict = compartment.generate()
    experiment = Engine(
        processes=compartment_dict['processes'],
        topology=compartment_dict['topology'],
        emitter=config['emitter'],
        initial_state=config['initial_state'],
    )
    return experiment

Notice that most of the function just sets up configurations. The main steps are:

  1. Instantiate the composite that your experiment will simulate.

  2. Generate the processes and topology dictionaries that describe the composite using vivarium.core.composer.Composer.generate().

  3. Instantiate the experiment, passing along the processes and topology dictionaries. We also specify the emitter the experiment should send data to and the initial state of the model. If we don’t specify an initial state, it will be constructed based on the defaults we specified in the composite’s processes.

Emitters

We provide a variety of emitter classes to save simulation output, which can be specified with arguments to engine. vivarium.core.emitter.RAMEmitter saves the simulation output to RAM – this is useful for small simulations that you might not need to return to. vivarium.core.emitter.DatabaseEmitter saves simulation output to MongoDB. vivarium.core.emitter.NullEmitter is an easy way to ignore all emits. Other emitters can be developed by subclassing the vivarium.core.emitter.Emitter class.

Data Representations

We represent simulation data in three ways:

  • Raw Data

  • Embedded Timeseries, which are sometimes called just “timeseries”

  • Path Timeseries

We describe each in more detail below.

Raw Data

The raw data is a dictionary with times as keys and the simulation state at that time as a dictionary. For example:

{
    0: {
        'agents': {
            'agent1': {
                'boundary': {
                    'volume': 20,
                },
            },
            'agent2': {
                'boundary': {
                    'volume': 10,
                },
            },
        },
    },
    1: {
        'agents': {
            'agent1': {
                'boundary': {
                    'volume': 30,
                },
            },
            'agent2': {
                'boundary': {
                    'volume': 20,
                },
            },
        },
    },
}

You can get this data from an emitter using its vivarium.core.emitter.Emitter.get_data() method. We recommend keeping the main copy of your data in this form throughout your code because it can be transformed into either of the other two forms using the functions:

Embedded Timeseries

Note

Embedded timeseries are sometimes called just “timeseries.”

An embedded timeseries is a dictionary with the same form as the simulation state dictionary, only with an additional time key. Each variable in the dictionary is a key nested arbitrarily deep within the state dictionary. Each of these keys has as its value a list of the variable’s values at each time in the list of timepoints associated with the time key. For example:

{
    'agents': {
        'agent1': {
            'boundary': {
                'volume': [20, 30],
            },
        },
        'agent2': {
            'boundary': {
                'volume': [10, 20],
            },
        },
    },
    'time': [0, 1],
}

You can get data in this format from an emitter using its vivarium.core.emitter.Emitter.get_timeseries() function.

Path Timeseries

A path timeseries is a flattened form of an embedded timeseries. We take each variable and its list of timepoints from an embedded timeseries and make each its own entry in the dictionary. The keys are tuples specifying the paths to each variable, and the values are the lists of timepoints. Like in embedded timeseries, we also have a time key with the time values for each timepoint. For example:

{
    ('agents', 'agent1', 'boundary', 'volume'): [20, 30],
    ('agents', 'agent2', 'boundary', 'volume'): [10, 20],
    'time': [0, 1],
}

Hierarchy

In Vivarium, we represent the simulation as a tree of processes and stores (panel A). The processes and stores are wired together by topologies to form compartments (panel B), which can then be nested to form a tree called the hierarchy (panel C).

A figure with 3 panels lettered A through C. In panel A, we see a red database symbol labeled "store" and with the text "variable values, units, mass, children, emitters, dividers, updaters" within it. Below, a yellow rectangle labeled "process" contains the text "variable names, parameters, mechanisms." A black line extending from the rectangle is labeled "port". In panel B, we see a blue square labeled "compartment". Inside are two stores and two processes, with the lower store connected to the ports of both processes, and the upper store connected only to the top process. A store outside the square labeled "boundary" is connected to a port of the upper process. In panel C, 4 compartments form a tree with one compartment at the top level and one at the bottom level. The tree's edges are formed by black lines to boundary stores.

The relationships between stores, processes (panel A), and compartments (panel B) in the hierarchy (panel C).

Note that in panel C, only the compartments and boundary stores are shown. The full hierarchy also contains the stores and processes within each compartment.

Note

Here we have shown a simplified picture where processes are only wired to stores in their own compartment or to boundary stores. In reality, processes can be wired to any store in the hierarchy, but keeping cross-compartment wiring to a minimum can help simplify your models.

We recommend using Vivarium compartments to represent spatial regions of your modeled system that are conceptually distinct. For example, you might model a cell as a compartment and its environment as another compartment. This is not a technical requirement though, so you can use compartments to represent whatever makes sense for your circumstances.

Compartment Interactions

We model cross-compartment interactions using boundary stores between compartments. For example, the boundary store between a cell and its environment might track the flux of metabolites between the cell and environment compartments.

When compartments are nested, these boundary stores also exist between the inner and the outer compartment. Thus nested compartments form a tree whose nodes are compartments and whose edges are boundary stores. A node’s parent is its outer compartment, while its children are the compartments within it.

Since boundary stores can also exist between compartments who share a parent, you may find it useful to think of compartments and their boundary stores as a bigraph (not a bipartite graph) where the tree denotes nesting and all the edges (including those in the tree) represent boundary stores.

Hierarchy Structure

In the example below, we print out the full hierarchy as a dictionary.

>>> from vivarium.experiments.glucose_phosphorylation import (
...     glucose_phosphorylation_experiment,
... )
>>> from vivarium.core.engine import Engine
>>> from vivarium.core.composer import Composer
>>> from vivarium.library.pretty import format_dict
>>>
>>>
>>> experiment = glucose_phosphorylation_experiment()
>>> print(format_dict(experiment.state.get_config()))
{
    "cell": {
        "ADP": {
            "_default": 0.0,
            "_emit": true,
            "_updater": "<function update_accumulate>",
            "_value": 0.0
        },
        "ATP": {
            "_default": 2.0,
            "_emit": true,
            "_updater": "<function update_accumulate>",
            "_value": 2.0
        },
        "G6P": {
            "_default": 0.0,
            "_emit": true,
            "_properties": {
                "mw": "1.0 gram / mole"
            },
            "_updater": "<function update_accumulate>",
            "_value": 0.0
        },
        "GLC": {
            "_default": 1.0,
            "_emit": true,
            "_properties": {
                "mw": "1.0 gram / mole"
            },
            "_updater": "<function update_accumulate>",
            "_value": 1.0
        },
        "HK": {
            "_default": 0.1,
            "_properties": {
                "mw": "1.0 gram / mole"
            },
            "_updater": "<function update_accumulate>",
            "_value": 0.1
        }
    },
    "global": {
        "initial_mass": {
            "_default": "0.0 femtogram",
            "_divider": "<function divide_split>",
            "_units": "<Unit('femtogram')>",
            "_updater": "<function update_set>",
            "_value": "0.0 femtogram"
        },
        "mass": {
            "_default": null,
            "_emit": true,
            "_updater": "<function update_set>",
            "_value": "1.826592973891231e-09 femtogram"
        }
    },
    "glucose_phosphorylation": {
        "_default": null,
        "_updater": "<function update_set>",
        "_value": "<vivarium.processes.glucose_phosphorylation.GlucosePhosphorylation object>"
    },
    "injector": {
        "_default": null,
        "_updater": "<function update_set>",
        "_value": "<vivarium.processes.injector.Injector object>"
    },
    "my_deriver": {
        "_default": null,
        "_updater": "<function update_set>",
        "_value": "<vivarium.processes.tree_mass.TreeMass object>"
    }
}

We can represent this hierarchy graphically like this:

A tree with root node "root". The root has children "cell", "global", "injector", "glucose_phosphorylation", and "my_deriver". The node "cell" has children "ATP", "ADP", "HK", "GLC", and "G6P". The node "global" has children "initial_mass" and "mass".

Notice that in the dictionary above, each leaf node in the tree is a key with a value that is a dictionary of schema keys.

Hierarchy Paths

A hierarchy in Vivarium is like a directory tree on a filesystem. In line with this analogy, we specify nodes in the hierarchy with paths. Each path is a tuple of node names (variable names or store names) relative to some other node. For example, in the topology from the example above, we used the path ('cell', ) to say that the cell store maps to the injector’s internal port. This path was relative to the compartment root (root in our diagram) as is the case for all topologies. Thus the path is analogous to ./cell in a directory.

Special Symbols

Continuing our analogy between hierarchy paths and file paths, the following symbols have special meanings in hierarchy paths:

  • .. refers to a parent node. One example use for this is a division process that needs to access the parent (environment) compartment to create the daughter cells. In fact, this is what we do in the growth and division compartment.

Versioning and API Stability

We want to keep our API stable so that you can rely on it. To help clearly communicate how our API is changing, we strive to following the guidelines below when releasing new versions of Vivarium.

We follow semantic versioning, which in brief means the following:

  • Our version numbers have the form major.minor.patch (e.g. 1.2.3).

  • We increment major for breaking changes to Vivarium’s supported application programming interface (API). Upgrading to a new major release may break your code.

  • We increment minor when we release new functionality. New minor releases should be backwards-compatible with the previous version of Vivarium.

  • We increment patch for bug fixes that neither release new functionality nor introduce breaking changes.

Note that the above rules only apply to Vivarium’s supported API. If you use unsupported features, they may break with a new minor or patch release. Our supported API consists of all public, documented interfaces that are not marked as being experimental in their documentation. Note that attributes beginning with an underscore (_) are private. The following are not considered breaking API changes:

  • Adding to a function a parameter with a default value (i.e. an optional parameter).

  • Adding a new key to a dictionary. For example, expanding the Composite dictionary to include an extra key.

  • When a function accepts a dictionary as an argument, adding more, optional keys to that dictionary. For example, letting the user specify a new key in a configuration dictionary.

Changes to the supported API will be reflected in Vivarium’s versioning. We will also try to mark interfaces as deprecated in their documentation and raise warnings at least one release before actually removing them.

Working with Documentation

We write Vivarium’s documentation in plain text that utilizes the reStructured Text markup language. You can compile it to HTML with Sphinx, and you can also read it as plain text.

Reading Documentation

You’re welcome to read the plain text documentation in this folder, but you’ll probably enjoy the pretty HTML version more. Read the Docs hosts the compiled HTML here: https://wc-vivarium.rtfd.io/

If you want to generate the HTML documentation yourself, check out the instructions on building documentation below.

Writing Documentation

Where to Write

We write four kinds of documentation for Vivarium:

  • Tutorials: These are step-by-step instructions that walk the reader through doing something with Vivarium. We store these in doc/tutorials. Make sure to list all tutorials in doc/tutorials/index.rst so that they appear in the sidebar and the list of tutorials.

  • Guides: These dive into the details of Vivarium and should be comprehensive. We store guides in doc/guides and list them in doc/guides/index.rst. Guides should focus on the conceptual aspects of Vivarium, leaving technical details to the API reference.

  • References: Reference material should cater to users who already know what they’re looking for and just need to find it. For example, a user looking up a particular process or term. Our reference material consists of a glossary and an API reference. The glossary is stored in doc/glossary.rst, while the API reference is auto-generated from docstrings in the code. These docstrings can take advantage of all the reStructuredText syntax we use elsewhere in Vivarium. Eventually, we will remove from the reference material the stubs for functions that aren’t user-facing and the auto-generated titles on each page.

    • For an example of reference documentation that defines an API, see the death process. For an example of documentation that explains how to use a process, look at the metabolism process (both are in vivarium-cell).

      Note

      From the compiled HTML reference documentation, you can click on [source] to see the source code, including the docstrings. This can be helpful for looking up reStructuredText syntax.

      Warning

      For each class, include at most one of the class and constructor docstrings. They are concatenated when the HTML is compiled, so you can provide either one.

      class MyClass:
          '''This is the class docstring'''
      
          def __init__(self):
              '''This is the constructor docstring'''
      
Glossary vs Guide vs API Reference

In the guide, describe the concept and perhaps our rationale behind any design choices we made. Link terms to the glossary, which succinctly describes the term and links to relevant API reference pages and guides. In the API reference, describe the technical details.

We try to keep technical details in the API reference because the API reference is built from docstrings. Since these docstrings live alongside the code, they are more likely to be kept up-to-date than a separate guide.

Pointers for Technical Writing

Here are resources for writing good documentation and technical writing in general:

Style Guide

Here we document the stylistic decisions we have made for this documentation:

  • We use first-person plural pronouns to refer to ourselves (e.g. “We decided”).

  • We write tutorials in the second-person, future tense, for example “First, you’ll need to install”. We also frequently use the imperative (“Install this”).

  • We use the following admonitions. We don’t want to overload our users with admonitions, so we don’t use any others.

    • We warn users about potential problems with warning admonitions. These often describe important steps that we think users might forget.

      Warning

      .. WARNING::

      These are also appropriate for warning users about experimental APIs.

    • We use notes to highlight important points. These should not be used for asides that aren’t important enough to integrate directly into the text.

      Note

      .. note::

    • We give users helpful tips using the tip admonition. These help highlight tips that some users might not use but that will help users who are debugging problems.

      Tip

      .. tip::

    • We use danger admonitions for the most critical warnings. Use these sparingly.

      Danger

      .. DANGER::

  • We use Vale to lint our documentation. You can run the linter by executing doc/test.sh. This linter checks some Vivarium-specific naming and capitalization conventions. It also runs the proselint and write-good linters, which check for generally good style.

What APIs to Document

A standard convention in software development is that documented APIs (e.g. functions, methods, classes, and constants) are supported, so other users can count on them to work. Any changes to the inputs these APIs accept or their behavior constitutes an API change, and any changes that are not backward-compatible are breaking API changes that require a new major version release according to Semantic Versioning.

For Vivarium, being “documented” means appearing in the compiled API reference. Sphinx includes docstrings in this reference as follows:

  • By default, everything appears in the reference, even if it doesn’t have a docstring.

  • Tests (any members whose names start with test_) are not included, even if they have docstrings.

  • Private members (members that start with _) are not included, even if they have docstrings.

Building the Documentation

To build the documentation, we will use Sphinx to generate HTML files from plain text. Here are stepwise instructions:

  1. (optional) Create a virtual environment for the documentation-building packages. You might want this to be separate from the environment you use for the rest of Vivarium.

  2. Install dependencies:

    $ pip install -r doc/requirements.txt
    
  3. Build the HTML!

    $ cd doc
    $ make html
    

    Your HTML will now be in doc/_build/html. To view it, open doc/_build/html/index.html in a web browser.

Tutorials

These Jupyter Notebooks illustrate how to use Vivarium. You can click on a notebook below to view a static rendering of the notebook’s code and output, or you can download and execute the notebook yourself. These notebooks can be found under notebooks/ on GitHub.

Vivarium interface basics

Overview

This notebook introduces the Vivarium interface protocol by working through a simple, qualitative example of transcription/translation, and iterating on model design to add more complexity.

Note: The included examples often skirt best coding practice in favor of simplicity. See Vivarium templates on github for better starting examples: https://github.com/vivarium-collective/vivarium-template

[1]:
#uncomment to install vivarium-core
#!pip install vivarium-core
[2]:
# Imports and Notebook Utilities
import os
import copy
import pylab as plt
import numpy as np
from scipy import constants
import matplotlib.pyplot as plt
import matplotlib

# Process, Deriver, and Composer base classes
from vivarium.core.process import Process, Deriver
from vivarium.core.composer import Composer
from vivarium.core.registry import process_registry

# other vivarium imports
from vivarium.core.engine import Engine, pp
from vivarium.library.units import units

# plotting functions
from vivarium.plots.simulation_output import (
    plot_simulation_output, plot_variables)
from vivarium.plots.simulation_output import _save_fig_to_dir as save_fig_to_dir
from vivarium.plots.agents_multigen import plot_agents_multigen
from vivarium.plots.topology import plot_topology

# supress warnings in notebook
import warnings
warnings.filterwarnings('ignore')

AVOGADRO = constants.N_A * 1 / units.mol


# helper functions for composition
def process_in_experiment(
    process, settings, initial_state):
    composite = process.generate()
    return Engine(
        composite=composite,
        initial_state=initial_state,
        **settings)

def composite_in_experiment(
    composite, settings, initial_state):
    return Engine(
        composite=composite,
        initial_state=initial_state,
        **settings)

store_cmap = matplotlib.cm.get_cmap('Dark2')
dna_color = matplotlib.colors.to_rgba(store_cmap(0))
rna_color = matplotlib.colors.to_rgba(store_cmap(1))
protein_color = matplotlib.colors.to_rgba(store_cmap(2))
global_color = matplotlib.colors.to_rgba(store_cmap(7))
store_colors = {
    'DNA': dna_color,
    'DNA\n(counts)': dna_color,
    'DNA\n(mg/mL)': dna_color,
    'mRNA': rna_color,
    'mRNA\n(counts)': rna_color,
    'mRNA\n(mg/mL)': rna_color,
    'Protein': protein_color,
    'Protein\n(mg/mL)': protein_color,
    'global': global_color}

# plotting configurations
topology_plot_config = {
    'settings': {
        'coordinates': {
            'Tl': (-1,0),
            'Tx': (-1,-1),
            'Protein': (1,0),
            'mRNA': (1,-1),
            'DNA': (1,-2),
        },
        'node_distance': 3.0,
        'process_color': 'k',
        'store_colors': store_colors,
        'dashed_edges': True,
        'graph_format': 'vertical',
        'color_edges': False},
    'out_dir': 'out/'}

plot_var_config = {
    'row_height': 2,
    'row_padding': 0.2,
    'column_width': 10,
    'out_dir': 'out'}

Make a Process: minimal transcription

Transcription is the biological process by which RNA is synthesized from a DNA template. Here, we define a model with a single mRNA species, \(C\), transcribed from a single gene, \(G\), at transcription rate \(k_{tsc}\). RNA also degrades at rate \(k_{deg}\).

This can written as the difference equation \(\Delta RNA_{C} = (k_{tsc}[Gene_{G}] - k_{deg}[RNA_{C}]) \Delta t\)

Vivarium’s basic elements
Processes can implement any kind of dynamical model - dynamic flux balance analysis, differential equation, stochastic process, Boolean logic, etc.
Stores are databases of state variables read by the Processes, with methods for applying each Processes’ updates.

process store

Process interface protocol

If standard modeling formats are an “HTML” for systems biology, we need an “interface protocol” such as TCP/IP serves for the internet – a protocol for connecting separate systems into a complex and open-ended network that anyone can contribute to.

Making a dynamical model into a Vivarium Process requires the following protocol: 1. A constructor that accepts parameters and configures the model. 2. A ports_schema that declares the ports and their schema. 3. A next_update that runs the model and returns an update.

Constructor
  • default parameters are used in absense of an other provided parameters.

  • The constructor’s parameters arguments overrides the default parameters.

class Tx(Process):

    defaults = {
        'ktsc': 1e-2,
        'kdeg': 1e-3}

    def __init__(self, parameters=None):
        super().__init__(parameters)
Ports Schema
  • Ports are the connections by which Process are wired to Stores.

  • ports_schema declares the ports, the variables that go through them, and how those variables operate.

  • Here, Tx declares a port for mRNA with variable C, and a port for DNA with variable G.

def ports_schema(self):
    return {
        'mRNA': {
            'C': {
                '_default': 0.0,
                '_updater': 'accumulate',
                '_divider': 'set',
                '_properties': {
                    'mw': 111.1 units.g / units.mol}},
        'DNA': {
            'G': {
                '_default': 1.0}}
Advanced ports_schema
  • dictionary comprehensions are useful for declaring schema for configured variables.

def ports_schema(self):
    molecule_schema = {
        '_default': 0.0,
        '_emit': True}

    return {
        'molecules': {
            mol_id: molecule_schema
            for mol_id in self.parameters['molecules']}}
Advanced ports_schema
  • Use the glob '*' schema to declare expected sub-store structure, and view all child values of the store:

def ports_schema(self):
    schema = {
        'port1': {
            '*': {
                '_default': 1.0
            }
        }
    }
Advanced ports_schema
  • Use the glob '**' schema to connect to an entire sub-branch, including child nodes, grandchild nodes, etc:

def ports_schema(self):
    schema = {
        'port1': {
            '*': {
                '_default': 1.0
            }
        }
    }
Advanced ports_schema
  • Schema methods can also be declared by passing in functions.

  • The asymmetric_division divider makes molecules in the ‘front’ go to one daughter cell upon division, and those in the ‘back’ go to the other daughter.

def asymmetric_division(value, topology):
    if 'front' in topology:
        return [value, 0.0]
    elif 'back' in topology:
        return [0.0, value]

def ports_schema(self):
    return {
        'front': {
            'molecule': {
                '_divider': {
                    'divider': asymmetric_division,
                    'topology': {'front': ('molecule',)},
                }}},
        'back': {
            'molecule': {
                '_divider': {
                    'divider': asymmetric_division,
                    'topology': {'back': ('molecule',)},
                }}}}
Initial State
  • Each Process MAY provide an initial_state method. This can be retrieved, reconfigured, and passed into a simulation.

  • If left empty, a simulation initializes at the '_default' values.

def initial_state(self, config):
    return {
        'DNA': {'G': 1.0},
        'mRNA': {'C': 0.0}}
Update Method
  • Retrieve the state variables through the ports.

  • Run the model for the timestep’s duration.

  • Return an update to the state variable through the ports.

def next_update(self, states, timestep):

    # Retrieve
    G = states['DNA']['G']
    C = states['mRNA']['C']

    # Run
    dC = (self.ktsc * G - self.kdeg * C) * timestep

    # Return
    return {
        'mRNA': {
            'C': dC}}
Tx: a deterministic transcription process

According to BioNumbers, the concentration of DNA in an E. coli cell is on the order of 11-18 mg/mL. The concentration of RNA is 75-120 mg/ml.

[3]:
class Tx(Process):

    defaults = {
        'ktsc': 1e-2,
        'kdeg': 1e-3}

    def __init__(self, parameters=None):
        super().__init__(parameters)

    def ports_schema(self):
        return {
            'DNA': {
                'G': {
                    '_default': 10 * units.mg / units.mL,
                    '_updater': 'accumulate',
                    '_emit': True}},
            'mRNA': {
                'C': {
                    '_default': 100 * units.mg / units.mL,
                    '_updater': 'accumulate',
                    '_emit': True}}}

    def next_update(self, timestep, states):
        G = states['DNA']['G']
        C = states['mRNA']['C']
        dC = (self.parameters['ktsc'] * G - self.parameters['kdeg'] * C) * timestep
        return {
            'mRNA': {
                'C': dC}}
plot Tx topology
[4]:
fig = plot_topology(Tx(), filename='tx_topology.pdf', **topology_plot_config)

Writing out/tx_topology.pdf
_images/tutorials_notebooks_Vivarium_interface_basics_17_1.png
run Tx
[5]:
# tsc configuration
tx_config = {'time_step': 10}
tx_sim_settings = {
    'experiment_id': 'TX'}
tx_initial_state = {
    'mRNA': {'C': 0.0 * units.mg/units.mL}}
tx_plot_config = {
    'variables': [
        {
            'variable': ('mRNA', ('C', 'milligram / milliliter')),
            'color': store_colors['mRNA']
        },
        {
            'variable': ('DNA', ('G', 'milligram / milliliter')),
            'color': store_colors['DNA']
        }],
    'filename': 'tx_output.pdf',
    **plot_var_config}
[6]:
# initialize
tx_process = Tx(tx_config)

# make the experiment
tx_exp = process_in_experiment(
    tx_process, tx_sim_settings, tx_initial_state)

# run
tx_exp.update(10000)

# retrieve the data as a timeseries
tx_output = tx_exp.emitter.get_timeseries()

# plot
fig = plot_variables(tx_output,  **tx_plot_config)

Simulation ID: TX
Created: 05/18/2022 at 09:43:24
Completed in 0.283171 seconds
Writing out/tx_output.pdf
_images/tutorials_notebooks_Vivarium_interface_basics_20_1.png
Tl: a deterministic translation process

Translation is the biological process by which protein is synthesized with an mRNA template. Here, we define a model with a single protein species, \(Protein_{X}\), transcribed from a single gene, \(RNA_{C}\), at translation rate \(k_{trl}\). Protein also degrades at rate \(k_{deg}\).

This can be represented by a chemical reaction network with the form: * $RNA_{C} \xrightarrow[]{k_{trl}} RNA_{C} + Protein_{X} $ * \(Protein_{X} \xrightarrow[]{k_{deg}} \emptyset\)

According to BioNumbers, the concentration of RNA in an E. coli cell is on the order of 75-120 mg/ml. The concentration of protein is 200-320 mg/ml.

[7]:
class Tl(Process):

    defaults = {
        'ktrl': 5e-4,
        'kdeg': 5e-5}

    def ports_schema(self):
        return {
            'mRNA': {
                'C': {
                    '_default': 100 * units.mg / units.mL,
                    '_divider': 'split',
                    '_emit': True}},
            'Protein': {
                'X': {
                    '_default': 200 * units.mg / units.mL,
                    '_divider': 'split',
                    '_emit': True}}}

    def next_update(self, timestep, states):
        C = states['mRNA']['C']
        X = states['Protein']['X']
        dX = (self.parameters['ktrl'] * C - self.parameters['kdeg'] * X) * timestep
        return {
            'Protein': {
                'X': dX}}
run Tl
[8]:
# trl configuration
tl_config = {'time_step': 10}
tl_sim_settings = {'experiment_id': 'TL'}
tl_initial_state = {
    'Protein': {'X': 0.0 * units.mg / units.mL}}
tl_plot_config = {
    'variables': [
        {
            'variable': ('Protein', ('X', 'milligram / milliliter')),
            'color': store_colors['Protein']
        },
        {
            'variable': ('mRNA', ('C', 'milligram / milliliter')),
            'color': store_colors['mRNA']
        },
        ],
    'filename': 'tl_output.pdf',
    **plot_var_config}
[9]:
# initialize
tl_process = Tl(tl_config)

# make the experiment
tl_exp = process_in_experiment(
    tl_process, tl_sim_settings, tl_initial_state)

# run
tl_exp.update(10000)

# retrieve the data as a timeseries
tl_output = tl_exp.emitter.get_timeseries()

# plot
fig = plot_variables(tl_output,  **tl_plot_config)

Simulation ID: TL
Created: 05/18/2022 at 09:43:26
Completed in 0.297745 seconds
Writing out/tl_output.pdf
_images/tutorials_notebooks_Vivarium_interface_basics_25_1.png

Make a Composite

A Composite is a set of Processes and Stores. Vivarium constructs the Stores from the Processes’s port_schema methods and wires them up as instructed by a Topology. The only communication between Processes is through variables in shared Stores.

composite

TxTl: a transcription/translation composite

We demonstrate composition by combining the Tx and Tl processes.

Composition protocol

Composers, which combine processes into composites are implemented with the protocol: 1. A constructor that accepts configuration data, which can override the consituent Processes’ default parameters. 1. A generate_processes method that constructs the Processes, passing model parameters as needed. 2. A generate_topology method that returns the Topology definition which tells Vivarium how to wire up the Processes to Stores.

composite constructor
class TxTl(Composer):

    defaults = {
        'Tx': {
            'ktsc': 1e-2},
        'Tl': {
            'ktrl': 1e-3}}

    def __init__(self, config=None):
        super().__init__(config)
generate topology
  • Here, generate_topology() returns the Topology definition that wires these Processes together with 3 Stores, one of them shared.

def generate_topology(self, config):
    return {
        'Tx': {
            'DNA': ('DNA',),     # connect TSC's 'DNA' Port to a 'DNA' Store
            'mRNA': ('mRNA',)},  # connect TSC's 'mRNA' Port to a 'mRNA' Store
        'Tl': {
            'mRNA': ('mRNA',),   # connect TRL's 'mRNA' Port to the same 'mRNA' Store
            'Protein': ('Protein',)}}
advanced generate topology
  • embedding in a hierarchy: to connect to sub-stores in a hierarchy, declare the path through each substore, as done to ‘lipids’.

    • To connect to supra-stores use '..' for each level up, as done to 'external'.

  • splitting ports: One port can connect to multiple stores by specifying the path for each variable, as is done to 'transport'.

    • This can be used to re-map variable names, for integration of different models.

def generate_topology(config):
    return {
        'process_1': {
            'lipids': ('organelle', 'membrane', 'lipid'),
            'external': ('..', 'environment'),
            'transport': {
                'glucose_external': ('external', 'glucose'),
                'glucose_internal': ('internal', 'glucose'),
            }
        }}
TxTl Composer
[10]:
class TxTl(Composer):

    defaults = {
        'Tx': {'time_step': 10},
        'Tl': {'time_step': 10}}

    def generate_processes(self, config):
        return {
            'Tx': Tx(config['Tx']),
            'Tl': Tl(config['Tl'])}

    def generate_topology(self, config):
        return {
            'Tx': {
                'DNA': ('DNA',),
                'mRNA': ('mRNA',)},
            'Tl': {
                'mRNA': ('mRNA',),
                'Protein': ('Protein',)}}
plot TxTl topology
[11]:
txtl_topology_plot_config = copy.deepcopy(topology_plot_config)
txtl_topology_plot_config['settings']['node_distance'] = 2
fig = plot_topology(TxTl(), filename='txtl_topology.pdf', **topology_plot_config)
Writing out/txtl_topology.pdf
_images/tutorials_notebooks_Vivarium_interface_basics_35_1.png
run TxTl
[12]:
# tsc_trl configuration
txtl_config = {}
txtl_exp_settings = {'experiment_id': 'TXTL'}
txtl_plot_config = {
    'variables':[
        {
            'variable': ('Protein', ('X', 'milligram / milliliter')),
            'color': store_colors['Protein']
        },
        {
            'variable': ('mRNA', ('C', 'milligram / milliliter')),
            'color': store_colors['mRNA']
        },
        {
            'variable': ('DNA', ('G', 'milligram / milliliter')),
            'color': store_colors['DNA']
        },
    ],
    'filename': 'txtl_output.pdf',
    **plot_var_config}
tl_initial_state = {
    'mRNA': {'C': 0.0 * units.mg / units.mL},
    'Protein': {'X': 0.0 * units.mg / units.mL}}
[13]:
# construct TxTl
txtl_composite = TxTl(txtl_config).generate()

# make the experiment
txtl_experiment = composite_in_experiment(
    txtl_composite, txtl_exp_settings, tl_initial_state)

# run it and retrieve the data that was emitted to the simulation log
txtl_experiment.update(20000)
txtl_output = txtl_experiment.emitter.get_timeseries()

# plot the output
fig = plot_variables(txtl_output, **txtl_plot_config)

Simulation ID: TXTL
Created: 05/18/2022 at 09:43:27
Completed in 0.862072 seconds
Writing out/txtl_output.pdf
_images/tutorials_notebooks_Vivarium_interface_basics_38_1.png

Adding Complexity

Process modularity allows modelers to iterate on model design by swapping out different models. We demonstrated this by replacing the deterministic Transcription Process with a Stochastic Transcription Process.

Stochastic transcription requires variable timesteps, which Vivarium accomodates with multi-timestepping.

dynamics

StochasticTx: a stochastic transcription process

This process uses the Gillespie algorithm in its next_update() method.

[14]:
stoch_exp_settings = {
    'settings': {
        'experiment_id': 'stochastic_txtl'},
    'initial_state': {
            'DNA\n(counts)': {
                'G': 1.0
            },
            'mRNA\n(counts)': {
                'C': 0.0
            },
            'Protein\n(mg/mL)': {
                'X': 0.0 * units.mg / units.mL
            }}}


stoch_plot_config = {
    'variables':[
        {
            'variable': ('Protein\n(mg/mL)', ('X', 'milligram / milliliter')),
            'color': store_colors['Protein'],
            'display': 'Protein: X (mg/mL)'},
        {
            'variable': ('mRNA\n(mg/mL)', ('C', 'milligram / milliliter')),
            'color': store_colors['mRNA'],
            'display': 'mRNA: C (mg/mL)'},
        {
            'variable': ('DNA\n(counts)', 'G'),
            'color': store_colors['DNA'],
            'display': 'DNA: G (counts)'},
    ],
    'filename': 'stochastic_txtl_output.pdf',
    **plot_var_config}
[15]:
class StochasticTx(Process):

    defaults = {
        'ktsc': 1e0,
        'kdeg': 1e-3}

    def __init__(self, parameters=None):
        super().__init__(parameters)
        self.ktsc = self.parameters['ktsc']
        self.kdeg = self.parameters['kdeg']
        self.stoichiometry = np.array([[0, 1], [0, -1]])
        self.time_left = None
        self.event = None

        # initialize the next timestep
        initial_state = self.initial_state()
        self.calculate_timestep(initial_state)

    def initial_state(self, config=None):
        return {
            'DNA': {
                'G': 1.0
            },
            'mRNA': {
                'C': 1.0
            }
        }

    def ports_schema(self):
        return {
            'DNA': {
                'G': {
                    '_default': 1.0,
                    '_emit': True}},
            'mRNA': {
                'C': {
                    '_default': 1.0,
                    '_emit': True}}}

    def calculate_timestep(self, states):
        # retrieve the state values
        g = states['DNA']['G']
        c = states['mRNA']['C']

        array_state = np.array([g, c])

        # Calculate propensities
        propensities = [
            self.ktsc * array_state[0], self.kdeg * array_state[1]]
        prop_sum = sum(propensities)

        # The wait time is distributed exponentially
        self.calculated_timestep = np.random.exponential(scale=prop_sum)
        return self.calculated_timestep

    def next_reaction(self, x):
        """get the next reaction and return a new state"""

        propensities = [self.ktsc * x[0], self.kdeg * x[1]]
        prop_sum = sum(propensities)

        # Choose the next reaction
        r_rxn = np.random.uniform()
        i = 0
        for i, _ in enumerate(propensities):
            if r_rxn < propensities[i] / prop_sum:
                # This means propensity i fires
                break
        x += self.stoichiometry[i]
        return x

    def next_update(self, timestep, states):

        if self.time_left is not None:
            if timestep >= self.time_left:
                event = self.event
                self.event = None
                self.time_left = None
                return event

            self.time_left -= timestep
            return {}

        # retrieve the state values, put them in array
        g = states['DNA']['G']
        c = states['mRNA']['C']
        array_state = np.array([g, c])

        # calculate the next reaction
        new_state = self.next_reaction(array_state)

        # get delta mRNA
        c1 = new_state[1]
        d_c = c1 - c

        update = {
            'mRNA': {
                'C': d_c}}

        if self.calculated_timestep > timestep:
            # didn't get all of our time, store the event for later
            self.time_left = self.calculated_timestep - timestep
            self.event = update
            return {}

        # return an update
        return {
            'mRNA': {
                'C': d_c}}
plot variable timesteps
[16]:
stoch_tx_process = StochasticTx(tx_config)
stoch_tx_exp = process_in_experiment(stoch_tx_process, **stoch_exp_settings)
stoch_tx_exp.update(10000)
stoch_tx_output = stoch_tx_exp.emitter.get_timeseries()



# calculate the timesteps
times = stoch_tx_output['time']
timesteps = []
for x, y in zip(times[0::], times[1::]):
#     if y-x != 1.0:
        timesteps.append(y-x)

fig = plt.hist(timesteps, 50, color='tab:gray')
plt.xlabel('timestep (seconds)')
plt.savefig('out/stochastic_timesteps.pdf')

Simulation ID: stochastic_txtl
Created: 05/18/2022 at 09:43:29
Completed in 0.473557 seconds
_images/tutorials_notebooks_Vivarium_interface_basics_44_1.png
Auxiliary Processes
  • Connecting different Processes may require addition ‘helper’ Processes to make conversions and adapt their unique requirements different values.

  • Derivers are a subclass of Process that runs after the other dynamic Processes and derives some states from others.

A concentration deriver convert the counts of the stochastic process to concentrations. This is available in the process_registry

concentrations_deriver = process_registry.access('concentrations_deriver')
Combining stochastic Tx with deterministic Tl
[17]:
# configuration data
mw_config = {'C': 1e8 * units.g / units.mol}
[18]:
class StochasticTxTl(Composer):
    defaults = {
        'stochastic_Tx': {},
        'Tl': {'time_step': 1},
        'concs': {
            'molecular_weights': mw_config}}

    def generate_processes(self, config):
        counts_to_concentration = process_registry.access('counts_to_concentration')
        return {
            'stochastic\nTx': StochasticTx(config['stochastic_Tx']),
            'Tl': Tl(config['Tl']),
            'counts\nto\nmg/mL': counts_to_concentration(config['concs'])}

    def generate_topology(self, config):
        return {
            'stochastic\nTx': {
                'DNA': ('DNA\n(counts)',),
                'mRNA': ('mRNA\n(counts)',)
            },
            'Tl': {
                'mRNA': ('mRNA\n(mg/mL)',),
                'Protein': ('Protein\n(mg/mL)',)
            },
            'counts\nto\nmg/mL': {
                'global': ('global',),
                'input': ('mRNA\n(counts)',),
                'output': ('mRNA\n(mg/mL)',)
            }}
plot StochasticTxTl topology
[19]:
# plot topology after merge
stochastic_topology_plot_config = copy.deepcopy(topology_plot_config)
stochastic_topology_plot_config['settings']['graph_format'] = 'horizontal'
stochastic_topology_plot_config['settings']['coordinates'] = {
    'stochastic\nTx': (2, 1),
    'counts\nto\nmg/mL': (3, 1),
    'Tl': (4, 1),
    'DNA\n(counts)': (1,-1),
    'mRNA\n(counts)': (2,-1),
    'mRNA\n(mg/mL)': (3,-1),
    'Protein\n(mg/mL)': (4,-1)}
stochastic_topology_plot_config['settings']['dashed_edges'] = True
stochastic_topology_plot_config['settings']['show_ports'] = False
stochastic_topology_plot_config['settings']['node_distance'] = 2.2


stochastic_txtl = StochasticTxTl()
fig = plot_topology(
    stochastic_txtl,
    filename='stochastic_txtl_topology.pdf',
    **stochastic_topology_plot_config)
Writing out/stochastic_txtl_topology.pdf
_images/tutorials_notebooks_Vivarium_interface_basics_50_1.png
run StochasticTxTl
[20]:
# make the experiment
stoch_experiment = composite_in_experiment(stochastic_txtl.generate(), **stoch_exp_settings)

# simulate and retrieve the data
stoch_experiment.update(1000)
stochastic_txtl_output = stoch_experiment.emitter.get_timeseries()

# plot output
fig = plot_variables(stochastic_txtl_output, **stoch_plot_config)

Simulation ID: stochastic_txtl
Created: 05/18/2022 at 09:43:30
Completed in 0.983180 seconds
Writing out/stochastic_txtl_output.pdf
_images/tutorials_notebooks_Vivarium_interface_basics_52_1.png

Growth and Division

We here extend the Transcription/Translation model with division. This require many instances of the processes to run simultaneously in a single simulation. To support such phenomena, Vivarium adopts an agent-based modeling bigraphical formalism, with embedded compartments that can spawn new compartments during runtime.

Hierarchical Embedding

To support this requirement, Processes can be embedded in a hierarchical representation of embedded compartments. Vivarium uses a bigraph formalism – a graph with embeddable nodes that can be placed within other nodes.

embedding

Hierarchy updates

The structure of a hierarchy has its own type of constructive dynamics with formation/destruction, merging/division, engulfing/expelling of compartments

constructive

[21]:
# add imported division processes
from vivarium.processes.divide_condition import DivideCondition
from vivarium.processes.meta_division import MetaDivision
from vivarium.processes.growth_rate import GrowthRate

TIMESTEP = 10


class TxTlDivision(Composer):
    defaults = {
        'stochastic_Tx': {'time_step': TIMESTEP},
        'Tl': {'time_step': TIMESTEP},
        'concs': {
            'molecular_weights': mw_config},
        'growth': {
            'time_step': 1,
            'default_growth_rate': 0.0005,
            'default_growth_noise': 0.001,
            'variables': ['volume']},
        'agent_id': np.random.randint(0, 100),
        'divide_condition': {
            'threshold': 2.5 * units.fL},
        'agents_path': ('..', '..', 'agents',),
        'daughter_path': tuple(),
        '_schema': {
            'concs': {
                'input': {'C': {'_divider': 'binomial'}},
                'output': {'C': {'_divider': 'set'}},
            }
        }
    }

    def generate_processes(self, config):
        counts_to_concentration = process_registry.access('counts_to_concentration')
        division_config = dict(
            daughter_path=config['daughter_path'],
            agent_id=config['agent_id'],
            composer=self)

        return {
            'stochastic_Tx': StochasticTx(config['stochastic_Tx']),
            'Tl': Tl(config['Tl']),
            'concs': counts_to_concentration(config['concs']),
            'growth': GrowthRate(config['growth']),
            'divide_condition': DivideCondition(config['divide_condition']),
            'division': MetaDivision(division_config),
        }

    def generate_topology(self, config):
        return {
            'stochastic_Tx': {
                'DNA': ('DNA',),
                'mRNA': ('RNA_counts',)
            },
            'Tl': {
                'mRNA': ('RNA',),
                'Protein': ('Protein',)
            },
            'concs': {
                'global': ('global',),
                'input': ('RNA_counts',),
                'output': ('RNA',)
            },
            'growth': {
                'variables': ('global',),
                'rates': ('rates',)
            },
            'divide_condition': {
                'variable': ('global', 'volume',),
                'divide': ('global', 'divide',)},
            'division': {
                'global': ('global',),
                'agents': config['agents_path']}
        }
Colony-level processes
[22]:
from vivarium.library.units import Quantity

def calculate_volume(value, path, node):
    if isinstance(node.value, Quantity) and node.units == units.fL:
        return value + node.value
    else:
        return value

class ColonyVolume(Deriver):
    defaults = {
        'colony_path': ('..', '..', 'agents')}
    def ports_schema(self):
        return {
            'colony': {
                'volume': {
                    '_default': 1.0 * units.fL,
                    '_updater': 'set',
                    '_emit': True}}}
    def next_update(self, timestep, states):
        return {
            'colony': {
                'volume': {
                    '_reduce': {
                        'reducer': calculate_volume,
                        'from': self.parameters['colony_path'],
                        'initial': 0.0 * units.fL}}}}
[23]:
# configure hierarchy
# agent config
agent_id = '0'
agent_config = {'agent_id': agent_id}

# environment config
env_config = {}

# initial state
hierarchy_initial_state = {
    'agents': {
        agent_id: {
            'global': {'volume': 1.2 * units.fL},
            'DNA': {'G': 1},
            'RNA': {'C': 5 * units.mg / units.mL},
            'Protein': {'X': 50 * units.mg / units.mL}}}}

# experiment settings
exp_settings = {
    'experiment_id': 'hierarchy_experiment',
    'initial_state': hierarchy_initial_state,
    'emit_step': 100.0}

# plot config
hierarchy_plot_settings = {
    'include_paths': [
        ('RNA_counts', 'C'),
        ('RNA', 'C'),
        ('DNA', 'G'),
        ('Protein', 'X'),
    ],
    'store_order': ('Protein', 'RNA_counts', 'DNA', 'RNA'),
    'titles_map': {
        ('Protein', 'X',): 'Protein',
        ('RNA_counts', 'C'): 'RNA',
        ('DNA', 'G',): 'DNA',
        'RNA': 'RNA',
    },
    'column_width': 6,
    'row_height': 1.0,
    'stack_column': True,
    'tick_label_size': 10,
    'linewidth': 1,
    'title_size': 10}

colony_plot_config = {
    'variables': [('global', ('volume', 'femtoliter'))],
    'filename': 'colony_growth.pdf',
    **plot_var_config}


# hierarchy topology plot
agent_0_string = 'agents\n0'
agent_1_string = 'agents\n00'
agent_2_string = 'agents\n01'
row_1 = 0
row_2 = -1
row_3 = -2
row_4 = -3
node_space = 0.75
vertical_space=0.9
bump = 0.1
process_column = -0.2
agent_row = -3.2
agent_column = bump/2 #0.5

hierarchy_topology_plot_config = {
    'settings': {
        'graph_format': 'hierarchy',
        'node_size': 6000,
        'process_color': 'k',
        'store_color': global_color,
        'store_colors': {
            f'{agent_0_string}\nDNA': dna_color,
            f'{agent_0_string}\nRNA': rna_color,
            f'{agent_0_string}\nRNA_counts': rna_color,
            f'{agent_0_string}\nProtein': protein_color,
        },
        'dashed_edges': True,
        'show_ports': False,
        'coordinates': {
            # Processes
            'ColonyVolume': (2.5, 0),
            'agents\n0\nstochastic_Tx': (agent_column, agent_row*vertical_space),
            'agents\n0\nTl': (agent_column+node_space, agent_row*vertical_space),
            'agents\n0\nconcs': (agent_column+2*node_space, agent_row*vertical_space),
            'agents\n0\ndivision': (agent_column+3*node_space, agent_row*vertical_space),
            # Stores
            'agents': (1.5*node_space, row_1*vertical_space),
            'agents\n0': (1.5*node_space, row_2*vertical_space),
            'agents\n0\nagents': (1.5*node_space, row_1*vertical_space),
            'agents\n0\nDNA': (0, row_3*vertical_space),
            'agents\n0\nRNA_counts': (node_space+bump, row_3*vertical_space),
            'agents\n0\nRNA': (node_space, (row_3-bump)*vertical_space),
            'agents\n0\nProtein': (2*node_space+bump, row_3*vertical_space),
            'agents\n0\nglobal': (3*node_space+bump, row_3*vertical_space),
        },
        'node_labels': {
            # Processes
            'ColonyVolume': 'Colony\nVolume',
            'agents\n0\nstochastic_Tx': 'stochastic\nTx',
            'agents\n0\nTl': 'Tl',
            'agents\n0\nconcs': 'counts\nto\nmg/mL',
            'agents\n0\ngrowth': 'growth',
            'agents\n0\ndivision': 'division',
            # Stores
            # third
            'agents\n0': '0',
            'agents\n0\nDNA': 'DNA',
            'agents\n0\nRNA': 'RNA',
            'agents\n0\nrates': 'rates',
            'agents\n0\nRNA_counts': '',
            'agents\n0\nglobal': 'global',
            'agents\n0\nProtein': 'Protein',
            # fourth
            'agents\n0\nrates\ngrowth_rate': 'growth_rate',
            'agents\n0\nrates\ngrowth_noise': 'growth_noise',
        },
        'remove_nodes': [
            'agents\n0\nrates\ngrowth_rate',
            'agents\n0\nrates\ngrowth_noise',
            'agents\n0\nrates',
            'agents\n0\ngrowth',
            'agents\n0\ndivide_condition',
            'agents\n0\nglobal\ndivide',
            'agents\n0\nglobal\nvolume',
        ]
    },
    'out_dir': 'out/'
}

# topology plot config for after division
agent_2_dist = 3.5
hierarchy_topology_plot_config2 = copy.deepcopy(hierarchy_topology_plot_config)

# redo coordinates, labels, store_colors, and removal
hierarchy_topology_plot_config2['settings']['node_distance'] = 2.5
hierarchy_topology_plot_config2['settings']['coordinates'] = {}
hierarchy_topology_plot_config2['settings']['node_labels'] = {}
hierarchy_topology_plot_config2['settings']['store_colors'] = {}
# hierarchy_topology_plot_config2['settings']['remove_nodes'] = []
for node_id, coord in hierarchy_topology_plot_config['settings']['coordinates'].items():
    if agent_0_string in node_id:
        new_id1 = node_id.replace(agent_0_string, agent_1_string)
        new_id2 = node_id.replace(agent_0_string, agent_2_string)
        hierarchy_topology_plot_config2['settings']['coordinates'][new_id1] = coord
        hierarchy_topology_plot_config2['settings']['coordinates'][new_id2] = (coord[0]+agent_2_dist, coord[1])
    else:
        hierarchy_topology_plot_config2['settings']['coordinates'][node_id] = (coord[0]+agent_2_dist/2, coord[1])
hierarchy_topology_plot_config2['settings']['coordinates']['ColonyVolume'] = (5.5, 0)

for node_id, label in hierarchy_topology_plot_config['settings']['node_labels'].items():
    if agent_0_string in node_id:
        new_id1 = node_id.replace(agent_0_string, agent_1_string)
        new_id2 = node_id.replace(agent_0_string, agent_2_string)
        hierarchy_topology_plot_config2['settings']['node_labels'][new_id1] = label
        hierarchy_topology_plot_config2['settings']['node_labels'][new_id2] = label
    else:
        hierarchy_topology_plot_config2['settings']['node_labels'][node_id] = label
hierarchy_topology_plot_config2['settings']['node_labels']['agents\n00'] = '1'
hierarchy_topology_plot_config2['settings']['node_labels']['agents\n01'] = '2'

for node_id, color in hierarchy_topology_plot_config['settings']['store_colors'].items():
    if agent_0_string in node_id:
        new_id1 = node_id.replace(agent_0_string, agent_1_string)
        new_id2 = node_id.replace(agent_0_string, agent_2_string)
        hierarchy_topology_plot_config2['settings']['store_colors'][new_id1] = color
        hierarchy_topology_plot_config2['settings']['store_colors'][new_id2] = color
    else:
        hierarchy_topology_plot_config2['settings']['store_colors'][node_id] = color

for node_id in hierarchy_topology_plot_config['settings']['remove_nodes']:
    if agent_0_string in node_id:
        new_id1 = node_id.replace(agent_0_string, agent_1_string)
        new_id2 = node_id.replace(agent_0_string, agent_2_string)
        hierarchy_topology_plot_config2['settings']['remove_nodes'].extend([new_id1, new_id2])

use composite.merge to combine colony processes with agents
[24]:
# make a txtl composite, embedded under an agents store
txtl_composer = TxTlDivision(agent_config)
hierarchy_composite = txtl_composer.generate(path=('agents', agent_id))

# make a colony composite, and a topology that connects its colony port to agents store
colony_composer = ColonyVolume(env_config)
colony_composite = colony_composer.generate()
colony_topology = {'ColonyVolume': {'colony': ('agents',)}}

# perform merge
hierarchy_composite.merge(composite=colony_composite, topology=colony_topology)
plot hierarchy topology with before division
[25]:
fig = plot_topology(
    hierarchy_composite,
    filename='hierarchy_topology.pdf',
    **hierarchy_topology_plot_config)
Writing out/hierarchy_topology.pdf
_images/tutorials_notebooks_Vivarium_interface_basics_63_1.png
plot hierarchy topology after division
[26]:
# initial state
initial_state = {
    'agents': {
        agent_id: {
            'global': {'volume': 1.2 * units.fL},
            'DNA': {'G': 1},
            'RNA': {'C': 5 * units.mg / units.mL},
            'Protein': {'X': 50 * units.mg / units.mL}}}}

# make a copy of the composite
txtl_composite1 = copy.deepcopy(hierarchy_composite)

# make the experiment
hierarchy_experiment1 = composite_in_experiment(
    composite=txtl_composite1,
    settings={},
    initial_state=initial_state)

# run the experiment long enough to divide
hierarchy_experiment1.update(2000)

Simulation ID: a7e6aa2e-d6c9-11ec-8a5b-8c85908ac627
Created: 05/18/2022 at 09:43:33
Completed in 5.25 seconds
[27]:
fig = plot_topology(
    txtl_composite1,
    filename='hierarchy_topology_2.pdf',
    **hierarchy_topology_plot_config2)
Writing out/hierarchy_topology_2.pdf
_images/tutorials_notebooks_Vivarium_interface_basics_66_1.png
run hierarchy experiment
[28]:
# initial state
initial_state = {
    'agents': {
        agent_id: {
            'global': {'volume': 1.2 * units.fL},
            'DNA': {'G': 1},
            'RNA': {'C': 5 * units.mg / units.mL},
            'Protein': {'X': 50 * units.mg / units.mL}}}}

# make the experiment
hierarchy_experiment = composite_in_experiment(
    composite=hierarchy_composite,
    settings={},
    initial_state=initial_state)

# run the experiment
hierarchy_experiment.update(6000)

Simulation ID: ab2e8210-d6c9-11ec-8a5b-8c85908ac627
Created: 05/18/2022 at 09:43:39
Completed in 117.13 seconds
[29]:
# retrieve the data
hierarchy_data = hierarchy_experiment.emitter.get_data_unitless()
path_ts = hierarchy_experiment.emitter.get_path_timeseries()

# add agent colors
dimgray = (0.4,0.4,0.4)
paths = list(path_ts.keys())
agent_ids = set([path[1] for path in paths if '0' in path[1]])
agent_colors = {agent_id: dimgray for agent_id in agent_ids}
hierarchy_plot_settings.update({'agent_colors': agent_colors})

[30]:
# initialize the plot
multigen_fig = plot_agents_multigen(hierarchy_data, hierarchy_plot_settings)
plt.close()
Colony-level metrics
[31]:
gd_timeseries = hierarchy_experiment.emitter.get_timeseries()
colony_series = gd_timeseries['agents'][('volume', 'femtoliter')]
time_vec = gd_timeseries['time']
[32]:
# get the RNA_counts axis, to replace with colony volume
allaxes = multigen_fig.get_axes()
ax = None
for axis in allaxes:
    if axis.get_title() == 'RNA \nC':
        ax = axis
[33]:
# multigen_fig
ax.clear()
ax.plot(time_vec, colony_series, linewidth=1.0, color='darkslategray')
ax.set_xlim([time_vec[0], time_vec[-1]])
ax.set_title('colony volume (fL)', rotation=0)
ax.set_xlabel('time (s)')
ax.spines['bottom'].set_position(('axes', -0.2))
save_fig_to_dir(multigen_fig, 'growth_division_output.pdf')
multigen_fig
Writing out/growth_division_output.pdf
[33]:
_images/tutorials_notebooks_Vivarium_interface_basics_74_1.png

We also have a tutorial for writing a new process that doesn’t use Jupyter Notebooks.

How to Write a Process

Why Write Processes?

Vivarium comes with a number of processes ready for you to use, but combining processes to form composites will only take you so far. For many models, the existing processes will be insufficient, so you will need to create your own.

Note

Processes are the building blocks of Vivarium models, so creating a process to model a phenomenon you know well lets other modelers build on your expertise. Processes are a great way to share your knowledge with the modeling community!

Let’s Write a Process!

Suppose we want to model how hexokinase helps maintain intracellular glucose concentrations. Hexokinase catalyzes glucose phosphorylation, which we can describe by the following reaction:

\[GLC + ATP \rightleftarrows G6P + ADP\]

In the reaction above and throughout this tutorial, we will use the following abbreviations:

  • GLC: D-Glucose

  • ATP: Adenosine triphosphate

  • G6P: α-D-Glucose-6-phosphate

  • ADP: Adenosine diphosphate

  • v: Rate of the forward reaction

  • HK: Hexokinase

Tip

Once you have worked through the example in this tutorial, try modeling a different reaction that you work with!

Conceptualize the Model
Make Assumptions

A critical component of modeling is deciding what parts to describe mechanistically and what to approximate. For our scenario, we could model this reaction using molecular dynamics, but this would be computationally intensive and would not scale to larger simulations. Instead, we will assume Michaelis-Menten, sequential bisubstrate kinetics:

\[v = \frac{k_{cat}[HK][GLC][ATP]}{K_{GLC}K_{ATP} + K_{GLC}[ATP] + K_{ATP}[GLC] + [GLC][ATP]}\]

Let’s also assume that diffusion is much faster than the reaction so that the concentrations of our enzyme and substrates are constant throughout the cell.

Note

If you actually wanted to model a reaction like this, the convenience kinetics process can be configured to model any Michaelis-Menten enzyme kinetics.

Translate Model into Updates

Processes in Vivarium work by repeatedly changing the state of the model. A process makes these changes by computing an update based on the model’s current state and a timestep \(t\).

For the current example, each update will include the following:

  • A decrease in GLC: \(-v t\)

  • A decrease in ATP: \(-v t\)

  • An increase in G6P: \(v t\)

  • An increase in ADP: \(v t\)

Note

In this example and most of the time in Vivarium, we work in terms of concentrations. We also normally use units of mM, but you can use different units so long as you are consistent.

Determine the Ports

We partition the overall simulation state into stores, which can be shared among processes. Each process declares ports, each of which will receive a store. When creating a process, you need to decide what ports to declare.

When someone else uses your process, they will create a composite of it and other processes. These processes will interact by sharing stores. While any number of your process’s ports may be linked to the same store, a port cannot be split between stores. This means that you should put in separate ports any variables that a user might want in separate stores.

For example, ATP and ADP are turned over rapidly in the cell, so a user might want to isolate those variables from others that get updated more slowly. We will therefore create two ports:

  • nucleoside_phosphates: This port will store the ATP and ADP variables.

  • cytoplasm: This port will store the GLC, G6P, and HK variables.

Implement the Model

To implement the model, create a new Python file named glucose_phosphorylation.py in the vivarium/processes/ directory. Then we create a new class that inherits from vivarium.core.process.Process:

from vivarium.core.process import Process

class GlucosePhosphorylation(Process):
    pass
The Constructor

In the constructor we can configure the process. We accept configurations as a dictionary called initial_parameters. For example, we can let the user configure the kinetic parameters \(k_{cat}\), \(K_{GLC}\), and \(K_{ATP}\). We can also provide default values for these parameters.

The configurations (with any missing parameters filled in with defaults) and ports are passed to the superclass constructor to instantiate the process.

import copy

from vivarium.core.process import Process

class GlucosePhosphorylation(Process):

    defaults = {
        'k_cat': 2e-3,
        'K_ATP': 5e-2,
        'K_GLC': 4e-2,
    }

    def __init__(self, initial_parameters=None):
        if initial_parameters == None:
            initial_parameters = {}
        parameters = copy.deepcopy(GlucosePhosphorylation.defaults)
        parameters.update(initial_parameters)
        super().__init__(parameters)

It turns out that the vivarium.core.process.Process constructor handles merging initial_parameters with the defaults automatically, so we don’t actually have to include a constructor at all!

Even though we’re just getting started on our process, let’s try it out! At the bottom of the glucose_phosphorylation.py file, instantiate the process and take a look at some of its attributes:

if __name__ == '__main__':
    parameters = {
        'k_cat': 1.5,
    }
    my_process = GlucosePhosphorylation(parameters)
    print(my_process.parameters['k_cat'])
    print(my_process.parameters['K_ATP'])

Then run your code by executing the whole file:

$ python glucose_phosphorylation.py
1.5
0.05

Notice that the k_cat parameter updated to the value we supplied and that k_ATP took on the default value.

Wait! Where did the parameters attribute come from? We never created that attribute, but vivarium.core.process.Process made it from the parameters argument we passed to its constructor. We’ll take advantage of this in the next step.

Generating Updates

Now we can write the next_update method, which generates updates for each port based on a provided simulation state and timestep.

Warning

The states parameter passed into the update function is a view of the overall state, so it must not be changed.

def next_update(self, timestep, states):
    # Get concentrations from state
    cytoplasm = states['cytoplasm']
    nucleoside_phosphates = states['nucleoside_phosphates']
    hk = cytoplasm['HK']
    glc = cytoplasm['GLC']
    atp = nucleoside_phosphates['ATP']

    # Get kinetic parameters
    k_cat = self.parameters['k_cat']
    k_atp = self.parameters['K_ATP']
    k_glc = self.parameters['K_GLC']

    # Compute reaction rate with michaelis-menten equation
    rate = k_cat * hk * glc * atp / (
        k_glc * k_atp + k_glc * atp + k_atp * glc + glc * atp)

    # Compute concentration changes from rate and timestep
    delta_glc = -rate * timestep
    delta_atp = -rate * timestep
    delta_g6p = rate * timestep
    delta_adp = rate * timestep

    # Compile changes into an update
    update = {
        'cytoplasm': {
            'GLC': delta_glc,
            'G6P': delta_g6p,
            # We exclude HK because it doesn't change
        },
        'nucleoside_phosphates': {
            'ATP': delta_atp,
            'ADP': delta_adp,
        },
    }

    return update

Now let’s test this update function by seeing how it changes a state we provide. Replace the testing code we added to the bottom of the file with this:

if __name__ == '__main__':
    parameters = {
        'k_cat': 1.5,
    }
    my_process = GlucosePhosphorylation(parameters)
    state = {
        'cytoplasm': {
            'GLC': 1.0,
            'G6P': 0.0,
            'HK': 0.1,
        },
        'nucleoside_phosphates': {
            'ATP': 2.0,
            'ADP': 0.0,
        },
    }
    update = my_process.next_update(3.0, state)
    print(update['cytoplasm']['G6P'])

With these parameters, we can calculate the reaction rate:

\[\begin{split}\begin{equation} \begin{aligned} v & = \frac{ k_{cat}[HK][GLC][ATP] }{ K_{GLC}K_{ATP} + K_{GLC}[ATP] + K_{ATP}[GLC] + [GLC][ATP] } \\ & = \frac{ (1.5)(0.1)(1)(2) }{ (0.04)(0.05) + (0.04)(2) + (0.05)(1) + (1)(2) } \\ & = 0.14 \\ \end{aligned} \end{equation}\end{split}\]

Therefore, we expect the change in concentration of G6P to be:

\[\begin{split}\begin{equation} \begin{aligned} \Delta_{[GLC]} & = v t \\ & = (0.14)(3) \\ & = 0.42 \\ \end{aligned} \end{equation}\end{split}\]

Let’s see if our process models this reaction as we expect:

$ python glucose_phosphorylation.py
0.4221388367729832

Hooray! This is what we expected.

Ports Schema

Our process works, but we had to manually set the state. We also haven’t shown yet how to apply the update we generate to the simulation state. Luckily for us, these steps will be handled automatically by Vivarium. We just need to create a ports_schema method that provides a schema. A schema is a nested dictionary that describes each variable the process will interact with. Each variable is defined by a dictionary of schema keys that specify its default value, how it should be updated, and other properties.

For this example, our updates are expressed as deltas that should be added to the old value of the variable. This is the default, so the schema can leave out the updater specification. Still, we’ll specify one of the updaters for demonstration.

def ports_schema(self):
    return {
        'cytoplasm': {
            'GLC': {
                # accumulate means to add the updates
                '_updater': 'accumulate',
                '_default': 1.0,
                '_properties': {
                    'mw': 1.0 * units.g / units.mol,
                },
                '_emit': True,
            },
            # accumulate is the default, so we don't need to specify
            # updaters for the rest of the variables
            'G6P': {
                '_default': 0.0,
                '_properties': {
                    'mw': 1.0 * units.g / units.mol,
                },
                '_emit': True,
            },
            'HK': {
                '_default': 0.1,
                '_properties': {
                    'mw': 1.0 * units.g / units.mol,
                },
            },
        },
        'nucleoside_phosphates': {
            'ATP': {
                '_default': 2.0,
                '_emit': True,
            },
            'ADP': {
                '_default': 0.0,
                '_emit': True,
            }
        },
    }

By convention, we usually put the ports_schema method before the next_update method.

Now, we can run a simulation using Vivarium’s Engine vivarium.core.engine.Engine() like this:

from vivarium.core.engine import Engine
from vivarium.plots.simulation_output import plot_simulation_output

...

if __name__ == '__main__':
    parameters = {
        'k_cat': 1.5,
    }
    my_process = GlucosePhosphorylation(parameters)

    # make a composite
    my_composite = my_process.generate()

    # run a simulation
    sim = Engine(composite=my_composite)
    total_time = 10
    sim.update(total_time)

    # get the timeseries
    timeseries = sim.emitter.get_timeseries()
    plot_simulation_output(timeseries, {}, './')

We use vivarium.plots.simulation_output.plot_simulation_output to plot the output from our simulation. In simulation.png you should see an output plot like this:

_images/process_tutorial_long_timestep.png

Tip

If a process is erroneously reporting negative values, try decreasing the timestep.

Oops, it looks like the cytoplasmic GLC concentration dropped below zero around time 8! This happens when the timestep is too long and so our approximation doesn’t adjust fast enough to dropping concentrations. To fix this, let’s change the timestep to 0.1.

Note

You may be wondering, “What unit is the timestep in?” The answer is that it doesn’t matter! We just need the parameters and timestep to use the same unit of time.

Here’s the parameters dictionary with the updated timestep:

parameters = {
    'k_cat': 1.5,
    'time_step': 0.1,
}

Now if we run the file again, we should get a simulation.png like this:

_images/process_tutorial_short_timestep.png

You can download the completed process file here.

Great job; you’ve written a new process! Now consider writing one to model a mechanism you are familiar with.

Reference Material

This reference material is for users who need to look up a specific piece of information. If you want to understand a topic more comprehensively, try a topic guide instead. If you are new to Vivarium, check out our getting started guide.

The API Reference is a representation of the docstrings from Vivarium’s code. This is a great place to look up details on how to use one of our classes or functions.

In our glossary we explain how we use terms specific to Vivarium.

Vivarium

Core Vivarium Package

Experiment Control

Run experiments and analyses from the command line.

class vivarium.core.control.Control(out_dir: Optional[str] = None, experiments: Optional[Dict[str, Any]] = None, composers: Optional[Dict[str, Any]] = None, plots: Optional[Dict[str, Any]] = None, workflows: Optional[Dict[str, Any]] = None, args: Optional[Sequence[str]] = None)[source]

Bases: object

Control experiments from the command line

Load experiments, plots, and workflows in this Control class, and trigger them from the command line

parse_args(args: Optional[Sequence[str]] = None)argparse.Namespace[source]
run_experiment(experiment_config: Union[str, dict])Dict[Union[Tuple, str], Any][source]
run_one_plot(plot_config: Union[str, dict], data: Dict[Union[Tuple, str], Any], out_dir: Optional[str] = None)None[source]
run_plots(plot_ids: Union[list, str], data: Dict[Union[Tuple, str], Any], out_dir: Optional[str] = None)None[source]
run_workflow(workflow_id: str)None[source]
vivarium.core.control.is_float(element: Any)bool[source]
vivarium.core.control.make_dir(out_dir: str = 'out')None[source]
vivarium.core.control.run_library_cli(library: dict, args: Optional[list] = None)None[source]

Run experiments from the command line

Parameters

library (dict) – maps experiment id to experiment function

vivarium.core.control.toy_control(args: Optional[Sequence[str]] = None)vivarium.core.control.Control[source]

a toy example of control

To run: > python vivarium/core/control.py -w 1

vivarium.core.control.toy_plot(data: Dict[Union[Tuple, str], Any], config: Optional[Dict] = None, out_dir: Optional[str] = 'out')None[source]
Emitters

Emitters log configuration data and time-series data somewhere.

class vivarium.core.emitter.DatabaseEmitter(config: Dict[str, Any])[source]

Bases: vivarium.core.emitter.Emitter

Emit data to a mongoDB database

Example:

>>> config = {
...     'host': 'localhost:27017',
...     'database': 'DB_NAME',
... }
>>> # The line below works only if you have to have 27017 open locally
>>> # emitter = DatabaseEmitter(config)

config may have ‘host’ and ‘database’ items.

client_dict: Dict[int, pymongo.mongo_client.MongoClient] = {}
classmethod create_indexes(table: Any, columns: List[Any])None[source]

Create the listed column indexes for the given DB table.

default_host = 'localhost:27017'
emit(data: Dict[str, Any])None[source]
get_data(query: Optional[list] = None)dict[source]
write_emit(table: Any, emit_data: Dict[str, Any])None[source]

Check that data size is less than emit limit.

Break up large emits into smaller pieces and emit them individually

class vivarium.core.emitter.Emitter(config: Dict[str, str])[source]

Bases: object

Base class for emitters.

This emitter simply emits to STDOUT.

Parameters

config – Emitter configuration.

emit(data: Dict[str, Any])None[source]

Emit data.

Parameters

data – The data to emit. This gets called by the Vivarium engine with a snapshot of the simulation state.

get_data(query: Optional[list] = None)dict[source]

Get the emitted data.

Returns

The data that has been emitted to the database in the raw data format. For this particular class, an empty dictionary is returned.

get_data_deserialized(query: Optional[list] = None) → Any[source]

Get the emitted data with variable values deserialized.

Returns

The data that has been emitted to the database in the raw data format. Before being returned, serialized values in the data are deserialized.

get_data_unitless(query: Optional[list] = None) → Any[source]

Get the emitted data with units stripped from variable values.

Returns

The data that has been emitted to the database in the raw data format. Before being returned, units are stripped from values.

get_path_timeseries(query: Optional[list] = None)dict[source]

Get the deserialized data as a path timeseries.

Returns

The deserialized emitted data, formatted as a path timeseries.

get_timeseries(query: Optional[list] = None)dict[source]

Get the deserialized data as an embedded timeseries.

Returns

The deserialized emitted data, formatted as an embedded timeseries.

class vivarium.core.emitter.NullEmitter(config: Dict[str, str])[source]

Bases: vivarium.core.emitter.Emitter

Don’t emit anything

Base class for emitters.

This emitter simply emits to STDOUT.

Parameters

config – Emitter configuration.

emit(data: Dict[str, Any])None[source]
class vivarium.core.emitter.RAMEmitter(config: Dict[str, Any])[source]

Bases: vivarium.core.emitter.Emitter

Accumulate the timeseries history portion of the “emitted” data to a table in RAM.

emit(data: Dict[str, Any])None[source]

Emit the timeseries history portion of data, which is data['data'] if data['table'] == 'history' and put it at data['data']['time'] in the history.

get_data(query: Optional[list] = None)dict[source]

Return the accumulated timeseries history of “emitted” data.

class vivarium.core.emitter.SharedRamEmitter(config: Dict[str, Any])[source]

Bases: vivarium.core.emitter.RAMEmitter

Accumulate the timeseries history portion of the “emitted” data to a table in RAM that is shared across all instances of the emitter.

saved_data: Dict[float, Dict[str, Any]] = {}
vivarium.core.emitter.apply_func(document: Any, field: Tuple, f: Optional[Callable[[], Any]] = None) → Any[source]
vivarium.core.emitter.assemble_data(data: list)dict[source]

re-assemble data

vivarium.core.emitter.breakdown_data(limit: float, data: Any, path: Tuple = (), size: Optional[float] = None)list[source]
vivarium.core.emitter.data_from_database(experiment_id: str, client: Any, query: Optional[list] = None, func_dict: Optional[dict] = None, f: Optional[Callable[[], Any]] = None, filters: Optional[dict] = None, start_time: Union[int, bson.min_key.MinKey] = MinKey(), end_time: Union[int, bson.max_key.MaxKey] = MaxKey(), cpus: int = 1) → Tuple[dict, Any][source]

Fetch something from a MongoDB.

Parameters
  • experiment_id – the experiment id which is being retrieved

  • client – a MongoClient instance connected to the DB

  • query – a list of tuples pointing to fields within the experiment data. In the format: [(‘path’, ‘to’, ‘field1’), (‘path’, ‘to’, ‘field2’)]

  • func_dict – a dict which maps the given query paths to a function that operates on the retrieved values and returns the results. If None then the raw values are returned. In the format: {(‘path’, ‘to’, ‘field1’): function}

  • f – a function that applies equally to all fields in query. func_dict is the recommended approach and takes priority over f.

  • filters – MongoDB query arguments to further filter results beyond matching the experiment ID.

  • start_time – first and last simulation time to query

  • end_time – first and last simulation time to query

  • cpus – splits query into this many chunks to run in parallel

Returns

data (dict)

vivarium.core.emitter.data_to_database(data: Dict[float, dict], environment_config: Any, client: Any) → Any[source]

Insert something into a MongoDB.

vivarium.core.emitter.delete_experiment(host: str = 'localhost', port: Any = 27017, query: Optional[dict] = None)None[source]

Helper function to delete experiment data in parallel

Parameters
  • host – Host name of database. This can usually be left as the default.

  • port – Port number of database. This can usually be left as the default.

  • query – Filter for documents to delete.

vivarium.core.emitter.delete_experiment_from_database(experiment_id: str, host: str = 'localhost', port: Any = 27017, cpus: int = 1)None[source]

Delete an experiment’s data from a database.

Parameters
  • experiment_id – Identifier of experiment.

  • host – Host name of database. This can usually be left as the default.

  • port – Port number of database. This can usually be left as the default.

  • cpus – Number of chunks to split delete operation into to be run in parallel. Useful if single-threaded delete does not saturate I/O.

vivarium.core.emitter.get_atlas_client(secrets_path: str) → Any[source]

Open a MongoDB client using the named secrets config JSON file.

vivarium.core.emitter.get_atlas_database_emitter_config(username: str, password: str, cluster_subdomain: Any, database: str)Dict[str, Any][source]

Construct an Emitter config for a MongoDB on the Atlas service.

vivarium.core.emitter.get_data_chunks(history_collection: Any, experiment_id: str, start_time: Union[int, bson.min_key.MinKey] = MinKey(), end_time: Union[int, bson.max_key.MaxKey] = MaxKey(), cpus: int = 8)list[source]

Helper function to get chunks for parallel queries

Parameters
  • history_collection – the MongoDB history collection to query

  • experiment_id – the experiment id which is being retrieved

  • start_time – first and last simulation time to query

  • end_time – first and last simulation time to query

  • cpus – number of chunks to create

Returns

List of ObjectId tuples that represent chunk boundaries. For each tuple, include {'_id': {$gte: tuple[0], $lt: tuple[1]}} in the query to search its corresponding chunk.

vivarium.core.emitter.get_emitter(config: Optional[Dict[str, str]])vivarium.core.emitter.Emitter[source]

Construct an Emitter using the provided config.

The available Emitter type names and their classes are:

Parameters

config – Must comtain the type key, which specifies the emitter type name (e.g. database).

Returns

A new Emitter instance.

vivarium.core.emitter.get_experiment_database(port: Any = 27017, database_name: str = 'simulations') → Any[source]

Get a database object.

Parameters
  • port – Port number of database. This can usually be left as the default.

  • database_name – Name of the database table. This can usually be left as the default.

Returns

The database object.

vivarium.core.emitter.get_history_data_db(history_collection: Any, experiment_id: Any, query: Optional[list] = None, func_dict: Optional[dict] = None, f: Optional[Callable[[], Any]] = None, filters: Optional[dict] = None, start_time: Union[int, bson.min_key.MinKey] = MinKey(), end_time: Union[int, bson.max_key.MaxKey] = MaxKey(), cpus: int = 1, host: str = 'localhost', port: Any = '27017')Dict[float, dict][source]

Query MongoDB for history data.

Parameters
  • history_collection – a MongoDB collection

  • experiment_id – the experiment id which is being retrieved

  • query – a list of tuples pointing to fields within the experiment data. In the format: [(‘path’, ‘to’, ‘field1’), (‘path’, ‘to’, ‘field2’)]

  • func_dict – a dict which maps the given query paths to a function that operates on the retrieved values and returns the results. If None then the raw values are returned. In the format: {(‘path’, ‘to’, ‘field1’): function}

  • f – a function that applies equally to all fields in query. func_dict is the recommended approach and takes priority over f.

  • filters – MongoDB query arguments to further filter results beyond matching the experiment ID.

  • start_time – first and last simulation time to query

  • end_time – first and last simulation time to query

  • cpus – splits query into this many chunks to run in parallel, useful if single-threaded query does not saturate I/O (e.g. on Google Cloud)

  • host – used if cpus>1 to create MongoClient in parallel processes

  • port – used if cpus>1 to create MongoClient in parallel processes

Returns

data (dict)

vivarium.core.emitter.get_local_client(host: str, port: Any, database_name: str) → Any[source]

Open a MongoDB client onto the given host, port, and DB.

vivarium.core.emitter.get_query(projection: dict, host: str, port: Any, query: dict)list[source]

Helper function for parallel queries

Parameters
  • projection – a MongoDB projection in dictionary form

  • host – used to create new MongoClient for each parallel process

  • port – used to create new MongoClient for each parallel process

  • query – a MongoDB query in dictionary form

Returns

List of projected documents for given query

vivarium.core.emitter.path_timeseries_from_data(data: dict)dict[source]

Convert from raw data to a path timeseries.

vivarium.core.emitter.path_timeseries_from_embedded_timeseries(embedded_timeseries: dict)dict[source]

Convert an embedded timeseries to a path timeseries.

vivarium.core.emitter.timeseries_from_data(data: dict)dict[source]

Convert raw data to an embedded timeseries.

Engine

Engine runs the simulation.

class vivarium.core.engine.Defer(defer: Any, f: Callable, args: Tuple)[source]

Bases: object

Allows for delayed application of a function to an update.

The object simply holds the provided arguments until it’s time for the computation to be performed. Then, the function is called.

Parameters
  • defer – An object with a .get_command_result() method whose output will be passed to the function. For example, the object could be an vivarium.core.process.Process object whose .get_command_result() method will return the process update.

  • function – The function. For example, invert_topology() to transform the returned update.

  • args – Passed as the second argument to the function.

get()Dict[str, Any][source]

Perform the deferred computation.

Returns

The result of calling the function.

class vivarium.core.engine.EmptyDefer[source]

Bases: vivarium.core.engine.Defer

get()Dict[str, Any][source]
class vivarium.core.engine.Engine(composite: Optional[vivarium.core.composer.Composite] = None, processes: Optional[Dict[str, Any]] = None, steps: Optional[Dict[str, Any]] = None, flow: Optional[Dict[str, Sequence[Tuple[str, ]]]] = None, topology: Optional[Dict[str, Union[Tuple[str, ], dict, object]]] = None, store: Optional[vivarium.core.store.Store] = None, initial_state: Optional[Dict[str, Any]] = None, experiment_id: Optional[str] = None, experiment_name: Optional[str] = None, metadata: Optional[dict] = None, description: str = '', emitter: Union[str, dict] = 'timeseries', store_schema: Optional[dict] = None, emit_topology: bool = True, emit_processes: bool = False, emit_config: bool = False, emit_step: float = 1, display_info: bool = True, progress_bar: bool = False, global_time_precision: Optional[int] = None, profile: bool = False, initial_global_time: float = 0)[source]

Bases: object

Defines simulations

Parameters
  • composite – A Composite, which specifies the processes, steps, flow, and topology. This is an alternative to passing in processes and topology dict, which can not be loaded at the same time.

  • processes – A dictionary that maps process names to process objects. You will usually get this from the processes key of the dictionary from vivarium.core.composer.Composer.generate().

  • steps – A dictionary that maps step names to step objects. You will usually get this from the steps key of the dictionary from vivarium.core.composer.Composer.generate().

  • flow – A dictionary that maps step names to sequences of paths to the steps that the step depends on. You will usually get this from the flow key of the dictionary from vivarium.core.composer.Composer.generate().

  • topology – A dictionary that maps process names to sub-dictionaries. These sub-dictionaries map the process’s port names to tuples that specify a path through the tree from the compartment root to the store that will be passed to the process for that port.

  • store – A pre-loaded Store. This is an alternative to passing in processes and topology dict, which can not be loaded at the same time. Note that if you provide this argument, you must ensure that all parallel processes (i.e. vivarium.core.process.Process objects with the parallel attribute set to True) are instances of vivarium.core.process.ParallelProcess. This constructor converts parallel processes to ParallelProcess objects automatically if you do not provide this store argument.

  • initial_state – By default an empty dictionary, this is the initial state of the simulation.

  • experiment_id – A unique identifier for the experiment. A UUID will be generated if none is provided.

  • metadata – A dictionary with additional data about the experiment, which is saved by the emitter with the configuration.

  • description – A description of the experiment. A blank string by default.

  • emitter – An emitter configuration which must conform to the specification in the documentation for vivarium.core.emitter.get_emitter(). The experiment ID will be added to the dictionary you provide as the value for the key experiment_id.

  • display_info – prints experiment info

  • progress_bar – shows a progress bar

  • global_time_precision – an optional int that sets the decimal precision of global_time. This is useful for remove floating- point rounding errors for the time keys of saved states.

  • store_schema – An optional dictionary to expand the store hierarchy configuration, and also to turn emits on or off. The dictionary needs to be structured as a hierarchy, which will expand the existing store hierarchy. Setting an emit value for a branch node will set the emits of all the leaves to that value.

  • emit_topology – If True, this will emit the topology with the configuration data.

  • emit_processes – If True, this will emit the serialized processes with the configuration data.

  • emit_config – If True, this will emit the serialized initial state with the configuration data.

  • profile – Whether to profile the simulation with cProfile.

  • initial_global_time – The initial time for the simulation. Useful when this Engine is part of a larger, older simulation.

apply_update(update: Dict[str, Any], state: vivarium.core.store.Store)bool[source]

Apply an update to the simulation state.

Parameters
  • update – The update to apply. Must be relative to state.

  • state – The store to which the update is relative (usually root of simulation state. We need this so to preserve the “perspective” from which the update was generated.

Returns

a bool indicating whether the topology_views expired.

end()None[source]

Terminate all processes running in parallel.

This MUST be called at the end of any simulation with parallel processes. This function also ends profiling and computes profiling stats, including stats from parallel sub-processes. These stats are stored in self.stats.

run_for(interval: float, force_complete: bool = False)None[source]

Run each process within the given interval and update their states.

run_for() gives the caller more control over the simulation loop than update(). In particular, it may be called repeatedly within a caller-managed simulation loop without forcing processes to complete after each call (as would be the case with update()). It is the responsibility of the caller to ensure that when run_for() is called for the last time in the caller-managed simulation loop, force_complete=True so that all the processes finish at the end of the simulation.

Parameters
  • interval – the amount of time to simulate the composite.

  • force_complete – a bool indicating whether to force processes to complete at the end of the interval.

run_steps()None[source]

Run all the steps in the simulation.

update(interval: float)None[source]

Run each process for the given interval and force them to complete at the end of the interval. See run_for for the keyword args.

vivarium.core.engine.empty_front(t: float)Dict[str, Union[float, dict]][source]
vivarium.core.engine.invert_topology(update: Dict[str, Any], args: Tuple[Tuple[str, ], Dict[str, Union[Tuple[str, ], dict, object]]])Dict[str, Any][source]

Wrapper function around inverse_topology.

Wraps vivarium.library.topology.inverse_topology().

Updates are produced relative to the process that produced them. To transform them such that they are relative to the root of the simulation hierarchy, this function “inverts” a topology.

Parameters
  • update – The update.

  • args – Tuple of the path to which the update is relative and the topology.

Returns

The update, relative to the root of path.

vivarium.core.engine.pf(x: Any)str[source]

Format x for display.

vivarium.core.engine.pp(x: Any)None[source]

Print x in a pretty format.

vivarium.core.engine.print_progress_bar(iteration: float, total: float, decimals: float = 1, length: int = 50)None[source]

Create terminal progress bar

Parameters
  • iteration – Current iteration

  • total – Total iterations

  • decimals – Positive number of decimals in percent complete

  • length – Character length of bar

vivarium.core.engine.starts_with(a_list: Tuple[str, ], sub: Tuple[str, ])bool[source]

Check whether one path is a prefix of another.

Parameters
  • a_list – Path to look for prefix in.

  • sub – Prefix.

Returns

True if sub is a prefix of a_list; False otherwise.

vivarium.core.engine.timestamp(dt: Optional[Any] = None)str[source]

Get a timestamp of the form YYYYMMDD.HHMMSS.

Parameters

dt – Datetime object to generate timestamp from. If not specified, the current time will be used.

Returns

Timestamp.

Process Classes
vivarium.core.process.Deriver

Deriver is just an alias for Step now that Derivers have been deprecated.

alias of vivarium.core.process.Step

class vivarium.core.process.ParallelProcess(process: vivarium.core.process.Process, profile: bool = False, stats_objs: Optional[List[pstats.Stats]] = None)[source]

Bases: vivarium.core.process.Process

Wraps a Process for multiprocessing.

To run a simulation distributed across multiple processors, we use Python’s multiprocessing tools. This object runs in the main process and manages communication between the main (parent) process and the child process with the Process that this object manages.

Most methods pass their name and arguments to Process.run_command.

Parameters
  • process – The Process to manage.

  • profile – Whether to use cProfile to profile the subprocess.

  • stats_objs – List to add cProfile stats objs to when process is deleted. Only used if profile is true.

calculate_timestep(states: Optional[Dict[str, Any]])float[source]
property condition_path
end()None[source]

End the child process.

If profiling was enabled, then when the child process ends, it will compile its profiling stats and send those to the parent. The parent then saves those stats in self.stats.

generate_flow(config: Optional[dict] = None)Dict[str, Sequence[Tuple[str, ]]][source]
generate_processes(config: Optional[dict] = None)Dict[str, Any][source]
generate_steps(config: Optional[dict] = None)Dict[str, Any][source]
generate_topology(config: Optional[dict] = None)Dict[str, Union[Tuple[str, ], dict, object]][source]
get_command_result()Dict[str, Any][source]

Get the result of a command sent to the parallel process.

Commands and their results work like a queue, so unlike Process, you can technically call this method multiple times and get different return values each time. This behavior is subject to change, so you should not rely on it.

Returns

The command result.

get_private_state()Dict[str, Any][source]
initial_state(config: Optional[dict] = None)Dict[str, Any][source]
is_step()bool[source]
merge_overrides(override: Dict[str, Any])None[source]
next_update(timestep: float, states: Dict[str, Any])Dict[str, Any][source]
property parameters
ports_schema()Dict[str, Any][source]
property schema
property schema_override
send_command(command: str, args: Optional[tuple] = None, kwargs: Optional[dict] = None, run_pre_check: bool = True)None[source]

Send a command to the parallel process.

See :py:func:_handle_parallel_process for details on how the command will be handled.

update_condition(timestep: float, states: Dict[str, Any])bool[source]
class vivarium.core.process.Process(parameters: Optional[dict] = None)[source]

Bases: object

Process parent class.

All process classes must inherit from this class. Each class can provide a defaults class variable to specify the process defaults as a dictionary.

Note that subclasses should call the superclass init function first. This allows the superclass to correctly save the initial parameters before they are mutated by subclass constructor code. We need access to the original parameters for serialization to work properly.

Parameters

parameters

Override the class defaults. This dictionary may also contain the following special keys:

ATTRIBUTE_READ_COMMANDS = ('schema_override', 'parameters', 'condition_path', 'schema')
ATTRIBUTE_WRITE_COMMANDS = ('set_schema',)
METHOD_COMMANDS = ('initial_state', 'generate_processes', 'generate_steps', 'generate_topology', 'generate_flow', 'merge_overrides', 'calculate_timestep', 'is_step', 'get_private_state', 'ports_schema', 'next_update', 'update_condition')
calculate_timestep(states: Optional[Dict[str, Any]]) → Union[float, int][source]

Return the next process time step

A process subclass may override this method to implement adaptive timesteps. By default it returns self.parameters[‘timestep’].

property condition_path
default_state()Dict[str, Any][source]

Get the default values of the variables in each port.

The default values are computed based on the schema.

Returns

A state dictionary that assigns each variable’s default value to that variable.

defaults: Dict[str, Any] = {}
generate(config: Optional[dict] = None, path: Tuple[str, ] = ())Dict[source]
generate_flow(config: Optional[dict] = None)Dict[str, Sequence[Tuple[str, ]]][source]
generate_processes(config: Optional[dict] = None)Dict[str, Any][source]

Do not override this method.

generate_steps(config: Optional[dict] = None)Dict[str, Any][source]

Do not override this method.

generate_topology(config: Optional[dict] = None)Dict[str, Union[Tuple[str, ], dict, object]][source]

Do not override this method.

get_command_result() → Any[source]

Retrieve the result from the last-run command.

Returns

The result of the last command run. Note that this method should only be called once immediately after each call to send_command().

Raises

RuntimeError – When there is no command pending. This can happen when this method is called twice without an intervening call to send_command().

get_private_state()Dict[str, Any][source]

Get the process’s private state.

Processes can store state in instance variables instead of in the stores that hold the simulation-wide state. These instance variables hold a private state that is not shared with other processes. You can override this function to let experiments emit the process’s private state.

Returns

An empty dictionary. You may override this behavior to return your process’s private state.

get_schema(override: Optional[Dict[str, Any]] = None)dict[source]

Get the process’s schema, optionally with a schema override.

Parameters

override – Override schema

Returns

The combined schema.

initial_state(config: Optional[dict] = None)Dict[str, Any][source]

Get initial state in embedded path dictionary.

Every subclass may override this method.

Parameters

config (dict) – A dictionary of configuration options. All subclass implementation must accept this parameter, but some may ignore it.

Returns

Subclass implementations must return a dictionary mapping state paths to initial values.

Return type

dict

is_deriver()bool[source]

Check whether this process is a deriver.

Deprecated since version 0.3.14: Derivers have been deprecated in favor of steps, so please override is_step instead of is_deriver. Support for Derivers may be removed in a future release.

Returns

Whether this process is a deriver. This class always returns False, but subclasses may change this.

is_step()bool[source]

Check whether this process is a step.

Returns

Whether this process is a step. This class always returns False, but subclasses may change this behavior.

merge_overrides(override: Dict[str, Any])None[source]

Add a schema override to the process’s schema overrides.

Parameters

override – The schema override to add.

abstract next_update(timestep: Union[float, int], states: Dict[str, Any])Dict[str, Any][source]

Compute the next update to the simulation state.

Parameters
  • timestep – The duration for which the update should be computed.

  • states – The pre-update simulation state. This will take the same form as the process’s schema, except with a value for each variable.

Returns

An empty dictionary for now. This should be overridden by each subclass to return an update.

property parallel
property parameters
ports()Dict[str, List[str]][source]

Get ports and each port’s variables.

Returns

A map from port names to lists of the variables that go into that port.

abstract ports_schema()Dict[str, Any][source]

Get the schemas for each port.

This must be overridden by any subclasses.

Use the glob ‘*’ schema to declare expected sub-store structure, and view all child values of the store:

schema = {
    'port1': {
        '*': {
            '_default': 1.0
        }
    }
}

Use the glob ‘**’ schema to connect to an entire sub-branch, including child nodes, grandchild nodes, etc:

schema = {
    'port1': '**'
}

Ports flagged as output-only won’t be viewed through the next_update’s states, which can save some overhead time:

schema = {
  'port1': {
     '_output': True,
     'A': {'_default': 1.0},
  }
}
Returns

A dictionary that declares which states are expected by the processes, and how each state will behave. State keys can be assigned properties through schema_keys declared in vivarium.core.store.Store.

pre_send_command(command: str, args: Optional[tuple], kwargs: Optional[dict])None[source]

Run pre-checks before starting a command.

This method should be called at the start of every implementation of send_command().

Parameters
  • command – The name of the command to run.

  • args – A tuple of positional arguments for the command.

  • kwargs – A dictionary of keyword arguments for the command.

Raises

RuntimeError – Raised when a user tries to send a command while a previous command is still pending (i.e. the user hasn’t called get_command_result() yet for the previous command).

run_command(command: str, args: Optional[tuple] = None, kwargs: Optional[dict] = None) → Any[source]

Helper function that sends a command and returns result.

run_command_method(command: str, args: tuple, kwargs: dict) → Any[source]

Run a command whose name and interface match a method.

Parameters
  • command – The command name, which must equal to a method of self.

  • args – The positional arguments to pass to the method.

  • kwargs – The keywords arguments for the method.

Returns

The result of calling self.command(*args, **kwargs) is returned for command command.

property schema
property schema_override
send_command(command: str, args: Optional[tuple] = None, kwargs: Optional[dict] = None, run_pre_check: bool = True)None[source]

Handle process commands.

This method handles the commands listed in METHOD_COMMANDS by passing args and kwargs to the method of self with the name of the command and saving the return value as the result.

This method handles the commands listed in ATTRIBUTE_READ_COMMANDS by returning the attribute of self with the name matching the command, and it handles the commands listed in ATTRIBUTE_WRITE_COMMANDS by setting the attribute in the command to the first argument in args. The command must be named set_attr for attribute attr.

To add support for a custom command, override this function in your subclass. Each command is defined by a name (a string) and accepts both positional and keyword arguments. Any custom commands you add should have associated methods such that:

  • The command name matches the method name.

  • The command and method accept the same positional and keyword arguments.

  • The command and method return the same values.

If all of the above are satisfied, you can use Process.run_command_method() to handle the command.

Your implementation of this function needs to handle all the commands you want to support. When presented with an unknown command, you should call the superclass method, which will either handle the command or call its superclass method. At the top of this recursive chain, this Process.send_command() method handles some built-in commands and will raise an error for unknown commands.

Any overrides of this method must also call pre_send_command() at the start of the method. This call will check that no command is currently pending to avoid confusing behavior when multiple commands are started without intervening retrievals of command results. Since your overriding method will have already performed the pre-check, it should pass run_pre_check=False when calling the superclass method.

Parameters
  • command – The name of the command to run.

  • args – A tuple of positional arguments for the command.

  • kwargs – A dictionary of keyword arguments for the command.

  • run_pre_check – Whether to run the pre-checks implemented in pre_send_command(). This should be left at its default value unless the pre-checks have already been performed (e.g. if this method is being called by a subclass’s overriding method.)

Returns

None. This method just starts the command running.

Raises

ValueError – For unknown commands.

update_condition(timestep: Union[float, int], states: Dict[str, Any]) → Any[source]

Determine whether this process runs.

Parameters
  • timestep – The duration for which an update.

  • states – The pre-update simulation state.

Returns

Boolean for whether this process runs. True by default.

class vivarium.core.process.Step(parameters: Optional[dict] = None)[source]

Bases: vivarium.core.process.Process

Base class for steps.

is_step()bool[source]

Returns True to signal that this process is a step.

class vivarium.core.process.ToyParallelProcess(parameters: Optional[dict] = None)[source]

Bases: vivarium.core.process.Process

compare_pid(pid: float)bool[source]
next_update(timestep: float, states: Dict[str, Any])Dict[str, Any][source]
ports_schema()Dict[str, Any][source]
send_command(command: str, args: Optional[tuple] = None, kwargs: Optional[dict] = None, run_pre_check: bool = True)None[source]
class vivarium.core.process.ToySerializedProcess(parameters: Optional[dict] = None)[source]

Bases: vivarium.core.process.Process

defaults: Dict[str, list] = {'list': []}
next_update(timestep: float, states: Dict[str, Any])Dict[str, Any][source]
ports_schema()Dict[str, Any][source]
class vivarium.core.process.ToySerializedProcessInheritance(parameters: Optional[dict] = None)[source]

Bases: vivarium.core.process.Process

defaults: Dict[str, Any] = {'1': 1}
next_update(timestep: float, states: Dict[str, Any])Dict[str, Any][source]
ports_schema()Dict[str, Any][source]
vivarium.core.process._handle_parallel_process(connection: multiprocessing.connection.Connection, process: vivarium.core.process.Process, profile: bool)None[source]

Handle a parallel Vivarium process.

This function is designed to be passed as target to Multiprocess(). In a loop, it receives process commands from a pipe, passes those commands to the parallel process, and passes the result back along the pipe.

The special command end is handled directly by this function. This command causes the function to exit and therefore shut down the OS process created by multiprocessing.

Parameters
  • connection – The child end of a multiprocessing pipe. All communications received from the pipe should be a 3-tuple of the form (command, args, kwargs), and the tuple contents will be passed to Process.run_command(). The result, which may be of any type, will be sent back through the pipe.

  • process – The process running in parallel.

  • profile – Whether to profile the process.

vivarium.core.process.assoc_in(d: dict, path: Tuple[str, ], value: Any)dict[source]

Insert a value into a dictionary at an arbitrary depth.

Empty dictionaries will be created as needed to insert the value at the specified depth.

>>> d = {'a': {'b': 1}}
>>> assoc_in(d, ('a', 'c', 'd'), 2)
{'a': {'b': 1, 'c': {'d': 2}}}
Parameters
  • d – Dictionary to insert into.

  • path – Path in the dictionary where the value will be inserted. Each element of the path is dictionary key, which will be added if not already present. Any given element (except the first) refers to a key in the dictionary that is the value associated with the immediately preceding path element.

  • value – The value to insert.

Returns

Dictionary with the value inserted.

Composite, Composer, and MetaComposer Classes
class vivarium.core.composer.Composer(config: Optional[dict] = None)[source]

Bases: object

Base class for composer classes.

Composers generate composites.

All composer classes must inherit from this class.

Parameters

config – Dictionary of configuration options that can override the class defaults.

defaults: Dict[str, Any] = {}
generate(config: Optional[dict] = None, path: Tuple[str, ] = ())vivarium.core.composer.Composite[source]

Generate processes and topology dictionaries.

Parameters
  • config – Updates values in the configuration declared in the constructor.

  • path – Tuple with (‘path’, ‘to’, ‘level’) associates the processes and topology at this level.

Returns

Dictionary with the following keys

The values of these keys are all dictionaries suitable to be passed to the constructor for vivarium.core.engine.Engine.

generate_flow(config: Optional[dict])Dict[str, Sequence[Tuple[str, ]]][source]

Generate the flow of step dependencies.

Parameters

config – A dictionary of configuration options. All subclass implementation must accept this parameter, but some may ignore it.

Returns

Subclass implementations should return a dictionary mapping step names to sequences (e.g. lists or tuples) of paths. Steps with no dependencies must be included, but they should be mapped to an empty sequence. Any steps returned by generate_steps() or generate_processes() that are not included in the flow will be treated as if they depend on every step previously added to the engine.

abstract generate_processes(config: Optional[dict])Dict[str, Any][source]

Generate processes dictionary.

Every subclass must override this method. For backwards compatibility, vivarium.core.process.Step objects may be included in the returned dictionary, but this practice is discouraged and may be disallowed in a future release.

Parameters

config – A dictionary of configuration options. All subclass implementation must accept this parameter, but some may ignore it.

Returns

Subclass implementations must return a dictionary mapping process names to instantiated and configured vivarium.core.process.Process objects.

generate_steps(config: Optional[dict])Dict[str, Any][source]

Generate the steps dictionary.

Subclasses that want to include steps should override this method. This method is the preferred way to specify steps, though they may also be returned by generate_processes().

Parameters

config – A dictionary of configuration options. All subclass implementation must accept this parameter, but some may ignore it.

Returns

Subclass implementations should return a dictionary mapping step names to instantiated and configured vivarium.core.process.Step objects.

generate_store(config: Optional[dict] = None)vivarium.core.store.Store[source]
abstract generate_topology(config: Optional[dict])Dict[str, Union[Tuple[str, ], dict, object]][source]

Generate topology dictionary.

Every subclass must override this method.

Parameters

config – A dictionary of configuration options. All subclass implementation must accept this parameter, but some may ignore it.

Returns

Subclass implementations must return a topology dictionary.

get_parameters()dict[source]

Get the parameters for all processes.

Returns

A map from process names to dictionaries of those processes’ parameters.

initial_state(config: Optional[dict] = None) → Optional[Dict[str, Any]][source]

Merge all processes’ initial states

Every subclass may override this method.

Parameters

config (dict) – A dictionary of configuration options. All subclass implementation must accept this parameter, but some may ignore it.

Returns

Subclass implementations must return a dictionary mapping state paths to initial values.

Return type

dict

class vivarium.core.composer.Composite(config: Optional[Dict[str, Any]] = None, store: Optional[vivarium.core.store.Store] = None, processes: Optional[Dict[str, Any]] = None, steps: Optional[Dict[str, Any]] = None, flow: Optional[Dict[str, Sequence[Tuple[str, ]]]] = None, topology: Optional[Dict[str, Union[Tuple[str, ], dict, object]]] = None, state: Optional[Dict[str, Any]] = None)[source]

Bases: vivarium.library.datum.Datum

Composite parent class.

Contains keys for processes and topology

default_state(config: Optional[dict] = None) → Optional[Dict[str, Any]][source]

Merge all processes’ default states :param config: A dictionary of configuration options. All :type config: dict :param subclass implementation must accept this parameter: :param but: :param some may ignore it.:

Returns

Subclass implementations must return a dictionary mapping state paths to default values.

Return type

(dict)

defaults: Dict[str, Any] = {'flow': {}, 'processes': {}, 'state': {}, 'steps': {}, 'topology': {}}
flow: Dict[str, Sequence[Tuple[str, ]]] = {}
generate_store(config: Optional[dict] = None)vivarium.core.store.Store[source]
get_parameters()Dict[source]

Get the parameters for all processes. :returns: A map from process names to parameters.

initial_state(config: Optional[dict] = None) → Optional[Dict[str, Any]][source]

Merge all processes’ initial states :param config: A dictionary of configuration options. All :type config: dict :param subclass implementation must accept this parameter: :param but: :param some may ignore it.:

Returns

Subclass implementations must return a dictionary mapping state paths to initial values.

Return type

(dict)

merge(composite: Optional[vivarium.core.composer.Composite] = None, processes: Optional[Dict[str, vivarium.core.process.Process]] = None, topology: Optional[Dict[str, Union[Tuple[str, ], dict, object]]] = None, steps: Optional[Dict[str, Any]] = None, flow: Optional[Dict[str, Sequence[Tuple[str, ]]]] = None, state: Optional[Dict[str, Any]] = None, path: Optional[Tuple[str, ]] = None, schema_override: Optional[Dict[str, Any]] = None)None[source]
processes: Dict[str, Any] = {}
state: Dict[str, Any] = {}
steps: Dict[str, Any] = {}
topology: Dict[str, Union[Tuple[str, ], dict, object]] = {}
class vivarium.core.composer.MetaComposer(composers: Iterable[Any] = (), config: Optional[dict] = None)[source]

Bases: vivarium.core.composer.Composer

A collection of Composer objects.

The MetaComposer can be used to create composites that combine all the composers in the collection.

Parameters
  • composers – Initial collection of composers.

  • config – Initial configuration.

add_composer(composer: vivarium.core.composer.Composer, config: Optional[Dict] = None)None[source]

Add a composer to the collection of stored composers.

Parameters
  • composer – The composer to add.

  • config – The composer’s configuration, which will be merged with the stored config.

add_composers(composers: List, config: Optional[Dict] = None)None[source]

Add multiple composers to the collection of stored composers.

Parameters
  • composers – The composers to add.

  • config – Configuration for the composers, which will be merged with the stored config.

generate_flow(config: Optional[dict] = None)Dict[str, Any][source]
generate_processes(config: Optional[dict] = None)Dict[str, Any][source]
generate_steps(config: Optional[dict] = None)Dict[str, Any][source]
generate_topology(config: Optional[dict] = None)Dict[str, Union[Tuple[str, ], dict, object]][source]
vivarium.core.composer.get_composite_from_store(store: vivarium.core.store.Store)vivarium.core.composer.Composite[source]

Make a Composite from a Store

Registry of Updaters, Dividers, and Serializers

You should interpret words and phrases that appear fully capitalized in this document as described in RFC 2119. Here is a brief summary of the RFC:

  • “MUST” indicates absolute requirements. Vivarium may not work correctly if you don’t follow these.

  • “SHOULD” indicates strong suggestions. You might have a valid reason for deviating from them, but be careful that you understand the ramifications.

  • “MAY” indicates truly optional features that you can include or exclude as you wish.

Updaters

Each updater is defined as a function whose name begins with update_. Vivarium uses these functions to apply updates to variables. Updater names are registered in updater_registry, which maps these names to updater functions.

Updater API

An updater function SHOULD have a name that begins with update_. The function MUST accept exactly three positional arguments: the first MUST be the current value of the variable (i.e. before applying the update), the second MUST be the value associated with the variable in the update, and the third MUST be either a dictionary of states from the simulation hierarchy or None if no port_mapping key was specified in the updater definition. The function SHOULD not accept any other parameters. The function MUST return the updated value of the variable only.

Dividers

Each divider is defined by a function that follows the API we describe below. Vivarium uses these dividers to generate daughter cell states from the mother cell’s state. Divider names are registered in divider_registry, which maps these names to divider functions.

Divider API

Each divider function SHOULD have a name prefixed with divide_. The function MUST accept a single positional argument, the value of the variable in the mother cell. It SHOULD accept no other arguments. The function MUST return either:

  1. A list with two elements: the values of the variables in each of the daughter cells.

  2. None, in which case division will be skipped for that variable.

Note

Dividers MAY not be deterministic and MAY not be symmetric. For example, a divider splitting an odd, integer-valued value may randomly decide which daughter cell receives the remainder.

Serializers

Each serializer is defined as a class that follows the API we describe below. Vivarium uses these serializers to convert emitted data into a BSON-compatible format for database storage. Serializer names are registered in serializer_registry, which maps these names to serializer subclasses. For maximum performance, register serializers using a key equal to the string representation of its designated type (e.g. str(Serializer.python_type)).

Serializer API

Serializers MUST define the following:

  1. The python_type class attributes that determines what types are handled by the serializer

  2. The vivarium.core.registry.Serializer.serialize() method which is called on all objects of type python_type

Avoid defining custom serializers for built-in or Numpy types as these are automatically handled by orjson, the package used to serialize data.

Note

All dictionary keys MUST be Python strings for orjson to work. Numpy strings (np.str_) are not allowed.

If it is necessary to redefine the how objects are serialized by orjson, assign custom serializers to the stores containing objects of the affected type(s) using the _serializer ports schema key. This can also be used to serialize objects of the same Python type differently. To ensure that objects serialized this way are deserialized correctly, you SHOULD consider implementing the following as well:

  1. vivarium.core.registry.Serializer.can_deserialize() to determine whether to call deserialize on data

  2. vivarium.core.registry.Serializer.deserialize() to deserialize data

If it is necessary to deserialize objects of the same BSON type differently, the corresponding serializer(s) MUST implement these 2 methods.

class vivarium.core.registry.Registry[source]

Bases: object

A Registry holds a collection of functions or objects.

access(key)[source]

Get an item by key from the registry.

list()[source]
register(key, item, alternate_keys=())[source]

Add an item to the registry.

Parameters
  • key – Item key.

  • item – The item to add.

  • alternate_keys

    Additional keys under which to register the item. These keys will not be included in the list returned by Registry.list().

    This may be useful if you want to be able to look up an item in the registry under multiple keys.

class vivarium.core.registry.Serializer[source]

Bases: object

Base serializer class.

Serializers work together to convert Python objects, which may be collections of many different kinds of objects, into BSON-compatible representations. Those representations can then be deserialized to recover the original object.

Serialization of Python’s built-in datatypes and most Numpy types is handled directly by the orjson.dumps() method.

The serialization routines in Serializers are compiled into a fallback function that is called on objects not handled by orjson.

If one wishes to modify how built-in/Numpy objects are serialized, the relevant stores can be assigned a custom serializer using the _serializer key. The vivarium.core.registry.Serializer.serialize() method of that serializer will be called on the values in these stores before they are passed to orjson.

During deserialization, the can_deserialize method of each Serializer is used to determine whether to call the deserialize method.

Parameters

name – Name of the serializer. Defaults to the class name.

can_deserialize(data)[source]

This tells vivarium.core.serialize.deserialize_value() whether to call deserialize on data.

deserialize(data)[source]

This allows for data of the same BSON type to be deserialized differently (see regex matching of strings in vivarium.core.serialize.UnitsSerializer.deserialize() for an example).

python_type: Any = None

Type matching is NOT exact (subclasses included)

serialize(data)[source]

Controls what happens to data of the type python_type

vivarium.core.registry.assert_no_divide(state)[source]

Assert that the variable is never divided

Raises

AssertionError – If the variable is divided

vivarium.core.registry.divide_binomial(state)[source]

Binomial Divider

vivarium.core.registry.divide_null(state)[source]

Divider that causes the variable to be skipped during division.

Returns

None so that no divided values are provided to the daughter cells. This is useful for process objects, which are handled separately during division.

vivarium.core.registry.divide_set(state)[source]

Set Divider

Returns

A list [state, state]. No copying is performed.

vivarium.core.registry.divide_set_value(state, config)[source]

Set Value Divider :param ‘state’: value

Returns

A list [value, value]. No copying is performed.

vivarium.core.registry.divide_split(state)[source]

Split Divider

Parameters

state – Must be an int, a float, or a str of value Infinity.

Returns

A list, each of whose elements contains half of state. If state is an int, the remainder is placed at random in one of the two elements. If state is infinite, the return value is [state, state] (no copying is done).

Raises

Exception – if state is of an unrecognized type.

vivarium.core.registry.divide_split_dict(state)[source]

Split-Dictionary Divider

Returns

A list of two dictionaries. The first dictionary stores the first half of the key-value pairs in state, and the second dictionary stores the rest of the key-value pairs.

Note

Since dictionaries are unordered, you should avoid making any assumptions about which keys will be sent to which daughter cell.

vivarium.core.registry.divide_zero(state)[source]

Zero Divider

Returns

[0, 0] regardless of input

vivarium.core.registry.divider_registry = <vivarium.core.registry.Registry object>

Map divider names to divider functions

vivarium.core.registry.emitter_registry = <vivarium.core.registry.Registry object>

Map serializer names to Emitter classes

vivarium.core.registry.process_registry = <vivarium.core.registry.Registry object>

Maps process names to process classes

vivarium.core.registry.serializer_registry = <vivarium.core.registry.Registry object>

Map serializer names to serializer classes

vivarium.core.registry.update_accumulate(current_value, new_value)[source]

Accumulate Updater

Returns

The sum of current_value and new_value.

vivarium.core.registry.update_dictionary(current, update)[source]

Dictionary Updater Updater that translates _add and _delete -style updates into operations on a dictionary.

Expects current to be a dictionary, with no restriction on the types of objects stored within it, and no defaults values.

vivarium.core.registry.update_merge(current_value, new_value)[source]

Merge Updater

Returns

The merger of current_value and new_value. For any shared keys, the value in new_value is used.

Return type

dict

vivarium.core.registry.update_nonnegative_accumulate(current_value, new_value)[source]

Non-negative Accumulate Updater

Returns

The sum of current_value and new_value if positive, 0 if negative.

vivarium.core.registry.update_null(current_value, new_value)[source]

Null Updater

Returns

The value provided in current_value.

vivarium.core.registry.update_set(current_value, new_value)[source]

Set Updater

Returns

The value provided in new_value.

vivarium.core.registry.updater_registry = <vivarium.core.registry.Registry object>

Map updater names to updater functions

Serialize

Collection of serializers that transform Python data into a BSON-compatible form.

class vivarium.core.serialize.DictDeserializer[source]

Bases: vivarium.core.registry.Serializer

Iterates through dictionaries and applies deserializers.

can_deserialize(data: Any)bool[source]
deserialize(data: dict)dict[source]
class vivarium.core.serialize.FunctionSerializer[source]

Bases: vivarium.core.registry.Serializer

Serializer for function objects.

serialize(data: collections.abc.Callable)str[source]
class vivarium.core.serialize.NumpyFallbackSerializer[source]

Bases: vivarium.core.registry.Serializer

Orjson does not handle Numpy arrays with strings

serialize(data: Any)list[source]
class vivarium.core.serialize.ProcessSerializer[source]

Bases: vivarium.core.registry.Serializer

Serializer for processes if emit_process is enabled.

serialize(data: vivarium.core.process.Process)str[source]
class vivarium.core.serialize.QuantitySerializer[source]

Bases: vivarium.core.registry.Serializer

Serializes data with units into strings of the form !units[...], where ... is the result of calling str(data). Deserializes strings of this form back into data with units.

serialize(data: Any) → Union[List[str], str][source]
class vivarium.core.serialize.SequenceDeserializer[source]

Bases: vivarium.core.registry.Serializer

Iterates through lists and applies deserializers.

can_deserialize(data: Any)bool[source]
deserialize(data: Any)List[Any][source]
class vivarium.core.serialize.SetSerializer[source]

Bases: vivarium.core.registry.Serializer

Serializer for set objects.

serialize(data: set)List[source]
class vivarium.core.serialize.UnitsSerializer[source]

Bases: vivarium.core.registry.Serializer

Serializes data with units into strings of the form !units[...], where ... is the result of calling str(data). Deserializes strings of this form back into data with units.

can_deserialize(data: Any)bool[source]
deserialize(data: str, unit: Optional[pint.unit.Unit] = None)pint.quantity.Quantity[source]

Deserialize data with units from a human-readable string.

Parameters
  • data – The data to deserialize. Providing a list here is deprecated. You should use deserialize_value instead, which uses a separate list deserializer.

  • unit – The units to convert data to after deserializing. If omitted, no conversion occurs. This option is deprecated.

Returns

A single deserialized object or, if data is a list, a list of deserialized objects.

serialize(data: Any) → Union[List[str], str][source]
vivarium.core.serialize.deserialize_value(value: Any) → Any[source]

Find and apply the correct serializer for a value by calling each registered serializer’s vivarium.core.registry.Serializer.can_deserialize() method. Returns the value as is if no compatible serializer is found.

Parameters

value (Any) – Data to be deserialized

Raises

ValueError – Only one serializer should apply for any given value

Returns

Deserialized data

Return type

Any

vivarium.core.serialize.find_numpy_and_non_strings(d: dict, curr_path: tuple = (), saved_paths: Optional[List[tuple]] = None)List[tuple][source]

Return list of paths which terminate in a non-string or Numpy string dictionary key. Orjson does not handle these types of keys by default.

vivarium.core.serialize.make_fallback_serializer_function()collections.abc.Callable[source]

Creates a fallback function that is called by orjson on data of types that are not natively supported. Define and register instances of vivarium.core.registry.Serializer() with serialization routines for the types in question.

vivarium.core.serialize.serialize_value(value: Any, default: Optional[collections.abc.Callable] = None) → Any[source]

Apply orjson-based serialization routine on value.

Parameters
  • value (Any) – Data to be serialized. All keys must be strings. Notably, Numpy strings (np.str_) are not acceptable keys.

  • default (Callable) – A function that is called on any data of a type that is not natively supported by orjson. Returns an object that can be handled by default up to 254 times before an exception is raised.

Returns

Serialized data

Return type

Any

Store

The file system for storing and updating state variables during an experiment.

class vivarium.core.store.Store(config, outer=None, source=None)[source]

Bases: object

Holds a subset of the overall model state

The total state of the model can be broken down into stores, each of which is represented by an instance of this Store class. The store’s state is a set of variables, each of which is defined by a set of schema key-value pairs. The valid schema keys are listed in schema_keys, and they are:

  • _default (Type should match the variable value): The default value of the variable.

  • _updater (str): The name of the updater to use. By default this is accumulate.

  • _divider (str): The name of the divider to use. Note that _divider is not included in the schema_keys set because it can be applied to any node in the hierarchy, not just leaves (which represent variables).

  • _value (Type should match the variable value): The current value of the variable. This is None by default.

  • _properties (dict): Extra properties of the variable that don’t have a specific schema key. This is an empty dictionary by default.

  • _emit (bool): Whether to emit the variable to the emitter. This is False by default.

  • _serializer (vivarium.core.registry.Serializer or str): Serializer (or name of serializer) whose serialize method should be called on data in this store before emitting and whose deserialize method should be called when repopulating this store from serialized data. Only define if it is necessary to serialize and deserialize data in this store differently from other data of the same type.

add(added)[source]
add_node(path, node)[source]

Add a node instance at the provided path

apply_defaults()[source]

If value is None, set to default.

apply_update(update, state=None)[source]

Given an arbitrary update, map all the values in that update to their positions in the tree where they apply, and update these values using each node’s _updater.

Parameters
  • update – The update being applied.

  • state – The state at the start of the time step.

There are five topology update methods, which use the following special update keys:

  • _add - Adds states into the subtree, given a list of dicts

    containing:

    • path - Path to the added state key.

    • state - The value of the added state.

  • _move - Moves a node from a source to a target location in the tree. This uses an update to an outer port, which contains both the source and target node locations. Can move multiple nodes according to a list of dicts containing:

    • source - the source path from an outer process port

    • target - the location where the node will be placed.

  • _generate - The value has four keys, which are essentially the arguments to the generate() function:

    • path - Path into the tree to generate this subtree.

    • processes - Tree of processes to generate.

    • topology - Connections of all the process’s ports_schema().

    • initial_state - Initial state for this new subtree.

  • _divide - Performs cell division by constructing two new daughter cells and removing the mother. Takes a dict with two keys:

    • mother - The id of the mother (for removal)

    • daughters - List of two new daughter generate directives, of the

      same form as the _generate value above.

  • _delete - The value here is a list of paths (tuples) to delete from the tree.

Additional special update keys for different update operations:

  • _updater - Override the default updater with any updater you want.

  • _reduce - This allows a reduction over the entire subtree from some point downward. Its three keys are:

    • from - What point to start the reduction.

    • initial - The initial value of the reduction.

    • reducer - A function of three arguments, which is called on every node from the from point in the tree down:

      • value - The current accumulated value of the reduction.

      • path - The path to this point in the tree

      • node - The actual node being visited.

      This function returns the next value for the reduction. The result of the reduction will be assigned to this point in the tree.

build_topology_views()[source]
connect(path, value, absolute=False)[source]

Wire a store’s process to another store.

This function must not be used unless self holds a process.

Parameters
  • path – Path of the port to connect.

  • value – The store (or the path to the store) to connect to the port at path.

Raises
create(path, value=None, absolute=False, **kwargs)[source]
delete(key, here=None)[source]
depth(path=(), filter_function=None)[source]

Create a mapping of every path in the tree to the node living at that path in the tree. An optional filter argument is a function that can declares the instances that will be returned, for example: * filter=lambda x: isinstance(x.value, Process)

divide(divide)[source]
divide_value()[source]

Apply the divider for each node to the value in that node to assemble two parallel divided states of this subtree.

emit_data()[source]

Emit the value at this Store.

Obeys the schema (namely emits only if _emit is true). Also applies serializers and converts units as necessary.

Returns

The value to emit, or None if nothing should be emitted.

generate(path, processes, steps, flow, topology, initial_state)[source]

Generate a subtree of this store at the given path. The processes will be mapped into locations in the tree by the topology, and once everything is constructed the initial_state will be applied.

generate_value(value)[source]

generate the structure for this value that don’t exist, but don’t overwrite any existing values.

get_config(sources=False)[source]

Assemble a dictionary representation of the config for this node. A desired property is that the node can be exactly recreated by applying the resulting config to an empty node again.

get_flow()[source]

Get the flow for all steps under this node.

For example:

>>> from vivarium.core.store import Store
>>> from vivarium.core.process import Step
>>> class MyStep(Step):
...     def ports_schema(self):
...         return {
...             'port': ['variable'],
...         }
...     def next_update(self, timestep, states):
...         return {}
>>> schema = {
...     'agent1': {
...         'store': {
...             'variable': {
...                 '_default': 0,
...             },
...         },
...         'step1': {
...             '_value': MyStep(),
...             '_topology': {
...                 'port': ('store',),
...             },
...             '_flow': [],
...         },
...         'step2': {
...             '_value': MyStep(),
...             '_topology': {
...                 'port': ('store',),
...             },
...             '_flow': [('step1',)],
...         },
...     },
... }
>>> store = Store(schema)
>>> store.get_flow()
{'agent1': {'step1': [], 'step2': [('step1',)]}}
get_in(path)[source]

Get the value at path relative to this store.

get_path(path)[source]

Get the node at the given path relative to this node.

get_paths(paths)[source]

Get the nodes at each of the specified paths.

Parameters

paths – Map from keys to paths.

Returns

A dictionary with the same keys as paths. Each key is mapped to the Store object at the associated path.

get_processes()[source]

Get all processes in this store. Does not include steps.

get_steps()[source]

Get all steps under this store.

get_template(template)[source]

Pass in a template dict with None for each value you want to retrieve from the tree!

get_topology()[source]

Get the topology for all processes in this store.

get_value(condition=None, f=None)[source]

Pull the values out of the tree in a structure symmetrical to the tree.

get_values(paths)[source]

Get the values at each of the provided paths.

Parameters

paths – Map from keys to paths.

Returns

A dictionary with the same keys as paths. Each key is mapped to the value at the associated path.

inner_value(key)[source]

Get the value of an inner state

insert(insertion)[source]
move(move, process_store)[source]
outer_path(path, source=None)[source]

Address a topology with the _path keyword if present, establishing a path to this node and using it as the starting point for future path operations.

path_for()[source]

Find the path to this node.

path_to(to)[source]

return a path from self to the given Store

recursive_end_process(value)[source]
schema_keys = {'_default', '_emit', '_properties', '_serializer', '_updater', '_value'}
schema_topology(schema, topology)[source]

Fill in the structure of the given schema with the connected stores according to the given topology.

set_emit_value(path=None, emit=False)[source]

Turn on/off emits for all inner nodes of path.

set_emit_values(paths=None, emit=False)[source]

Turn on/off emits for all inner nodes of the list of paths.

set_path(path, value)[source]

Set a value at a path in the hierarchy.

Parameters
  • path – The path relative to self where the value should be set.

  • value – The value to set. The store node at path will hold value when this function returns.

set_value(value)[source]

Set the value for the given tree elements directly instead of using the updaters from their nodes.

state_for(path, keys)[source]

Get the value of a state at a given path

top()[source]

Find the top of this tree.

topology_state(topology)[source]

Fill in the structure of the given topology with the values at all the paths the topology points at. Essentially, anywhere in the topology that has a tuple path will be filled in with the value at that path.

This is the inverse function of the standalone inverse_topology.

vivarium.core.store.convert_path(path)[source]
vivarium.core.store.generate_state(processes: Dict[str, Any], topology: Dict[str, Union[Tuple[str, ], dict, object]], initial_state: Optional[Dict[str, Any]], steps: Optional[Dict[str, Any]] = None, flow: Optional[Dict[str, Sequence[Tuple[str, ]]]] = None)vivarium.core.store.Store[source]

Initialize a simulation’s state.

Parameters
  • processes – Simulation processes.

  • topology – Topology linking process ports to stores.

  • initial_state – Initial simulation state. Omitted variables will be assigned values based on schema defaults.

Returns

Initialized state.

vivarium.core.store.hierarchy_depth(hierarchy, path=())[source]

Create a mapping of every path in the hierarchy to the node living at that path in the hierarchy.

vivarium.core.store.insert_topology(topology, port_path, target_path)[source]
vivarium.core.store.key_for_value(d, looking)[source]

Get the key associated with a value in a dictionary.

Only top-level keys are searched.

Parameters
  • d – The dictionary.

  • looking – The value to look for.

Returns

The associated key, or None if no key found.

vivarium.core.store.topology_path(topology, path)[source]

get the subtopology at the path inside the given topology.

vivarium.core.store.view_values(states: dict)Dict[str, Any][source]
Types
vivarium.core.types.CompositeDict

A dictionary that specifies the processes and topology of a composite.

alias of Dict[str, Any]

vivarium.core.types.Flow

Mapping from step names to sequences of HierarchyPaths that specify the step’s dependencies.

alias of Dict[str, Sequence[Tuple[str, …]]]

vivarium.core.types.HierarchyPath

Relative path between nodes in the hierarchy. Like Unix file paths, “..” refers to the parent directory.

alias of Tuple[str, …]

vivarium.core.types.OutputDict

A dictionary that contains the retrieved output of an experiment

alias of Dict[Union[Tuple, str], Any]

vivarium.core.types.Processes

Mapping from processes names to Processes, which can be embedded in a hierarchy.

alias of Dict[str, Any]

vivarium.core.types.Schema

A dictionary that specifies a schema.

alias of Dict[str, Any]

vivarium.core.types.State

A dictionary that has the form of a schema, except instead of specifying the properties of each variable, it specifies each variable’s value.

alias of Dict[str, Any]

vivarium.core.types.Steps

Mapping from step names to Steps, which can be embedded in a hierarchy.

alias of Dict[str, Any]

vivarium.core.types.Topology

Mapping from ports to paths that specify which node in the hierarchy should be wired to each port.

alias of Dict[str, Union[Tuple[str, …], dict, object]]

vivarium.core.types.Update

A dictionary defining an update.

alias of Dict[str, Any]

Processes Package

Tree Mass
class vivarium.processes.tree_mass.TreeMass(parameters=None)[source]

Bases: vivarium.core.process.Step

Derive total mass from molecular counts and weights.

Parameters

parameters (dict) –

Dictionary of parameters. The following keys are required:

  • from_path (tuple): Path to the root of the subtree whose mass will be summed.

defaults: Dict[str, Any] = {'from_path': ('..', '..'), 'initial_mass': <Quantity(0, 'femtogram')>}
initial_state(config=None)[source]
name = 'mass_deriver'
next_update(timestep, states)[source]

Return a _reduce update to store the total mass.

Store mass in ('global', 'mass').

ports_schema()[source]
vivarium.processes.tree_mass.calculate_mass(value, path, node)[source]

Reducer for summing masses in hierarchy

Parameters
  • value – The value to add mass to.

  • path – Unused.

  • node – The node whose mass will be added.

Returns

The mass of the node (accounting for the node’s molecular weight, which should be stored in its mw property) added to value.

Library Package

Utilities for Formatting Output
vivarium.library.pretty.format_dict(d, sort_keys=True)[source]

Format a dict as a pretty string

Aside from the normal JSON-serializable data types, data of type numpy.int64 are supported.

For example:

>>> import numpy as np
>>> d = {
...     'foo': {
...         'bar': 1,
...         '3.0': np.int64(5),
...     },
...     'a': 'hi!',
...     'quantity': 1 * units.fg,
...     'unit': units.fg,
... }
>>> print(format_dict(d))
{
    "a": "hi!",
    "foo": {
        "3.0": 5,
        "bar": 1
    },
    "quantity": "1 femtogram",
    "unit": "<Unit('femtogram')>"
}
Parameters
  • d – The dictionary to format

  • sort_keys – Whether to sort the dictionary keys. This is useful for reproducible output.

Returns

A string of the prettily-formatted dictionary

Topology Utilities
class vivarium.library.topology.TestUpdateIn[source]

Bases: object

d = {'bar': {'c': 'd'}, 'foo': {1: {'a': 'b'}}}
vivarium.library.topology.assoc_path(d, path, value)[source]

Insert value into the dictionary d at path.

>>> d = {'a': {'b': 'c'}}
>>> assoc_path(d, ('a', 'd'), 'e')
{'a': {'b': 'c', 'd': 'e'}}
>>> d
{'a': {'b': 'c', 'd': 'e'}}

Create new dictionaries recursively as needed.

vivarium.library.topology.convert_path_style(path)[source]
vivarium.library.topology.delete_in(d, path)[source]

Delete an item from a dictionary by its path.

>>> d = {'a': {'b': 'c', 'd': 'e'}}
>>> delete_in(d, ('a', 'b'))
>>> d
{'a': {'d': 'e'}}
vivarium.library.topology.dict_to_paths(root, d)[source]

Get all the paths in a dictionary.

For example:

>>> root = ('root', 'subroot')
>>> d = {
...     'a': {
...         'b': 'c',
...     },
...     'd': 'e',
... }
>>> dict_to_paths(root, d)
[(('root', 'subroot', 'a', 'b'), 'c'), (('root', 'subroot', 'd'), 'e')]
vivarium.library.topology.get_in(d, path, default=None)[source]

Get the value from a dictionary by its path.

>>> d = {'a': {'b': 'c', 'd': 'e'}}
>>> get_in(d, ('a', 'b'))
'c'
>>> get_in(d, ('a', 'z'))
>>> get_in(d, ('a', 'z'), 'y')
'y'
vivarium.library.topology.inverse_topology(outer, update, topology, inverse=None, multi_updates=True)[source]

Transform an update from the form its process produced into one aligned to the given topology.

The inverse of this function (using a topology to construct a view for the perspective of a Process ports_schema()) lives in Store, called topology_state. This one stands alone as it does not require a store to calculate.

vivarium.library.topology.normalize_path(path)[source]

Make a path absolute by resolving .. elements.

vivarium.library.topology.paths_to_dict(path_list, f=<function <lambda>>)[source]

Create a new dictionary that has the paths in path_list.

Parameters
  • path_list – A list of tuples (path, value).

  • f – A function to apply to each value before inserting it into the dictionary.

Returns

A new dictionary with the specified values (after being passed through f) at each associated path.

vivarium.library.topology.update_in(d, path, f)[source]

Update every value in a dictionary based on f.

Parameters
  • d – The dictionary path applies to. This object is not modified.

  • path – Path to the sub-dictionary within d that should be updated.

  • f – Function to call on every value in the dictionary to update. The updated dictionary’s values will be return values from f.

Returns

A copy of d with all the values under path updated to the value returned when f is called on the original value.

Datum
class vivarium.library.datum.Datum(config)[source]

Bases: dict

The Datum class enables functions to be defined on dicts of a certain schema. It provides two class level variables:

  • defaults: a dictionary of keys to default values this Datum will have if none is provided to __init__

  • schema: a dictionary of keys to constructors which invoke subdata.

Once these are defined, a Datum subclass can be constructed with a dict that provides any values beyond the defaults, and then all of the defined methods for that Datum subclass are available to operate on its values. Once the modifications are complete, it can be rendered back into a dict using the to_dict() method.

defaults: Dict[str, Any] = {}
fields()[source]

Get the keys in the datum.

schema: Dict[str, Callable] = {}
to_dict()[source]

Convert the datum to a dictionary.

Plotting Utilities Package

Simulation Output Utilities
vivarium.plots.simulation_output.get_variable_title(path)[source]

Get figure title from a variable path.

Parameters

path – Path to the variable.

Returns

String representation of the variable suitable for a figure title.

vivarium.plots.simulation_output.plot_simulation_output(timeseries_raw, settings: Optional[Dict[str, Any]] = None, out_dir=None, filename='simulation')[source]

Plot simulation output, with rows organized into separate columns.

Arguments::

timeseries (dict): This can be obtained from simulation output with convert_to_timeseries() settings (dict): Accepts the following keys:

  • column_width (int): the width (inches) of each column in the figure

  • max_rows (int): ports with more states than this number of states get wrapped into a new column

  • remove_zeros (bool): if True, timeseries with all zeros get removed

  • remove_flat (bool): if True, timeseries with all the same value get removed

  • remove_first_timestep (bool): if True, skips the first timestep

  • skip_ports (list): entire ports that won’t be plotted

  • show_state (list): with [('port_id', 'state_id')] for all states that will be highlighted, even if they are otherwise to be removed TODO: Obsolete?

vivarium.plots.simulation_output.plot_variables(output, variables, column_width=8, row_height=1.2, row_padding=0.8, linewidth=3.0, sci_notation=False, default_color='tab:blue', out_dir=None, filename='variables')[source]

Create a simple figure with a timeseries for every variable.

Parameters
  • output – Simulation output as a map from variable names or paths to timeseries data. Should contain a time key whose value is a list of time points.

  • variables – The variables to plot. May be a list of variable names (if simulation output keys are just variable names) or a dictionary with keys variable (for the variable path), color (for the color to use for the plot), and display (the variable name to display). If display is not provided, the result of calling get_variable_title() on the variable path is used.

  • column_width – Figure width.

  • row_height – Height of each row. Each variable gets one row.

  • row_padding – Space between rows.

  • linewidth – Width of timeseries lines.

  • sci_notation – Either False for no scientific notation or an integer \(x\) such that scientific notation will be used for values outside the range \([10^{-x}, 10^x]\).

  • default_color – Default timeseries color.

  • out_dir – Output directory.

  • filename – Output filename.

Returns

The figure.

vivarium.plots.simulation_output.set_axes(ax, show_xaxis=False, sci_notation=False, y_offset=0.0)[source]

Set up plot axes.

Parameters
  • ax – The axes to set up.

  • show_xaxis – Whether to show the x axis.

  • sci_notation – Either False to not use scientific notation or an integer \(x\) such that scientific notation will be used outside the range \([10^{-x}, 10^x]\).

  • y_offset – Horizontal distance between axis offset text (typically for scientific notation) and the y-axis.

Glossary

Note

Some fully-capitalized words and phrases have the meanings specified in RFC 2119.

ABM
Agent-Based Model
Agent-based model
agent-based model
Agent-Based Models
Agent-based models
agent-based models

Agent-based modeling is a modeling paradigm in which population-level phenomena are emergent from interactions among simple agents. An agent-based model is a model constructed using this paradigm.

Boundary Store
boundary store
Boundary Stores
boundary stores

Compartments interact through boundary stores that represent how the compartments affect each other. For example, between an environment compartment and a cell compartment, there might be a boundary store to track the flux of metabolites from the cell to the environment and vice versa.

Compartment
compartment
Compartments
compartments

Compartments are composites at a single level of a hierarchy. Each compartment is like an agent in an agent-based model, which can be nested in an environment and interact with neighbors, parent, and child compartments. These interactions are possible through boundary stores that connect internal processes to states outside of the compartment. Thus, a model might contain a compartment for an environment which contains two child compartments for the two cells in the environment. For more details, see our guide to compartments.

composer
Composer

An object with a generate method, which returns a Composite with Processes and Topologies.

Composite
composite
Composites
composites

Composites are dictionaries with special keys for processes and topology. The processes key map to a dictionary with initialized processes, and the topology specifies how they are wire to stores.

Deriver
deriver
Derivers
derivers

Derivers have been deprecated in favor of steps.

Divider
divider
Dividers
dividers

When a cell divides, we have to decide how to generate the states of its daughter cells. Dividers specify how to generate these daughter cells, for example by assigning half of the value of the variable in the mother cell to each of the daughter cells. We assign a divider to each variable in the schema. For more details, see the documentation for vivarium.core.registry.

Embedded Timeseries
Embedded timeseries
embedded timeseries

An embedded timeseries has nearly the same shape as a simulation state dictionary, only each variable’s value is a list of values over time, and there is an additional time key. For details, see the guide on emitters.

Emitter
emitter
Emitters
emitters

While a simulation is running, the current state is stored in stores, but this information is overwritten at each timestep with an updated state. When we want to save off variable values for later analysis, we send these data to one of our emitters, each of which formats the data for a storage medium, for example a database or a Kafka message. We then query the emitter to get the formatted data.

Exchange
exchange
Exchanges
exchanges

The flux between a cell and its environment. This is stored in a boundary store.

Engine
engine
Experiment
experiment
Experiments
experiments

Vivarium defines simulations using vivarium.core.engine.Engine objects. These simulations can contain arbitrarily nested compartments, and you can run them to simulate your model over time. See the documentation for the Engine class and our guide to experiments for more details.

Flow
flow
Flows
flows

Flows specify dependency graphs for running steps. They have the same structure as topologies, but instead of their leaf values being paths, they are lists of paths where each path specifies a dependency step.

Inner
inner

A once-removed internal node position relative to a given node in the tree. Nodes can have multiple inners connected to them. The reciprocal relationship is an outer, but a node can have at most one outer.

Masking
masking

When Vivarium passes stores to processes, it includes only the variables the process has requested. We call this filtering masking.

MSM
Multiscale Model
Multiscale model
multiscale model
Multiscale Models
Multiscale models
multiscale models

Multiscale models use different spatial and temporal scales for their component sub-models. For example, Vivarium models a cell’s internal processes and the interactions between cells and their environment at different temporal scales since these processes require different degrees of temporal precision.

Outer
outer

A once-removed external node position relative to a given node in the tree. Each node, except for the top-most node, has one outer node that it exists within. The reciprocal relationship is an inner, but a node can have many inners.

Path
path
Paths
paths

In its most general form, a path specifies an item in a nested dictionary as a tuple of dictionary keys. For example, consider the following nested dictionary:

{
    'a': {
        'b': {
            'c': None,
        },
        'd': {},
    },
}

In this example, the path ('a', 'b', 'c') points to None.

In Vivarium, we often use paths to identify nodes within the hierarchy. This works because the hierarchy is just a collection of nested dictionaries.

Path Timeseries
Path timeseries
path timeseries

A path timeseries is a flattened form of an embedded timeseries where keys are paths in the simulation state dictionary and values are lists of the variable value over time. We describe simulation data formats in more detail in our guide to emitters.

Port
port
Ports
ports

When a process needs access to part of the model state, it will be provided a store. The ports of a process are what the process calls those stores. When running a process, you provide a store to each of the process’s ports. Think of the ports as physical ports into which a cable to a store can be plugged.

Process
process
Processes
processes

A process in Vivarium models a cellular process by defining how the state of the model should change at each timepoint, given the current state of the model. During the simulation, each process is provided with the current state of the model and the timestep, and the process returns an update that changes the state of the model. Each process is an instance of a process class.

To learn how to write a process, check out our process-writing tutorial. For a detailed guide to processes, see our guide to processes.

Process Class
Process class
process class
Process Classes
Process classes
process classes

A process class is a Python class that defines a process’s model. These classes can be instantiated, and optionally configured, to create processes. Each process class must subclass either vivarium.core.process.Process or another process class.

Process Command
Process command
process command
Process Commands
Process commands
process commands

Instructions that let Vivarium communicate with parallel processes in a remote-procedure-call-like fashion. See the processes guide for details.

Raw Data
Raw data
raw data

The primary format for simulation data is “raw data.” See the guide on emitters.

Schema
schema
Schemas
schemas

A schema defines the properties of a set of variables by associating with each variable a set of schema key-value pairs.

Schema Key
Schema key
schema key
Schema Keys
Schema keys
schema keys
Schema Value
Schema value
schema value
Schema Values
Schema values
schema values
Schema Key-Value Pair
Schema key-value pair
schema key-value pair
Schema Key-Value Pairs
Schema key-value pairs
schema key-value pairs

Each variable is defined by a set of schema key-value pairs. The available keys are defined in vivarium.core.store.Store.schema_keys. These keys are described in more detail in the documentation for vivarium.core.store.Store.

Serializer
serializer
Serializers
serializers

A serializer is an object that converts data of a certain type into a format that can transmitted and stored.

Step
step
Steps
steps

Steps run after all processes have run for a timepoint and compute values from the state of the model. These steps may have dependencies on each other, and these dependencies are specified in the flow key of vivarium.core.composer.Composite dictionaries. For more information, see the documentation for vivarium.core.composer.Composer.

Store
store
Stores
stores

The state of the model is broken down into stores, each of which represents the state of some physical or conceptual subset of the overall state. For example, a cell model might have a store for the proteins in the cytoplasm, another for the transcripts in the cytoplasm, and one for the transcripts in the nucleus. Each variable must belong to exactly one store.

Store API
store API

An experimental API that allows for simulations to be built up from stores.

Template
template
Templates
templates

A template describes a genetic element, its binding site, and the available downstream termination sites on genetic material. A chromosome has operons as its templates which include sites for RNA binding and release. An mRNA transcript also has templates which describe where a ribosome can bind and will subsequently release the transcript. Templates are defined in template specifications.

Template Specification
Template specification
template specification
Template Specifications
Template specifications
template specifications

Template specifications define templates as dict objects with the following keys:

  • id (str): The template name. You SHOULD use the name of the associated operon or transcript.

  • position (int): The index in the genetic sequence of the start of the genetic element being described. In a chromosome, for example, this would denote the start of the modeled operon’s promoter. On mRNA transcripts (where we are describing how ribosomes bind), this SHOULD be set to 0.

  • direction (int): 1 if the template should be read in the forward direction, -1 to proceed in the reverse direction. For mRNA transcripts, this SHOULD be 1.

  • sites (list): A list of binding sites. Each binding site is specified as a dict with the following keys:

    • position (int): The offset in the sequence from the template position to the start of the binding site. This value is not currently used and MAY be set to 0.

    • length (int): The length, in base-pairs, of the binding site. This value is not currently used and MAY be set to 0.

    • thresholds (list): A list of tuples, each of which has a factor name as the first element and a concentration threshold as the second. When the concentration of the factor exceeds the threshold, the site will bind the factor. For example, in an operon the factor would be a transcription factor.

  • terminators (list): A list of terminators, which halt reading of the template. As such, which genes are encoded on a template depends on which terminator halts transcription or translation. Each terminator is specified as a dict with the following keys:

    • position (int): The index in the genetic sequence of the terminator.

    • strength (int): The relative strength of the terminator. For example, if there remain two terminators ahead of RNA polymerase, the first of strength 3 and the second of strength 1, then there is a 75% chance that the polymerase will stop at the first terminator. If the polymerase does not stop, it is guaranteed to stop at the second terminator.

    • products (list): A list of the genes that will be transcribed or translated should transcription/translation halt at this terminator.

Timepoint
timepoint
Timepoints
timepoints

We discretize time into timepoints and update the model state at each timepoint. We collect data from the model at each timepoint. Note that each compartment may be running with different timesteps depending on how finely we need to discretize time.

Timeseries
timeseries

“Timeseries” can refer to the general way in whcih we store simulation data or to an embedded timeseries. See the guide on emitters for details.

Timestep
timestep
Timesteps
timesteps

The amount of time elapsed between two timepoints. This is the amount of time for which processes compute an update. For example, if we discretize time into two-second intervals, then each process will be asked to compute an update for how the state changes over the next two seconds. The timestep is two seconds.

Topology
topology
Topologies
topologies

A topology defines how stores are associated to ports. This tells Vivarium which store to pass to each port of each process during the simulation. See the constructor documentation for vivarium.core.engine.Engine for a more detailed specification of the form of a topology.

Hierarchy
hierarchy
Hierarchies
hierarchies
Compartment Hierarchy
compartment hierarchy
Tree
tree
Trees
trees

We nest the stores of a model to form a tree called a hierarchy. Each internal node is a store and each leaf node is a variable. This tree can be traversed like a directory tree, and stores are identified by paths. For details see the hierarchy guide. Note that this used to be called a tree.

Update
update
Updates
updates

An update describes how the model state should change due to the influence of a process over some period of time (usually a timestep).

Updater
updater
Updaters
updaters

An updater describes how an update should be applied to the model state to produce the updated state. For example, the update could be added to the old value or replace it. Updaters are described in more detail in the documentation for vivarium.core.registry.

Variable
variable
Variables
variables

The state of the model is a collection of variables. Each variable stores a piece of information about the full model state. For example, the concentration of glucose in the cytoplasm might be a variable, while the concentration of glucose-6-phosphate in the cytoplasm is another variable. The extracellular concentration of glucose might be a third variable. As these examples illustrate, variables are often track the amount of a molecule in a physical region. Exceptions exist though, for instance whether a cell is dead could also be a variable.

Each variable is defined by a set of schema key-value pairs.

WCM
Whole-Cell Model
Whole-cell model
whole-cell model
Whole-Cell Models
Whole-cell models
whole-cell models

Whole-cell models seek to simulate a cell by modeling the molecular mechanisms that occur within it. For example, a cell’s export of antibiotics might be modeled by the transcription of the appropriate genes, translation of the produced transcripts, and finally complexation of the translated subunits. Ideally the simulated phenotype is emergent from the modeled processes, though many such models also include assumptions that simplify the model.

Introduction

A vivarium, literally a “place of life,” is a controlled environment in which organisms can be studied. You might have encountered examples of vivaria like an aquarium or a terrarium. The Vivarium project provides a framework for building biological systems in-silico.

Vivarium does not include any specific modeling frameworks, but instead focuses on the interface between such frameworks, and provides a powerful multiscale simulation engine that combines and runs them. Users of Vivarium can therefore implement any type of model module they prefer – whether it is a custom module of a specific biophysical system, a configurable model with its own standard format, or a wrapper for an off-the-shelf library. The multiscale engine supports complex agents operating at multiple timescales, and facilitates parallel computation across multiple CPUs.

Using This Documentation

If you want to run Vivarium, start with our getting started guide, which will walk you through getting Vivarium up and running.

For step-by-step instructions, check out our tutorials.

For a technical deep-dive into the important concepts in Vivarium, check out our topic guides.

If you want to look something up like the configuration options for some process or the definition of a word, see our reference information.

Citing Vivarium

Please use the following reference when citing Vivarium:
  • Agmon, E., Spangler, R.K., Skalnik, C.J., Poole, W., Morrison, J.H., Peirce, S.M., and Covert, M.W. (2022). Vivarium: an interface and engine for integrative multiscale modeling in computational biology. Bioinformatics, btac049. link to article