Using Catalyst

In this tutorial we provide an introduction to using Catalyst to specify chemical reaction networks, and then to solve ODE, jump, and SDE models generated from them. At the end we show what mathematical rate laws and transition rate functions (i.e. intensities or propensities) are generated by Catalyst for ODE, SDE and jump process models.

Let's start by using the Catalyst @reaction_network macro to specify a simple chemical reaction network: the well-known repressilator.

We first import the basic packages we'll need:

# If not already installed, first hit "]" within a Julia REPL. Then type:
# add Catalyst DifferentialEquations Plots Latexify

using Catalyst, DifferentialEquations, Plots, Latexify

We now construct the reaction network. The basic types of arrows and predefined rate laws one can use are discussed in detail within the tutorial, The Reaction DSL. Here, we use a mix of first order, zero order, and repressive Hill function rate laws. Note, $\varnothing$ corresponds to the empty state, and is used for zeroth order production and first order degradation reactions:

repressilator = @reaction_network Repressilator begin
    hillr(P₃,α,K,n), ∅ --> m₁
    hillr(P₁,α,K,n), ∅ --> m₂
    hillr(P₂,α,K,n), ∅ --> m₃
    (δ,γ), m₁ <--> ∅
    (δ,γ), m₂ <--> ∅
    (δ,γ), m₃ <--> ∅
    β, m₁ --> m₁ + P₁
    β, m₂ --> m₂ + P₂
    β, m₃ --> m₃ + P₃
    μ, P₁ --> ∅
    μ, P₂ --> ∅
    μ, P₃ --> ∅
end α K n δ γ β μ
Model Repressilator
States (6):
  m₁(t)
  m₂(t)
  m₃(t)
  P₁(t)
  P₂(t)
  P₃(t)
Parameters (7):
  α
  K
  n
  δ
  γ
  β
  μ

showing that we've created a new network model named Repressilator with the listed chemical species and states. @reaction_network returns a ReactionSystem, which we saved in the repressilator variable. It can be converted to a variety of other mathematical models represented as ModelingToolkit.AbstractSystems, or analyzed in various ways using the Catalyst.jl API. For example, to see the chemical species, parameters, and reactions we can use

species(repressilator)
6-element Vector{Term{Real, Base.ImmutableDict{DataType, Any}}}:
 m₁(t)
 m₂(t)
 m₃(t)
 P₁(t)
 P₂(t)
 P₃(t)
parameters(repressilator)
7-element Vector{Sym{Real, Base.ImmutableDict{DataType, Any}}}:
 α
 K
 n
 δ
 γ
 β
 μ

and

reactions(repressilator)
15-element Vector{Reaction}:
 Catalyst.hillr(P₃(t), α, K, n), ∅ --> m₁
 Catalyst.hillr(P₁(t), α, K, n), ∅ --> m₂
 Catalyst.hillr(P₂(t), α, K, n), ∅ --> m₃
 δ, m₁ --> ∅
 γ, ∅ --> m₁
 δ, m₂ --> ∅
 γ, ∅ --> m₂
 δ, m₃ --> ∅
 γ, ∅ --> m₃
 β, m₁ --> m₁ + P₁
 β, m₂ --> m₂ + P₂
 β, m₃ --> m₃ + P₃
 μ, P₁ --> ∅
 μ, P₂ --> ∅
 μ, P₃ --> ∅

We can also use Latexify to see the corresponding reactions in Latex, which shows what the hillr terms correspond to mathematically

latexify(repressilator)

\[ \begin{align*} \require{mhchem} \ce{ \varnothing &->[$\frac{\alpha K^{n}}{K^{n} + P_3^{n}}$] m_1}\\ \ce{ \varnothing &->[$\frac{\alpha K^{n}}{K^{n} + P_1^{n}}$] m_2}\\ \ce{ \varnothing &->[$\frac{\alpha K^{n}}{K^{n} + P_2^{n}}$] m_3}\\ \ce{ m_1 &<=>[$\delta$][$\gamma$] \varnothing}\\ \ce{ m_2 &<=>[$\delta$][$\gamma$] \varnothing}\\ \ce{ m_3 &<=>[$\delta$][$\gamma$] \varnothing}\\ \ce{ m_1 &->[$\beta$] m_1 + P_1}\\ \ce{ m_2 &->[$\beta$] m_2 + P_2}\\ \ce{ m_3 &->[$\beta$] m_3 + P_3}\\ \ce{ P_1 &->[$\mu$] \varnothing}\\ \ce{ P_2 &->[$\mu$] \varnothing}\\ \ce{ P_3 &->[$\mu$] \varnothing} \end{align*} \]

Assuming Graphviz is installed and commandline accessible, within a Jupyter notebook we can also graph the reaction network by

g = Graph(repressilator)

giving

Repressilator solution

The network graph shows a variety of information, representing each species as a blue node, and each reaction as an orange dot. Black arrows from species to reactions indicate reactants, and are labelled with their input stoichiometry. Similarly, black arrows from reactions to species indicate products, and are labelled with their output stoichiometry. In contrast, red arrows from a species to reactions indicate the species is used within the reactions' rate expressions. For the repressilator, the reactions

hillr(P₃,α,K,n), ∅ --> m₁
hillr(P₁,α,K,n), ∅ --> m₂
hillr(P₂,α,K,n), ∅ --> m₃

have rates that depend on the proteins, and hence lead to red arrows from each Pᵢ.

Note, from the REPL or scripts one can always use savegraph to save the graph (assuming Graphviz is installed).

Mass Action ODE Models

