1
+ # ## Species Structure ###
2
+
3
+ # Catalyst specific symbolics to support SBML
4
+ struct ParameterConstantSpecies end
5
+ struct VariableBCSpecies end
6
+ struct VariableSpecies end
7
+ Symbolics. option_to_metadata_type (:: Val{:isconstantspecies} ) = ParameterConstantSpecies
8
+ Symbolics. option_to_metadata_type (:: Val{:isbcspecies} ) = VariableBCSpecies
9
+ Symbolics. option_to_metadata_type (:: Val{:isspecies} ) = VariableSpecies
10
+
11
+ """
12
+ Catalyst.isconstant(s)
13
+
14
+ Tests if the given symbolic variable corresponds to a constant species.
15
+ """
16
+ isconstant (s:: Num ) = isconstant (MT. value (s))
17
+ function isconstant (s)
18
+ MT. getmetadata (s, ParameterConstantSpecies, false )
19
+ end
20
+
21
+ """
22
+ Catalyst.isbc(s)
23
+
24
+ Tests if the given symbolic variable corresponds to a boundary condition species.
25
+ """
26
+ isbc (s:: Num ) = isbc (MT. value (s))
27
+ function isbc (s)
28
+ MT. getmetadata (s, VariableBCSpecies, false )
29
+ end
30
+
31
+ """
32
+ isspecies(s)
33
+
34
+ Tests if the given symbolic variable corresponds to a chemical species.
35
+ """
36
+ isspecies (s:: Num ) = isspecies (MT. value (s))
37
+ function isspecies (s)
38
+ MT. getmetadata (s, VariableSpecies, false )
39
+ end
40
+
41
+ """
42
+ tospecies(s)
43
+
44
+ Convert the given symbolic variable to be a species by adding the isspecies metadata.
45
+
46
+ Notes:
47
+ - Will error if passed a parameter.
48
+ """
49
+ function tospecies (s)
50
+ MT. isparameter (s) &&
51
+ error (" Parameters can not be converted to species. Please pass a variable." )
52
+ MT. setmetadata (s, VariableSpecies, true )
53
+ end
54
+
55
+ # true for species which shouldn't change from the reactions, including non-species
56
+ # variables
57
+ drop_dynamics (s) = isconstant (s) || isbc (s) || (! isspecies (s))
58
+
59
+
60
+ # ## Reaction Structure ###
61
+
62
+
63
+ """
64
+ $(TYPEDEF)
65
+
66
+ One chemical reaction.
67
+
68
+ # Fields
69
+ $(FIELDS)
70
+
71
+ # Examples
72
+
73
+ ```julia
74
+ using Catalyst
75
+ t = default_t()
76
+ @parameters k[1:20]
77
+ @species A(t) B(t) C(t) D(t)
78
+ rxs = [Reaction(k[1], nothing, [A]), # 0 -> A
79
+ Reaction(k[2], [B], nothing), # B -> 0
80
+ Reaction(k[3],[A],[C]), # A -> C
81
+ Reaction(k[4], [C], [A,B]), # C -> A + B
82
+ Reaction(k[5], [C], [A], [1], [2]), # C -> A + A
83
+ Reaction(k[6], [A,B], [C]), # A + B -> C
84
+ Reaction(k[7], [B], [A], [2], [1]), # 2B -> A
85
+ Reaction(k[8], [A,B], [A,C]), # A + B -> A + C
86
+ Reaction(k[9], [A,B], [C,D]), # A + B -> C + D
87
+ Reaction(k[10], [A], [C,D], [2], [1,1]), # 2A -> C + D
88
+ Reaction(k[11], [A], [A,B], [2], [1,1]), # 2A -> A + B
89
+ Reaction(k[12], [A,B,C], [C,D], [1,3,4], [2, 3]), # A+3B+4C -> 2C + 3D
90
+ Reaction(k[13], [A,B], nothing, [3,1], nothing), # 3A+B -> 0
91
+ Reaction(k[14], nothing, [A], nothing, [2]), # 0 -> 2A
92
+ Reaction(k[15]*A/(2+A), [A], nothing; only_use_rate=true), # A -> 0 with custom rate
93
+ Reaction(k[16], [A], [B]; only_use_rate=true), # A -> B with custom rate.
94
+ Reaction(k[17]*A*exp(B), [C], [D], [2], [1]), # 2C -> D with non constant rate.
95
+ Reaction(k[18]*B, nothing, [B], nothing, [2]), # 0 -> 2B with non constant rate.
96
+ Reaction(k[19]*t, [A], [B]), # A -> B with non constant rate.
97
+ Reaction(k[20]*t*A, [B,C], [D],[2,1],[2]) # 2A +B -> 2C with non constant rate.
98
+ ]
99
+ ```
100
+
101
+ Notes:
102
+ - `nothing` can be used to indicate a reaction that has no reactants or no products.
103
+ In this case the corresponding stoichiometry vector should also be set to `nothing`.
104
+ - The three-argument form assumes all reactant and product stoichiometric coefficients
105
+ are one.
106
+ """
107
+ struct Reaction{S, T}
108
+ """ The rate function (excluding mass action terms)."""
109
+ rate:: Any
110
+ """ Reaction substrates."""
111
+ substrates:: Vector
112
+ """ Reaction products."""
113
+ products:: Vector
114
+ """ The stoichiometric coefficients of the reactants."""
115
+ substoich:: Vector{T}
116
+ """ The stoichiometric coefficients of the products."""
117
+ prodstoich:: Vector{T}
118
+ """ The net stoichiometric coefficients of all species changed by the reaction."""
119
+ netstoich:: Vector{Pair{S, T}}
120
+ """
121
+ `false` (default) if `rate` should be multiplied by mass action terms to give the rate law.
122
+ `true` if `rate` represents the full reaction rate law.
123
+ """
124
+ only_use_rate:: Bool
125
+ """
126
+ Contain additional data, such whenever the reaction have a specific noise-scaling expression for
127
+ the chemical Langevin equation.
128
+ """
129
+ metadata:: Vector{Pair{Symbol, Any}}
130
+ end
131
+
132
+ """
133
+ isvalidreactant(s)
134
+
135
+ Test if a species is valid as a reactant (i.e. a species variable or a constant parameter).
136
+ """
137
+ isvalidreactant (s) = MT. isparameter (s) ? isconstant (s) : (isspecies (s) && ! isconstant (s))
138
+
139
+ function Reaction (rate, subs, prods, substoich, prodstoich;
140
+ netstoich = nothing , metadata = Pair{Symbol, Any}[],
141
+ only_use_rate = metadata_only_use_rate_check (metadata), kwargs... )
142
+ (isnothing (prods) && isnothing (subs)) &&
143
+ throw (ArgumentError (" A reaction requires a non-nothing substrate or product vector." ))
144
+ (isnothing (prodstoich) && isnothing (substoich)) &&
145
+ throw (ArgumentError (" Both substrate and product stochiometry inputs cannot be nothing." ))
146
+
147
+ if isnothing (subs)
148
+ prodtype = typeof (value (first (prods)))
149
+ subs = Vector {prodtype} ()
150
+ ! isnothing (substoich) &&
151
+ throw (ArgumentError (" If substrates are nothing, substrate stoichiometries have to be so too." ))
152
+ substoich = typeof (prodstoich)()
153
+ else
154
+ subs = value .(subs)
155
+ end
156
+ allunique (subs) ||
157
+ throw (ArgumentError (" Substrates can not be repeated in the list provided to `Reaction`, please modify the stoichiometry for any repeated substrates instead." ))
158
+ S = eltype (substoich)
159
+
160
+ if isnothing (prods)
161
+ prods = Vector {eltype(subs)} ()
162
+ ! isnothing (prodstoich) &&
163
+ throw (ArgumentError (" If products are nothing, product stoichiometries have to be so too." ))
164
+ prodstoich = typeof (substoich)()
165
+ else
166
+ prods = value .(prods)
167
+ end
168
+ allunique (prods) ||
169
+ throw (ArgumentError (" Products can not be repeated in the list provided to `Reaction`, please modify the stoichiometry for any repeated products instead." ))
170
+ T = eltype (prodstoich)
171
+
172
+ # try to get a common type for stoichiometry, using Any if have Syms
173
+ stoich_type = promote_type (S, T)
174
+ if stoich_type <: Num
175
+ stoich_type = Any
176
+ substoich′ = Any[value (s) for s in substoich]
177
+ prodstoich′ = Any[value (p) for p in prodstoich]
178
+ else
179
+ substoich′ = (S == stoich_type) ? substoich : convert .(stoich_type, substoich)
180
+ prodstoich′ = (T == stoich_type) ? prodstoich : convert .(stoich_type, prodstoich)
181
+ end
182
+
183
+ if ! (all (isvalidreactant, subs) && all (isvalidreactant, prods))
184
+ badsts = union (filter (! isvalidreactant, subs), filter (! isvalidreactant, prods))
185
+ throw (ArgumentError (""" To be a valid substrate or product, non-constant species must be declared via @species, while constant species must be parameters with the isconstantspecies metadata. The following reactants do not follow this convention:\n $badsts """ ))
186
+ end
187
+
188
+ ns = if netstoich === nothing
189
+ get_netstoich (subs, prods, substoich′, prodstoich′)
190
+ else
191
+ (netstoich_stoichtype (netstoich) != stoich_type) ?
192
+ convert .(stoich_type, netstoich) : netstoich
193
+ end
194
+
195
+ # Check that all metadata entries are unique. (cannot use `in` since some entries may be symbolics).
196
+ if ! allunique (entry[1 ] for entry in metadata)
197
+ error (" Repeated entries for the same metadata encountered in the following metadata set: $([entry[1 ] for entry in metadata]) ." )
198
+ end
199
+
200
+ # Deletes potential `:only_use_rate => ` entries from the metadata.
201
+ if any (:only_use_rate == entry[1 ] for entry in metadata)
202
+ deleteat! (metadata, findfirst (:only_use_rate == entry[1 ] for entry in metadata))
203
+ end
204
+
205
+ # Ensures metadata have the correct type.
206
+ metadata = convert (Vector{Pair{Symbol, Any}}, metadata)
207
+
208
+ Reaction (value (rate), subs, prods, substoich′, prodstoich′, ns, only_use_rate, metadata)
209
+ end
210
+
211
+ # Checks if a metadata input has an entry :only_use_rate => true
212
+ function metadata_only_use_rate_check (metadata)
213
+ only_use_rate_idx = findfirst (:only_use_rate == entry[1 ] for entry in metadata)
214
+ isnothing (only_use_rate_idx) && return false
215
+ return Bool (metadata[only_use_rate_idx][2 ])
216
+ end
217
+
218
+ # three argument constructor assumes stoichiometric coefs are one and integers
219
+ function Reaction (rate, subs, prods; kwargs... )
220
+ sstoich = isnothing (subs) ? nothing : ones (Int, length (subs))
221
+ pstoich = isnothing (prods) ? nothing : ones (Int, length (prods))
222
+ Reaction (rate, subs, prods, sstoich, pstoich; kwargs... )
223
+ end
224
+
225
+ function print_rxside (io:: IO , specs, stoich)
226
+ # reactants/substrates
227
+ if isempty (specs)
228
+ print (io, " ∅" )
229
+ else
230
+ for (i, spec) in enumerate (specs)
231
+ prspec = (MT. isparameter (spec) || (MT. operation (spec) == getindex)) ?
232
+ spec : MT. operation (spec)
233
+ if isequal (stoich[i], one (stoich[i]))
234
+ print (io, prspec)
235
+ elseif istree (stoich[i])
236
+ print (io, " (" , stoich[i], " )*" , prspec)
237
+ else
238
+ print (io, stoich[i], " *" , prspec)
239
+ end
240
+
241
+ (i < length (specs)) && print (io, " + " )
242
+ end
243
+ end
244
+ nothing
245
+ end
246
+
247
+ function Base. show (io:: IO , rx:: Reaction )
248
+ print (io, rx. rate, " , " )
249
+ print_rxside (io, rx. substrates, rx. substoich)
250
+ arrow = rx. only_use_rate ? " ⇒" : " -->"
251
+ print (io, " " , arrow, " " )
252
+ print_rxside (io, rx. products, rx. prodstoich)
253
+ end
254
+
255
+ function apply_if_nonempty (f, v)
256
+ isempty (v) && return v
257
+ s = similar (v)
258
+ map! (f, s, v)
259
+ s
260
+ end
261
+
262
+ function ModelingToolkit. namespace_equation (rx:: Reaction , name; kw... )
263
+ f = Base. Fix2 (namespace_expr, name)
264
+ rate = f (rx. rate)
265
+ subs = apply_if_nonempty (f, rx. substrates)
266
+ prods = apply_if_nonempty (f, rx. products)
267
+ substoich = apply_if_nonempty (f, rx. substoich)
268
+ prodstoich = apply_if_nonempty (f, rx. prodstoich)
269
+ netstoich = if isempty (rx. netstoich)
270
+ rx. netstoich
271
+ else
272
+ ns = similar (rx. netstoich)
273
+ map! (n -> f (n[1 ]) => f (n[2 ]), ns, rx. netstoich)
274
+ end
275
+ Reaction (rate, subs, prods, substoich, prodstoich, netstoich, rx. only_use_rate, rx. metadata)
276
+ end
277
+
278
+ netstoich_stoichtype (:: Vector{Pair{S, T}} ) where {S, T} = T
279
+
280
+ # calculates the net stoichiometry of a reaction as a vector of pairs (sub,substoich)
281
+ function get_netstoich (subs, prods, sstoich, pstoich)
282
+ # stoichiometry as a Dictionary
283
+ nsdict = Dict {Any, eltype(sstoich)} (sub => - sstoich[i] for (i, sub) in enumerate (subs))
284
+ for (i, p) in enumerate (prods)
285
+ coef = pstoich[i]
286
+ @inbounds nsdict[p] = haskey (nsdict, p) ? nsdict[p] + coef : coef
287
+ end
288
+
289
+ # stoichiometry as a vector
290
+ [el for el in nsdict if ! _iszero (el[2 ])]
291
+ end
292
+
293
+ """
294
+ isbcbalanced(rx::Reaction)
295
+
296
+ True if any BC species in `rx` appears as a substrate and product with the same
297
+ stoichiometry.
298
+ """
299
+ function isbcbalanced (rx:: Reaction )
300
+ # any substrate BC must be a product with the same stoichiometry
301
+ for (sidx, sub) in enumerate (rx. substrates)
302
+ if isbc (sub)
303
+ pidx = findfirst (Base. Fix1 (isequal, sub), rx. products)
304
+ (pidx === nothing ) && return false
305
+ isequal (rx. prodstoich[pidx], rx. substoich[sidx]) || return false
306
+ end
307
+ end
308
+
309
+ for prod in rx. products
310
+ if isbc (prod)
311
+ any (Base. Fix1 (isequal, prod), rx. substrates) || return false
312
+ end
313
+ end
314
+
315
+ true
316
+ end
317
+
318
+ # ## Reaction Acessor Functions ###
319
+
320
+ # Overwrites functions in ModelingToolkit to give the correct input.
321
+ ModelingToolkit. is_diff_equation (rx:: Reaction ) = false
322
+ ModelingToolkit. is_alg_equation (rx:: Reaction ) = false
0 commit comments