Skip to content

Commit 8cfa3a1

Browse files
authored
Merge pull request #963 from vyudu/linkageclasses
Strong and terminal linkage classes
2 parents 58f5f15 + 28be9eb commit 8cfa3a1

File tree

4 files changed

+142
-1
lines changed

4 files changed

+142
-1
lines changed

src/Catalyst.jl

+2-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ export @reaction_network, @network_component, @reaction, @species
127127
include("network_analysis.jl")
128128
export reactioncomplexmap, reactioncomplexes, incidencemat
129129
export complexstoichmat
130-
export complexoutgoingmat, incidencematgraph, linkageclasses, deficiency, subnetworks
130+
export complexoutgoingmat, incidencematgraph, linkageclasses, stronglinkageclasses,
131+
terminallinkageclasses, deficiency, subnetworks
131132
export linkagedeficiencies, isreversible, isweaklyreversible
132133
export conservationlaws, conservedquantities, conservedequations, conservationlaw_constants
133134

src/network_analysis.jl

+50
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,56 @@ end
348348

349349
linkageclasses(incidencegraph) = Graphs.connected_components(incidencegraph)
350350

351+
"""
352+
stronglinkageclasses(rn::ReactionSystem)
353+
354+
Return the strongly connected components of a reaction network's incidence graph (i.e. sub-groups of reaction complexes such that every complex is reachable from every other one in the sub-group).
355+
"""
356+
357+
function stronglinkageclasses(rn::ReactionSystem)
358+
nps = get_networkproperties(rn)
359+
if isempty(nps.stronglinkageclasses)
360+
nps.stronglinkageclasses = stronglinkageclasses(incidencematgraph(rn))
361+
end
362+
nps.stronglinkageclasses
363+
end
364+
365+
stronglinkageclasses(incidencegraph) = Graphs.strongly_connected_components(incidencegraph)
366+
367+
"""
368+
terminallinkageclasses(rn::ReactionSystem)
369+
370+
Return the terminal strongly connected components of a reaction network's incidence graph (i.e. sub-groups of reaction complexes that are 1) strongly connected and 2) every outgoing reaction from a complex in the component produces a complex also in the component).
371+
"""
372+
373+
function terminallinkageclasses(rn::ReactionSystem)
374+
nps = get_networkproperties(rn)
375+
if isempty(nps.terminallinkageclasses)
376+
slcs = stronglinkageclasses(rn)
377+
tslcs = filter(lc -> isterminal(lc, rn), slcs)
378+
nps.terminallinkageclasses = tslcs
379+
end
380+
nps.terminallinkageclasses
381+
end
382+
383+
# Helper function for terminallinkageclasses. Given a linkage class and a reaction network, say whether the linkage class is terminal,
384+
# i.e. all outgoing reactions from complexes in the linkage class produce a complex also in the linkage class
385+
function isterminal(lc::Vector, rn::ReactionSystem)
386+
imat = incidencemat(rn)
387+
388+
for r in 1:size(imat, 2)
389+
# Find the index of the reactant complex for a given reaction
390+
s = findfirst(==(-1), @view imat[:, r])
391+
392+
# If the reactant complex is in the linkage class, check whether the product complex is also in the linkage class. If any of them are not, return false.
393+
if s in Set(lc)
394+
p = findfirst(==(1), @view imat[:, r])
395+
p in Set(lc) ? continue : return false
396+
end
397+
end
398+
true
399+
end
400+
351401
@doc raw"""
352402
deficiency(rn::ReactionSystem)
353403

src/reactionsystem.jl

+6
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Base.@kwdef mutable struct NetworkProperties{I <: Integer, V <: BasicSymbolic{Re
7878
isempty::Bool = true
7979
netstoichmat::Union{Matrix{Int}, SparseMatrixCSC{Int, Int}} = Matrix{Int}(undef, 0, 0)
8080
conservationmat::Matrix{I} = Matrix{I}(undef, 0, 0)
81+
cyclemat::Matrix{I} = Matrix{I}(undef, 0, 0)
8182
col_order::Vector{Int} = Int[]
8283
rank::Int = 0
8384
nullity::Int = 0
@@ -93,6 +94,8 @@ Base.@kwdef mutable struct NetworkProperties{I <: Integer, V <: BasicSymbolic{Re
9394
complexoutgoingmat::Union{Matrix{Int}, SparseMatrixCSC{Int, Int}} = Matrix{Int}(undef, 0, 0)
9495
incidencegraph::Graphs.SimpleDiGraph{Int} = Graphs.DiGraph()
9596
linkageclasses::Vector{Vector{Int}} = Vector{Vector{Int}}(undef, 0)
97+
stronglinkageclasses::Vector{Vector{Int}} = Vector{Vector{Int}}(undef, 0)
98+
terminallinkageclasses::Vector{Vector{Int}} = Vector{Vector{Int}}(undef, 0)
9699
deficiency::Int = 0
97100
end
98101
#! format: on
@@ -116,6 +119,7 @@ function reset!(nps::NetworkProperties{I, V}) where {I, V}
116119
nps.isempty && return
117120
nps.netstoichmat = Matrix{Int}(undef, 0, 0)
118121
nps.conservationmat = Matrix{I}(undef, 0, 0)
122+
nps.cyclemat = Matrix{Int}(undef, 0, 0)
119123
empty!(nps.col_order)
120124
nps.rank = 0
121125
nps.nullity = 0
@@ -131,6 +135,8 @@ function reset!(nps::NetworkProperties{I, V}) where {I, V}
131135
nps.complexoutgoingmat = Matrix{Int}(undef, 0, 0)
132136
nps.incidencegraph = Graphs.DiGraph()
133137
empty!(nps.linkageclasses)
138+
empty!(nps.stronglinkageclasses)
139+
empty!(nps.terminallinkageclasses)
134140
nps.deficiency = 0
135141

136142
# this needs to be last due to setproperty! setting it to false

test/network_analysis/network_properties.jl

+84
Original file line numberDiff line numberDiff line change
@@ -325,3 +325,87 @@ let
325325
rates = Dict(zip(parameters(rn), k))
326326
@test Catalyst.iscomplexbalanced(rn, rates) == true
327327
end
328+
329+
### STRONG LINKAGE CLASS TESTS
330+
331+
332+
# a) Checks that strong/terminal linkage classes are correctly found. Should identify the (A, B+C) linkage class as non-terminal, since B + C produces D
333+
let
334+
rn = @reaction_network begin
335+
(k1, k2), A <--> B + C
336+
k3, B + C --> D
337+
k4, D --> E
338+
(k5, k6), E <--> 2F
339+
k7, 2F --> D
340+
(k8, k9), D + E <--> G
341+
end
342+
343+
rcs, D = reactioncomplexes(rn)
344+
slcs = stronglinkageclasses(rn)
345+
tslcs = terminallinkageclasses(rn)
346+
@test length(slcs) == 3
347+
@test length(tslcs) == 2
348+
@test issubset([[1,2], [3,4,5], [6,7]], slcs)
349+
@test issubset([[3,4,5], [6,7]], tslcs)
350+
end
351+
352+
# b) Makes the D + E --> G reaction irreversible. Thus, (D+E) becomes a non-terminal linkage class. Checks whether correctly identifies both (A, B+C) and (D+E) as non-terminal
353+
let
354+
rn = @reaction_network begin
355+
(k1, k2), A <--> B + C
356+
k3, B + C --> D
357+
k4, D --> E
358+
(k5, k6), E <--> 2F
359+
k7, 2F --> D
360+
(k8, k9), D + E --> G
361+
end
362+
363+
rcs, D = reactioncomplexes(rn)
364+
slcs = stronglinkageclasses(rn)
365+
tslcs = terminallinkageclasses(rn)
366+
@test length(slcs) == 4
367+
@test length(tslcs) == 2
368+
@test issubset([[1,2], [3,4,5], [6], [7]], slcs)
369+
@test issubset([[3,4,5], [7]], tslcs)
370+
end
371+
372+
# From a), makes the B + C <--> D reaction reversible. Thus, the non-terminal (A, B+C) linkage class gets absorbed into the terminal (A, B+C, D, E, 2F) linkage class, and the terminal linkage classes and strong linkage classes coincide.
373+
let
374+
rn = @reaction_network begin
375+
(k1, k2), A <--> B + C
376+
(k3, k4), B + C <--> D
377+
k5, D --> E
378+
(k6, k7), E <--> 2F
379+
k8, 2F --> D
380+
(k9, k10), D + E <--> G
381+
end
382+
383+
rcs, D = reactioncomplexes(rn)
384+
slcs = stronglinkageclasses(rn)
385+
tslcs = terminallinkageclasses(rn)
386+
@test length(slcs) == 2
387+
@test length(tslcs) == 2
388+
@test issubset([[1,2,3,4,5], [6,7]], slcs)
389+
@test issubset([[1,2,3,4,5], [6,7]], tslcs)
390+
end
391+
392+
# Simple test for strong and terminal linkage classes
393+
let
394+
rn = @reaction_network begin
395+
(k1, k2), A <--> 2B
396+
k3, A --> C + D
397+
(k4, k5), C + D <--> E
398+
k6, 2B --> F
399+
(k7, k8), F <--> 2G
400+
(k9, k10), 2G <--> H
401+
k11, H --> F
402+
end
403+
404+
rcs, D = reactioncomplexes(rn)
405+
slcs = stronglinkageclasses(rn)
406+
tslcs = terminallinkageclasses(rn)
407+
@test length(slcs) == 3
408+
@test length(tslcs) == 2
409+
@test issubset([[1,2], [3,4], [5,6,7]], slcs)
410+
@test issubset([[3,4], [5,6,7]], tslcs)
411+
end

0 commit comments

Comments
 (0)