Skip to content

Commit 5277a2c

Browse files
authored
Merge pull request #822 from vyudu/master
Complex balancing
2 parents 1076687 + 2e7709a commit 5277a2c

File tree

4 files changed

+249
-3
lines changed

4 files changed

+249
-3
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ uuid = "479239e8-5488-4da2-87a7-35f2df7eef83"
33
version = "13.5.1"
44

55
[deps]
6+
Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa"
67
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
78
DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e"
89
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
@@ -38,6 +39,7 @@ CatalystStructuralIdentifiabilityExtension = "StructuralIdentifiability"
3839
[compat]
3940
BifurcationKit = "0.3"
4041
DataStructures = "0.18"
42+
Combinatorics = "1.0.2"
4143
DiffEqBase = "6.83.0"
4244
DocStringExtensions = "0.8, 0.9"
4345
DynamicPolynomials = "0.5"

src/Catalyst.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module Catalyst
66
using DocStringExtensions
77
using SparseArrays, DiffEqBase, Reexport, Setfield
88
using LaTeXStrings, Latexify, Requires
9+
using LinearAlgebra, Combinatorics
910
using JumpProcesses: JumpProcesses, JumpProblem,
1011
MassActionJump, ConstantRateJump, VariableRateJump,
1112
SpatialMassActionJump

src/network_analysis.jl

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,3 +718,164 @@ function conservationlaw_errorcheck(rs, pre_varmap)
718718
isempty(conservedequations(Catalyst.flatten(rs))) ||
719719
error("The system has conservation laws but initial conditions were not provided for some species.")
720720
end
721+
722+
"""
723+
iscomplexbalanced(rs::ReactionSystem, parametermap)
724+
725+
Constructively compute whether a network will have complex-balanced equilibrium
726+
solutions, following the method in van der Schaft et al., [2015](https://link.springer.com/article/10.1007/s10910-015-0498-2#Sec3). Accepts a dictionary, vector, or tuple of variable-to-value mappings, e.g. [k1 => 1.0, k2 => 2.0,...].
727+
"""
728+
729+
function iscomplexbalanced(rs::ReactionSystem, parametermap::Dict)
730+
if length(parametermap) != numparams(rs)
731+
error("Incorrect number of parameters specified.")
732+
end
733+
734+
pmap = symmap_to_varmap(rs, parametermap)
735+
pmap = Dict(ModelingToolkit.value(k) => v for (k,v) in pmap)
736+
737+
sm = speciesmap(rs)
738+
cm = reactioncomplexmap(rs)
739+
complexes, D = reactioncomplexes(rs)
740+
rxns = reactions(rs)
741+
nc = length(complexes); nr = numreactions(rs); nm = numspecies(rs)
742+
743+
if !all(r->ismassaction(r, rs), rxns)
744+
error("The supplied ReactionSystem has reactions that are not ismassaction. Testing for being complex balanced is currently only supported for pure mass action networks.")
745+
end
746+
747+
rates = [substitute(rate, pmap) for rate in reactionrates(rs)]
748+
749+
# Construct kinetic matrix, K
750+
K = zeros(nr, nc)
751+
for c in 1:nc
752+
complex = complexes[c]
753+
for (r, dir) in cm[complex]
754+
rxn = rxns[r]
755+
if dir == -1
756+
K[r, c] = rates[r]
757+
end
758+
end
759+
end
760+
761+
L = -D*K
762+
S = netstoichmat(rs)
763+
764+
# Compute ρ using the matrix-tree theorem
765+
g = incidencematgraph(rs)
766+
R = ratematrix(rs, rates)
767+
ρ = matrixtree(g, R)
768+
769+
# Determine if 1) ρ is positive and 2) D^T Ln ρ lies in the image of S^T
770+
if all(>(0), ρ)
771+
img = D'*log.(ρ)
772+
if rank(S') == rank(hcat(S', img)) return true else return false end
773+
else
774+
return false
775+
end
776+
end
777+
778+
function iscomplexbalanced(rs::ReactionSystem, parametermap::Vector{Pair{Symbol, Float64}})
779+
pdict = Dict(parametermap)
780+
iscomplexbalanced(rs, pdict)
781+
end
782+
783+
function iscomplexbalanced(rs::ReactionSystem, parametermap::Tuple{Pair{Symbol, Float64}})
784+
pdict = Dict(parametermap)
785+
iscomplexbalanced(rs, pdict)
786+
end
787+
788+
iscomplexbalanced(rs::ReactionSystem, parametermap) = error("Parameter map must be a dictionary, tuple, or vector of symbol/value pairs.")
789+
790+
791+
"""
792+
ratematrix(rs::ReactionSystem, parametermap)
793+
794+
Given a reaction system with n complexes, outputs an n-by-n matrix where R_{ij} is the rate constant of the reaction between complex i and complex j. Accepts a dictionary, vector, or tuple of variable-to-value mappings, e.g. [k1 => 1.0, k2 => 2.0,...].
795+
"""
796+
797+
function ratematrix(rs::ReactionSystem, rates::Vector{Float64})
798+
complexes, D = reactioncomplexes(rs)
799+
n = length(complexes)
800+
rxns = reactions(rs)
801+
ratematrix = zeros(n, n)
802+
803+
for r in 1:length(rxns)
804+
rxn = rxns[r]
805+
s = findfirst(==(-1), @view D[:,r])
806+
p = findfirst(==(1), @view D[:,r])
807+
ratematrix[s, p] = rates[r]
808+
end
809+
ratematrix
810+
end
811+
812+
function ratematrix(rs::ReactionSystem, parametermap::Dict)
813+
if length(parametermap) != numparams(rs)
814+
error("Incorrect number of parameters specified.")
815+
end
816+
817+
pmap = symmap_to_varmap(rs, parametermap)
818+
pmap = Dict(ModelingToolkit.value(k) => v for (k,v) in pmap)
819+
820+
rates = [substitute(rate, pmap) for rate in reactionrates(rs)]
821+
ratematrix(rs, rates)
822+
end
823+
824+
function ratematrix(rs::ReactionSystem, parametermap::Vector{Pair{Symbol, Float64}})
825+
pdict = Dict(parametermap)
826+
ratematrix(rs, pdict)
827+
end
828+
829+
function ratematrix(rs::ReactionSystem, parametermap::Tuple{Pair{Symbol, Float64}})
830+
pdict = Dict(parametermap)
831+
ratematrix(rs, pdict)
832+
end
833+
834+
ratematrix(rs::ReactionSystem, parametermap) = error("Parameter map must be a dictionary, tuple, or vector of symbol/value pairs.")
835+
836+
### BELOW: Helper functions for iscomplexbalanced
837+
838+
function matrixtree(g::SimpleDiGraph, distmx::Matrix)
839+
n = nv(g)
840+
if size(distmx) != (n, n)
841+
error("Size of distance matrix is incorrect")
842+
end
843+
844+
π = zeros(n)
845+
846+
if !Graphs.is_connected(g)
847+
ccs = Graphs.connected_components(g)
848+
for cc in ccs
849+
sg, vmap = Graphs.induced_subgraph(g, cc)
850+
distmx_s = distmx[cc, cc]
851+
π_j = matrixtree(sg, distmx_s)
852+
π[cc] = π_j
853+
end
854+
return π
855+
end
856+
857+
# generate all spanning trees
858+
ug = SimpleGraph(SimpleDiGraph(g))
859+
trees = collect(Combinatorics.combinations(collect(edges(ug)), n-1))
860+
trees = SimpleGraph.(trees)
861+
trees = filter!(t->isempty(Graphs.cycle_basis(t)), trees)
862+
863+
# constructed rooted trees for every vertex, compute sum
864+
for v in 1:n
865+
rootedTrees = [reverse(Graphs.bfs_tree(t, v, dir=:in)) for t in trees]
866+
π[v] = sum([treeweight(t, g, distmx) for t in rootedTrees])
867+
end
868+
869+
# sum the contributions
870+
return π
871+
end
872+
873+
function treeweight(t::SimpleDiGraph, g::SimpleDiGraph, distmx::Matrix)
874+
prod = 1
875+
for e in edges(t)
876+
s = Graphs.src(e); t = Graphs.dst(e)
877+
prod *= distmx[s, t]
878+
end
879+
prod
880+
end
881+

