Compartments¶
To model a whole cell, we need to simulate processes running concurrently and interacting with each other. For example, we might want to model the synthesis of ATP by a metabolism process concurrently with the consumption of ATP by a transport process such as a sodium pump. In Vivarium, we model this by creating a compartment.
Overview¶
Below we see an overview of the topics we will discuss in this document. Processes and stores are combined to form compartments as shown in panel B, and these compartments form a tree known as the hierarchy and shown in panel C.
Processes and Stores¶
A compartment models cellular functions running concurrently. We model each of these functions as a process in the compartment. To let these processes interact, for example by producing and consuming a shared resource like ATP, the processes share parts of the state, called stores. Each store is a collection of variables such as cytoplasmic ATP concentration, and in the compartment we define which process operates on which stores using a topology.
In our ATP example, we might assign a “cytoplasm” store to both the metabolism and sodium pump processes. Now when we simulate the compartment, 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 compartment, 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.ode_expression.ODE_expression
.
We uniquely name each process in the compartment. This lets us include instances of the same process class.
Stores¶
We represent stores with the vivarium.core.experiment.Store
class; see its documentation for further details.
Tip
To see the data held by a store, you can use the
vivarium.core.experiment.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()
.
Ports Make Processes Modular¶
We don’t want process creators to worry about what kind of compartment someone will use their processes in. Conversely, if you are creating a compartment, you should be able to use any processes you like, even if they weren’t written with your use case in mind. Vivarium achieves this modularity with ports.
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 compartment 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 Processes Define Ports¶
A process specifies its port names in its constructor by calling the
superclass (vivarium.core.process.Process
)
constructor. For example, the
vivarium.processes.convenience_kinetics.ConvenienceKinetics
class contains this line:
super(ConvenienceKinetics, self).__init__(ports, parameters)
The ports
variable takes the form of a dictionary with port names as
keys and lists of variable names as values. For example, if ports
looked like this:
{
'cytoplasm': ['ATP', 'sodium'],
'extracellular': ['sodium']
}
then the process would be declaring that it cares about the ATP
and
sodium
variables in the cytoplasm
port and the sodium
variable in the extracellular
port. 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:
{
'cytoplasm': {
'ATP': 5.0,
'sodium': 1e-2,
},
'extracellular': {
'sodium': 1e-1,
},
}
This would happen even if the store linked to the cytoplasm
port
contained more variables. We call this stripping-out of variables the
process doesn’t need masking.
Topologies¶
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 store names 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',
},
}
Example Compartment¶
To put all this information together, let’s take a look at an example
compartment that combines the glucose phosphorylation process from the
process-writing tutorial with
vivarium.processes.injector
, which lets us “inject”
molecules into a store.
class InjectedGlcPhosphorylation(Compartment):
defaults = {
'glucose_phosphorylation': {
'k_cat': 1e-2,
},
'injector': {
'substrate_rate_map': {
'GLC': 1e-4,
'ATP': 1e-3,
},
},
}
def __init__(self, config):
self.config = self.defaults
self.config.update(config)
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.
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 hierarhcy guide.
Compartment Interactions¶
Even though compartments represent segregated sub-models, they still need to interact. We model these 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.