• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SciML / Catalyst.jl / 11263951587

09 Oct 2024 10:20PM UTC coverage: 92.353% (+1.1%) from 91.23%
11263951587

push

github

web-flow
Merge pull request #1074 from vyudu/master

disable SI for now

3611 of 3910 relevant lines covered (92.35%)

171067.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.41
/src/network_analysis.jl
1
### Reaction Complex Handling ###
2

3
# get the species indices and stoichiometry while filtering out constant species.
4
function filter_constspecs(specs, stoich::AbstractVector{V}, smap) where {V <: Integer}
1,252✔
5
    isempty(specs) && (return Vector{Int}(), Vector{V}())
1,252✔
6

7
    if any(isconstant, specs)
1,098✔
8
        ids = Vector{Int}()
2✔
9
        filtered_stoich = Vector{V}()
2✔
10
        for (i, s) in enumerate(specs)
2✔
11
            if !isconstant(s)
13✔
12
                push!(ids, smap[s])
3✔
13
                push!(filtered_stoich, stoich[i])
3✔
14
            end
15
        end
8✔
16
    else
17
        ids = map(Base.Fix1(getindex, smap), specs)
1,096✔
18
        filtered_stoich = copy(stoich)
1,096✔
19
    end
20
    ids, filtered_stoich
1,098✔
21
end
22

23
"""
24
    reactioncomplexmap(rn::ReactionSystem)
25

26
Find each [`ReactionComplex`](@ref) within the specified system, constructing a mapping from
27
the complex to vectors that indicate which reactions it appears in as substrates and
28
products.
29

30
Notes:
31
- Each [`ReactionComplex`](@ref) is mapped to a vector of pairs, with each pair having the
32
  form `reactionidx => ± 1`, where `-1` indicates the complex appears as a substrate and
33
  `+1` as a product in the reaction with integer label `reactionidx`.
34
- Constant species are ignored as part of a complex. i.e. if species `A` is constant then
35
  the reaction `A + B --> C + D` is considered to consist of the complexes `B` and `C + D`.
36
  Likewise `A --> B` would be treated as the same as `0 --> B`.
37
"""
38
function reactioncomplexmap(rn::ReactionSystem)
268✔
39
    isempty(get_systems(rn)) ||
268✔
40
        error("reactioncomplexmap does not currently support subsystems.")
41

42
    # check if previously calculated and hence cached
43
    nps = get_networkproperties(rn)
268✔
44
    !isempty(nps.complextorxsmap) && return nps.complextorxsmap
268✔
45
    complextorxsmap = nps.complextorxsmap
107✔
46

47
    rxs = reactions(rn)
107✔
48
    smap = speciesmap(rn)
107✔
49
    numreactions(rn) > 0 ||
107✔
50
        error("There must be at least one reaction to find reaction complexes.")
51
    for (i, rx) in enumerate(rxs)
107✔
52
        subids, substoich = filter_constspecs(rx.substrates, rx.substoich, smap)
626✔
53
        subrc = sort!(ReactionComplex(subids, substoich))
626✔
54
        if haskey(complextorxsmap, subrc)
626✔
55
            push!(complextorxsmap[subrc], i => -1)
433✔
56
        else
57
            complextorxsmap[subrc] = [i => -1]
193✔
58
        end
59

60
        prodids, prodstoich = filter_constspecs(rx.products, rx.prodstoich, smap)
626✔
61
        prodrc = sort!(ReactionComplex(prodids, prodstoich))
626✔
62
        if haskey(complextorxsmap, prodrc)
626✔
63
            push!(complextorxsmap[prodrc], i => 1)
291✔
64
        else
65
            complextorxsmap[prodrc] = [i => 1]
335✔
66
        end
67
    end
626✔
68
    complextorxsmap
107✔
69
end
70

71
@doc raw"""
72
    reactioncomplexes(network::ReactionSystem; sparse=false)
73

74
Calculate the reaction complexes and complex incidence matrix for the given
75
[`ReactionSystem`](@ref).
76

77
Notes:
78
- returns a pair of a vector of [`ReactionComplex`](@ref)s and the complex incidence matrix.
79
- An empty [`ReactionComplex`](@ref) denotes the null (∅) state (from reactions like ∅ -> A
80
  or A -> ∅).
81
- Constant species are ignored in generating a reaction complex. i.e. if A is constant then
82
  A --> B consists of the complexes ∅ and B.
83
- The complex incidence matrix, ``B``, is number of complexes by number of reactions with
84
```math
85
B_{i j} = \begin{cases}
86
-1, &\text{if the i'th complex is the substrate of the j'th reaction},\\
87
1, &\text{if the i'th complex is the product of the j'th reaction},\\
88
0, &\text{otherwise.}
89
\end{cases}
90
```
91
- Set sparse=true for a sparse matrix representation of the incidence matrix
92
"""
93
function reactioncomplexes(rn::ReactionSystem; sparse = false)
524✔
94
    isempty(get_systems(rn)) ||
223✔
95
        error("reactioncomplexes does not currently support subsystems.")
96
    nps = get_networkproperties(rn)
223✔
97
    if isempty(nps.complexes) || (sparse != issparse(nps.complexes))
332✔
98
        complextorxsmap = reactioncomplexmap(rn)
140✔
99
        nps.complexes, nps.incidencemat = if sparse
140✔
100
            reactioncomplexes(SparseMatrixCSC{Int, Int}, rn, complextorxsmap)
