Skip to content

Commit 710b164

Browse files
committed
Refactored complex balance to not use MetaGraphs
1 parent fe432b5 commit 710b164

File tree

2 files changed

+153
-64
lines changed

2 files changed

+153
-64
lines changed

src/networkapi.jl

Lines changed: 87 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1654,33 +1654,35 @@ function validate(rs::ReactionSystem, info::String = "")
16541654
end
16551655

16561656
"""
1657-
complexbalanced(rs::ReactionSystem, rates::Vector)
1657+
iscomplexbalanced(rs::ReactionSystem, rates::Vector)
16581658
16591659
Constructively compute whether a network will have complex-balanced equilibrium
1660-
solutions, following the method in [this paper](https://link.springer.com/article/10.1007/s10910-015-0498-2#Sec3).
1660+
solutions, following the method in [this paper](https://link.springer.com/article/10.1007/s10910-015-0498-2#Sec3). Accepts a map of rates [k1 => 1.0, k2 => 2.0,...]k2
16611661
"""
16621662

1663-
using MetaGraphs, Combinatorics, LinearAlgebra
1663+
using Combinatorics, LinearAlgebra
16641664

1665-
function complexbalanced(rs::ReactionSystem, rates::Vector)
1665+
function iscomplexbalanced(rs::ReactionSystem, rates::Dict{Any, Float64})
16661666
if length(rates) != numparams(rs)
16671667
error("The number of reaction rates must be equal to the number of parameters")
16681668
end
1669-
rxm = Dict(zip(reactionparams(rs), rates))
1669+
16701670
sm = speciesmap(rs)
16711671
cm = reactioncomplexmap(rs)
16721672
complexes, D = reactioncomplexes(rs)
16731673
rxns = reactions(rs)
16741674
nc = length(complexes); nr = numreactions(rs); nm = numspecies(rs)
16751675

1676+
if !all(r->ismassaction(r, rs), rxns) error("Not mass action") end
1677+
16761678
# Construct kinetic matrix, K
16771679
K = zeros(nr, nc)
16781680
for c in 1:nc
16791681
complex = complexes[c]
16801682
for (r, dir) in cm[complex]
16811683
rxn = rxns[r]
16821684
if dir == -1
1683-
K[r, c] = rxm[rxn.rate]
1685+
K[r, c] = rates[rxn.rate]
16841686
end
16851687
end
16861688
end
@@ -1689,93 +1691,122 @@ function complexbalanced(rs::ReactionSystem, rates::Vector)
16891691
S = netstoichmat(rs)
16901692

16911693
# Compute ρ using the matrix-tree theorem
1692-
ρ = zeros(nc)
1693-
lcs = linkageclasses(rs)
1694-
rwg = rateweightedgraph(rs, rates)
1694+
g = incidencematgraph(rs); R = ratematrix(rs, rates)
1695+
ρ = matrixtree(g, R)
1696+
@assert isapprox(L*ρ, zeros(nc), atol=1e-12)
16951697

1696-
for lc in lcs
1697-
sg, vmap = Graphs.induced_subgraph(rwg, lc)
1698-
ρ_j = matrixtree(sg)
1699-
ρ[lc] = ρ_j
1700-
end
1701-
17021698
# Determine if 1) ρ is positive and 2) D^T Ln ρ lies in the image of S^T
17031699
if all(x -> x > 0, ρ)
17041700
img = D'*log.(ρ)
1705-
if rank(S') == rank(hcat(S', img))
1706-
return true
1707-
else
1708-
return false
1709-
end
1701+
if rank(S') == rank(hcat(S', img)) return true else return false end
17101702
else
17111703
return false
17121704
end
17131705
end
17141706

