Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deficiency one and concnetration robustness #964

Merged
merged 27 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 85 additions & 5 deletions src/network_analysis.jl
Original file line number Diff line number Diff line change
Expand Up @@ -421,11 +421,15 @@ end
"""
function deficiency(rn::ReactionSystem)
nps = get_networkproperties(rn)
conservationlaws(rn)
r = nps.rank
ig = incidencematgraph(rn)
lc = linkageclasses(rn)
nps.deficiency = Graphs.nv(ig) - length(lc) - r

# Check if deficiency has been computed already (initialized to -1)
if nps.deficiency == -1
conservationlaws(rn)
r = nps.rank
ig = incidencematgraph(rn)
lc = linkageclasses(rn)
nps.deficiency = Graphs.nv(ig) - length(lc) - r
end
nps.deficiency
end

Expand Down Expand Up @@ -938,3 +942,79 @@ end
function fluxvectors(rs::ReactionSystem)
cycles(rs)
end

### Deficiency one

"""
satisfiesdeficiencyone(rn::ReactionSystem)

Check if a reaction network obeys the conditions of the deficiency one theorem, which ensures that there is only one equilibrium for every positive stoichiometric compatibility class.
"""

function satisfiesdeficiencyone(rn::ReactionSystem)
complexes, D = reactioncomplexes(rn)
δ = deficiency(rn)
δ_l = linkagedeficiencies(rn)

lcs = linkageclasses(rn)
tslcs = terminallinkageclasses(rn)

# Check the conditions for the deficiency one theorem:
# 1) the deficiency of each individual linkage class is at most 1;
# 2) the sum of the linkage deficiencies is the total deficiency, and
# 3) there is only one terminal linkage class per linkage class.
all(<=(1), δ_l) && (sum(δ_l) == δ) && (length(lcs) == length(tslcs))
end

"""
satisfiesdeficiencyzero(rn::ReactionSystem)

Check if a reaction network obeys the conditions of the deficiency zero theorem, which ensures that there is only one equilibrium for every positive stoichiometric compatibility class, this equilibrium is asymptotically stable, and this equilibrium is complex balanced.
"""

function satisfiesdeficiencyzero(rn::ReactionSystem)
δ = deficiency(rn)
δ == 0 && isweaklyreversible(rn, subnetworks(rn))
end

"""
robustspecies(rn::ReactionSystem)

Return a vector of indices corresponding to species that are concentration robust, i.e. for every positive equilbrium, the concentration of species s will be the same.