33✔
101
        else
102
            reactioncomplexes(Matrix{Int}, rn, complextorxsmap)
166✔
103
        end
104
    end
105
    nps.complexes, nps.incidencemat
223✔
106
end
107

108
function reactioncomplexes(::Type{SparseMatrixCSC{Int, Int}}, rn::ReactionSystem,
33✔
109
        complextorxsmap)
110
    complexes = collect(keys(complextorxsmap))
66✔
111
    Is = Int[]
33✔
112
    Js = Int[]
33✔
113
    Vs = Int[]
33✔
114
    for (i, c) in enumerate(complexes)
33✔
115
        for (j, σ) in complextorxsmap[c]
180✔
116
            push!(Is, i)
446✔
117
            push!(Js, j)
446✔
118
            push!(Vs, σ)
446✔
119
        end
446✔
120
    end
327✔
121
    B = sparse(Is, Js, Vs, length(complexes), numreactions(rn))
33✔
122
    complexes, B
33✔
123
end
124

125
function reactioncomplexes(::Type{Matrix{Int}}, rn::ReactionSystem, complextorxsmap)
107✔
126
    complexes = collect(keys(complextorxsmap))
214✔
127
    B = zeros(Int, length(complexes), numreactions(rn))
4,487✔
128
    for (i, c) in enumerate(complexes)
107✔
129
        for (j, σ) in complextorxsmap[c]
528✔
130
            B[i, j] = σ
1,252✔
131
        end
1,252✔
132
    end
949✔
133
    complexes, B
107✔
134
end
135

136
"""
137
    incidencemat(rn::ReactionSystem; sparse=false)
138

139
Calculate the incidence matrix of `rn`, see [`reactioncomplexes`](@ref).
140

141
Notes:
142
- Is cached in `rn` so that future calls, assuming the same sparsity, will also be fast.
143
"""
144
incidencemat(rn::ReactionSystem; sparse = false) = reactioncomplexes(rn; sparse)[2]
152✔
145

146
"""
147
    complexstoichmat(network::ReactionSystem; sparse=false)
148

149
Given a [`ReactionSystem`](@ref) and vector of reaction complexes, return a
150
matrix with positive entries of size number of species by number of complexes,
151
where the non-zero positive entries in the kth column denote stoichiometric
152
coefficients of the species participating in the kth reaction complex.
153

154
Notes:
155
- Set sparse=true for a sparse matrix representation
156
"""
157
function complexstoichmat(rn::ReactionSystem; sparse = false)
128✔
158
    isempty(get_systems(rn)) ||
64✔
159
        error("complexstoichmat does not currently support subsystems.")
160
    nps = get_networkproperties(rn)
64✔
161
    if isempty(nps.complexstoichmat) || (sparse != issparse(nps.complexstoichmat))
95✔
162
        nps.complexstoichmat = if sparse
64✔
163
            complexstoichmat(SparseMatrixCSC{Int, Int}, rn, keys(reactioncomplexmap(rn)))
31✔
164
        else
165
            complexstoichmat(Matrix{Int}, rn, keys(reactioncomplexmap(rn)))
95✔
166
        end
167
    end
168
    nps.complexstoichmat
64✔
169
end
170

171
function complexstoichmat(::Type{SparseMatrixCSC{Int, Int}}, rn::ReactionSystem, rcs)
31✔
172
    Is = Int[]
31✔
173
    Js = Int[]
31✔
174
    Vs = Int[]
31✔
175
    for (i, rc) in enumerate(rcs)
62✔
176
        for rcel in rc
315✔
177
            push!(Is, rcel.speciesid)
182✔
178
            push!(Js, i)
182✔
179
            push!(Vs, rcel.speciesstoich)
182✔
180
        end
220✔
181
    end
311✔
182
    Z = sparse(Is, Js, Vs, numspecies(rn), length(rcs))
31✔
183
end
184

185
function complexstoichmat(::Type{Matrix{Int}}, rn::ReactionSystem, rcs)
33✔
186
    Z = zeros(Int, numspecies(rn), length(rcs))
787✔
187
    for (i, rc) in enumerate(rcs)
66✔
188
        for rcel in rc
345✔
189
            Z[rcel.speciesid, i] = rcel.speciesstoich
205✔
190
        end
251✔
191
    end
339✔
192
    Z
33✔
193
end
194

195
@doc raw"""
196
    complexoutgoingmat(network::ReactionSystem; sparse=false)
197

198
Given a [`ReactionSystem`](@ref) and complex incidence matrix, ``B``, return a
199
matrix of size num of complexes by num of reactions that identifies substrate
200
complexes.
201

202
Notes:
203
- The complex outgoing matrix, ``\Delta``, is defined by
204
```math
205
\Delta_{i j} = \begin{cases}
206
    = 0,    &\text{if } B_{i j} = 1, \\
207
    = B_{i j}, &\text{otherwise.}
208
\end{cases}
209
```
210
- Set sparse=true for a sparse matrix representation
211
"""
212
function complexoutgoingmat(rn::ReactionSystem; sparse = false)
36✔
213
    isempty(get_systems(rn)) ||
18✔
214
        error("complexoutgoingmat does not currently support subsystems.")
215
    nps = get_networkproperties(rn)
18✔
216
    if isempty(nps.complexoutgoingmat) || (sparse != issparse(nps.complexoutgoingmat))
