Skip to content

Commit 81d30a6

Browse files
authored
Merge pull request #1115 from vyudu/GraphMakie
GraphMakie update
2 parents 5a66d73 + 82a575d commit 81d30a6

File tree

5 files changed

+339
-5
lines changed

5 files changed

+339
-5
lines changed

ext/CatalystGraphMakieExtension.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
module CatalystGraphMakieExtension
22

33
# Fetch packages.
4-
using Catalyst, GraphMakie
5-
import Catalyst: lattice_plot, lattice_animation, extract_vals
6-
import Graphs: AbstractGraph, SimpleGraph
4+
using Catalyst, GraphMakie, Graphs, Symbolics
5+
using Symbolics: get_variables!
6+
import Catalyst: species_reaction_graph, incidencematgraph, lattice_plot, lattice_animation
77

8-
# Creates and exports hc_steady_states function.
8+
# Creates and exports graph plotting functions.
99
include("CatalystGraphMakieExtension/graph_makie_extension_spatial_modelling.jl")
10-
10+
include("CatalystGraphMakieExtension/rn_graph_plot.jl")
1111
end
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
#############
2+
# Adapted from https://github.com/MakieOrg/GraphMakie.jl/issues/52#issuecomment-1018527479
3+
#############
4+
5+
"""
6+
SRGraphWrap{T}
7+
8+
Wrapper for the species-reaction graph containing edges for rate-dependence on species. Intended to allow plotting of multiple edges.
9+
"""
10+
struct SRGraphWrap{T} <: Graphs.AbstractGraph{T}
11+
g::SimpleDiGraph{T}
12+
rateedges::Vector{Graphs.SimpleEdge{T}}
13+
edgeorder::Vector{Int64}
14+
end
15+
16+
# Create the SimpleDiGraph corresponding to the species and reactions
17+
function SRGraphWrap(rn::ReactionSystem)
18+
srg = species_reaction_graph(rn)
19+
rateedges = Vector{Graphs.SimpleEdge{Int}}()
20+
sm = speciesmap(rn); specs = species(rn)
21+
22+
deps = Set()
23+
for (i, rx) in enumerate(reactions(rn))
24+
empty!(deps)
25+
get_variables!(deps, rx.rate, specs)
26+
if !isempty(deps)
27+
for spec in deps
28+
specidx = sm[spec]
29+
push!(rateedges, Graphs.SimpleEdge(specidx, i + length(specs)))
30+
end
31+
end
32+
end
33+
edgelist = vcat(collect(Graphs.edges(srg)), rateedges)
34+
edgeorder = sortperm(edgelist)
35+
SRGraphWrap(srg, rateedges, edgeorder)
36+
end
37+
38+
Base.eltype(g::SRGraphWrap) = eltype(g.g)
39+
Graphs.edgetype(g::SRGraphWrap) = edgetype(g.g)
40+
Graphs.has_edge(g::SRGraphWrap, s, d) = has_edge(g.g, s, d)
41+
Graphs.has_vertex(g::SRGraphWrap, i) = has_vertex(g.g, i)
42+
Graphs.inneighbors(g::SRGraphWrap{T}, i) where T = inneighbors(g.g, i)
43+
Graphs.outneighbors(g::SRGraphWrap{T}, i) where T = outneighbors(g.g, i)
44+
Graphs.ne(g::SRGraphWrap) = length(g.rateedges) + length(Graphs.edges(g.g))
45+
Graphs.nv(g::SRGraphWrap) = nv(g.g)
46+
Graphs.vertices(g::SRGraphWrap) = vertices(g.g)
47+
Graphs.is_directed(g::SRGraphWrap) = is_directed(g.g)
48+
49+
function Graphs.edges(g::SRGraphWrap)
50+
edgelist = vcat(collect(Graphs.edges(g.g)), g.rateedges)[g.edgeorder]
51+
end
52+
53+
function gen_distances(g::SRGraphWrap; inc = 0.2)
54+
edgelist = edges(g)
55+
distances = zeros(length(edgelist))
56+
for i in 2:Base.length(edgelist)
57+
edgelist[i] == edgelist[i-1] && (distances[i] = inc)
58+
end
59+
distances
60+
end
61+
62+
"""
63+
plot_network(rn::ReactionSystem; interactive=false)
64+
65+
Converts a [`ReactionSystem`](@ref) into a GraphMakie plot of the species reaction graph
66+
(or Petri net representation). Reactions correspond to small green circles, and
67+
species to blue circles.
68+
69+
Notes:
70+
- Black arrows from species to reactions indicate reactants, and are labelled
71+
with their input stoichiometry.
72+
- Black arrows from reactions to species indicate products, and are labelled
73+
with their output stoichiometry.
74+
- Red arrows from species to reactions indicate that species is used within the
75+
rate expression. For example, in the reaction `k*A, B --> C`, there would be a
76+
red arrow from `A` to the reaction node. In `k*A, A+B --> C`, there would be
77+
red and black arrows from `A` to the reaction node.
78+
"""
79+
# TODO: update docs for interacting with plots. The `interactive` flag sets the ability to interactively drag nodes and edges in the generated plot. Only allowed if `GLMakie` is the loaded Makie backend.
80+
function Catalyst.plot_network(rn::ReactionSystem)
81+
srg = SRGraphWrap(rn)
82+
ns = length(species(rn))
83+
nodecolors = vcat([:skyblue3 for i in 1:ns],
84+
[:green for i in ns+1:nv(srg)])
85+
ilabels = vcat(map(s -> String(tosymbol(s, escape=false)), species(rn)),
86+
fill("", nv(srg.g) - ns))
87+
nodesizes = vcat([30 for i in 1:ns],
88+
[10 for i in ns+1:nv(srg)])
89+
90+
ssm = substoichmat(rn); psm = prodstoichmat(rn)
91+
# Get stoichiometry of reaction
92+
edgelabels = map(Graphs.edges(srg.g)) do e
93+
string(src(e) > ns ?
94+
psm[dst(e), src(e)-ns] :
95+
ssm[src(e), dst(e)-ns])
96+
end
97+
edgecolors = [:black for i in 1:ne(srg)]
98+
99+
num_e = ne(srg.g)
100+
for i in 1:length(srg.edgeorder)
101+
if srg.edgeorder[i] > num_e
102+
edgecolors[i] = :red
103+
insert!(edgelabels, i, "")
104+
end
105+
end
106+
107+
graphplot(srg;
108+
edge_color = edgecolors,
109+
elabels = edgelabels,
110+
elabels_rotation = 0,
111+
ilabels = ilabels,
112+
node_color = nodecolors,
113+
node_size = nodesizes,
114+
arrow_shift = :end,
115+
arrow_size = 20,
116+
curve_distance_usage = true,
117+
curve_distance = gen_distances(srg)
118+
)
119+
end
120+
121+
"""
122+
plot_complexes(rn::ReactionSystem; interactive=false)
123+
124+
Creates a GraphMakie plot of the [`ReactionComplex`](@ref)s in `rn`. Reactions
125+
correspond to arrows and reaction complexes to blue circles.
126+
127+
Notes:
128+
- Black arrows from complexes to complexes indicate reactions whose rate is a
129+
parameter or a `Number`. i.e. `k, A --> B`.
130+
- Red arrows from complexes to complexes indicate reactions whose rate
131+
depends on species. i.e. `k*C, A --> B` for `C` a species.
132+
"""
133+
function Catalyst.plot_complexes(rn::ReactionSystem)
134+
img = incidencematgraph(rn)
135+
specs = species(rn); rxs = reactions(rn)
136+
edgecolors = [:black for i in 1:ne(img)]
137+
nodelabels = complexlabels(rn)
138+
edgelabels = [repr(rx.rate) for rx in rxs]
139+
140+
deps = Set()
141+
for (i, rx) in enumerate(rxs)
142+
empty!(deps)
143+
get_variables!(deps, rx.rate, specs)
144+
(!isempty(deps)) && (edgecolors[i] = :red)
145+
end
146+
147+
graphplot(img;
148+
edge_color = edgecolors,
149+
elabels = edgelabels,
150+
ilabels = complexlabels(rn),
151+
node_color = :skyblue3,
152+
elabels_rotation = 0,
153+
arrow_shift = :end,
154+
curve_distance = 0.2
155+
)
156+
end
157+
158+
function complexelem_tostr(e::Catalyst.ReactionComplexElement, specstrs)
159+
if e.speciesstoich == 1
160+
return "$(specstrs[e.speciesid])"
161+
else
162+
return "$(e.speciesstoich)$(specstrs[e.speciesid])"
163+
end
164+
end
165+
166+
# Get the strings corresponding to the reaction complexes
167+
function complexlabels(rn::ReactionSystem)
168+
labels = String[]
169+
170+
specstrs = map(s -> String(tosymbol(s, escape=false)), species(rn))
171+
complexes, B = reactioncomplexes(rn)
172+
173+
for complex in complexes
174+
if isempty(complex)
175+
push!(labels, "")
176+
elseif length(complex) == 1
177+
push!(labels, complexelem_tostr(complex[1], specstrs))
178+
else
179+
elems = map(c -> complexelem_tostr(c, specstrs), complex)
180+
str = reduce((e1, e2) -> *(e1, " + ", e2), @view elems[2:end]; init = elems[1])
181+
push!(labels, str)
182+
end
183+
end
184+
labels
185+
end