1715-
"""
1716-
rateweightedgraph(rs::ReactionSystem, rates::Vector)
1717-
1718-
Generate an annotated reaction complex graph of a reaction system, where the nodes are annotated with the reaction complex they correspond to and the edges are annotated with the reaction they correspond to and the rate of the reaction.
1719-
"""
1720-
function rateweightedgraph(rs::ReactionSystem, rates::Vector)
1707+
# """
1708+
# rateweightedgraph(rs::ReactionSystem, rates::Vector)
1709+
#
1710+
# Generate an annotated reaction complex graph of a reaction system, where the nodes are annotated with the reaction complex they correspond to and the edges are annotated with the reaction they correspond to and the rate of the reaction.
1711+
# """
1712+
#
1713+
# function rateweightedgraph(rs::ReactionSystem, rates::Dict{Any, Float64})
1714+
# if length(rates) != numparams(rs)
1715+
# error("The number of reaction rates must be equal to the number of parameters")
1716+
# end
1717+
#
1718+
# complexes, D = reactioncomplexes(rs)
1719+
# rxns = reactions(rs)
1720+
#
1721+
# g = incidencematgraph(rs)
1722+
# rwg = MetaDiGraph(g)
1723+
#
1724+
# for v in vertices(rwg)
1725+
# set_prop!(rwg, v, :complex, complexes[v])
1726+
# end
1727+
#
1728+
# for (i, e) in collect(enumerate(edges(rwg)))
1729+
# rxn = rxns[i]
1730+
# set_prop!(rwg, Graphs.src(e), Graphs.dst(e), :reaction, rxn)
1731+
# set_prop!(rwg, Graphs.src(e), Graphs.dst(e), :rate, rates[rxn.rate])
1732+
# end
1733+
#
1734+
# rwg
1735+
# end
1736+
1737+
function ratematrix(rs::ReactionSystem, rates::Dict{Any, Float64})
17211738
if length(rates) != numparams(rs)
17221739
error("The number of reaction rates must be equal to the number of parameters")
17231740
end
1724-
rm = Dict(zip(reactionparams(rs), rates))
17251741

17261742
complexes, D = reactioncomplexes(rs)
1743+
n = length(complexes)
17271744
rxns = reactions(rs)
1745+
ratematrix = zeros(n, n)
17281746

1729-
g = incidencematgraph(rs)
1730-
rwg = MetaDiGraph(g)
1731-
1732-
for v in vertices(rwg)
1733-
set_prop!(rwg, v, :complex, complexes[v])
1747+
for r in 1:length(rxns)
1748+
rxn = rxns[r]
1749+
s = findfirst(x->x==-1, D[:,r])
1750+
p = findfirst(x->x==1, D[:,r])
1751+
ratematrix[s, p] = rates[rxn.rate]
17341752
end
1753+
ratematrix
1754+
end
17351755

1736-
for (i, e) in collect(enumerate(edges(rwg)))
1737-
rxn = rxns[i]
1738-
set_prop!(rwg, Graphs.src(e), Graphs.dst(e), :reaction, rxn)
1739-
set_prop!(rwg, Graphs.src(e), Graphs.dst(e), :rate, rm[rxn.rate])
1756+
"""
1757+
"""
1758+
function matrixtree(g::SimpleDiGraph, distmx::Matrix)
1759+
n = nv(g)
1760+
if size(distmx) != (n, n)
1761+
error("Size of distance matrix is incorrect")
17401762
end
17411763

1742-
rwg
1743-
end
1764+
π = zeros(n)
1765+
1766+
if !Graphs.is_connected(g)
1767+
ccs = Graphs.connected_components(g)
1768+
for cc in ccs
1769+
sg, vmap = Graphs.induced_subgraph(g, cc)
1770+
distmx_s = distmx[cc, cc]
1771+
π_j = matrixtree(sg, distmx_s)
1772+
π[cc] = π_j
1773+
end
1774+
return π
1775+
end
17441776