27✔
217
        B = reactioncomplexes(rn, sparse = sparse)[2]
13✔
218
        nps.complexoutgoingmat = if sparse
11✔
219
            complexoutgoingmat(SparseMatrixCSC{Int, Int}, rn, B)
4✔
220
        else
221
            complexoutgoingmat(Matrix{Int}, rn, B)
29✔
222
        end
223
    end
224
    nps.complexoutgoingmat
18✔
225
end
226

227
function complexoutgoingmat(::Type{SparseMatrixCSC{Int, Int}}, rn::ReactionSystem, B)
2✔
228
    n = size(B, 2)
2✔
229
    rows = rowvals(B)
2✔
230
    vals = nonzeros(B)
2✔
231
    Is = Int[]
2✔
232
    Js = Int[]
2✔
233
    Vs = Int[]
2✔
234
    sizehint!(Is, div(length(vals), 2))
2✔
235
    sizehint!(Js, div(length(vals), 2))
2✔
236
    sizehint!(Vs, div(length(vals), 2))
2✔
237
    for j in 1:n
2✔
238
        for i in nzrange(B, j)
11✔
239
            if vals[i] != one(eltype(vals))
22✔
240
                push!(Is, rows[i])
11✔
241
                push!(Js, j)
11✔
242
                push!(Vs, vals[i])
11✔
243
            end
244
        end
33✔
245
    end
20✔
246
    sparse(Is, Js, Vs, size(B, 1), size(B, 2))
2✔
247
end
248

249
function complexoutgoingmat(::Type{Matrix{Int}}, rn::ReactionSystem, B)
9✔
250
    Δ = copy(B)
9✔
251
    for (I, b) in pairs(Δ)
18✔
252
        (b == 1) && (Δ[I] = 0)
347✔
253
    end
685✔
254
    Δ
9✔
255
end
256

257
"""
258
    incidencematgraph(rn::ReactionSystem)
259

260
Construct a directed simple graph where nodes correspond to reaction complexes and directed
261
edges to reactions converting between two complexes.
262

263
For example,
264
```julia
265
sir = @reaction_network SIR begin
266
    β, S + I --> 2I
267
    ν, I --> R
268
end
269
incidencematgraph(sir)
270
```
271
"""
272
function incidencematgraph(rn::ReactionSystem)
129✔
273
    nps = get_networkproperties(rn)
129✔
274
    if Graphs.nv(nps.incidencegraph) == 0
129✔
275
        isempty(nps.incidencemat) && reactioncomplexes(rn)
66✔
276
        nps.incidencegraph = incidencematgraph(nps.incidencemat)
73✔
277
    end
278
    nps.incidencegraph
129✔
279
end
280

281
function incidencematgraph(incidencemat::Matrix{Int})
59✔
282
    @assert all(∈([-1, 0, 1]), incidencemat)
59✔
283
    n = size(incidencemat, 1)  # no. of nodes/complexes
59✔
284
    graph = Graphs.DiGraph(n)
59✔
285
    for col in eachcol(incidencemat)
118✔
286
        src = 0
344✔
287
        dst = 0
344✔
288
        for i in eachindex(col)
344✔
289
            (col[i] == -1) && (src = i)
1,748✔
290
            (col[i] == 1) && (dst = i)
1,748✔
291
            (src != 0) && (dst != 0) && break
1,748✔
292
        end
1,404✔
293
        Graphs.add_edge!(graph, src, dst)
344✔
294
    end
629✔
295
    return graph
59✔
296
end
297

298
function incidencematgraph(incidencemat::SparseMatrixCSC{Int, Int})
14✔
299
    @assert all(∈([-1, 0, 1]), incidencemat)
14✔
300
    m, n = size(incidencemat)
14✔
301
    graph = Graphs.DiGraph(m)
14✔
302
    rows = rowvals(incidencemat)
14✔
303
    vals = nonzeros(incidencemat)
14✔
304
    for j in 1:n
14✔
305
        inds = nzrange(incidencemat, j)
80✔
306
        row = rows[inds]
160✔
307
        val = vals[inds]
160✔
308
        if val[1] == -1
80✔
309
            Graphs.add_edge!(graph, row[1], row[2])
56✔
310
        else
311
            Graphs.add_edge!(graph, row[2], row[1])
24✔
312
        end
313
    end
146✔
314
    return graph
14✔
315
end
316

317
### Linkage, Deficiency, Reversibility ###
318

319
"""
320
    linkageclasses(rn::ReactionSystem)
321

322
Given the incidence graph of a reaction network, return a vector of the
323
connected components of the graph (i.e. sub-groups of reaction complexes that
324
are connected in the incidence graph).
325

326
For example,
327
```julia
328
sir = @reaction_network SIR begin
329
    β, S + I --> 2I
330
    ν, I --> R
331
end
332
linkageclasses(sir)
333
```
334
gives
335
```julia
336
2-element Vector{Vector{Int64}}:
337
 [1, 2]
338
 [3, 4]
339
```
340
"""
341
function linkageclasses(rn::ReactionSystem)
103✔
342
    nps = get_networkproperties(rn)
103✔
343
    if isempty(nps.linkageclasses)
103✔
344
        nps.linkageclasses = linkageclasses(incidencematgraph(rn))
34✔
345
    end
346
    nps.linkageclasses
103✔
347
end
348

