Compositional Modeling of Reaction Systems
Catalyst supports the construction of models in a compositional fashion, based on ModelingToolkit's subsystem functionality. In this tutorial we'll see how we can construct the earlier repressilator model by composing together three identically repressed genes, and how to use compositional modeling to create compartments.
Compositional Modeling Tooling
Catalyst supports two ModelingToolkit interfaces for composing multiple ReactionSystem
s together into a full model. The first mechanism for extending a system is the extend
command
using Catalyst
basern = @reaction_network rn1 begin
k, A + B --> C
end k
newrn = @reaction_network rn2 begin
r, C --> A + B
end r
@named rn = extend(newrn, basern)
\[ \begin{align*} \require{mhchem} \ce{ A + B &<=>[$k$][$r$] C} \end{align*} \]
Here we extended basern
with newrn
giving a system with all the reactions. Note, if a name is not specified via @named
or the name
keyword then rn
will have the same name as newrn
.
The second main compositional modeling tool is the use of subsystems. Suppose we now add to basern
two subsystems, newrn
and newestrn
, we get a different result:
newestrn = @reaction_network rn3 begin
v, A + D --> 2D
end v
@named rn = compose(basern, [newrn, newestrn])
\[ \begin{align*} \require{mhchem} \ce{ A + B &->[$k$] C}\\ \ce{ rn2_{+}C &->[$rn2_{+}r$] rn2_{+}A + rn2_{+}B}\\ \ce{ rn3_{+}A + rn3_{+}D &->[$rn3_{+}v$] 2 rn3_{+}D} \end{align*} \]
Here we have created a new ReactionSystem
that adds newrn
and newestrn
as subsystems of basern
. The variables and parameters in the sub-systems are considered distinct from those in other systems, and so are namespaced (i.e. prefaced) by the name of the system they come from.
We can see the subsystems of a given system by
ModelingToolkit.get_systems(rn)
2-element Vector{Any}:
ReactionSystem{Nothing, Catalyst.NetworkProperties{Int64, Term{Real, Base.ImmutableDict{DataType, Any}}}}(Reaction[r, C --> A + B], t, Term{Real, Base.ImmutableDict{DataType, Any}}[C(t), A(t), B(t)], Sym{Real, Base.ImmutableDict{DataType, Any}}[r], Dict{Symbol, Any}(:A => A(t), :B => B(t), :r => r, :C => C(t)), Equation[], :rn2, Any[], Dict{Any, Any}(), nothing, nothing, Conserved Equations:
, true)
ReactionSystem{Nothing, Catalyst.NetworkProperties{Int64, Term{Real, Base.ImmutableDict{DataType, Any}}}}(Reaction[v, A + D --> 2*D], t, Term{Real, Base.ImmutableDict{DataType, Any}}[A(t), D(t)], Sym{Real, Base.ImmutableDict{DataType, Any}}[v], Dict{Symbol, Any}(:A => A(t), :D => D(t), :v => v), Equation[], :rn3, Any[], Dict{Any, Any}(), nothing, nothing, Conserved Equations:
, true)
They naturally form a tree-like structure
using Plots, GraphRecipes
plot(TreePlot(rn), method=:tree, fontsize=12, nodeshape=:ellipse)
We could also have directly constructed rn
using the same reaction as in basern
as
@parameters k
@variables t, A(t), B(t), C(t)
rxs = [Reaction(k, [A,B], [C])]
@named rn = ReactionSystem(rxs, t; systems = [newrn, newestrn])
\[ \begin{align*} \require{mhchem} \ce{ A + B &->[$k$] C}\\ \ce{ rn2_{+}C &->[$rn2_{+}r$] rn2_{+}A + rn2_{+}B}\\ \ce{ rn3_{+}A + rn3_{+}D &->[$rn3_{+}v$] 2 rn3_{+}D} \end{align*} \]
Catalyst provides several different accessors for getting information from a single system, or all systems in the tree. To get the species, parameters, and equations only within a given system (i.e. ignoring subsystems), we can use
ModelingToolkit.get_states(rn)
3-element Vector{Term{Real, Base.ImmutableDict{DataType, Any}}}:
A(t)
B(t)
C(t)
ModelingToolkit.get_ps(rn)
1-element Vector{Sym{Real, Base.ImmutableDict{DataType, Any}}}:
k
ModelingToolkit.get_eqs(rn)
1-element Vector{Reaction}:
k, A + B --> C
To see all the species, parameters and reactions in the tree we can use
species(rn) # or states(rn)
8-element Vector{Term{Real, Base.ImmutableDict{DataType, Any}}}:
A(t)
B(t)
C(t)
rn2₊C(t)
rn2₊A(t)
rn2₊B(t)
rn3₊A(t)
rn3₊D(t)
parameters(rn) # or reactionparameters(rn)
3-element Vector{Sym{Real, Base.ImmutableDict{DataType, Any}}}:
k
rn2₊r
rn3₊v
reactions(rn) # or equations(rn)
3-element Vector{Reaction}:
k, A + B --> C
rn2₊r, rn2₊C --> rn2₊A + rn2₊B
rn3₊v, rn3₊A + rn3₊D --> 2*rn3₊D
If we want to collapse rn
down to a single system with no subsystems we can use
flatrn = Catalyst.flatten(rn)
\[ \begin{align*} \require{mhchem} \ce{ A + B &->[$k$] C}\\ \ce{ rn2_{+}C &->[$rn2_{+}r$] rn2_{+}A + rn2_{+}B}\\ \ce{ rn3_{+}A + rn3_{+}D &->[$rn3_{+}v$] 2 rn3_{+}D} \end{align*} \]
where
ModelingToolkit.get_systems(flatrn)
Any[]
More about ModelingToolkit's interface for compositional modeling can be found in the ModelingToolkit docs.
Compositional Model of the Repressilator
Let's apply the tooling we've just seen to create the repressilator in a more modular fashion. We start by defining a function that creates a negatively repressed gene, taking the repressor as input
function repressed_gene(; R, name)
@reaction_network $name begin
hillr($R,α,K,n), ∅ --> m
(δ,γ), m <--> ∅
β, m --> m + P
μ, P --> ∅
end α K n δ γ β μ
end
repressed_gene (generic function with 1 method)
Here we assume the user will pass in the repressor species as a ModelingToolkit variable, and specify a name for the network. We use Catalyst's interpolation ability to substitute the value of these variables into the DSL (see Interpolation of Julia Variables). To make the repressilator we now make three genes, and then compose them together
@variables t, G3₊P(t)
@named G1 = repressed_gene(; R=ParentScope(G3₊P))
@named G2 = repressed_gene(; R=ParentScope(G1.P))
@named G3 = repressed_gene(; R=ParentScope(G2.P))
@named repressilator = ReactionSystem(t; systems=[G1,G2,G3])
\[ \begin{align*} \require{mhchem} \ce{ \varnothing &<=>[$\frac{G1_+\alpha G1_{+}K^{G1_{+}n}}{G1_{+}K^{G1_{+}n} + G3_{+}P^{G1_{+}n}}$][$G1_+\delta$] G1_{+}m}\\ \ce{ \varnothing &->[$G1_+\gamma$] G1_{+}m}\\ \ce{ G1_{+}m &->[$G1_+\beta$] G1_{+}m + G1_{+}P}\\ \ce{ G1_{+}P &->[$G1_+\mu$] \varnothing}\\ \ce{ \varnothing &<=>[$\frac{G2_+\alpha G2_{+}K^{G2_{+}n}}{G2_{+}K^{G2_{+}n} + G1_{+}P^{G2_{+}n}}$][$G2_+\delta$] G2_{+}m}\\ \ce{ \varnothing &->[$G2_+\gamma$] G2_{+}m}\\ \ce{ G2_{+}m &->[$G2_+\beta$] G2_{+}m + G2_{+}P}\\ \ce{ G2_{+}P &->[$G2_+\mu$] \varnothing}\\ \ce{ \varnothing &<=>[$\frac{G3_+\alpha G3_{+}K^{G3_{+}n}}{G3_{+}K^{G3_{+}n} + G2_{+}P^{G3_{+}n}}$][$G3_+\delta$] G3_{+}m}\\ \ce{ \varnothing &->[$G3_+\gamma$] G3_{+}m}\\ \ce{ G3_{+}m &->[$G3_+\beta$] G3_{+}m + G3_{+}P}\\ \ce{ G3_{+}P &->[$G3_+\mu$] \varnothing} \end{align*} \]
Notice, in this system each gene is a child node in the system graph of the repressilator
plot(TreePlot(repressilator), method=:tree, fontsize=12, nodeshape=:ellipse)
In building the repressilator we needed to use two new features. First, we needed to create a symbolic variable that corresponds to the protein produced by the third gene before we created the corresponding system. We did this via @variables t, G3₊P(t)
. We also needed to set the scope where each repressor lived. Here ParentScope(G3₊P)
, ParentScope(G1.P)
, and ParentScope(G2.P)
signal Catalyst that these variables will come from parallel systems in the tree that have the same parent as the system being constructed (in this case the top-level repressilator
system).
Compartment-based Models
Finally, let's see how we can make a compartment-based model. Let's create a simple eukaryotic gene expression model with negative feedback by protein dimers. Transcription and gene inhibition by the protein dimer occur in the nucleus, translation and dimerization occur in the cytosol, and nuclear import and export reactions couple the two compartments. We'll include volume parameters for the nucleus and cytosol, and assume we are working with species having units of number of molecules. Rate constants will have their common concentration units, i.e. if $V$ denotes the volume of a compartment then
Reaction Type | Example | Rate Constant Units | Effective rate constant (units of per time) |
---|---|---|---|
Zero order | $\varnothing \overset{\alpha}{\to} A$ | concentration / time | $\alpha V$ |
First order | $A \overset{\beta}{\to} B$ | (time)⁻¹ | $\beta$ |
Second order | $A + B \overset{\gamma}{\to} C$ | (concentration × time)⁻¹ | $\gamma/V$ |
In our model we'll therefore add the conversions of the last column to properly account for compartment volumes:
# transcription and regulation
nuc = @reaction_network nuc begin
α, G --> G + M
(κ₊/V,κ₋), D + G <--> DG
end α V κ₊ κ₋
# translation and dimerization
cyto = @reaction_network cyto begin
β, M --> M + P
(k₊/V,k₋), 2P <--> D
σ, P --> 0
μ, M --> 0
end β k₊ k₋ V σ μ
# export reactions,
# γ,δ=probability per time to be exported/imported
model = @reaction_network model begin
γ, $(nuc.M) --> $(cyto.M)
δ, $(cyto.D) --> $(nuc.D)
end γ δ
@named model = compose(model, [nuc, cyto])
\[ \begin{align*} \require{mhchem} \ce{ nuc_{+}M &->[$\gamma$] cyto_{+}M}\\ \ce{ cyto_{+}D &->[$\delta$] nuc_{+}D}\\ \ce{ nuc_{+}G &->[$nuc_+\alpha$] nuc_{+}G + nuc_{+}M}\\ \ce{ nuc_{+}D + nuc_{+}G &<=>[$\frac{nuc_+\kappa_+}{nuc_{+}V}$][$nuc_+\kappa_-$] nuc_{+}DG}\\ \ce{ cyto_{+}M &->[$cyto_+\beta$] cyto_{+}M + cyto_{+}P}\\ \ce{ 2 cyto_{+}P &<=>[$\frac{cyto_{+}k_+}{cyto_{+}V}$][$cyto_{+}k_-$] cyto_{+}D}\\ \ce{ cyto_{+}P &->[$cyto_+\sigma$] \varnothing}\\ \ce{ cyto_{+}M &->[$cyto_+\mu$] \varnothing} \end{align*} \]
A graph of the resulting network is
Graph(model)