test/network_analysis/network_properties.jl

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
### Prepares Tests ###
22

33
# Fetch packages.
4-
using Catalyst, LinearAlgebra, Test
4+
using Catalyst, LinearAlgebra, Test, StableRNGs
5+
6+
rng = StableRNG(514)
57

68
### Basic Tests ###
79

@@ -40,6 +42,10 @@ let
4042
@test isweaklyreversible(MAPK, subnetworks(MAPK)) == false
4143
cls = conservationlaws(MAPK)
4244
@test Catalyst.get_networkproperties(MAPK).rank == 15
45+
46+
k = rand(rng, numparams(MAPK))
47+
rates = Dict(zip(parameters(MAPK), k))
48+
@test Catalyst.iscomplexbalanced(MAPK, rates) == false
4349
# i=0;
4450
# for lcs in linkageclasses(MAPK)
4551
# i=i+1
@@ -77,6 +83,10 @@ let
7783
@test isweaklyreversible(rn2, subnetworks(rn2)) == false
7884
cls = conservationlaws(rn2)
7985
@test Catalyst.get_networkproperties(rn2).rank == 6
86+
87+
k = rand(rng, numparams(rn2))
88+
rates = Dict(zip(parameters(rn2), k))
89+
@test Catalyst.iscomplexbalanced(rn2, rates) == false
8090
# i=0;
8191
# for lcs in linkageclasses(rn2)
8292
# i=i+1
@@ -117,6 +127,10 @@ let
117127
@test isweaklyreversible(rn3, subnetworks(rn3)) == false
118128
cls = conservationlaws(rn3)
119129
@test Catalyst.get_networkproperties(rn3).rank == 10
130+
131+
k = rand(rng, numparams(rn3))
132+
rates = Dict(zip(parameters(rn3), k))
133+
@test Catalyst.iscomplexbalanced(rn3, rates) == false
120134
# i=0;
121135
# for lcs in linkageclasses(rn3)
122136
# i=i+1
@@ -132,6 +146,18 @@ let
132146
# end
133147
end
134148