349
linkageclasses(incidencegraph) = Graphs.connected_components(incidencegraph)
41✔
350

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)
14✔
358
    nps = get_networkproperties(rn)
14✔
359
    if isempty(nps.stronglinkageclasses)
14✔
360
        nps.stronglinkageclasses = stronglinkageclasses(incidencematgraph(rn))
10✔
361
    end
362
    nps.stronglinkageclasses
14✔
363
end
364

365
stronglinkageclasses(incidencegraph) = Graphs.strongly_connected_components(incidencegraph)
10✔
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)
10✔
374
    nps = get_networkproperties(rn)
10✔
375
    if isempty(nps.terminallinkageclasses)
10✔
376
        slcs = stronglinkageclasses(rn)
10✔
377
        tslcs = filter(lc -> isterminal(lc, rn), slcs)
41✔
378
        nps.terminallinkageclasses = tslcs
10✔
379
    end
380
    nps.terminallinkageclasses
10✔
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)
31✔
386
    imat = incidencemat(rn)
62✔
387

388
    for r in 1:size(imat, 2)
31✔
389
        # Find the index of the reactant complex for a given reaction
390
        s = findfirst(==(-1), @view imat[:, r])
1,301✔
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)
242✔
394
            p = findfirst(==(1), @view imat[:, r])
459✔
395
            p in Set(lc) ? continue : return false
93✔
396
        end
397
    end
232✔
398
    true
×
399
end
400

401
@doc raw"""
402
    deficiency(rn::ReactionSystem)
403

404
Calculate the deficiency of a reaction network.
405

406
Here the deficiency, ``\delta``, of a network with ``n`` reaction complexes,
407
``\ell`` linkage classes and a rank ``s`` stoichiometric matrix is
408

409
```math
410
\delta = n - \ell - s
411
```
412

413
For example,
414
```julia
415
sir = @reaction_network SIR begin
416
    β, S + I --> 2I
417
    ν, I --> R
418
end
419
δ = deficiency(sir)
420
```
421
"""
422
function deficiency(rn::ReactionSystem)
28✔
423
    nps = get_networkproperties(rn)
28✔
424

425
    # Check if deficiency has been computed already (initialized to -1)
426
    if nps.deficiency == -1
28✔
427
        conservationlaws(rn)
21✔
428
        r = nps.rank
21✔
429
        ig = incidencematgraph(rn)
21✔
430
        lc = linkageclasses(rn)
21✔
431
        nps.deficiency = Graphs.nv(ig) - length(lc) - r
21✔
432
    end
433
    nps.deficiency
28✔
434
end
435

436
# Used in the subsequent function.
437
function subnetworkmapping(linkageclass, allrxs, complextorxsmap, p)
101✔
438
    rxinds = sort!(collect(Set(rxidx for rcidx in linkageclass
106✔
439
    for rxidx in complextorxsmap[rcidx])))
440
    rxs = allrxs[rxinds]
101✔
441
    specset = Set(s for rx in rxs for s in rx.substrates if !isconstant(s))
101✔
442
    for rx in rxs
101✔
443
        for product in rx.products
346✔
444
            !isconstant(product) && push!(specset, product)
884✔
445
        end
442✔
446
    end
346✔
447
    specs = collect(specset)
101✔
448
    newps = Vector{eltype(p)}()
101✔
449
    for rx in rxs
101✔
450
        Symbolics.get_variables!(newps, rx.rate, p)
346✔
451
    end
346✔
452
    rxs, specs, newps   # reactions and species involved in reactions of subnetwork
101✔
453
end
454

455
"""
456
    subnetworks(rn::ReactionSystem)
457

458
Find subnetworks corresponding to each linkage class of the reaction network.
459

460
For example,
461
```julia
462
sir = @reaction_network SIR begin
463
    β, S + I --> 2I
464
    ν, I --> R
465
end
466
subnetworks(sir)
467
```
468
"""
469
function subnetworks(rs::ReactionSystem)
45✔
470
    isempty(get_systems(rs)) || error("subnetworks does not currently support subsystems.")
45✔
471
    lcs = linkageclasses(rs)
45✔
472
    rxs = reactions(rs)
45✔
473
    p = parameters(rs)
45✔
474
    t = get_iv(rs)
45✔
475
    spatial_ivs = get_sivs(rs)
45✔
476
    complextorxsmap = [map(first, rcmap) for rcmap in values(reactioncomplexmap(rs))]
45✔
477
    subnetworks = Vector{ReactionSystem}()
45✔
478
    for i in 1:length(lcs)
45✔
479
        reacs, specs, newps = subnetworkmapping(lcs[i], rxs, complextorxsmap, p)
101✔
480
        newname = Symbol(nameof(rs), "_", i)
101✔
481
        push!(subnetworks,
101✔
482
            ReactionSystem(reacs, t, specs, newps; name = newname, spatial_ivs))
483
    end
157✔
484
    subnetworks
45✔
485
end
486

487
"""
488
    linkagedeficiencies(network::ReactionSystem)
489

490
Calculates the deficiency of each sub-reaction network within `network`.
491

492
For example,
493
```julia
494
sir = @reaction_network SIR begin
495
    β, S + I --> 2I
496
    ν, I --> R
497
end
498
linkage_deficiencies = linkagedeficiencies(sir)
499
```
500
"""
501
function linkagedeficiencies(rs::ReactionSystem)
22✔
502
    lcs = linkageclasses(rs)