src/Catalyst.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ export hc_steady_states
167167
function make_si_ode end
168168
export make_si_ode
169169

170+
# GraphMakie
171+
function plot_network end
172+
function plot_complexes end
173+
export plot_network, plot_complexes
174+
170175
### Spatial Reaction Networks ###
171176

172177
# Spatial reactions.

src/network_analysis.jl

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,44 @@ function incidencematgraph(incidencemat::SparseMatrixCSC{Int, Int})
314314
return graph
315315
end
316316

317+
318+
"""
319+
species_reaction_graph(rn::ReactionSystem)
320+
321+
Construct a directed simple graph where there are two types of nodes: species and reactions.
322+
An edge from a species *s* to reaction *r* indicates that *s* is a reactant for *r*, and
323+
an edge from a reaction *r* to a species *s* indicates that *s* is a product of *r*. By
324+
default, the species vertices are listed first, so the first *n* indices correspond to
325+
species nodes.
326+
327+
Note: this is equivalent to the Petri net representation of a chemical reaction network.
328+
329+
For example,
330+
```julia
331+
sir = @reaction_network SIR begin
332+
β, S + I --> 2I
333+
ν, I --> R
334+
end
335+
species_reaction_graph(sir)
336+
"""
337+
function species_reaction_graph(rn::ReactionSystem)
338+
specs = species(rn)
339+
rxs = reactions(rn)
340+
sm = speciesmap(rn)
341+
s = length(specs)
342+
343+
edgelist = Graphs.Edge[]
344+
for (i, rx) in enumerate(rxs)
345+
for spec in rx.substrates
346+
push!(edgelist, Graphs.Edge(sm[spec], s+i))
347+
end
348+
for spec in rx.products
349+
push!(edgelist, Graphs.Edge(s+i, sm[spec]))
350+
end
351+
end
352+
srg = Graphs.SimpleDiGraphFromIterator(edgelist)
353+
end
354+
317355
### Linkage, Deficiency, Reversibility ###
318356