1745-
function matrixtree(g::MetaDiGraph)
17461777
# generate all spanning trees
1747-
# TODO: implement Winter's algorithm for generating spanning trees
1748-
n = nv(g)
17491778
ug = SimpleGraph(SimpleDiGraph(g))
17501779
trees = collect(Combinatorics.combinations(collect(edges(ug)), n-1))
17511780
trees = SimpleGraph.(trees)
17521781
trees = filter!(t->isempty(Graphs.cycle_basis(t)), trees)
1753-
1754-
π = zeros(n)
1755-
1756-
function treeweight(t::SimpleDiGraph)
1757-
prod = 1
1758-
for e in edges(t)
1759-
rate = Graphs.has_edge(g, Graphs.src(e), Graphs.dst(e)) ? get_prop(g, e, :rate) : 0
1760-
prod *= rate
1761-
end
1762-
prod
1763-
end
1782+
# trees = spanningtrees(g)
17641783

17651784
# constructed rooted trees for every edge, compute sum
17661785
for v in 1:n
17671786
rootedTrees = [reverse(Graphs.bfs_tree(t, v, dir=:in)) for t in trees]
1768-
π[v] = sum([treeweight(t) for t in rootedTrees])
1787+
π[v] = sum([treeweight(t, g, distmx) for t in rootedTrees])
17691788
end
17701789

17711790
# sum the contributions
17721791
return π
17731792
end
17741793

1775-
function massactionrate(rs::ReactionSystem, rxn_idx::Int, dir::Int, conc::Vector)
1776-
if dir != -1 && dir != 1
1777-
error("Direction must be either +1 in the case of production or -1 in the case of consumption")
1794+
function treeweight(t::SimpleDiGraph, g::SimpleDiGraph, distmx::Matrix)
1795+
prod = 1
1796+
for e in edges(t)
1797+
s = Graphs.src(e); t = Graphs.dst(e)
1798+
prod *= distmx[s, t]
17781799
end
1800+
prod
1801+
end
1802+
1803+
# TODO: implement Winter's algorithm for generating spanning trees
1804+
function spanningtrees(g::SimpleGraph)
1805+
1806+
end
1807+
1808+
# Checks if a unit consist of exponents with base 1 (and is this unitless).
1809+
unitless_exp(u) = istree(u) && (operation(u) == ^) && (arguments(u)[1] == 1)
17791810

17801811
rxn = reactions(rs)[rxn_idx]
17811812
sm = speciesmap(rs)

test/network_analysis/network_properties.jl

Lines changed: 66 additions & 8 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

@@ -41,8 +43,9 @@ let
4143
cls = conservationlaws(MAPK)
4244
@test Catalyst.get_networkproperties(MAPK).rank == 15
4345

44-
rates = rand(numparams(MAPK))
45-
@test Catalyst.complexbalanced(MAPK, rates) == false
46+
k = rand(rng, numparams(MAPK))
47+
rates = Dict(zip(reactionparams(MAPK), k))
48+
@test Catalyst.iscomplexbalanced(MAPK, rates) == false
4649
# i=0;
4750
# for lcs in linkageclasses(MAPK)
4851
# i=i+1
@@ -81,8 +84,9 @@ let
8184
cls = conservationlaws(rn2)
8285
@test Catalyst.get_networkproperties(rn2).rank == 6
8386

84-
rates = rand(numparams(rn2))
85-
@test Catalyst.complexbalanced(rn2, rates) == false
87+
k = rand(rng, numparams(rn2))
88+
rates = Dict(zip(reactionparams(rn2), k))
89+
@test Catalyst.iscomplexbalanced(rn2, rates) == false
8690
# i=0;
8791
# for lcs in linkageclasses(rn2)
8892
# i=i+1
@@ -123,9 +127,10 @@ let
123127
@test isweaklyreversible(rn3, subnetworks(rn3)) == false
124128
cls = conservationlaws(rn3)
125129
@test Catalyst.get_networkproperties(rn3).rank == 10
126-
127-
rates = rand(numparams(rn3))
128-
@test Catalyst.complexbalanced(rn3, rates) == false
130+
131+
k = rand(rng, numparams(rn3))
132+
rates = Dict(zip(reactionparams(rn3), k))
133+
@test Catalyst.iscomplexbalanced(rn3, rates) == false
129134
# i=0;
130135
# for lcs in linkageclasses(rn3)
131136
# i=i+1
@@ -141,6 +146,18 @@ let
141146
# end
142147
end
143148

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(reactionparams(rn4), k))
158+
@test Catalyst.iscomplexbalanced(rn4, rates) == true
159+
end
160+
144161
### Tests Reversibility ###
145162