22✔
503
    subnets = subnetworks(rs)
22✔
504
    δ = zeros(Int, length(lcs))
53✔
505
    for (i, subnet) in enumerate(subnets)
22✔
506
        conservationlaws(subnet)
53✔
507
        nps = get_networkproperties(subnet)
53✔
508
        δ[i] = length(lcs[i]) - 1 - nps.rank
53✔
509
    end
84✔
510
    δ
22✔
511
end
512

513
"""
514
    isreversible(rn::ReactionSystem)
515

516
Given a reaction network, returns if the network is reversible or not.
517

518
For example,
519
```julia
520
sir = @reaction_network SIR begin
521
    β, S + I --> 2I
522
    ν, I --> R
523
end
524
isreversible(sir)
525
```
526
"""
527
function isreversible(rn::ReactionSystem)
14✔
528
    ig = incidencematgraph(rn)
14✔
529
    Graphs.reverse(ig) == ig
14✔
530
end
531

532
"""
533
    isweaklyreversible(rn::ReactionSystem, subnetworks)
534

535
Determine if the reaction network with the given subnetworks is weakly reversible or not.
536

537
For example,
538
```julia
539
sir = @reaction_network SIR begin
540
    β, S + I --> 2I
541
    ν, I --> R
542
end
543
subnets = subnetworks(rn)
544
isweaklyreversible(rn, subnets)
545
```
546
"""
547
function isweaklyreversible(rn::ReactionSystem, subnets)
17✔
548
    nps = get_networkproperties(rn)
17✔
549
    isempty(nps.incidencemat) && reactioncomplexes(rn)
17✔
550
    sparseig = issparse(nps.incidencemat)
17✔
551

552
    for subnet in subnets
17✔
553
        subnps = get_networkproperties(subnet)
35✔
554
        isempty(subnps.incidencemat) && reactioncomplexes(subnet; sparse = sparseig)
35✔
555
    end
35✔
556

557
    # A network is weakly reversible if all of its subnetworks are strongly connected
558
    all(Graphs.is_strongly_connected ∘ incidencematgraph, subnets)
17✔
559
end
560

561
### Conservation Laws ###
562

563
# Implements the `conserved` parameter metadata.
564
struct ConservedParameter end
565
Symbolics.option_to_metadata_type(::Val{:conserved}) = ConservedParameter
×
566

567
"""
568
isconserved(p)
569

570
Checks if the input parameter (`p`) is a conserved quantity (i.e. have the `conserved`)
571
metadata.
572
"""
573
isconserved(x::Num, args...) = isconserved(Symbolics.unwrap(x), args...)
4✔
574
function isconserved(x, default = false)
8✔
575
    p = Symbolics.getparent(x, nothing)
8✔
576
    p === nothing || (x = p)
4✔
577
    Symbolics.getmetadata(x, ConservedParameter, default)
4✔
578
end
579

580
"""
581
    conservedequations(rn::ReactionSystem)
582

583
Calculate symbolic equations from conservation laws, writing dependent variables as
584
functions of independent variables and the conservation law constants.
585

586
Notes:
587
- Caches the resulting equations in `rn`, so will be fast on subsequent calls.
588

589
Examples:
590
```@repl
591
rn = @reaction_network begin
592
    k, A + B --> C
593
    k2, C --> A + B
594
    end
595
conservedequations(rn)
596
```
597
gives
598
```
599
2-element Vector{Equation}:
600
 B(t) ~ A(t) + Γ[1]
601
 C(t) ~ Γ[2] - A(t)
602
```
603
"""
604
function conservedequations(rn::ReactionSystem)
66✔
605
    conservationlaws(rn)
66✔
606
    nps = get_networkproperties(rn)
66✔
607
    nps.conservedeqs
66✔
608
end
609

610
"""
611
    conservationlaw_constants(rn::ReactionSystem)
612

613
Calculate symbolic equations from conservation laws, writing the conservation law constants
614
in terms of the dependent and independent variables.
615

616
Notes:
617
- Caches the resulting equations in `rn`, so will be fast on subsequent calls.
618

619
Examples:
620
```@julia
621
rn = @reaction_network begin
622
    k, A + B --> C
623
    k2, C --> A + B
624
    end
625
conservationlaw_constants(rn)
626
```
627
gives
628
```
629
2-element Vector{Equation}:
630
 Γ[1] ~ B(t) - A(t)
631
 Γ[2] ~ A(t) + C(t)
632
```
633
"""
634
function conservationlaw_constants(rn::ReactionSystem)
1✔
635
    conservationlaws(rn)
1✔
636
    nps = get_networkproperties(rn)
1✔
637
    nps.constantdefs
1✔
638
end
639