Note: This function currently only works for networks of deficiency one, and is not currently guaranteed to return *all* the concentration-robust species in the network. Any species returned by the function will be robust, but this may not include all of them. Use with caution. Support for higher deficiency networks and necessary conditions for robustness will be coming in the future.
"""

function robustspecies(rn::ReactionSystem)
complexes, D = reactioncomplexes(rn)
nps = get_networkproperties(rn)

if deficiency(rn) != 1
error("This algorithm currently only checks for robust species in networks with deficiency one.")
end

# A species is concnetration robust in a deficiency one network if there are two non-terminal complexes (i.e. complexes
# belonging to a linkage class that is not terminal) that differ only in species s (i.e. their difference is some
# multiple of s. (A + B, A) differ only in B. (A + 2B, B) differ in both A and B, since A + 2B - B = A + B).

if !nps.checkedrobust
tslcs = terminallinkageclasses(rn)
Z = complexstoichmat(rn)

# Find the complexes that do not belong to a terminal linkage class
nonterminal_complexes = deleteat!([1:length(complexes);], vcat(tslcs...))
robust_species = Int64[]

for (c_s, c_p) in collect(Combinatorics.combinations(nonterminal_complexes, 2))
# Check the difference of all the combinations of complexes. The support is the set of indices that are non-zero
supp = findall(!=(0), Z[:, c_s] - Z[:, c_p])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The column difference could be stored in a pre-calculated vector from outside the loop like

@. tmp = @views Z[:, c_2] - Z[:, c_p]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More generally, seems like this could be written as a non-allocating loop that bails if a second match is found instead of continuing the search, and if a second match isn't found saves it in robust_species.

Copy link
Member Author

@vyudu vyudu Jul 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re the first comment: would I accomplish the same thing by adding @views in the supp line, so it becomes

supp = findall(!=(0), @views Z[:, c_s] - Z[:, c_p])

Seems like allocating it entirely outside might take a lot of space (would have to be a 3D array, where each difference vector is indexed by (c_s, c_p))

For the second comment, what do you mean by second match? I don't immediately see a way to skip a check for a species we already know is robust because we can't know that it's in the support of a difference of two complexes until we calculate it


# If the support has length one, then they differ in only one species, and that species is concentration robust.
length(supp) == 1 && supp[1] ∉ robust_species && push!(robust_species, supp...)
end
nps.checkedrobust = true
nps.robustspecies = robust_species
end

nps.robustspecies
end
7 changes: 5 additions & 2 deletions src/reactionsystem.jl
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@ Base.@kwdef mutable struct NetworkProperties{I <: Integer, V <: BasicSymbolic{Re
linkageclasses::Vector{Vector{Int}} = Vector{Vector{Int}}(undef, 0)
stronglinkageclasses::Vector{Vector{Int}} = Vector{Vector{Int}}(undef, 0)
terminallinkageclasses::Vector{Vector{Int}} = Vector{Vector{Int}}(undef, 0)
deficiency::Int = 0

checkedrobust::Bool = false
robustspecies::Vector{Int} = Vector{Int}(undef, 0)
deficiency::Int = -1
end
#! format: on

Expand Down Expand Up @@ -137,7 +140,7 @@ function reset!(nps::NetworkProperties{I, V}) where {I, V}
empty!(nps.linkageclasses)
empty!(nps.stronglinkageclasses)
empty!(nps.terminallinkageclasses)
nps.deficiency = 0
nps.deficiency = -1

# this needs to be last due to setproperty! setting it to false
nps.isempty = true
Expand Down
120 changes: 120 additions & 0 deletions test/network_analysis/network_properties.jl
Original file line number Diff line number Diff line change
Expand Up @@ -608,3 +608,123 @@ let
Catalyst.ratematrix(rn, rates_dict) == rate_mat
@test_throws Exception Catalyst.iscomplexbalanced(rn, rates_invalid)
end

### CONCENTRATION ROBUSTNESS TESTS

# Check whether concentration-robust species are correctly identified for two well-known reaction networks: the glyoxylate IDHKP-IDH system, and the EnvZ_OmpR signaling pathway.

let
IDHKP_IDH = @reaction_network begin
(k1, k2), EIp + I <--> EIpI
k3, EIpI --> EIp + Ip
(k4, k5), E + Ip <--> EIp
k6, EIp --> E + I
end

@test Catalyst.robustspecies(IDHKP_IDH) == [2]
end

let
EnvZ_OmpR = @reaction_network begin
(k1, k2), X <--> XT
k3, XT --> Xp
(k4, k5), Xp + Y <--> XpY
k6, XpY --> X + Yp
(k7, k8), XT + Yp <--> XTYp
k9, XTYp --> XT + Y
end

@test Catalyst.robustspecies(EnvZ_OmpR) == [6]
end

### DEFICIENCY ONE TESTS

# Fails because there are two terminal linkage classes in the linkage class
let
rn = @reaction_network begin
k1, A + B --> 2B
k2, A + B --> 2A
end

@test Catalyst.satisfiesdeficiencyone(rn) == false
end

# Fails because linkage deficiencies do not sum to total deficiency
let
rn = @reaction_network begin
(k1, k2), A <--> 2A
(k3, k4), A + B <--> C
(k5, k6), C <--> B
end

@test Catalyst.satisfiesdeficiencyone(rn) == false
end

# Fails because a linkage class has deficiency two
let
rn = @reaction_network begin
k1, 3A --> A + 2B
k2, A + 2B --> 3B
k3, 3B --> 2A + B
k4, 2A + B --> 3A
end

@test Catalyst.satisfiesdeficiencyone(rn) == false
end

let
rn = @reaction_network begin
(k1, k2), 2A <--> D
(k3, k4), D <--> A + B
(k5, k6), A + B <--> C
(k7, k8), C <--> 2B
(k9, k10), C + D <--> E + F
(k11, k12), E + F <--> H
(k13, k14), H <--> C + E
(k15, k16), C + E <--> D + F
(k17, k18), A + D <--> G
(k19, k20), G <--> B + H
end

@test Catalyst.satisfiesdeficiencyone(rn) == true
end

### Some tests for deficiency zero networks.

let
rn = @reaction_network begin
(k1, k2), A <--> 2B
(k3, k4), A + C <--> D
k5, D --> B + E
k6, B + E --> A + C
end

# No longer weakly reversible
rn2 = @reaction_network begin
(k1, k2), A <--> 2B
(k3, k4), A + C <--> D
k5, B + E --> D
k6, B + E --> A + C
end

# No longer weakly reversible
rn3 = @reaction_network begin
k1, A --> 2B
(k3, k4), A + C <--> D
k5, D --> B + E
k6, B + E --> A + C
end

# Weakly reversible but deficiency one
rn4 = @reaction_network begin
(k1, k2), A <--> 2A
(k3, k4), A + B <--> C
(k5, k6), C <--> B
end

@test Catalyst.satisfiesdeficiencyzero(rn) == true
@test Catalyst.satisfiesdeficiencyzero(rn2) == false
@test Catalyst.satisfiesdeficiencyzero(rn3) == false
@test Catalyst.satisfiesdeficiencyzero(rn4) == false
end

Loading