Skip to content

Commit f9cab9e

Browse files
committed
Init
1 parent bad52ef commit f9cab9e

File tree

6 files changed

+304
-79
lines changed

6 files changed

+304
-79
lines changed

docs/pages.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pages = Any["Home" => "index.md",
77
"catalyst_functionality/constraint_equations.md",
88
"catalyst_functionality/parametric_stoichiometry.md",
99
"catalyst_functionality/network_analysis.md",
10+
"catalyst_functionality/chemistry_related_functionality.md",
1011
"Model creation examples" => Any["catalyst_functionality/example_networks/basic_CRN_examples.md",
1112
"catalyst_functionality/example_networks/hodgkin_huxley_equation.md",
1213
"catalyst_functionality/example_networks/smoluchowski_coagulation_equation.md"]],
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# [Chemistry-related functionality](@id chemistry_functionality)
2+
3+
While Catalyst has primarily been designed around the modelling of biological systems, reaction network models are also common across chemistry. This section describes two types of functionality, that while of general interest, should be especially useful in the modelling of chemical systems.
4+
- The `@compound` option, allowing the user to designate that a specific species is composed of certain subspecies.
5+
- The `balance_reaction` function enabling the user to balance a reactions so the same number of compounds occur on both sides.
6+
7+
## Modelling with compound species
8+
Defining compound species is currently only supported for [programmatic construction](@ref programmatic_CRN_construction) of reactions and reaction network models. To create a compound species, use the `@compound` macro, first designating the compound, followed by its components (and stoichiometries). In this example, we will create a CO₂ compound species, consisting of 1 C species and 2 O species. First we create species corresponding to the components:
9+
```@example chem1
10+
@variables t
11+
@species C(t) O(t)
12+
```
13+
Next, we create the `CO2` compound:
14+
```@example chem1
15+
@compound CO2(t) = C + 2O
16+
```
17+
Here, the compound is the first argument to the macro, followed by its component. The `(t)` indicates that `CO2` is a time-dependant variable. Components with non-unitary stoichiometry have this value written before the component (generally, the rules for designating the components of a compounds are identical to hose of designating the substrates or products of a reaction). The created compound, `CO2`, is a species in every sense, and can be used wherever e.g. `C` could be used:
18+
```@example chem1
19+
isspecies(CO2)
20+
```
21+
However, in its metadata is stored the information of its components, which can be retrieved using the `components` (returning a vector of its component species) and `coefficients` (returning a vector with each component's stoichiometry) functions:
22+
```@example chem1
23+
components(CO2)
24+
```
25+
```@example chem1
26+
coefficients(CO2)
27+
```
28+
Alternatively, we can retrieve the components and their stoichiometric coefficients as a single vector using the `component_coefficients` function:
29+
```@example chem1
30+
component_coefficients(CO2)
31+
```
32+
Finally, it is possible to check whether a species is a compound or not using the `iscompound` function:
33+
```@example chem1
34+
iscompound(CO2)
35+
```
36+
37+
Compound components that are also compounds are allowed, e.g. we can create a carbonic acid compound (H₂CO₃) that consists of CO₂ and H₂O:
38+
```@example chem1
39+
@species H(t)
40+
@compound H2O(t) = 2H + O
41+
@compound H2CO3(t) = CO2 + H2O
42+
```
43+
44+
When multiple compounds are created, they can be created simultaneously using the `@compounds` macro, e.g. the previous code-block could have been executed using:
45+
```@example chem1
46+
@species H(t)
47+
@compounds begin
48+
H2O(t) = 2H + O
49+
H2CO3(t) = CO2 + H2O
50+
end
51+
```
52+
53+
One use defining a species as a compound is that they can be used to balance reactions to that the number of compounds are the same on both side.
54+
55+
## Balancing chemical reactions
56+
Catalyst provides the `balance_reaction` function, which takes a reaction, and returns a balanced version. E.g. let us consider a reaction when carbon dioxide is formed from carbon and oxide `C + O --> CO2`. Here, `balance_reaction` enable us to find coefficients creating a balanced reaction (in this case, where the number of carbon and oxygen atoms are teh same on both sides). To demonstrate, we first created the unbalanced reactions:
57+
```@example chem1
58+
rx = @reaction k, C + O --> $CO2
59+
```
60+
Here, we set a reaction rate `k` (which is not involved in the reaction balancing). We also use interpolation of `CO2`, ensuring that the `CO2` used in the reaction is the same one we previously defined as a compound of `C` and `O`. Next, we call the `balance_reaction` function
61+
```@example chem1
62+
balance_reaction(rx)
63+
```
64+
which correctly finds the (rather trivial) solution `C + 2O --> CO2`. Here we note that `balance_reaction` actually returns a vector. The reason is that the reaction balancing problem may have several solutions. Typically, there is only a single solution (in which case this is the vector's only element).
65+
66+
Let us consider a more elaborate example, the reaction between ammonia (NH₃) and oxygen (O₂) to form nitrogen monoxide (NO) and water (H₂O). Let us first create the components and the unbalanced reaction:
67+
```@example chem2
68+
using Catalyst # hide
69+
@variables t
70+
@species N(t) H(t) O(t)
71+
@compounds begin
72+
NH3(t) = N + 3H
73+
O2(t) = 2O
74+
NO(t) = N + O
75+
H2O(t) = 2H + O
76+
end
77+
unbalanced_reaction = @reaction k, $NH3 + $O2 --> $NO + $H2O
78+
```
79+
We can now created a balanced version (where the amount of H, N, and O is the same on both sides):
80+
```@example chem2
81+
balanced_reaction = balance_reaction(unbalanced_reaction)[1]
82+
```
83+
84+
!!! note
85+
Reaction balancing is currently not supported for reactions involving compounds of compounds.

docs/src/catalyst_functionality/constraint_equations.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ $\lambda$. For now we'll assume the cell can grow indefinitely. We'll also keep
1717
track of one protein $P(t)$, which is produced at a rate proportional $V$ and
1818
can be degraded.
1919

20-
## Coupling ODE constraints via extending a system
20+
## [Coupling ODE constraints via extending a system](@id constraint_equations_coupling_constratins)
2121

2222
There are several ways we can create our Catalyst model with the two reactions
2323
and ODE for $V(t)$. One approach is to use compositional modeling, create

src/Catalyst.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ export Graph, savegraph, complexgraph
102102

103103
# for creating compounds
104104
include("chemistry_functionality.jl")
105-
export @compound
106-
export components, iscompound, coefficients
105+
export @compound, @compounds
106+
export iscompound, components, coefficients, component_coefficients
107107
export balance_reaction
108108

109109
### Extensions ###

src/chemistry_functionality.jl

Lines changed: 152 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
### Declares Compound Related Metadata ###
12
struct CompoundSpecies end
23
struct CompoundComponents end
34
struct CompoundCoefficients end
@@ -6,93 +7,138 @@ Symbolics.option_to_metadata_type(::Val{:iscompound}) = CompoundSpecies
67
Symbolics.option_to_metadata_type(::Val{:components}) = CompoundComponents
78
Symbolics.option_to_metadata_type(::Val{:coefficients}) = CompoundCoefficients
89

9-
macro compound(species_expr, arr_expr...)
10-
# Ensure the species name is a valid expression
11-
if !(species_expr isa Expr && species_expr.head == :call)
12-
error("Invalid species name in @compound macro")
13-
end
10+
### Create @compound Macro(s) ###
1411

15-
# Parse the species name to extract the species name and argument
16-
species_name = species_expr.args[1]
17-
species_arg = species_expr.args[2]
18-
19-
# Construct the expressions that define the species
20-
species_expr = Expr(:macrocall, Symbol("@species"), LineNumberNode(0),
21-
Expr(:call, species_name, species_arg))
22-
23-
# Construct the expression to set the iscompound metadata
24-
setmetadata_expr = :($(species_name) = ModelingToolkit.setmetadata($(species_name),
25-
Catalyst.CompoundSpecies,
26-
true))
27-
28-
# Ensure the expressions are evaluated in the correct scope by escaping them
29-
escaped_species_expr = esc(species_expr)
30-
escaped_setmetadata_expr = esc(setmetadata_expr)
31-
32-
# Construct the array from the remaining arguments
33-
arr = Expr(:vect, (arr_expr)...)
34-
coeffs = []
35-
species = []
36-
37-
for expr in arr_expr
38-
if isa(expr, Expr) && expr.head == :call && expr.args[1] == :*
39-
push!(coeffs, expr.args[2])
40-
push!(species, expr.args[3])
41-
else
42-
push!(coeffs, 1)
43-
push!(species, expr)
44-
end
45-
end
12+
"""
13+
@compound
14+
15+
Macro that creates a compound species, which is composed of smaller constituent species.
16+
17+
Example:
18+
```julia
19+
@variables t
20+
@species C(t) O(t)
21+
@compound CO2(t) = C + 2O
22+
```
23+
24+
Notes:
25+
- The constituent species must be defined before using the `@compound` macro.
26+
"""
27+
macro compound(expr)
28+
make_compound(MacroTools.striplines(expr))
29+
end
4630

47-
coeffs_expr = Expr(:vect, coeffs...)
48-
species_expr = Expr(:vect, species...)
31+
# Function managing the @compound macro.
32+
function make_compound(expr)
33+
# Error checks.
34+
!(expr isa Expr) || (expr.head != :(=)) && error("Malformed expression. Compounds should be declared using a \"=\".")
35+
(length(expr.args) != 2) && error("Malformed expression. Compounds should be consists of two expression, separated by a \"=\".")
36+
((expr.args[1] isa Symbol) || (expr.args[1].head != :call)) && error("Malformed expression. There must be a single compound which depend on an independent variable, e.g. \"CO2(t)\".")
37+
38+
# Extracts the composite species name, and a Vector{ReactantStruct} of its components.
39+
species_expr = expr.args[1] # E.g. :(CO2(t))
40+
species_name = expr.args[1].args[1] # E.g. :CO2
41+
composition = Catalyst.recursive_find_reactants!(expr.args[2].args[1], 1, Vector{ReactantStruct}(undef, 0))
42+
components = :([]) # Extra step required here to get something like :([C, O]), rather than :([:C, :O])
43+
foreach(comp -> push!(components.args, comp.reactant), composition) # E.g. [C, O]
44+
coefficients = getfield.(composition, :stoichiometry) # E.g. [1, 2]
45+
46+
# Creates the found expressions that will create the compound species.
47+
# The `Expr(:escape, :(...))` is required so that teh expressions are evaluated in the scope the users use the macro in (to e.g. detect already exiting species).
48+
species_declaration_expr = Expr(:escape, :(@species $species_expr)) # E.g. `@species CO2(t)`
49+
compound_designation_expr = Expr(:escape, :($species_name = ModelingToolkit.setmetadata($species_name, Catalyst.CompoundSpecies, true))) # E.g. `CO2 = ModelingToolkit.setmetadata(CO2, Catalyst.CompoundSpecies, true)`
50+
components_designation_expr = Expr(:escape, :($species_name = ModelingToolkit.setmetadata($species_name, Catalyst.CompoundComponents, $components))) # E.g. `CO2 = ModelingToolkit.setmetadata(CO2, Catalyst.CompoundSpecies, [C, O])`
51+
coefficients_designation_expr = Expr(:escape, :($species_name = ModelingToolkit.setmetadata($species_name, Catalyst.CompoundCoefficients, $coefficients))) # E.g. `CO2 = ModelingToolkit.setmetadata(CO2, Catalyst.CompoundSpecies, [1, 2])`
52+
53+
# Returns the rephrased expression.
54+
return quote
55+
$species_declaration_expr
56+
$compound_designation_expr
57+
$components_designation_expr
58+
$coefficients_designation_expr
59+
end
60+
end
4961

50-
# Construct the expression to set the components metadata
51-
setcomponents_expr = :($(species_name) = ModelingToolkit.setmetadata($(species_name),
52-
Catalyst.CompoundComponents,
53-
$species_expr))
62+
"""
63+
@compounds
5464
55-
# Ensure the expression is evaluated in the correct scope by escaping it
56-
escaped_setcomponents_expr = esc(setcomponents_expr)
65+
Macro that creates several compound species, which each is composed of smaller constituent species. Uses the same syntax as `@compound`, but with one compound species one each line.
5766
58-
# Construct the expression to set the coefficients metadata
59-
setcoefficients_expr = :($(species_name) = ModelingToolkit.setmetadata($(species_name),
60-
Catalyst.CompoundCoefficients,
61-
$coeffs_expr))
67+
Example:
68+
```julia
69+
@variables t
70+
@species C(t) H(t) O(t)
71+
@compounds
72+
CH4(t) = C + 4H
73+
O2(t) = 2O
74+
CO2(t) = C + 2O
75+
H2O(t) = 2H + O
76+
end
77+
```
6278
63-
escaped_setcoefficients_expr = esc(setcoefficients_expr)
79+
Notes:
80+
- The constituent species must be defined before using the `@compound` macro.
81+
"""
82+
macro compounds(expr)
83+
make_compounds(MacroTools.striplines(expr))
84+
end
6485

65-
# Return a block that contains the escaped expressions
66-
return Expr(:block, escaped_species_expr, escaped_setmetadata_expr,
67-
escaped_setcomponents_expr, escaped_setcoefficients_expr)
86+
# Function managing the @compound macro.
87+
function make_compounds(expr)
88+
# Creates a separate compound call for each compound.
89+
compound_calls = [make_compound(line) for line in expr.args] # Crates a vector containing the quote for each compound.
90+
return Expr(:block, vcat([c_call.args for c_call in compound_calls]...)...) # Combines the quotes to a single one. Don't want the double "...". But had problems getting past them for various metaprogramming reasons :(.)
6891
end
6992

70-
# Check if a species is a compound
93+
## Compound Getters ###
94+
95+
"""
96+
iscompound(s)
97+
98+
Returns `true` if the input is a compound species (else false).
99+
"""
71100
iscompound(s::Num) = iscompound(MT.value(s))
72101
function iscompound(s)
73102
MT.getmetadata(s, CompoundSpecies, false)
74103
end
75104

76-
coefficients(s::Num) = coefficients(MT.value(s))
77-
function coefficients(s)
78-
MT.getmetadata(s, CompoundCoefficients)
79-
end
105+
"""
106+
components(s)
80107
108+
Returns a vector with a list of all the components of a compound species (created using e.g. the @compound macro).
109+
"""
81110
components(s::Num) = components(MT.value(s))
82111
function components(s)
83112
MT.getmetadata(s, CompoundComponents)
84113
end
85114

115+
"""
116+
coefficients(s)
117+
118+
Returns a vector with a list of all the stoichiometric coefficients of the components of a compound species (created using e.g. the @compound macro).
119+
"""
120+
coefficients(s::Num) = coefficients(MT.value(s))
121+
function coefficients(s)
122+
MT.getmetadata(s, CompoundCoefficients)
123+
end
124+
125+
"""
126+
component_coefficients(s)
127+
128+
Returns a Vector{Pari{Symbol,Int64}}, listing a compounds species (created using e.g. the @compound macro) all the coefficients and their stoichiometric coefficients.
129+
"""
86130
component_coefficients(s::Num) = component_coefficients(MT.value(s))
87131
function component_coefficients(s)
88132
return [c => co for (c, co) in zip(components(s), coefficients(s))]
89133
end
90134

135+
### Reaction Balancing Functionality ###
91136

92-
### Balancing Code
137+
# Reaction balancing error.
93138
const COMPOUND_OF_COMPOUND_ERROR = ErrorException("Reaction balancing does not currently work for reactions involving compounds of compounds.")
94139

95-
# note this does not correctly handle compounds of compounds currently
140+
# Note this does not correctly handle compounds of compounds currently.
141+
# Internal function used by "balance_reaction" (via "get_balanced_stoich").
96142
function create_matrix(reaction::Catalyst.Reaction)
97143
@unpack substrates, products = reaction
98144
unique_atoms = [] # Array to store unique atoms
@@ -143,6 +189,7 @@ function create_matrix(reaction::Catalyst.Reaction)
143189
return A
144190
end
145191

192+
# Internal function used by "balance_reaction".
146193
function get_balanced_stoich(reaction::Reaction)
147194
# Create the reaction matrix A that is m atoms by n compounds
148195
A = create_matrix(reaction)
@@ -171,6 +218,52 @@ function get_balanced_stoich(reaction::Reaction)
171218
return stoichvecs
172219
end
173220

221+
"""
222+
balance_reaction(reaction::Reaction)
223+
224+
Returns a vector of all possible stoichiometrically balanced `Reaction` objects for the given `Reaction`.
225+
226+
Example:
227+
```julia
228+
@variables t
229+
@species Si(t) Cl(t) H(t) O(t)
230+
@compound SiCl4(t) = Si + 4Cl
231+
@compound H2O(t) = 2H + O
232+
@compound H4SiO4(t) = 4H + Si + 4O
233+
@compound HCl(t) = H + Cl
234+
rx = Reaction(1.0,[SiCl4,H2O],[H4SiO4,HCl])
235+
balance_reaction(rx) # Exactly one solution.
236+
```
237+
238+
```julia
239+
@variables t
240+
@species C(t) H(t) O(t)
241+
@compound CO(t) = C + O
242+
@compound CO2(t) = C + 2O
243+
@compound H2(t) = 2H
244+
@compound CH4(t) = C + 4H
245+
@compound H2O(t) = 2H + O
246+
rx = Reaction(1.0, [CO, CO2, H2], [CH4, H2O])
247+
balance_reaction(rx) # Multiple solutions.
248+
```
249+
250+
```julia
251+
@variables t
252+
@species Fe(t) S(t) O(t) H(t) N(t)
253+
@compound FeS2(t) = Fe + 2S
254+
@compound HNO3(t) = H + N + 3O
255+
@compound Fe2S3O12(t) = 2Fe + 3S + 12O
256+
@compound NO(t) = N + O
257+
@compound H2SO4(t) = 2H + S + 4O
258+
rx = Reaction(1.0, [FeS2, HNO3], [Fe2S3O12, NO, H2SO4])
259+
brxs = balance_reaction(rx) # No solution.
260+
```
261+
262+
Notes:
263+
- Balancing reactions that contain compounds of compounds is currently not supported.
264+
- A reaction may not always yield a single solution; it could have an infinite number of solutions or none at all. When there are multiple solutions, a vector of all possible `Reaction` objects is returned. However, substrates and products may be interchanged as we currently do not solve for a linear combination that maintains the set of substrates and products.
265+
- If the reaction cannot be balanced, an empty `Reaction` vector is returned.
266+
"""
174267
function balance_reaction(reaction::Reaction)
175268
# Calculate the stoichiometric coefficients for the balanced reaction.
176269
stoichiometries = get_balanced_stoich(reaction)

0 commit comments

Comments
 (0)