149+
let
150+
rn4 = @reaction_network begin
151+
(k1, k2), C1 <--> C2
152+
(k3, k4), C2 <--> C3
153+
(k5, k6), C3 <--> C1
154+
end
155+
156+
k = rand(rng, numparams(rn4))
157+
rates = Dict(zip(parameters(rn4), k))
158+
@test Catalyst.iscomplexbalanced(rn4, rates) == true
159+
end
160+
135161
### Tests Reversibility ###
136162

137163
# Test function.
@@ -154,7 +180,12 @@ let
154180
rev = false
155181
weak_rev = false
156182
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
183+
184+
k = rand(rng, numparams(rn))
185+
rates = Dict(zip(parameters(rn), k))
186+
@test Catalyst.iscomplexbalanced(rn, rates) == false
157187
end
188+
158189
let
159190
rn = @reaction_network begin
160191
(k2, k1), A1 <--> A2 + A3
@@ -167,6 +198,10 @@ let
167198
rev = false
168199
weak_rev = false
169200
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
201+
202+
k = rand(rng, numparams(rn))
203+
rates = Dict(zip(parameters(rn), k))
204+
@test Catalyst.iscomplexbalanced(rn, rates) == false
170205
end
171206
let
172207
rn = @reaction_network begin
@@ -176,6 +211,9 @@ let
176211
rev = false
177212
weak_rev = false
178213
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
214+
k = rand(rng, numparams(rn))
215+
rates = Dict(zip(parameters(rn), k))
216+
@test Catalyst.iscomplexbalanced(rn, rates) == false
179217
end
180218
let
181219
rn = @reaction_network begin
@@ -186,17 +224,25 @@ let
186224
rev = false
187225
weak_rev = false
188226
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
227+
228+
k = rand(rng, numparams(rn))
229+
rates = Dict(zip(parameters(rn), k))
230+
@test Catalyst.iscomplexbalanced(rn, rates) == false
189231
end
190232
let
191233
rn = @reaction_network begin
192234
(k2, k1), A <--> 2B
193-
(k4, k3), A + C --> D
235+
(k4, k3), A + C <--> D
194236
k5, D --> B + E
195237
k6, B + E --> A + C
196238
end
197239
rev = false
198240
weak_rev = true
199241
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
242+
243+
k = rand(rng, numparams(rn))
244+
rates = Dict(zip(parameters(rn), k))
245+
@test Catalyst.iscomplexbalanced(rn, rates) == true
200246
end
201247
let
202248
rn = @reaction_network begin
@@ -206,6 +252,10 @@ let
206252
rev = false
207253
weak_rev = false
208254
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
255+
256+
k = rand(rng, numparams(rn))
257+
rates = Dict(zip(parameters(rn), k))
258+
@test Catalyst.iscomplexbalanced(rn, rates) == false
209259
end
210260
let
211261
rn = @reaction_network begin
@@ -215,12 +265,20 @@ let
215265
rev = true
216266
weak_rev = true
217267
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
268+
269+
k = rand(rng, numparams(rn))
270+
rates = Dict(zip(parameters(rn), k))
271+
@test Catalyst.iscomplexbalanced(rn, rates) == true
218272
end
219273
let
220274
rn = @reaction_network begin (k2, k1), A + B <--> 2A end
221275
rev = true
222276
weak_rev = true
223277
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
278+
279+
k = rand(rng, numparams(rn))
280+
rates = Dict(zip(parameters(rn), k))
281+
@test Catalyst.iscomplexbalanced(rn, rates) == true
224282
end
225283
let
226284
rn = @reaction_network begin
@@ -232,6 +290,10 @@ let
232290
rev = false
233291
weak_rev = true
234292
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
293+
294+
k = rand(rng, numparams(rn))
295+
rates = Dict(zip(parameters(rn), k))
296+
@test Catalyst.iscomplexbalanced(rn, rates) == true
235297
end
236298
let
237299
rn = @reaction_network begin
@@ -243,4 +305,24 @@ let
243305
rev = false
244306
weak_rev = false
245307
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
246-
end
308+
309+
k = rand(rng, numparams(rn))
310+
rates = Dict(zip(parameters(rn), k))
311+
@test Catalyst.iscomplexbalanced(rn, rates) == false
312+
end
313+
314+
let
315+
rn = @reaction_network begin
316+
k1, 3A + 2B --> 3C
317+
k2, B + 4D --> 2E
318+
k3, 2E --> 3C
319+
(k4, k5), B + 4D <--> 3A + 2B
320+
k6, F --> B + 4D
321+
k7, 3C --> F
322+
end
323+
324+
k = rand(rng, numparams(rn))
325+
rates = Dict(zip(parameters(rn), k))
326+
@test Catalyst.iscomplexbalanced(rn, rates) == true
327+
end
328+

0 commit comments

Comments
 (0)