640
"""
641
    conservationlaws(netstoichmat::AbstractMatrix)::Matrix
642

643
Given the net stoichiometry matrix of a reaction system, computes a matrix of
644
conservation laws, each represented as a row in the output.
645
"""
646
function conservationlaws(nsm::Matrix; col_order = nothing)
608✔
647
    conslaws = positive_nullspace(nsm'; col_order = col_order)
304✔
648
    Matrix(conslaws)
304✔
649
end
650

651
# Used in the subsequent function.
652
function cache_conservationlaw_eqs!(rn::ReactionSystem, N::AbstractMatrix, col_order)
303✔
653
    nullity = size(N, 1)
303✔
654
    r = numspecies(rn) - nullity     # rank of the netstoichmat
303✔
655
    sts = species(rn)
303✔
656
    indepidxs = col_order[begin:r]
606✔
657
    indepspecs = sts[indepidxs]
303✔
658
    depidxs = col_order[(r + 1):end]
394✔
659
    depspecs = sts[depidxs]
303✔
660
    constants = MT.unwrap(only(
303✔
661
        @parameters $(CONSERVED_CONSTANT_SYMBOL)[1:nullity] [conserved = true]))
662

663
    conservedeqs = Equation[]
303✔
664
    constantdefs = Equation[]
303✔
665
    for (i, depidx) in enumerate(depidxs)
303✔
666
        scaleby = (N[i, depidx] != 1) ? N[i, depidx] : one(eltype(N))
166✔
667
        (scaleby != 0) || error("Error, found a zero in the conservation law matrix where "
166✔
668
              *
669
              "one was not expected.")
670
        coefs = @view N[i, indepidxs]
166✔
671
        terms = sum(p -> p[1] / scaleby * p[2], zip(coefs, indepspecs))
633✔
672
        eq = depspecs[i] ~ constants[i] - terms
166✔
673
        push!(conservedeqs, eq)
166✔
674
        eq = constants[i] ~ depspecs[i] + terms
166✔
675
        push!(constantdefs, eq)
166✔
676
    end
241✔
677

678
    # cache in the system
679
    nps = get_networkproperties(rn)
303✔
680
    nps.rank = r
303✔
681
    nps.nullity = nullity
303✔
682
    nps.indepspecs = Set(indepspecs)
303✔
683
    nps.depspecs = Set(depspecs)
303✔
684
    nps.conservedeqs = conservedeqs
303✔
685
    nps.constantdefs = constantdefs
303✔
686

687
    nothing
303✔
688
end
689

690
"""
691
    conservationlaws(rs::ReactionSystem)
692

693
Return the conservation law matrix of the given `ReactionSystem`, calculating it if it is
694
not already stored within the system, or returning an alias to it.
695

696
Notes:
697
- The first time being called it is calculated and cached in `rn`, subsequent calls should
698
  be fast.
699
"""
700
function conservationlaws(rs::ReactionSystem)
382✔
701
    nps = get_networkproperties(rs)
382✔
702
    !isempty(nps.conservationmat) && (return nps.conservationmat)
382✔
703
    nsm = netstoichmat(rs)
303✔
704
    nps.conservationmat = conservationlaws(nsm; col_order = nps.col_order)
303✔
705
    cache_conservationlaw_eqs!(rs, nps.conservationmat, nps.col_order)
303✔
706
    nps.conservationmat
303✔
707
end
708

709
"""
710
    conservedquantities(state, cons_laws)
711

712
Compute conserved quantities for a system with the given conservation laws.
713
"""
714
conservedquantities(state, cons_laws) = cons_laws * state
1✔
715

716
# If u0s are not given while conservation laws are present, throws an error.
717
# Used in HomotopyContinuation and BifurcationKit extensions.
718
# Currently only checks if any u0s are given
719
# (not whether these are enough for computing conserved quantities, this will yield a less informative error).
720
function conservationlaw_errorcheck(rs, pre_varmap)
177✔
721
    vars_with_vals = Set(p[1] for p in pre_varmap)
177✔
722
    any(s -> s in vars_with_vals, species(rs)) && return
403✔
723
    isempty(conservedequations(Catalyst.flatten(rs))) ||
35✔
724
        error("The system has conservation laws but initial conditions were not provided for some species.")
725
end
726

727
"""
728
    iscomplexbalanced(rs::ReactionSystem, parametermap)
729

730
Constructively compute whether a network will have complex-balanced equilibrium
731
solutions, following the method in van der Schaft et al., [2015](https://link.springer.com/article/10.1007/s10910-015-0498-2#Sec3). 
732
Accepts a dictionary, vector, or tuple of variable-to-value mappings, e.g. [k1 => 1.0, k2 => 2.0,...]. 
733
"""
734

735
function iscomplexbalanced(rs::ReactionSystem, parametermap::Dict)
19✔
736
    if length(parametermap) != numparams(rs)
19✔
737
        error("Incorrect number of parameters specified.")
×
738
    end
739

740
    pmap = symmap_to_varmap(rs, parametermap)
19✔
741
    pmap = Dict(ModelingToolkit.value(k) => v for (k, v) in pmap)
19✔
742

743
    sm = speciesmap(rs)
19✔
744
    cm = reactioncomplexmap(rs)
19✔
745
    complexes, D = reactioncomplexes(rs)
35✔
746
    rxns = reactions(rs)
19✔
747
    nc = length(complexes)
19✔
748
    nr = numreactions(rs)
19✔
749
    nm = numspecies(rs)
19✔
750

751
    if !all(r -> ismassaction(r, rs), rxns)
171✔
752
        error("The supplied ReactionSystem has reactions that are not ismassaction. Testing for being complex balanced is currently only supported for pure mass action networks.")
×
753
    end
754

755
    rates = [substitute(rate, pmap) for rate in reactionrates(rs)]
19✔
756

757
    # Construct kinetic matrix, K
758
    K = zeros(nr, nc)
1,688✔
759
    for c in 1:nc
19✔
760
        complex = complexes[c]
126✔
761
        for (r, dir) in cm[complex]
126✔
762
            rxn = rxns[r]
304✔
763
            if dir == -1
304✔
764
                K[r, c] = rates[r]
152✔
765
            end
766
        end
304✔
767
    end
233✔
768

769
    L = -D * K
38✔
770
    S = netstoichmat(rs)
19✔
771

772
    # Compute ρ using the matrix-tree theorem
773
    g = incidencematgraph(rs)
19✔
774
    R = ratematrix(rs, rates)
19✔
775
    ρ = matrixtree(g, R)
19✔
776

777
    # Determine if 1) ρ is positive and 2) D^T Ln ρ lies in the image of S^T
778
    if all(>(0), ρ)
19✔
779
        img = D' * log.(ρ)
20✔
780
        if rank(S') == rank(hcat(S', img))