319357
"""

test/extensions/graphmakie.jl

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using Catalyst, GraphMakie, GLMakie, Graphs
2+
include("../test_networks.jl")
3+
4+
# Test that speciesreactiongraph is generated correctly
5+
let
6+
brusselator = @reaction_network begin
7+
A, ∅ --> X
8+
1, 2X + Y --> 3X
9+
B, X --> Y
10+
1, X -->
11+
end
12+
13+
srg = Catalyst.species_reaction_graph(brusselator)
14+
s = length(species(brusselator))
15+
edgel = Graphs.Edge.([(s+1, 1),
16+
(1, s+2),
17+
(2, s+2),
18+
(s+2, 1),
19+
(s+3, 2),
20+
(1, s+3),
21+
(1, s+4)])
22+
@test all((collect(Graphs.edges(srg))), edgel)
23+
24+
MAPK = @reaction_network MAPK begin
25+
(k₁, k₂),KKK + E1 <--> KKKE1
26+
k₃, KKKE1 --> KKK_ + E1
27+
(k₄, k₅), KKK_ + E2 <--> KKKE2
28+
k₆, KKKE2 --> KKK + E2
29+
(k₇, k₈), KK + KKK_ <--> KK_KKK_
30+
k₉, KK_KKK_ --> KKP + KKK_
31+
(k₁₀, k₁₁), KKP + KKK_ <--> KKPKKK_
32+
k₁₂, KKPKKK_ --> KKPP + KKK_
33+
(k₁₃, k₁₄), KKP + KKPase <--> KKPKKPase
34+
k₁₅, KKPPKKPase --> KKP + KKPase
35+
k₁₆,KKPKKPase --> KK + KKPase
36+
(k₁₇, k₁₈), KKPP + KKPase <--> KKPPKKPase
37+
(k₁₉, k₂₀), KKPP + K <--> KKPPK
38+
k₂₁, KKPPK --> KKPP + KP
39+
(k₂₂, k₂₃), KKPP + KP <--> KPKKPP
40+
k₂₄, KPKKPP --> KPP + KKPP
41+
(k₂₅, k₂₆), KP + KPase <--> KPKPase
42+
k₂₇, KKPPKPase --> KP + KPase
43+
k₂₈, KPKPase --> K + KPase
44+
(k₂₉, k₃₀), KPP + KPase <--> KKPPKPase
45+
end
46+
srg = Catalyst.species_reaction_graph(MAPK)
47+
@test nv(srg) == length(species(MAPK)) + length(reactions(MAPK))
48+
@test ne(srg) == 90
49+
50+
# Test that figures are generated properly.
51+
f = plot_network(MAPK)
52+
save("fig.png", f)
53+
@test isfile("fig.png")
54+
rm("fig.png")
55+
f = plot_network(brusselator)
56+
save("fig.png", f)
57+
@test isfile("fig.png")
58+
rm("fig.png")
59+
60+
f = plot_complexes(MAPK); save("fig.png", f)
61+
@test isfile("fig.png")
62+
rm("fig.png")
63+
f = plot_complexes(brusselator); save("fig.png", f)
64+
@test isfile("fig.png")
65+
rm("fig.png")
66+
end
67+
68+
CGME = Base.get_extension(parentmodule(ReactionSystem), :CatalystGraphMakieExtension)
69+
# Test that rate edges are inferred correctly. We should see two for the following reaction network.
70+
let
71+
# Two rate edges, one to species and one to product
72+
rn = @reaction_network begin
73+
k, A --> B
74+
k * C, A --> C
75+
k * B, B --> C
76+
end
77+
srg = CGME.SRGraphWrap(rn)
78+
s = length(species(rn))
79+
@test ne(srg) == 8
80+
@test Graphs.Edge(3, s+2) srg.rateedges
81+
@test Graphs.Edge(2, s+3) srg.rateedges
82+
# Since B is both a dep and a reactant
83+
@test count(==(Graphs.Edge(2, s+3)), edges(srg)) == 2
84+
85+
f = plot_network(rn)
86+
save("fig.png", f)
87+
@test isfile("fig.png")
88+
rm("fig.png")
89+
f = plot_complexes(rn); save("fig.png", f)
90+
@test isfile("fig.png")
91+
rm("fig.png")
92+
93+
# Two rate edges, both to reactants
94+
rn = @reaction_network begin
95+
k, A --> B
96+
k * A, A --> C
97+
k * B, B --> C
98+
end
99+
srg = CGME.SRGraphWrap(rn)
100+
s = length(species(rn))
101+
@test ne(srg) == 8
102+
# Since A, B is both a dep and a reactant
103+
@test count(==(Graphs.Edge(1, s+2)), edges(srg)) == 2
104+
@test count(==(Graphs.Edge(2, s+3)), edges(srg)) == 2
105+
end
106+

0 commit comments

Comments
 (0)