Working with conservation laws
Catalyst can detect, and eliminate for differential-equation based models, conserved quantities, i.e. linear combinations of species which are conserved via the chemistry. This functionality is both automatically utilised by Catalyst (e.g. to remove singular Jacobians during steady state computations), but is also available for users to utilise directly (e.g. to potentially improve simulation performance).
To illustrate conserved quantities, let us consider the following two-state model:
using Catalyst
rs = @reaction_network begin
(k₁,k₂), X₁ <--> X₂
end
\[ \begin{align*} \mathrm{X_1} &\xrightleftharpoons[k_2]{k_1} \mathrm{X_2} \end{align*} \]
If we simulate it, we note that while the concentrations of $X₁$ and $X₂$ change throughout the simulation, the total concentration of $X$ ($= X₁ + X₂$) is constant:
using OrdinaryDiffEq, Plots
u0 = [:X₁ => 80.0, :X₂ => 20.0]
ps = [:k₁ => 10.0, :k₂ => 2.0]
oprob = ODEProblem(rs, u0, (0.0, 1.0), ps)
sol = solve(oprob)
plot(sol; idxs = [rs.X₁, rs.X₂, rs.X₁ + rs.X₂], label = ["X₁" "X₂" "X₁ + X₂ (a conserved quantity)"])
This makes sense, as while $X$ is converted between two different forms ($X₁$ and $X₂$), it is neither produced nor degraded. That is, it is a conserved quantity. Next, if we consider the ODE that our model generates:
using Latexify
latexify(rs; form = :ode)
\[\begin{align} \frac{\mathrm{d} X_1\left( t \right)}{\mathrm{d}t} &= - k_1 X_1\left( t \right) + k_2 X_2\left( t \right) \\ \frac{\mathrm{d} X_2\left( t \right)}{\mathrm{d}t} &= k_1 X_1\left( t \right) - k_2 X_2\left( t \right) \end{align} \]
we note that it essentially generates the same equation twice (i.e. $\frac{dX₁(t)}{dt} = -\frac{dX₂(t)}{dt}$). By designating our conserved quantity $X₁ + X₂ = Γ$, we can rewrite our differential equation model as a differential-algebraic equation (with a single differential equation and a single algebraic equation):
\[\frac{dX₁(t)}{dt} = - k₁X₁(t) + k₂(-X₁(t) + Γ) \\ X₂(t) = -X₁(t) + Γ\]
Using Catalyst, it is possible to detect any such conserved quantities and eliminate them from the system. Here, when we convert our ReactionSystem
to an ODESystem
, we provide the remove_conserved = true
argument to instruct Catalyst to perform this elimination:
osys = convert(ODESystem, rs; remove_conserved = true)
\[ \begin{align} \frac{\mathrm{d} X_1\left( t \right)}{\mathrm{d}t} &= - k_1 X_1\left( t \right) + k_2 \left( - X_1\left( t \right) + \Gamma_{1} \right) \end{align} \]
We note that the output system only contains a single (differential) equation and can hence be solved with an ODE solver. The second (algebraic) equation is stored as an observable, and can be retrieved using the observed
function:
observed(osys)
\[ \begin{align} X_2\left( t \right) &= - X_1\left( t \right) + \Gamma_{1} \end{align} \]
Using the unknowns
function we can confirm that the ODE only has a single unknown variable:
unknowns(osys)
1-element Vector{SymbolicUtils.BasicSymbolic{Real}}:
X₁(t)
Next, using parameters
we note that an additional parameter, Γ[1]
has been added to the system:
parameters(osys)
3-element Vector{SymbolicUtils.BasicSymbolic{Real}}:
k₁
k₂
Γ[1]
Here, Catalyst encodes all conserved quantities in a single, vector-valued, parameter called Γ
.
Practically, the remove_conserved = true
argument can be provided when a ReactionSystem
is converted to an ODEProblem
:
using OrdinaryDiffEq, Plots
u0 = [:X₁ => 80.0, :X₂ => 20.0]
ps = [:k₁ => 10.0, :k₂ => 2.0]
oprob = ODEProblem(rs, u0, (0.0, 1.0), ps; remove_conserved = true)
┌ Warning: You are creating a system or problem while eliminating conserved quantities. Please note,
│ due to limitations / design choices in ModelingToolkit if you use the created system to
│ create a problem (e.g. an `ODEProblem`), or are directly creating a problem, you *should not*
│ modify that problem's initial conditions for species (e.g. using `remake`). Changing initial
│ conditions must be done by creating a new Problem from your reaction system or the
│ ModelingToolkit system you converted it into with the new initial condition map.
│ Modification of parameter values is still possible, *except* for the modification of any
│ conservation law constants (Γ), which is not possible. You might
│ get this warning when creating a problem directly.
│
│ You can remove this warning by setting `remove_conserved_warn = false`.
└ @ Catalyst ~/work/Catalyst.jl/Catalyst.jl/src/reactionsystem_conversions.jl:456
Here, while Γ[1]
becomes a parameter of the new system, it has a default value equal to the corresponding conservation law. Hence, its value is computed from the initial condition [:X₁ => 80.0, :X₂ => 20.0]
, and does not need to be provided in the parameter vector. Next, we can simulate and plot our model using normal syntax:
sol = solve(oprob)
plot(sol)
Any species eliminated using remove_conserved = true
will not be plotted by default. However, it can be added to the plot using the idxs
plotting option. E.g. here we would use plot(sol; idxs = [:X₁, :X₂])
to plot both species.
Currently, there is a bug in MTK where the parameters, Γ
, associated with conservation laws are not updated properly in response to remake
(or other problem-updating functions, such as getu
). Hence, problems created using remove_conserved = true
should not be modified, i.e. by changing initial conditions or the values of the Γ
's directly. Instead, to change these values new problems must be generated with the new initial condition map.
While X₂
is an observable (and not unknown) of the ODE, we can access it just like if remove_conserved = true
had not been used:
sol[:X₂]
14-element Vector{Float64}:
20.0
43.56620284189201
56.39450336401671
67.61060115947159
74.28429177308591
78.61346950718746
81.00276209445914
82.28947026842505
82.90853333133784
83.18215980724716
83.28750769854895
83.32210994972924
83.3312120554613
83.33290877375747
Generally, remove_conserved = true
should not change any model workflows. I.e. anything that works without this option should also work when an ODEProblem
is created using remove_conserved = true
.
The remove_conserved = true
option is available when creating SDEProblem
s, NonlinearProblem
s, and SteadyStateProblem
s (and their corresponding systems). However, it cannot be used when creating JumpProblem
s.
Conservation law accessor functions
For any given ReactionSystem
model, we can use conservationlaw_constants
to compute all of a system's conserved quantities:
conservationlaw_constants(rs)
\[ \begin{align} \Gamma_{1} &= X_1\left( t \right) + X_2\left( t \right) \end{align} \]
Next, the conservedequations
function can be used to compute the algebraic equations produced when a system's conserved quantities are eliminated:
conservedequations(rs)
\[ \begin{align} X_2\left( t \right) &= - X_1\left( t \right) + \Gamma_{1} \end{align} \]
Finally, the conservationlaws
function yields a $m \times n$ matrix, where $n$ is a system's number of species, $m$ its number of conservation laws, and element $(i,j)$ corresponds to the copy number of the $j$th species that occurs in the $i$th conserved quantity:
conservationlaws(rs)
1×2 Matrix{Int64}:
1 1
I.e. in this case we have a single conserved quantity, which contains a single copy each of the system's two species.