Skip to content

Commit 3d4afd7

Browse files
committed
init
1 parent a649acc commit 3d4afd7

File tree

6 files changed

+287
-0
lines changed

6 files changed

+287
-0
lines changed

docs/src/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ speciesmap
166166
paramsmap
167167
reactionparamsmap
168168
isspecies
169+
is_autonomous
169170
Catalyst.isconstant
170171
Catalyst.isbc
171172
```
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Steady state stability computation
2+
After system steady states have been found using [HomotopyContinuation.jl](@ref homotopy_continuation), [NonlinearSolve.jl](@ref nonlinear_solve), or other means, their stability can be computed using Catalyst's `steady_state_stability` function. Systems with conservation laws will automatically have these removed, permitting stability computation on systems with singular Jacobian.
3+
4+
## Basic examples
5+
Let us consider the following basic example:
6+
```@example stability_1
7+
using Catalyst
8+
rn = @reaction_network begin
9+
(p,d), 0 <--> X
10+
end
11+
```
12+
It has a single (stable) steady state at $X = p/d$. We can confirm stability using the `steady_state_stability` function, to which we provide the steady state, the reaction system, and the parameter values:
13+
```@example stability_1
14+
ps = [:p => 2.0, :d => 0.5]
15+
steady_state = [:X => 4.0]
16+
steady_state_stability(steady_state, rn, ps)
17+
```
18+
19+
Next, let us consider the following self-activation loop:
20+
```@example stability_1
21+
sa_loop = @reaction_network begin
22+
(hill(X,v,K,n),d), 0 <--> X
23+
end
24+
```
25+
For certain parameter choices, this system exhibits multi-stability. Here, we can find the steady states [using homotopy continuation](@ref homotopy_continuation):
26+
```@example stability_1
27+
import HomotopyContinuation
28+
ps = [:v => 2.0, :K => 0.5, :n => 3, :d => 1.0]
29+
steady_states = hc_steady_states(sa_loop, ps)
30+
```
31+
Next, we can apply `steady_state_stability` directly to this steady state vector, receiving the stability for each:
32+
```@example stability_1
33+
steady_state_stability(steady_states, sa_loop, ps)
34+
```
35+
36+
## Pre-computing the Jacobian to increase performance when computing stability for many steady states
37+
Catalyst uses the system Jacobian to compute steady state stability, and the Jacobian is computed once for each call to `steady_state_stability`. If you repeatedly compute stability for steady states of the same system, pre-computing the Jacobian and supplying it to the `steady_state_stability` function can improve performance.
38+
39+
In this example we use the self-activation loop from previously, pre-computes the Jacobian, and uses it to multiple `steady_state_stability` calls:
40+
```@example stability_1
41+
ss_jac = steady_state_jac(sa_loop)
42+
43+
ps_1 = [:v => 2.0, :K => 0.5, :n => 3, :d => 1.0]
44+
steady_states_1 = hc_steady_states(sa_loop, ps)
45+
stability_1 = steady_state_stability(steady_states_1, sa_loop, ps_1; ss_jac=ss_jac)
46+
47+
ps_2 = [:v => 4.0, :K => 1.5, :n => 2, :d => 1.0]
48+
steady_states_2 = hc_steady_states(sa_loop, ps)
49+
stability_2 = steady_state_stability(steady_states_2, sa_loop, ps_2; ss_jac=ss_jac)
50+
nothing # hide
51+
```
52+
53+
!!! note
54+
For systems with [conservation laws](@ref homotopy_continuation_conservation_laws), `steady_state_jac` must be supplied a `u0` vector (indicating species concentrations for conservation law computation). This is required to eliminate the conserved quantities, preventing a singular Jacobian. These are supplied using the `u0` optional argument.

src/Catalyst.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import Base: (==), hash, size, getindex, setindex, isless, Sort.defalg, length,
4242
import MacroTools, Graphs
4343
import Graphs: DiGraph, SimpleGraph, SimpleDiGraph, vertices, edges, add_vertices!, nv, ne
4444
import DataStructures: OrderedDict, OrderedSet
45+
import LinearAlgebra.eigvals
4546
import Parameters: @with_kw_noshow
4647
import Symbolics: occursin, wrap
4748

@@ -149,6 +150,10 @@ export @compound, @compounds
149150
export iscompound, components, coefficients, component_coefficients
150151
export balance_reaction
151152

153+
# for functions I am unsure where to best place them.
154+
include("steady_state_stability.jl")
155+
export steady_state_stability, steady_state_jac
156+
152157
### Extensions ###
153158

154159
# HomotopyContinuation

src/steady_state_stability.jl

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
### Stability Analysis ###
2+
3+
"""
4+
stability(u::Vector{T}, rs::ReactionSystem, p; sparse=false, ss_jac = steady_state_jac(u, rs, p; sparse=sparse), t=Inf, non_autonomous_war=true)
5+
6+
Compute the stability of a steady state (Returned as a `Bool`, with `true` indicating stability).
7+
8+
Arguments:
9+
- `u`: The steady state for which we want to compute stability.
10+
- `rs`: The reaction system model for which we want to compute stability.
11+
- `p`: The parameter set for which we want to compute stability.
12+
- `sparse=false`: If we wish to create a sparse Jacobian for the stability computation.
13+
- `ss_jac = steady_state_jac(u, rs; sparse=sparse)`: It is possible to pre-compute the Jacobian used for stability computation using `steady_state_jac`. If stability is computed for many states, precomputing the Jacobian may speed up evaluation.
14+
- `t=Inf`: The time point at which stability is computed.
15+
- `non_autonomous_war=true`: If the system is non-autonomous (e.g. a rate depends on t), a warning will be given. Set this to false to remove that. Alternatively, specify a nonInf value for `t`.
16+
17+
Example:
18+
```julia
19+
# Creates model.
20+
rn = @reaction_network begin
21+
(p,d), 0 <--> X
22+
end
23+
p = [:p => 1.0, :d => 0.5]
24+
25+
# Finds (the trivial) steady state, and computes stability.
26+
steady_state = [2.0]
27+
steady_state_stability(steady_state, rn, p)
28+
```
29+
30+
Notes:
31+
y states (each being a `Vector`) is provided as `u`, stability is computed for each state separately.
32+
"""
33+
function steady_state_stability(u::Vector{T}, rs::ReactionSystem, p;
34+
sparse=false, ss_jac = steady_state_jac(rs; u0=u, sparse=sparse), t=Inf, non_autonomous_war=true) where T
35+
# Warning checks.
36+
!is_autonomous(rs) && non_autonomous_war && @warn "Attempting to compute stability for a non-autonomous system. Set `non_autonomous_war=false` to disable this warning."
37+
38+
# Because Jacobian currently requires ps to be a normal vector, can be removed once this get fixed in MTK.
39+
if (u isa Vector{<:Pair}) || (u isa Dict)
40+
u_dict = Dict(symmap_to_varmap(rs, u))
41+
u = [u_dict[var] for var in states(rs)]
42+
end
43+
if (p isa Vector{<:Pair}) || (p isa Dict)
44+
p_dict = Dict(symmap_to_varmap(rs, p))
45+
p = [p_dict[var] for var in parameters(rs)]
46+
end
47+
48+
# Computes stability
49+
jac = ss_jac(u, p, Inf)
50+
return maximum(real.(eigvals(jac))) < 0
51+
end
52+
# Computes the stability for a vector of steady states.
53+
function steady_state_stability(us::Vector{Vector{T}}, rs::ReactionSystem, p; sparse=false, ss_jac = steady_state_jac(rs; u0=us[1], sparse=sparse)) where T
54+
return [steady_state_stability(u, rs, p) for u in us]
55+
end
56+
57+
"""
58+
steady_state_jac(rs::ReactionSystem; u0=[], sparse=false)
59+
60+
Creates the Jacobian function which can be used as input to `steady_state_stability`. Useful when a large number of stability computation has to be carried out in a performant manner.
61+
62+
Example:
63+
```julia
64+
# Creates model.
65+
rn = @reaction_network begin
66+
(p,d), 0 <--> X
67+
end
68+
p = [:p => 1.0, :d => 0.5]
69+
70+
# Creates the steady state jacobian.
71+
ss_jac = steady_state_jacobian(rn)
72+
73+
# Finds (the trivial) steady state, and computes stability.
74+
steady_state = [2.0]
75+
steady_state_stability(steady_state, rn, p; ss_jac)
76+
```
77+
78+
Arguments:
79+
- `rs`: The reaction system model for which we want to compute stability.
80+
- `u0=[]`: For systems with conservation laws, a `u` is required to compute the conserved quantities.
81+
- `sparse=false`: If we wish to create a sparse Jacobian for the stability computation.
82+
"""
83+
function steady_state_jac(rs::ReactionSystem; u0=[], sparse=false) where T
84+
# If u0 is a vector of values, must be converted to something MTK understands.
85+
if (u0 isa Vector{<:Number})
86+
if length(u0) == length(states(rs))
87+
u0 = Pair.(states(rs), u0)
88+
elseif !isempty(u0)
89+
error("You are trying to compute a stability matrix, providing u0 to compute conservation laws. Your provided u0 vector has length < the number of system states. If you provide a u0 vector, these have to be identical.")
90+
end
91+
end
92+
93+
# Converts u0 to values MTK understands, and checks that potential conservation laws are accounted for.
94+
u0 = symmap_to_varmap(rs, u0)
95+
conservationlaw_errorcheck(rs, u0)
96+
97+
# Creates a ODESystem and jacobian.
98+
osys = convert(ODESystem, rs; remove_conserved = true, defaults=Dict(u0))
99+
return ODEFunction(osys; jac=true, sparse=sparse).jac
100+
end

test/miscellaneous_tests/api.jl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,3 +446,51 @@ let
446446
neweqs = getfield.(equations(ns), :rhs)
447447
@test_throws MethodError Catalyst.to_multivariate_poly(neweqs)
448448
end
449+
450+
# Tests `is_autonomous` function.
451+
let
452+
# Using default iv.
453+
rn1 = @reaction_network begin
454+
(p + X*(p1/(t+p3)),d), 0 <--> X
455+
(kB,kD), 2X <--> X
456+
end
457+
rn2 = @reaction_network begin
458+
(hill(X, v/t, K, n),d), 0 <--> X
459+
(kB,kD), 2X <--> X
460+
end
461+
rn3 = @reaction_network begin
462+
(p + X*(p1+p2),d), 0 <--> X
463+
(kB,kD), 2X <--> X
464+
end
465+
466+
@test !is_autonomous(rn1)
467+
@test !is_autonomous(rn2)
468+
@test is_autonomous(rn3)
469+
470+
# Using non-default iv.
471+
rn4 = @reaction_network begin
472+
@ivs i1 i2
473+
(p + X*(p1/(1+i1)),d), 0 <--> X
474+
(kB,kD), 2X <--> X
475+
end
476+
rn5 = @reaction_network begin
477+
@ivs i1 i2
478+
(p + X*(i2+p2),d), 0 <--> X
479+
(kB,kD), 2X <--> X
480+
end
481+
rn6 = @reaction_network begin
482+
@ivs i1 i2
483+
(hill(X, v/i1, i2, n),d), 0 <--> X
484+
(kB,kD), 2X <--> X
485+
end
486+
rn7 = @reaction_network begin
487+
@ivs i1 i2
488+
(p + X*(p1+p2),d), 0 <--> X
489+
(kB,kD), 2X <--> X
490+
end
491+
492+
@test !is_autonomous(rn4)
493+
@test !is_autonomous(rn5)
494+
@test !is_autonomous(rn6)
495+
@test is_autonomous(rn7)
496+
end
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
### Fetch Packages ###
2+
3+
# Fetch packages.
4+
using Catalyst, OrdinaryDiffEq
5+
using Random, Test
6+
7+
# Sets rnd number.
8+
using StableRNGs
9+
rng = StableRNG(12345)
10+
11+
### Run Tests ###
12+
13+
# Tests that stability is correctly assessed (using simulation) in multi stable system.
14+
# Tests that `steady_state_jac` function works.
15+
# Tests with and without sparsity.
16+
# tests using symbolic input.
17+
let
18+
# System which may have between 1 and 7 fixed points.
19+
rn = @reaction_network begin
20+
v/20.0 + hillar(X,Y,v,K,n), 0 --> X
21+
v/20.0 + hillar(Y,X,v,K,n), 0 --> Y
22+
d, (X,Y) --> 0
23+
end
24+
ss_jac = steady_state_jac(rn)
25+
ss_jac_sparse = steady_state_jac(rn; sparse=true)
26+
27+
# Repeats several times, most cases should be encountered several times.
28+
for i = 1:50
29+
# Generates random parameter values (which can generate all steady states cases).
30+
p = [:v => 1.0 + 3*rand(rng), :K => 0.5 + 2*rand(rng), :n => rand(rng,[1,2,3,4]), :d => 0.5 + rand(rng)]
31+
32+
sss = hc_steady_states(rn, p)
33+
stabs_1 = steady_state_stability(sss, rn, p)
34+
stabs_2 = steady_state_stability(sss, rn, p; sparse=true)
35+
stabs_3 = steady_state_stability(sss, rn, p; ss_jac=ss_jac)
36+
stabs_4 = steady_state_stability(sss, rn, p; ss_jac=ss_jac_sparse)
37+
38+
for (idx,ss) in enumerate(sss)
39+
oprob = ODEProblem(rn, [1.001, 0.999] .* ss, (0.0,1000.0), p)
40+
sol_end = solve(oprob, Rosenbrock23())[end]
41+
stabs_5 = ss sol_end
42+
@test stabs_1[idx] == stabs_2[idx] == stabs_3[idx] == stabs_4[idx] == stabs_5
43+
end
44+
end
45+
end
46+
47+
# Checks stability for system with known stability structure.
48+
# Tests for system with conservation laws.
49+
# Tests for various input forms of u0 and ps.
50+
let
51+
rn = complete(@reaction_network begin
52+
k1+Z, Y --> 2X
53+
k2, 2X --> X + Y
54+
k3, X + Y --> Y
55+
k4, X --> 0
56+
(kD1+X, kD2), 2Z <--> Z2
57+
end)
58+
@unpack k1, k2, k3, k4, kD1, kD2, X, Y, Z, Z2 = rn
59+
u0_1 = [X => 1.0, Y => 1.0, Z => 1.0, Z2 => 1.0]
60+
u0_2 = [:X => 1.0, :Y => 1.0, :Z => 1.0, :Z2 => 1.0]
61+
u0_3 = [rn.X => 1.0, rn.Y => 1.0, rn.Z => 1.0, rn.Z2 => 1.0]
62+
u0_4 = [1.0, 1.0, 1.0, 1.0]
63+
ps_1 = [k1 => 8.0, k2 => 2.0, k3 => 1.0, k4 => 1.5, kD1 => 0.5, kD2 => 2.0]
64+
ps_2 = [:k1 => 8.0, :k2 => 2.0, :k3 => 1.0, :k4 => 1.5, :kD1 => 0.5, :kD2 => 2.0]
65+
ps_3 = [rn.k1 => 8.0, rn.k2 => 2.0, rn.k3 => 1.0, rn.k4 => 1.5, rn.kD1 => 0.5, rn.kD2 => 4.0]
66+
ps_4 = [8.0, 2.0, 1.0, 1.5, 0.5, 4.0]
67+
68+
sss = hc_steady_states(rn, ps_1; u0=u0_1)
69+
for u0 in [u0_1, u0_2, u0_3, u0_4], ps in [ps_1, ps_2, ps_3, ps_4]
70+
stab_1 = steady_state_stability(sss, rn, ps)
71+
@test length(stab_1) == 3
72+
@test count(stab_1) == 2
73+
74+
ss_jac = steady_state_jac(rn; u0=u0)
75+
stab_2 = steady_state_stability(sss, rn, ps; ss_jac=ss_jac)
76+
@test length(stab_2) == 3
77+
@test count(stab_2) == 2
78+
end
79+
end

0 commit comments

Comments
 (0)