10✔
781
            return true
10✔
782
        else
783
            return false
×
784
        end
785
    else
786
        return false
9✔
787
    end
788
end
789

790
function iscomplexbalanced(rs::ReactionSystem, parametermap::Vector{<:Pair})
1✔
791
    pdict = Dict(parametermap)
1✔
792
    iscomplexbalanced(rs, pdict)
1✔
793
end
794

795
function iscomplexbalanced(rs::ReactionSystem, parametermap::Tuple)
2✔
796
    pdict = Dict(parametermap)
2✔
797
    iscomplexbalanced(rs, pdict)
2✔
798
end
799

800
function iscomplexbalanced(rs::ReactionSystem, parametermap)
2✔
801
    error("Parameter map must be a dictionary, tuple, or vector of symbol/value pairs.")
2✔
802
end
803

804
"""
805
    ratematrix(rs::ReactionSystem, parametermap)
806

807
    Given a reaction system with n complexes, outputs an n-by-n matrix where R_{ij} is the rate 
808
    constant of the reaction between complex i and complex j. Accepts a dictionary, vector, or tuple 
809
    of variable-to-value mappings, e.g. [k1 => 1.0, k2 => 2.0,...]. 
810
"""
811

812
function ratematrix(rs::ReactionSystem, rates::Vector{Float64})
23✔
813
    complexes, D = reactioncomplexes(rs)
45✔
814
    n = length(complexes)
23✔
815
    rxns = reactions(rs)
23✔
816
    ratematrix = zeros(n, n)
1,606✔
817

818
    for r in 1:length(rxns)
23✔
819
        rxn = rxns[r]
184✔
820
        s = findfirst(==(-1), @view D[:, r])
184✔
821
        p = findfirst(==(1), @view D[:, r])
184✔
822
        ratematrix[s, p] = rates[r]
184✔
823
    end
184✔
824
    ratematrix
23✔
825
end
826

827
function ratematrix(rs::ReactionSystem, parametermap::Dict)
3✔
828
    if length(parametermap) != numparams(rs)
3✔
829
        error("Incorrect number of parameters specified.")
×
830
    end
831

832
    pmap = symmap_to_varmap(rs, parametermap)
3✔
833
    pmap = Dict(ModelingToolkit.value(k) => v for (k, v) in pmap)
3✔
834

835
    rates = [substitute(rate, pmap) for rate in reactionrates(rs)]
3✔
836
    ratematrix(rs, rates)
3✔
837
end
838

839
function ratematrix(rs::ReactionSystem, parametermap::Vector{<:Pair})
1✔
840
    pdict = Dict(parametermap)
1✔
841
    ratematrix(rs, pdict)
1✔
842
end
843

844
function ratematrix(rs::ReactionSystem, parametermap::Tuple)
1✔
845
    pdict = Dict(parametermap)
1✔
846
    ratematrix(rs, pdict)
1✔
847
end
848

849
function ratematrix(rs::ReactionSystem, parametermap)
×
850
    error("Parameter map must be a dictionary, tuple, or vector of symbol/value pairs.")
×
851
end
852

853
### BELOW: Helper functions for iscomplexbalanced
854

855
function matrixtree(g::SimpleDiGraph, distmx::Matrix)
42✔
856
    n = nv(g)
42✔
857
    if size(distmx) != (n, n)
84✔
858
        error("Size of distance matrix is incorrect")
×
859
    end
860

861
    π = zeros(n)
209✔
862

863
    if !Graphs.is_connected(g)
42✔
864
        ccs = Graphs.connected_components(g)
8✔
865
        for cc in ccs
8✔
866
            sg, vmap = Graphs.induced_subgraph(g, cc)
23✔
867
            distmx_s = distmx[cc, cc]
23✔
868
            π_j = matrixtree(sg, distmx_s)
23✔
869
            π[cc] = π_j
23✔
870
        end
23✔
871
        return π
8✔
872
    end
873

874
    # generate all spanning trees
875
    ug = SimpleGraph(SimpleDiGraph(g))
34✔
876
    trees = collect(Combinatorics.combinations(collect(edges(ug)), n - 1))
68✔
877
    trees = SimpleGraph.(trees)
34✔
878
    trees = filter!(t -> isempty(Graphs.cycle_basis(t)), trees)
153✔
879

880
    # constructed rooted trees for every vertex, compute sum
881
    for v in 1:n
34✔
882
        rootedTrees = [reverse(Graphs.bfs_tree(t, v, dir = :in)) for t in trees]
126✔
883
        π[v] = sum([treeweight(t, g, distmx) for t in rootedTrees])
252✔
884
    end
218✔
885

886
    # sum the contributions
887
    return π
34✔
888
end
889