146163
# Test function.
@@ -163,7 +180,12 @@ let
163180
rev = false
164181
weak_rev = false
165182
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
183+
184+
k = rand(rng, numparams(rn))
185+
rates = Dict(zip(reactionparams(rn), k))
186+
@test Catalyst.iscomplexbalanced(rn, rates) == false
166187
end
188+
167189
let
168190
rn = @reaction_network begin
169191
(k2, k1), A1 <--> A2 + A3
@@ -176,6 +198,10 @@ let
176198
rev = false
177199
weak_rev = false
178200
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
201+
202+
k = rand(rng, numparams(rn))
203+
rates = Dict(zip(reactionparams(rn), k))
204+
@test Catalyst.iscomplexbalanced(rn, rates) == false
179205
end
180206
let
181207
rn = @reaction_network begin
@@ -185,6 +211,9 @@ let
185211
rev = false
186212
weak_rev = false
187213
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
214+
k = rand(rng, numparams(rn))
215+
rates = Dict(zip(reactionparams(rn), k))
216+
@test Catalyst.iscomplexbalanced(rn, rates) == false
188217
end
189218
let
190219
rn = @reaction_network begin
@@ -195,6 +224,10 @@ let
195224
rev = false
196225
weak_rev = false
197226
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
227+
228+
k = rand(rng, numparams(rn))
229+
rates = Dict(zip(reactionparams(rn), k))
230+
@test Catalyst.iscomplexbalanced(rn, rates) == false
198231
end
199232
let
200233
rn = @reaction_network begin
@@ -206,6 +239,11 @@ let
206239
rev = false
207240
weak_rev = true
208241
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
242+
243+
# Breaks when a reaction has multiple rates
244+
k = rand(rng, numparams(rn))
245+
rates = Dict(zip(reactionparams(rn), k))
246+
# @test Catalyst.iscomplexbalanced(rn, rates) == true
209247
end
210248
let
211249
rn = @reaction_network begin
@@ -215,6 +253,10 @@ let
215253
rev = false
216254
weak_rev = false
217255
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
256+
257+
k = rand(rng, numparams(rn))
258+
rates = Dict(zip(reactionparams(rn), k))
259+
@test Catalyst.iscomplexbalanced(rn, rates) == false
218260
end
219261
let
220262
rn = @reaction_network begin
@@ -224,12 +266,20 @@ let
224266
rev = true
225267
weak_rev = true
226268
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
269+
270+
k = rand(rng, numparams(rn))
271+
rates = Dict(zip(reactionparams(rn), k))
272+
@test Catalyst.iscomplexbalanced(rn, rates) == true
227273
end
228274
let
229275
rn = @reaction_network begin (k2, k1), A + B <--> 2A end
230276
rev = true
231277
weak_rev = true
232278
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
279+
280+
k = rand(rng, numparams(rn))
281+
rates = Dict(zip(reactionparams(rn), k))
282+
@test Catalyst.iscomplexbalanced(rn, rates) == true
233283
end
234284
let
235285
rn = @reaction_network begin
@@ -241,6 +291,10 @@ let
241291
rev = false
242292
weak_rev = true
243293
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
294+
295+
k = rand(rng, numparams(rn))
296+
rates = Dict(zip(reactionparams(rn), k))
297+
@test Catalyst.iscomplexbalanced(rn, rates) == true
244298
end
245299
let
246300
rn = @reaction_network begin
@@ -252,4 +306,8 @@ let
252306
rev = false
253307
weak_rev = false
254308
testreversibility(rn, reactioncomplexes(rn)[2], rev, weak_rev)
309+
310+
k = rand(rng, numparams(rn))
311+
rates = Dict(zip(reactionparams(rn), k))
312+
@test Catalyst.iscomplexbalanced(rn, rates) == false
255313
end

0 commit comments

Comments
 (0)