Skip to content

Commit 127c49c

Browse files
authored
Merge pull request #1172 from vyudu/network-analysis-touchup
network_analysis cleanup: Updating `ratematrix` to be more similar to new matrix API functions
2 parents 1205ea1 + 49a56fa commit 127c49c

File tree

2 files changed

+85
-73
lines changed

2 files changed

+85
-73
lines changed

src/network_analysis.jl

Lines changed: 76 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,6 @@ function laplacianmat(rn::ReactionSystem, pmap::Dict = Dict(); sparse = false)
206206
D * K
207207
end
208208

209-
Base.zero(::Type{Union{R, Symbolics.BasicSymbolic{Real}}}) where R <: Real = zero(R)
210-
Base.one(::Type{Union{R, Symbolics.BasicSymbolic{Real}}}) where R <: Real = one(R)
211-
212209
@doc raw"""
213210
fluxmat(rn::ReactionSystem, pmap = Dict(); sparse=false)
214211
@@ -218,15 +215,20 @@ Base.one(::Type{Union{R, Symbolics.BasicSymbolic{Real}}}) where R <: Real = one(
218215
**Warning**: Unlike other Catalyst functions, the `fluxmat` function will return a `Matrix{Num}` in the symbolic case. This is to allow easier computation of the matrix decomposition of the ODEs, and to ensure that multiplying the sparse form of the matrix will work.
219216
"""
220217
function fluxmat(rn::ReactionSystem, pmap::Dict = Dict(); sparse=false)
218+
deps = Set()
219+
for (i, rx) in enumerate(reactions(rn))
220+
empty!(deps)
221+
get_variables!(deps, rx.rate, species(rn))
222+
(!isempty(deps)) && (error("Reaction $rx's rate constant depends on species $(join(deps, ", ")). `fluxmat` cannot support rate constants of this form."))
223+
end
224+
221225
rates = if isempty(pmap)
222226
reactionrates(rn)
223227
else
224228
substitutevals(rn, pmap, parameters(rn), reactionrates(rn))
225229
end
226230