890
function treeweight(t::SimpleDiGraph, g::SimpleDiGraph, distmx::Matrix)
445✔
891
    prod = 1
445✔
892
    for e in edges(t)
990✔
893
        s = Graphs.src(e)
1,656✔
894
        t = Graphs.dst(e)
1,656✔
895
        prod *= distmx[s, t]
2,101✔
896
    end
4,868✔
897
    prod
445✔
898
end
899

900
"""
901
    cycles(rs::ReactionSystem)
902

903
    Returns the matrix of a basis of cycles (or flux vectors), or a basis for reaction fluxes for which the system is at steady state. 
904
    These correspond to right eigenvectors of the stoichiometric matrix. Equivalent to [`fluxmodebasis`](@ref). 
905
"""
906

907
function cycles(rs::ReactionSystem)
2✔
908
    nps = get_networkproperties(rs)
2✔
909
    nsm = netstoichmat(rs)
2✔
910
    !isempty(nps.cyclemat) && return nps.cyclemat
2✔
911
    nps.cyclemat = cycles(nsm; col_order = nps.col_order)
2✔
912
    nps.cyclemat
2✔
913
end
914

915
function cycles(nsm::Matrix; col_order = nothing)
8✔
916
    positive_nullspace(nsm; col_order)
4✔
917
end
918

919
function positive_nullspace(M::T; col_order = nothing) where {T <: AbstractMatrix}
616✔
920
    # compute the left nullspace over the integers
921
    N = MT.nullspace(M; col_order)
308✔
922

923
    # if all coefficients for a cycle are negative, make positive
924
    for Ncol in eachcol(N)
403✔
925
        all(r -> r <= 0, Ncol) && (Ncol .*= -1)
1,169✔
926
    end
289✔
927

928
    # check we haven't overflowed
929
    iszero(M * N) || error("Calculation of the cycle matrix was inaccurate, "
308✔
930
          * "likely due to numerical overflow. Please use a larger integer "
931
          * "type like Int128 or BigInt for the net stoichiometry matrix.")
932

933
    T(N)
308✔
934
end
935

936
"""
937
    fluxvectors(rs::ReactionSystem)
938

939
    See documentation for [`cycles`](@ref). 
940
"""
941

942
function fluxvectors(rs::ReactionSystem)
×
943
    cycles(rs)
×
944
end
945

946
### Deficiency one
947

948
"""
949
    satisfiesdeficiencyone(rn::ReactionSystem)
950

951
    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.
952
"""
953

954
function satisfiesdeficiencyone(rn::ReactionSystem)
4✔
955
    all(r -> ismassaction(r, rn), reactions(rn)) ||
36✔
956
        error("The deficiency one theorem is only valid for reaction networks that are mass action.")
957
    complexes, D = reactioncomplexes(rn)
4✔
958
    δ = deficiency(rn)
4✔
959
    δ_l = linkagedeficiencies(rn)
4✔
960

961
    lcs = linkageclasses(rn)
4✔
962
    tslcs = terminallinkageclasses(rn)
4✔
963

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

971
"""
972
    satisfiesdeficiencyzero(rn::ReactionSystem)
973

974
    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.
975
"""
976

977
function satisfiesdeficiencyzero(rn::ReactionSystem)
4✔
978
    all(r -> ismassaction(r, rn), reactions(rn)) ||
27✔
979
        error("The deficiency zero theorem is only valid for reaction networks that are mass action.")
980
    δ = deficiency(rn)
4✔
981
    δ == 0 && isweaklyreversible(rn, subnetworks(rn))
4✔
982
end
983

984
"""
985
    robustspecies(rn::ReactionSystem)
986

987
    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. 
988

989
    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.  
990
"""
991

992
function robustspecies(rn::ReactionSystem)
2✔
993
    complexes, D = reactioncomplexes(rn)
2✔
994
    nps = get_networkproperties(rn)
2✔
995

996
    if deficiency(rn) != 1
2✔
997
        error("This algorithm currently only checks for robust species in networks with deficiency one.")
×
998
    end
999

1000
    # A species is concentration robust in a deficiency one network if there are two non-terminal complexes (i.e. complexes 
1001
    # belonging to a linkage class that is not terminal) that differ only in species s (i.e. their difference is some 
1002
    # 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). 
1003

1004
    if !nps.checkedrobust
2✔
1005
        tslcs = terminallinkageclasses(rn)
2✔
1006
        Z = complexstoichmat(rn)
2✔
1007

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

1012
        for (c_s, c_p) in Combinatorics.combinations(nonterminal_complexes, 2)
2✔
1013
            # Check the difference of all the combinations of complexes. The support is the set of indices that are non-zero 
1014
            suppcnt = 0
21✔
1015
            supp = 0
21✔
1016
            for i in 1:size(Z, 1)
21✔
1017
                (Z[i, c_s] != Z[i, c_p]) && (suppcnt += 1; supp = i)
130✔
1018
                (suppcnt > 1) && break
90✔
1019
            end
71✔
1020

1021
            # If the support has length one, then they differ in only one species, and that species is concentration robust. 
1022
            (suppcnt == 1) && (supp ∉ robust_species) && push!(robust_species, supp)
21✔
1023
        end
40✔
1024
        nps.checkedrobust = true
2✔
1025
        nps.robustspecies = robust_species
2✔
1026
    end
1027

1028
    nps.robustspecies
2✔
1029
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc