Skip to content

Commit be35bf8

Browse files
authored
Merge pull request #1140 from vyudu/graph-improvement
Improvements to graph plotting
2 parents 19d9652 + 18f8478 commit be35bf8

File tree

4 files changed

+103
-60
lines changed

4 files changed

+103
-60
lines changed

Project.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,13 @@ BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665"
3232
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
3333
GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2"
3434
HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327"
35+
NetworkLayout = "46757867-2c16-5918-afeb-47bfcb05e46a"
3536
# StructuralIdentifiability = "220ca800-aa68-49bb-acd8-6037fa93a544"
3637

3738
[extensions]
3839
CatalystBifurcationKitExtension = "BifurcationKit"
3940
CatalystCairoMakieExtension = "CairoMakie"
40-
CatalystGraphMakieExtension = "GraphMakie"
41+
CatalystGraphMakieExtension = ["GraphMakie", "NetworkLayout"]
4142
CatalystHomotopyContinuationExtension = "HomotopyContinuation"
4243
# CatalystStructuralIdentifiabilityExtension = "StructuralIdentifiability"
4344

@@ -58,6 +59,7 @@ LaTeXStrings = "1.3.0"
5859
Latexify = "0.16.5"
5960
MacroTools = "0.5.5"
6061
ModelingToolkit = "9.32"
62+
NetworkLayout = "0.4.7"
6163
Parameters = "0.12"
6264
Reexport = "0.2, 1.0"
6365
Requires = "1.0"

ext/CatalystGraphMakieExtension.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module CatalystGraphMakieExtension
22

33
# Fetch packages.
4-
using Catalyst, GraphMakie, Graphs, Symbolics, SparseArrays
4+
using Catalyst, GraphMakie, Graphs, Symbolics, SparseArrays, NetworkLayout
55
using Symbolics: get_variables!
66
import Catalyst: species_reaction_graph, incidencematgraph, lattice_plot, lattice_animation
77

ext/CatalystGraphMakieExtension/rn_graph_plot.jl

Lines changed: 91 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
#############
1+
#################################
22
# Adapted from https://github.com/MakieOrg/GraphMakie.jl/issues/52#issuecomment-1018527479
3-
#############
3+
#################################
44