Let's now use our ReactionSystem to generate and solve a corresponding mass action ODE model. We first convert the system to a ModelingToolkit.ODESystem by

odesys = convert(ODESystem, repressilator)

\[ \begin{align} \frac{dm_1(t)}{dt} =& \gamma - \delta m_1\left( t \right) + \mathrm{hillr}\left( P_3\left( t \right), \alpha, K, n \right) \\ \frac{dm_2(t)}{dt} =& \gamma - \delta m_2\left( t \right) + \mathrm{hillr}\left( P_1\left( t \right), \alpha, K, n \right) \\ \frac{dm_3(t)}{dt} =& \gamma - \delta m_3\left( t \right) + \mathrm{hillr}\left( P_2\left( t \right), \alpha, K, n \right) \\ \frac{dP_1(t)}{dt} =& \beta m_1\left( t \right) - \mu P_1\left( t \right) \\ \frac{dP_2(t)}{dt} =& \beta m_2\left( t \right) - \mu P_2\left( t \right) \\ \frac{dP_3(t)}{dt} =& \beta m_3\left( t \right) - \mu P_3\left( t \right) \end{align} \]

(Here Latexify is used automatically to display odesys in Latex within Markdown documents or notebook environments like Pluto.jl.)

Before we can solve the ODEs, we need to specify the values of the parameters in the model, the initial condition, and the time interval to solve the model on. To do this we need to build mappings from the symbolic parameters and the species to the corresponding numerical values for parameters and initial conditions. We can build such mappings in several ways. One is to use Julia Symbols to specify the values like

pmap  = (:α => .5, :K => 40, :n => 2, :δ => log(2)/120,
         :γ => 5e-3, :β => log(2)/6, :μ => log(2)/60)
u₀map = [:m₁ => 0., :m₂ => 0., :m₃ => 0., :P₁ => 20., :P₂ => 0., :P₃ => 0.]
6-element Vector{Pair{Symbol, Float64}}:
 :m₁ => 0.0
 :m₂ => 0.0
 :m₃ => 0.0
 :P₁ => 20.0
 :P₂ => 0.0
 :P₃ => 0.0

Alternatively, we can use ModelingToolkit symbolic variables to specify these mappings like

@parameters  α K n δ γ β μ
@variables t m₁(t) m₂(t) m₃(t) P₁(t) P₂(t) P₃(t)
psymmap  = (α => .5, K => 40, n => 2, δ => log(2)/120,
         γ => 5e-3, β => 20*log(2)/120, μ => log(2)/60)
u₀symmap = [m₁ => 0., m₂ => 0., m₃ => 0., P₁ => 20., P₂ => 0., P₃ => 0.]
6-element Vector{Pair{Num, Float64}}:
 m₁(t) => 0.0
 m₂(t) => 0.0
 m₃(t) => 0.0
 P₁(t) => 20.0
 P₂(t) => 0.0
 P₃(t) => 0.0

Knowing these mappings we can set up the ODEProblem we want to solve:

# time interval to solve on
tspan = (0., 10000.)

# create the ODEProblem we want to solve
oprob = ODEProblem(repressilator, u₀map, tspan, pmap)
ODEProblem with uType Vector{Float64} and tType Float64. In-place: true
timespan: (0.0, 10000.0)
u0: 6-element Vector{Float64}:
  0.0
  0.0
  0.0
 20.0
  0.0
  0.0

By passing repressilator directly to the ODEProblem, Catalyst has to (internally) call convert(ODESystem, repressilator) again to generate the symbolic ODEs. We could instead pass odesys directly like

oprob2 = ODEProblem(odesys, u₀symmap, tspan, psymmap)
ODEProblem with uType Vector{Float64} and tType Float64. In-place: true
timespan: (0.0, 10000.0)
u0: 6-element Vector{Float64}:
  0.0
  0.0
  0.0
 20.0
  0.0
  0.0

oprob and oprob2 are functionally equivalent, each representing the same underlying problem.

Note

When passing odesys to ODEProblem we needed to use the symbolic variable-based parameter mappings, u₀symmap and psymmap, while when directly passing repressilator we could use either those or the Symbol-based mappings, u₀map and pmap. Symbol-based mappings can always be converted to symbolic mappings using symmap_to_varmap, see the Basic Syntax section for more details.

At this point we are all set to solve the ODEs. We can now use any ODE solver from within the DifferentialEquations.jl package. We'll use the recommended default explicit solver, Tsit5(), and then plot the solutions:

sol = solve(oprob, Tsit5(), saveat=10.)
plot(sol)

We see the well-known oscillatory behavior of the repressilator! For more on the choices of ODE solvers, see the DifferentialEquations.jl documentation.


Stochastic Simulation Algorithms (SSAs) for Stochastic Chemical Kinetics

Let's now look at a stochastic chemical kinetics model of the repressilator, modeling it with jump processes. Here, we will construct a JumpProcesses JumpProblem that uses Gillespie's Direct method, and then solve it to generate one realization of the jump process:

# redefine the initial condition to be integer valued
u₀map = [:m₁ => 0, :m₂ => 0, :m₃ => 0, :P₁ => 20, :P₂ => 0, :P₃ => 0]

# next we create a discrete problem to encode that our species are integer valued:
dprob = DiscreteProblem(repressilator, u₀map, tspan, pmap)

# now, we create a JumpProblem, and specify Gillespie's Direct Method as the solver:
jprob = JumpProblem(repressilator, dprob, Direct(), save_positions=(false,false))

# now, let's solve and plot the jump process:
sol = solve(jprob, SSAStepper(), saveat=10.)
plot(sol)