227231
rcmap = reactioncomplexmap(rn)
228-
nc = length(rcmap)
229-
nr = length(rates)
230232
mtype = eltype(rates) <: Symbolics.BasicSymbolic ? Num : eltype(rates)
231233
if sparse
232234
return fluxmat(SparseMatrixCSC{mtype, Int}, rcmap, rates)
@@ -295,6 +297,10 @@ function massactionvector(rn::ReactionSystem, scmap::Dict = Dict(); combinatoric
295297
rxs = reactions(rn)
296298
sm = speciesmap(rn)
297299

300+
for rx in rxs
301+
!ismassaction(rx, rn) && error("The supplied ReactionSystem has non-mass action reaction $rx. The `massactionvector` can only be constructed for mass action networks.")
302+
end
303+
298304
specs = if isempty(scmap)
299305
species(rn)
300306
else
@@ -936,7 +942,7 @@ function isdetailedbalanced(rs::ReactionSystem, parametermap::Dict; abstol=0, re
936942
complexes, D = reactioncomplexes(rs)
937943
img = incidencematgraph(rs)
938944
undir_img = SimpleGraph(incidencematgraph(rs))
939-
K = ratematrix(rs, pmap)
945+
K = adjacencymat(rs, pmap)
940946

941947
spanning_forest = Graphs.kruskal_mst(undir_img)
942948
outofforest_edges = setdiff(collect(edges(undir_img)), spanning_forest)
@@ -993,52 +999,25 @@ function isdetailedbalanced(rs::ReactionSystem, parametermap)
993999
end
9941000

9951001
"""
996-
iscomplexbalanced(rs::ReactionSystem, parametermap)
1002+
iscomplexbalanced(rs::ReactionSystem, parametermap; sparse = false)
9971003
9981004
Constructively compute whether a network will have complex-balanced equilibrium
9991005
solutions, following the method in van der Schaft et al., [2015](https://link.springer.com/article/10.1007/s10910-015-0498-2#Sec3).
1000-
Accepts a dictionary, vector, or tuple of variable-to-value mappings, e.g. [k1 => 1.0, k2 => 2.0,...].
1001-
"""
1002-
function iscomplexbalanced(rs::ReactionSystem, parametermap::Dict)
1003-
if length(parametermap) != numparams(rs)
1004-
error("Incorrect number of parameters specified.")
1005-
end
1006-
1007-
pmap = symmap_to_varmap(rs, parametermap)
1008-
pmap = Dict(ModelingToolkit.value(k) => v for (k, v) in pmap)
10091006
1010-
sm = speciesmap(rs)
1011-
cm = reactioncomplexmap(rs)
1012-
complexes, D = reactioncomplexes(rs)
1007+
Requires the specification of values for the parameters/rate constants. Accepts a dictionary, vector, or tuple of parameter-to-value mappings, e.g. [k1 => 1.0, k2 => 2.0,...].
1008+
"""
1009+
function iscomplexbalanced(rs::ReactionSystem, parametermap::Dict; sparse = false)
10131010
rxns = reactions(rs)
1014-
nc = length(complexes)
1015-
nr = numreactions(rs)
1016-
nm = numspecies(rs)
1017-
10181011
if !all(r -> ismassaction(r, rs), rxns)
10191012
error("The supplied ReactionSystem has reactions that are not ismassaction. Testing for being complex balanced is currently only supported for pure mass action networks.")
10201013
end
10211014

1022-
rates = [substitute(rate, pmap) for rate in reactionrates(rs)]
1023-
1024-
# Construct kinetic matrix, K
1025-
K = zeros(nr, nc)
1026-
for c in 1:nc
1027-
complex = complexes[c]
1028-
for (r, dir) in cm[complex]
1029-
rxn = rxns[r]
1030-
if dir == -1
1031-
K[r, c] = rates[r]
1032-
end
1033-
end
1034-
end
1035-
1036-
L = -D * K
1037-
S = netstoichmat(rs)
1015+
D = incidencemat(rs; sparse)
1016+
S = netstoichmat(rs; sparse)
10381017

10391018
# Compute ρ using the matrix-tree theorem
10401019
g = incidencematgraph(rs)
1041-
R = ratematrix(rs, rates)
1020+
R = adjacencymat(rs, parametermap; sparse)
10421021
ρ = matrixtree(g, R)
10431022

10441023
# Determine if 1) ρ is positive and 2) D^T Ln ρ lies in the image of S^T
@@ -1069,50 +1048,81 @@ function iscomplexbalanced(rs::ReactionSystem, parametermap)
10691048
end
10701049

10711050
"""
1072-
ratematrix(rs::ReactionSystem, parametermap)
1051+
adjacencymat(rs::ReactionSystem, pmap = Dict(); sparse = false)
10731052
10741053
Given a reaction system with n complexes, outputs an n-by-n matrix where R_{ij} is the rate
10751054
constant of the reaction between complex i and complex j. Accepts a dictionary, vector, or tuple
10761055
of variable-to-value mappings, e.g. [k1 => 1.0, k2 => 2.0,...].
1056+
1057+
Equivalent to the adjacency matrix of the weighted graph whose weights are the
1058+
reaction rates.
1059+
Returns a symbolic matrix by default, but will return a numerical matrix if parameter values are specified via pmap.
1060+
1061+
The difference between this matrix and [`fluxmat`](@ref) is quite subtle. The non-zero entries to both matrices are the rate constants. However:
1062+
- In `fluxmat`, the rows represent complexes and columns represent reactions, and an entry (c, r) is non-zero if reaction r's source complex is c.
1063+
- In `adjacencymat`, the rows and columns both represent complexes, and an entry (c1, c2) is non-zero if there is a reaction c1 --> c2.
10771064
"""
1078-
function ratematrix(rs::ReactionSystem, rates::Vector{Float64})
1079-
complexes, D = reactioncomplexes(rs)
1080-
n = length(complexes)
1081-
rxns = reactions(rs)
1082-
ratematrix = zeros(n, n)
1065+
function adjacencymat(rn::ReactionSystem, pmap::Dict = Dict(); sparse = false)
1066+
deps = Set()
1067+
for (i, rx) in enumerate(reactions(rn))
1068+
empty!(deps)
1069+
get_variables!(deps, rx.rate, species(rn))
1070+
(!isempty(deps)) && (error("Reaction $rx's rate constant depends on species $(join(deps, ", ")). `adjacencymat` cannot support rate constants of this form."))
1071+
end
10831072

1084-
for r in 1:length(rxns)
1085-
rxn = rxns[r]
1086-
s = findfirst(==(-1), @view D[:, r])
1087-
p = findfirst(==(1), @view D[:, r])
1088-
ratematrix[s, p] = rates[r]
1073+
rates = if isempty(pmap)
1074+
reactionrates(rn)
1075+
else
1076+
substitutevals(rn, pmap, parameters(rn), reactionrates(rn))
1077+
end
1078+
mtype = eltype(rates) <: Symbolics.BasicSymbolic ? Num : eltype(rates)
1079+
1080+
if sparse
1081+
return adjacencymat(SparseMatrixCSC{mtype, Int}, incidencemat(rn), rates)
1082+
else
1083+
return adjacencymat(Matrix{mtype}, incidencemat(rn), rates)
10891084
end
1090-
ratematrix
10911085
end
10921086

1093-
function ratematrix(rs::ReactionSystem, parametermap::Dict)
1094-
if length(parametermap) != numparams(rs)
1095-
error("Incorrect number of parameters specified.")
1087+
function adjacencymat(::Type{SparseMatrixCSC{T, Int}}, D, rates) where T
1088+
Is = Int[]
1089+
Js = Int[]
1090+
Vs = T[]
1091+
nc = size(D, 1)
1092+
1093+
for r in 1:length(rates)
1094+
s = findfirst(==(-1), @view D[:, r])
1095+
p = findfirst(==(1), @view D[:, r])
1096+
push!(Is, s)
1097+
push!(Js, p)
1098+
push!(Vs, rates[r])
10961099
end
1100+
A = sparse(Is, Js, Vs, nc, nc)
1101+
end
10971102

1098-
pmap = symmap_to_varmap(rs, parametermap)
1099-
pmap = Dict(ModelingToolkit.value(k) => v for (k, v) in pmap)
1103+
function adjacencymat(::Type{Matrix{T}}, D, rates) where T
1104+
nc = size(D, 1)
1105+
A = zeros(T, nc, nc)
11001106

1101-
rates = [substitute(rate, pmap) for rate in reactionrates(rs)]
1102-
ratematrix(rs, rates)
1107+
for r in 1:length(rates)
1108+
s = findfirst(==(-1), @view D[:, r])
1109+
p = findfirst(==(1), @view D[:, r])
1110+
A[s, p] = rates[r]
1111+
end
1112+
A
11031113
end
11041114

1105-
function ratematrix(rs::ReactionSystem, parametermap::Vector{<:Pair})
1106-
pdict = Dict(parametermap)
1107-
ratematrix(rs, pdict)
1115+
function adjacencymat(rn::ReactionSystem, pmap::Vector{<:Pair}; sparse=false)
1116+
pdict = Dict(pmap)
1117+
adjacencymat(rn, pdict; sparse)
11081118
end
11091119

1110-
function ratematrix(rs::ReactionSystem, parametermap::Tuple)
1111-
pdict = Dict(parametermap)
1112-
ratematrix(rs, pdict)
1120+
function adjacencymat(rn::ReactionSystem, pmap::Tuple; sparse=false)
1121+
pdict = Dict(pmap)
1122+
adjacencymat(rn, pdict; sparse)
11131123
end
11141124

1115-
function ratematrix(rs::ReactionSystem, parametermap)
1125+
function adjacencymat(rn::ReactionSystem, pmap)
11161126
error("Parameter map must be a dictionary, tuple, or vector of symbol/value pairs.")
11171127
end
11181128

test/network_analysis/crn_theory.jl

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# Tests for properties from chemical reaction network theory: deficiency theorems, complex/detailed balance, etc.
22
using Catalyst, StableRNGs, LinearAlgebra, Test
33
rng = StableRNG(514)
4-
54
# Tests that `iscomplexbalanced` works for different rate inputs.
65
# Tests that non-valid rate input yields and error
76
let
@@ -69,18 +68,21 @@ let
6968
rates_invalid = reshape(rate_vals, 1, 8)
7069

7170
# Tests that all input types generates the correct rate matrix.
72-
Catalyst.ratematrix(rn, rate_vals) == rate_mat
73-
Catalyst.ratematrix(rn, rates_vec) == rate_mat
74-
Catalyst.ratematrix(rn, rates_tup) == rate_mat
75-
Catalyst.ratematrix(rn, rates_dict) == rate_mat
71+
@test Catalyst.adjacencymat(rn, rates_vec) == rate_mat
72+
@test Catalyst.adjacencymat(rn, rates_tup) == rate_mat
73+
@test Catalyst.adjacencymat(rn, rates_dict) == rate_mat
74+
@test_throws Exception Catalyst.adjacencymat(rn, rate_vals)
7675

7776
# Tests that throws error in rate matrix.
7877
incorrect_param_dict = Dict(:k1 => 1.0)
7978

80-
@test_throws ErrorException Catalyst.ratematrix(rn, 123)
81-
@test_throws ErrorException Catalyst.ratematrix(rn, incorrect_param_dict)
79+
@test_throws ErrorException Catalyst.adjacencymat(rn, 123)
80+
@test_throws ErrorException Catalyst.adjacencymat(rn, incorrect_param_dict)
8281

8382
@test_throws Exception Catalyst.iscomplexbalanced(rn, rates_invalid)
83+
84+
# Test sparse matrix
85+
@test Catalyst.adjacencymat(rn, rates_vec; sparse = true) == rate_mat
8486
end
8587

8688
### CONCENTRATION ROBUSTNESS TESTS

0 commit comments

Comments
 (0)