55
"""
66
MultiGraphWrap{T}
@@ -14,14 +14,16 @@ Wrapper intended to allow plotting of multiple edges. This is needed in the foll
1414
struct MultiGraphWrap{T} <: Graphs.AbstractGraph{T}
1515
g::SimpleDiGraph{T}
1616
multiedges::Vector{Graphs.SimpleEdge{T}}
17-
edgeorder::Vector{Int64}
17+
"""Sets the drawing order of the edges. Needed because multiedges need to be consecutive to be drawn properly."""
18+
edgeorder::Vector{Int64}
1819
end
1920

2021
# Create the SimpleDiGraph corresponding to the species and reactions, the species-reaction graph
21-
function MultiGraphWrap(rn::ReactionSystem)
22+
function SRGraphWrap(rn::ReactionSystem)
2223
srg = species_reaction_graph(rn)
23-
rateedges = Vector{Graphs.SimpleEdge{Int}}()
24-
sm = speciesmap(rn); specs = species(rn)
24+
multiedges = Vector{Graphs.SimpleEdge{Int}}()
25+
sm = speciesmap(rn)
26+
specs = species(rn)
2527

2628
deps = Set()
2729
for (i, rx) in enumerate(reactions(rn))
@@ -30,22 +32,57 @@ function MultiGraphWrap(rn::ReactionSystem)
3032
if !isempty(deps)
3133
for spec in deps
3234
specidx = sm[spec]
33-
push!(rateedges, Graphs.SimpleEdge(specidx, i + length(specs)))
35+
has_edge(srg, specidx, i + length(specs)) ?
36+
push!(multiedges, Graphs.SimpleEdge(specidx, i + length(specs))) :
37+
add_edge!(srg, Graphs.SimpleEdge(specidx, i + length(specs)))
3438
end
3539
end
3640
end
37-
edgelist = vcat(collect(Graphs.edges(srg)), rateedges)
41+
edgelist = vcat(collect(Graphs.edges(srg)), multiedges)
3842
edgeorder = sortperm(edgelist)
39-
MultiGraphWrap(srg, rateedges, edgeorder)
43+
MultiGraphWrap(srg, multiedges, edgeorder)
4044
end
4145

42-
# Automatically set edge order if not supplied
46+
# Automatically set edge drawing order if not supplied
4347
function MultiGraphWrap(g::SimpleDiGraph{T}, multiedges::Vector{Graphs.SimpleEdge{T}}) where T
4448
edgelist = vcat(collect(Graphs.edges(g)), multiedges)
4549
edgeorder = sortperm(edgelist)
4650
MultiGraphWrap(g, multiedges, edgeorder)
4751
end
4852

53+
# Return the multigraph and reaction order corresponding to the complex graph. The reaction order is the order of reactions(rn) that would match the edge order given by g.edgeorder.
54+
function ComplexGraphWrap(rn::ReactionSystem)
55+
img = incidencematgraph(rn)
56+
D = incidencemat(rn; sparse=true)
57+
specs = species(rn)
58+
rxs = reactions(rn)
59+
60+
deps = Set()
61+
edgelist = Vector{Graphs.SimpleEdge{Int}}()
62+
rows = rowvals(D)
63+
vals = nonzeros(D)
64+
65+
# Construct the edge order for reactions.
66+
for (i, rx) in enumerate(rxs)
67+
inds = nzrange(D, i)
68+
val = vals[inds]
69+
row = rows[inds]
70+
(sub, prod) = val[1] == -1 ? (row[1], row[2]) : (row[2], row[1])
71+
push!(edgelist, Graphs.SimpleEdge(sub, prod))
72+
73+
empty!(deps)
74+
get_variables!(deps, rx.rate, specs)
75+
end
76+
77+
rxorder = sortperm(edgelist)
78+
edgelist = edgelist[rxorder]
79+
multiedges = Vector{Graphs.SimpleEdge{Int}}()
80+
for i in 2:length(edgelist)
81+
isequal(edgelist[i], edgelist[i-1]) && push!(multiedges, edgelist[i])
82+
end
83+
MultiGraphWrap(img, multiedges), rxorder
84+
end
85+
4986
Base.eltype(g::MultiGraphWrap) = eltype(g.g)
5087
Graphs.edgetype(g::MultiGraphWrap) = edgetype(g.g)
5188
Graphs.has_edge(g::MultiGraphWrap, s, d) = has_edge(g.g, s, d)
@@ -57,12 +94,21 @@ Graphs.nv(g::MultiGraphWrap) = nv(g.g)
5794
Graphs.vertices(g::MultiGraphWrap) = vertices(g.g)
5895
Graphs.is_directed(::Type{<:MultiGraphWrap}) = true
5996
Graphs.is_directed(g::MultiGraphWrap) = is_directed(g.g)
97+
Graphs.is_connected(g::MultiGraphWrap) = is_connected(g.g)
98+
99+
function Graphs.adjacency_matrix(g::MultiGraphWrap)
100+
adj = Graphs.adjacency_matrix(g.g)
101+
for e in g.multiedges
102+
adj[src(e), dst(e)] = 1
103+
end
104+
adj
105+
end
60106

61107
function Graphs.edges(g::MultiGraphWrap)
62108
edgelist = vcat(collect(Graphs.edges(g.g)), g.multiedges)[g.edgeorder]
63109
end
64110

65-
function gen_distances(g::MultiGraphWrap; inc = 0.4)
111+
function gen_distances(g::MultiGraphWrap; inc = 0.2)
66112
edgelist = edges(g)
67113
distances = zeros(length(edgelist))
68114
edgedict = Dict(edgelist[1] => [1])
@@ -95,7 +141,7 @@ function gen_distances(g::MultiGraphWrap; inc = 0.4)
95141
end
96142

97143
"""
98-
plot_network(rn::ReactionSystem)
144+
plot_network(rn::ReactionSystem; kwargs...)
99145
100146
Converts a [`ReactionSystem`](@ref) into a GraphMakie plot of the species reaction graph
101147
(or Petri net representation). Reactions correspond to small green circles, and
@@ -110,18 +156,19 @@ Notes:
110156
rate expression. For example, in the reaction `k*A, B --> C`, there would be a
111157
red arrow from `A` to the reaction node. In `k*A, A+B --> C`, there would be
112158
red and black arrows from `A` to the reaction node.
159+
160+
For a list of accepted keyword arguments to the graph plot, please see the [GraphMakie documentation](https://graph.makie.org/stable/#The-graphplot-Recipe).
113161
"""
114-
function Catalyst.plot_network(rn::ReactionSystem)
115-
srg = MultiGraphWrap(rn)
162+
function Catalyst.plot_network(rn::ReactionSystem; kwargs...)
163+
srg = SRGraphWrap(rn)
116164
ns = length(species(rn))
117165
nodecolors = vcat([:skyblue3 for i in 1:ns],
118166
[:green for i in ns+1:nv(srg)])
119167
ilabels = vcat(map(s -> String(tosymbol(s, escape=false)), species(rn)),
120-
fill("", nv(srg.g) - ns))
121-
nodesizes = vcat([30 for i in 1:ns],
122-
[10 for i in ns+1:nv(srg)])
168+
["R$i" for i in 1:nv(srg)-ns])
123169

124-
ssm = substoichmat(rn); psm = prodstoichmat(rn)
170+
ssm = substoichmat(rn)
171+
psm = prodstoichmat(rn)
125172
# Get stoichiometry of reaction
126173
edgelabels = map(Graphs.edges(srg.g)) do e
127174
string(src(e) > ns ?
@@ -131,29 +178,38 @@ function Catalyst.plot_network(rn::ReactionSystem)
131178
edgecolors = [:black for i in 1:ne(srg)]
132179

133180
num_e = ne(srg.g)
181+
# Handle the rate edges
134182
for i in 1:length(srg.edgeorder)
135-
if srg.edgeorder[i] > num_e
183+
# If there are stoichiometry and rate edges from the same species to reaction
184+
if srg.edgeorder[i] > num_e
136185
edgecolors[i] = :red
137186
insert!(edgelabels, i, "")
187+
elseif edgelabels[i] == "0"
188+
edgecolors[i] = :red
189+
edgelabels[i] = ""
138190
end
139191
end
140192

193+
layout = if !haskey(kwargs, :layout)
194+
is_connected(srg) ? Stress() : Spring()
195+
end
141196
graphplot(srg;
197+
layout,
142198
edge_color = edgecolors,
143199
elabels = edgelabels,
144200
elabels_rotation = 0,
145201
ilabels = ilabels,
146202
node_color = nodecolors,
147-
node_size = nodesizes,
148203
arrow_shift = :end,
149204
arrow_size = 20,
150205
curve_distance_usage = true,
151206
curve_distance = gen_distances(srg),
207+
kwargs...
152208
)
153209
end
154210

155211
"""
156-
plot_complexes(rn::ReactionSystem)
212+
plot_complexes(rn::ReactionSystem; kwargs...)
157213
158214
Creates a GraphMakie plot of the [`ReactionComplex`](@ref)s in `rn`. Reactions
159215
correspond to arrows and reaction complexes to blue circles.
@@ -163,48 +219,39 @@ end
163219
parameter or a `Number`. i.e. `k, A --> B`.
164220
- Red arrows from complexes to complexes indicate reactions whose rate
165221
depends on species. i.e. `k*C, A --> B` for `C` a species.
222+
223+
For a list of accepted keyword arguments to the graph plot, please see the [GraphMakie documentation](https://graph.makie.org/stable/#The-graphplot-Recipe).
166224
"""
167-
function Catalyst.plot_complexes(rn::ReactionSystem)
168-
img = incidencematgraph(rn); D = incidencemat(rn; sparse=true)
169-
specs = species(rn); rxs = reactions(rn)
225+
function Catalyst.plot_complexes(rn::ReactionSystem; kwargs...)
226+
rxs = reactions(rn)
227+
specs = species(rn)
170228
edgecolors = [:black for i in 1:length(rxs)]
171229
edgelabels = [repr(rx.rate) for rx in rxs]
172230

173231
deps = Set()
174-
edgelist = Vector{Graphs.SimpleEdge{Int}}()
175-
rows = rowvals(D)
176-
vals = nonzeros(D)
177-
178-
# Construct the edge order for reactions.
179232
for (i, rx) in enumerate(rxs)
180-
inds = nzrange(D, i)
181-
val = vals[inds]
182-
row = rows[inds]
183-
(sub, prod) = val[1] == -1 ? (row[1], row[2]) : (row[2], row[1])
184-
push!(edgelist, Graphs.SimpleEdge(sub, prod))
185-
186233
empty!(deps)
187234
get_variables!(deps, rx.rate, specs)
188235
(!isempty(deps)) && (edgecolors[i] = :red)
189236
end
190237

191-
# Resolve differences between reaction order and edge order in the incidencematgraph.
192-
rxorder = sortperm(edgelist); edgelist = edgelist[rxorder]
193-
multiedges = Vector{Graphs.SimpleEdge{Int}}()
194-
for i in 2:length(edgelist)
195-
isequal(edgelist[i], edgelist[i-1]) && push!(multiedges, edgelist[i])
196-
end
197-
img_ = MultiGraphWrap(img, multiedges)
238+
# Get complex graph and reaction order for edgecolors and edgelabels. rxorder gives the order of reactions(rn) that would match the edge order in edges(cg).
239+
cg, rxorder = ComplexGraphWrap(rn)
198240

199-
graphplot(img_;
241+
layout = if !haskey(kwargs, :layout)
242+
is_connected(cg) ? Stress() : Spring()
243+
end
244+
graphplot(cg;
245+
layout,
200246
edge_color = edgecolors[rxorder],
201247
elabels = edgelabels[rxorder],
202248
ilabels = complexlabels(rn),
203249
node_color = :skyblue3,
204250
elabels_rotation = 0,
205251
arrow_shift = :end,
206252
curve_distance_usage = true,
207-
curve_distance = gen_distances(img_)
253+
curve_distance = gen_distances(cg),
254+
kwargs...
208255
)
209256
end
210257

test/extensions/graphmakie.jl

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,9 @@ let
7474
k * C, A --> C
7575
k * B, B --> C
7676
end
77-
srg = CGME.MultiGraphWrap(rn)
77+
srg = CGME.SRGraphWrap(rn)
7878
s = length(species(rn))
7979
@test ne(srg) == 8
80-
@test Graphs.Edge(3, s+2) srg.multiedges
8180
@test Graphs.Edge(2, s+3) srg.multiedges
8281
# Since B is both a dep and a reactant
8382
@test count(==(Graphs.Edge(2, s+3)), edges(srg)) == 2
@@ -96,7 +95,7 @@ let
9695
k * A, A --> C
9796
k * B, B --> C
9897
end
99-
srg = CGME.MultiGraphWrap(rn)
98+
srg = CGME.SRGraphWrap(rn)
10099
s = length(species(rn))
101100
@test ne(srg) == 8
102101
# Since A, B is both a dep and a reactant
@@ -106,29 +105,24 @@ end
106105

107106
function test_edgeorder(rn)
108107
# The initial edgelabels in `plot_complexes` is given by the order of reactions in reactions(rn).
109-
D = incidencemat(rn; sparse=true); img = incidencematgraph(rn)
108+
D = incidencemat(rn; sparse=true)
110109
rxs = reactions(rn)
111110
edgelist = Vector{Graphs.SimpleEdge{Int}}()
112111
rows = rowvals(D)
113112
vals = nonzeros(D)
114-
113+
115114
for (i, rx) in enumerate(rxs)
116115
inds = nzrange(D, i)
117116
val = vals[inds]
118117
row = rows[inds]
119118
(sub, prod) = val[1] == -1 ? (row[1], row[2]) : (row[2], row[1])
120119
push!(edgelist, Graphs.SimpleEdge(sub, prod))
121120
end
121+
122+
img, rxorder = CGME.ComplexGraphWrap(rn)
122123

123-
rxorder = sortperm(edgelist); sortededgelist = edgelist[rxorder]
124-
multiedges = Vector{Graphs.SimpleEdge{Int}}()
125-
for i in 2:length(sortededgelist)
126-
isequal(sortededgelist[i], sortededgelist[i-1]) && push!(multiedges, sortededgelist[i])
127-
end
128-
img_ = CGME.MultiGraphWrap(img, multiedges)
129-
130-
# Label iteration order is given by edgelist[rxorder. Actual edge drawing iteration order is given by edges(g)
131-
@test edgelist[rxorder] == Graphs.edges(img_)
124+
# Label iteration order is given by edgelist[rxorder]. Actual edge drawing iteration order is given by edges(g)
125+
@test edgelist[rxorder] == Graphs.edges(img)
132126
return rxorder
133127
end
134128

0 commit comments

Comments
 (0)