Skip to content

Commit 3abba83

Browse files
authored
Merge pull request #739 from SciML/new_stability_computation
[v14 ready] New stability computation
2 parents 75ece2b + b466c10 commit 3abba83

File tree

16 files changed

+445
-24
lines changed

16 files changed

+445
-24
lines changed

HISTORY.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,20 @@ plot(bif_dia; xguide="k1", yguide="X")
8686
```
8787
- Automatically handles elimination of conservation laws for computing bifurcation diagrams.
8888
- Updated Bifurcation documentation with respect to this new feature.
89+
- Added function `isautonomous` to check if a `ReactionSystem` is autonomous.
90+
- Added function `steady_state_stability` to compute stability for steady states. Example:
91+
```julia
92+
# Creates model.
93+
rn = @reaction_network begin
94+
(p,d), 0 <--> X
95+
end
96+
p = [:p => 1.0, :d => 0.5]
97+
98+
# Finds (the trivial) steady state, and computes stability.
99+
steady_state = [2.0]
100+
steady_state_stability(steady_state, rn, p)
101+
```
102+
Here, `steady_state_stability` take an optional argument `tol = 10*sqrt(eps())`, which is used to determine whether a eigenvalue real part is reliably less that 0.
89103

90104
## Catalyst 13.5
91105
- Added a CatalystHomotopyContinuationExtension extension, which exports the `hc_steady_state` function if HomotopyContinuation is exported. `hc_steady_state` finds the steady states of a reaction system using the homotopy continuation method. This feature is only available for julia versions 1.9+. Example:

docs/pages.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ pages = Any[
3737
"Steady state analysis" => Any[
3838
"steady_state_functionality/homotopy_continuation.md",
3939
"steady_state_functionality/nonlinear_solve.md",
40-
# Stability analysis.
40+
"steady_state_functionality/steady_state_stability_computation.md",
4141
"steady_state_functionality/bifurcation_diagrams.md"
4242
# Dynamical systems analysis.
4343
],

docs/src/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ speciesmap
163163
paramsmap
164164
reactionsystemparamsmap
165165
isspecies
166+
isautonomous
166167
Catalyst.isconstant
167168
Catalyst.isbc
168169
```
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
!!! warn
5+
Catalyst currently computes steady state stabilities using the naive approach of checking whether a system's largest eigenvalue real part is negative. While more advanced stability computation methods exist (and would be a welcome addition to Catalyst), there is no direct plans to implement these. Furthermore, Catalyst uses a tolerance `tol = 10*sqrt(eps())` to determine whether a computed eigenvalue is far away enough from 0 to be reliably used. This threshold can be changed through the `tol` keyword argument.
6+
7+
## Basic examples
8+
Let us consider the following basic example:
9+
```@example stability_1
10+
using Catalyst
11+
rn = @reaction_network begin
12+
(p,d), 0 <--> X
13+
end
14+
```
15+
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:
16+
```@example stability_1
17+
ps = [:p => 2.0, :d => 0.5]
18+
steady_state = [:X => 4.0]
19+
steady_state_stability(steady_state, rn, ps)
20+
```
21+
22+
Next, let us consider the following [self-activation loop](@ref ref):
23+
```@example stability_1
24+
sa_loop = @reaction_network begin
25+
(hill(X,v,K,n),d), 0 <--> X
26+
end
27+
```
28+
For certain parameter choices, this system exhibits multi-stability. Here, we can find the steady states [using homotopy continuation](@ref homotopy_continuation):
29+
```@example stability_1
30+
import HomotopyContinuation
31+
ps = [:v => 2.0, :K => 0.5, :n => 3, :d => 1.0]
32+
steady_states = hc_steady_states(sa_loop, ps)
33+
```
34+
Next, we can apply `steady_state_stability` to each steady state yielding a vector of their stabilities:
35+
```@example stability_1
36+
[steady_state_stability(sstate, sa_loop, ps) for sstate in steady_states]
37+
```
38+
39+
Finally, as described above, Catalyst uses an optional argument, `tol`, to determine how strict to make the stability check. I.e. below we set the tolerance to `1e-6` (a larger value, that is stricter, than the default of `10*sqrt(eps())`)
40+
```@example stability_1
41+
[steady_state_stability(sstate, sa_loop, ps; tol = 1e-6) for sstate in steady_states]
42+
nothing# hide
43+
```
44+
45+
## Pre-computing the Jacobian to increase performance when computing stability for many steady states
46+
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.
47+
48+
In this example we use the self-activation loop from previously, pre-computes its Jacobian, and uses it to multiple `steady_state_stability` calls:
49+
```@example stability_1
50+
ss_jac = steady_state_jac(sa_loop)
51+
52+
ps_1 = [:v => 2.0, :K => 0.5, :n => 3, :d => 1.0]
53+
steady_states_1 = hc_steady_states(sa_loop, ps)
54+
stability_1 = steady_state_stability(steady_states_1, sa_loop, ps_1; ss_jac=ss_jac)
55+
56+
ps_2 = [:v => 4.0, :K => 1.5, :n => 2, :d => 1.0]
57+
steady_states_2 = hc_steady_states(sa_loop, ps)
58+
stability_2 = steady_state_stability(steady_states_2, sa_loop, ps_2; ss_jac=ss_jac)
59+
nothing # hide
60+
```
61+
62+
!!! warn
63+
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.

ext/CatalystBifurcationKitExtension/bifurcation_kit_extension.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
# Creates a BifurcationProblem, using a ReactionSystem as an input.
44
function BK.BifurcationProblem(rs::ReactionSystem, u0_bif, ps, bif_par, args...;
55
plot_var=nothing, record_from_solution=BK.record_sol_default, jac=true, u0=[], kwargs...)
6+
if !isautonomous(rs)
7+
error("Attempting to create a `BifurcationProblem` for a non-autonomous system (e.g. where some rate depend on $(rs.iv)). This is not possible.")
8+
end
69

710
# Converts symbols to symbolics.
811
(bif_par isa Symbol) && (bif_par = ModelingToolkit.get_var_to_name(rs)[bif_par])

ext/CatalystHomotopyContinuationExtension/homotopy_continuation_extension.jl

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ Notes:
3636
```
3737
"""
3838
function Catalyst.hc_steady_states(rs::ReactionSystem, ps; filter_negative=true, neg_thres=-1e-20, u0=[], kwargs...)
39+
if !isautonomous(rs)
40+
error("Attempting to compute steady state for a non-autonomous system (e.g. where some rate depend on $(rs.iv)). This is not possible.")
41+
end
3942
ss_poly = steady_state_polynomial(rs, ps, u0)
4043
sols = HC.real_solutions(HC.solve(ss_poly; kwargs...))
4144
reorder_sols!(sols, ss_poly, rs)
@@ -57,14 +60,6 @@ function steady_state_polynomial(rs::ReactionSystem, ps, u0)
5760
return poly_type_convert(ss_poly)
5861
end
5962

60-
# If u0s are not given while conservation laws are present, throws an error.
61-
function conservationlaw_errorcheck(rs, pre_varmap)
62-
vars_with_vals = Set(p[1] for p in pre_varmap)
63-
any(s -> s in vars_with_vals, species(rs)) && return
64-
isempty(conservedequations(rs)) ||
65-
error("The system has conservation laws but initial conditions were not provided for some species.")
66-
end
67-
6863
# Parses and expression and return a version where any exponents that are Float64 (but an int, like 2.0) are turned into Int64s.
6964
make_int_exps(expr) = wrap(Rewriters.Postwalk(Rewriters.PassThrough(___make_int_exps))(unwrap(expr))).val
7065
function ___make_int_exps(expr)

src/Catalyst.jl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ using DynamicQuantities#, Unitful # Having Unitful here as well currently gives
1818

1919
@reexport using ModelingToolkit
2020
using Symbolics
21-
21+
using LinearAlgebra
2222
using RuntimeGeneratedFunctions
2323
RuntimeGeneratedFunctions.init(@__MODULE__)
2424

@@ -102,6 +102,7 @@ export species, nonspecies, reactions, nonreactions, speciesmap, paramsmap
102102
export numspecies, numreactions, setdefaults!
103103
export make_empty_network
104104
export dependants, dependents, substoichmat, prodstoichmat, netstoichmat
105+
export isautonomous
105106
export reactionrates
106107
export isequivalent
107108
export set_default_noise_scaling
@@ -149,6 +150,10 @@ export @compound, @compounds
149150
export iscompound, components, coefficients, component_coefficients
150151
export balance_reaction, balance_system
151152

153+
# Functionality for computing the stability of system steady states.
154+
include("steady_state_stability.jl")
155+
export steady_state_stability, steady_state_jac
156+
152157
### Extensions ###
153158

154159
# HomotopyContinuation

src/reactionsystem.jl

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,36 @@ function dependants(rx, network)
11611161
dependents(rx, network)
11621162
end
11631163

1164+
"""
1165+
isautonomous(rs::ReactionSystem)
1166+
1167+
Checks if a system is autonomous (i.e. no rate or equation depend on the independent variable(s)).
1168+
Example:
1169+
```julia
1170+
rs1 = @reaction_system
1171+
(p,d), 0 <--> X
1172+
end
1173+
isautonomous(rs1) # Returns `true`.
1174+
1175+
rs2 = @reaction_system
1176+
(p/t,d), 0 <--> X
1177+
end
1178+
isautonomous(rs2) # Returns `false`.
1179+
```
1180+
"""
1181+
function isautonomous(rs::ReactionSystem)
1182+
# Get all variables occurring in reactions and equations.
1183+
vars = Set()
1184+
for eq in equations(rs)
1185+
(eq isa Reaction) ? get_variables!(vars, eq.rate) : get_variables!(vars, eq)
1186+
end
1187+
1188+
# Checks for iv and spatial ivs
1189+
(get_iv(rs) in vars) && return false
1190+
any(in(vars), get_sivs(rs)) && return false
1191+
return true
1192+
end
1193+
11641194

11651195
### `ReactionSystem` Remaking ###
11661196

src/reactionsystem_conversions.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,8 +518,14 @@ function Base.convert(::Type{<:NonlinearSystem}, rs::ReactionSystem; name = name
518518
include_zero_odes = true, remove_conserved = false, checks = false,
519519
default_u0 = Dict(), default_p = Dict(), defaults = _merge(Dict(default_u0), Dict(default_p)),
520520
all_differentials_permitted = false, kwargs...)
521+
# Error checks.
521522
iscomplete(rs) || error(COMPLETENESS_ERROR)
522523
spatial_convert_err(rs::ReactionSystem, NonlinearSystem)
524+
if !isautonomous(rs)
525+
error("Attempting to convert a non-autonomous `ReactionSystem` (e.g. where some rate depend on $(rs.iv)) to a `NonlinearSystem`. This is not possible. if you are intending to compute system steady states, consider creating and solving a `SteadyStateProblem.")
526+
end
527+
528+
# Generates system equations.
523529
fullrs = Catalyst.flatten(rs)
524530
remove_conserved && conservationlaws(fullrs)
525531
ists, ispcs = get_indep_sts(fullrs, remove_conserved)

src/steady_state_stability.jl

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
### Stability Analysis ###
2+
3+
"""
4+
stability(u::Vector{T}, rs::ReactionSystem, p; tol = 10*sqrt(eps())
5+
ss_jac = steady_state_jac(u, rs, p))
6+
7+
Compute the stability of a steady state (Returned as a `Bool`, with `true` indicating stability).
8+
9+
Arguments:
10+
- `u`: The steady state for which we want to compute stability.
11+
- `rs`: The `ReactionSystem` model for which we want to compute stability.
12+
- `p`: The parameter set for which we want to compute stability.
13+
- `tol = 10*sqrt(eps())`: The tolerance used for determining whether computed eigenvalues real
14+
parts can reliably be considered negative/positive. In practise, a steady state is considered
15+
stable if the corresponding Jacobian's maximum eigenvalue real part is < 0. However, if this maximum
16+
eigenvalue is in the range `-tol< eig < tol`, and error is throw, as we do not deem that stability
17+
can be ensured with enough certainty. The choice `tol = 10*sqrt(eps())` has *not* been subject
18+
to much analysis.
19+
- `ss_jac = steady_state_jac(u, rs)`: It is possible to pre-compute the
20+
Jacobian used for stability computation using `steady_state_jac`. If stability is computed
21+
for many states, precomputing the Jacobian may speed up evaluation.
22+
23+
Example:
24+
```julia
25+
# Creates model.
26+
rn = @reaction_network begin
27+
(p,d), 0 <--> X
28+
end
29+
p = [:p => 1.0, :d => 0.5]
30+
31+
# Finds (the trivial) steady state, and computes its stability.
32+
steady_state = [2.0]
33+
steady_state_stability(steady_state, rn, p)
34+
35+
Notes:
36+
- The input `u` can (currently) be both a vector of values (like `[1.0, 2.0]`) and a vector map
37+
(like `[X => 1.0, Y => 2.0]`). The reason is that most methods for computing stability only
38+
produces vectors of values. However, if possible, it is recommended to work with values on the
39+
form of maps.
40+
- Catalyst currently computes steady state stabilities using the naive approach of checking whether
41+
a system's largest eigenvalue real part is negative. While more advanced stability computation
42+
methods exist (and would be a welcome addition to Catalyst), there is no direct plans to implement
43+
these. Furthermore, Catalyst uses a tolerance `tol = 10*sqrt(eps())` to determine whether a
44+
computed eigenvalue is far away enough from 0 to be reliably used. This selected threshold can be changed through the `tol` argument.
45+
```
46+
"""
47+
function steady_state_stability(u::Vector, rs::ReactionSystem, ps; tol = 10*sqrt(eps(ss_val_type(u))),
48+
ss_jac = steady_state_jac(rs; u0 = u))
49+
# Warning checks.
50+
if !isautonomous(rs)
51+
error("Attempting to compute stability for a non-autonomous system (e.g. where some rate depend on $(rs.iv)). This is not possible.")
52+
end
53+
54+
# If `u` is a vector of values, we convert it to a map. Also, if there are conservation laws,
55+
# corresponding values must be eliminated from `u`.
56+
u = steady_state_u_conversion(u, rs)
57+
if length(u) > length(unknowns(ss_jac.f.sys))
58+
u = filter(u_v -> any(isequal(u_v[1]), unknowns(ss_jac.f.sys)), u)
59+
end
60+
61+
# Generates the Jacobian at the steady state (technically, `ss_jac` is an `ODEProblem` with dummy values for `u0` and `p`).
62+
J = zeros(length(u), length(u))
63+
ss_jac = remake(ss_jac; u0 = u, p = ps)
64+
ss_jac.f.jac(J, ss_jac.u0, ss_jac.p, Inf)
65+
66+
# Computes stability (by checking that the real part of all eigenvalues is negative).
67+
max_eig = maximum(real(ev) for ev in eigvals(J))
68+
if abs(max_eig) < tol
69+
error("The system Jacobian's maximum eigenvalue at the steady state is within the tolerance range (abs($max_eig) < $tol). Hence, stability could not be reliably determined. If you still wish to compute the stability, reduce the `tol` argument, but note that floating point error in the eigenvalue calculation could lead to incorrect results.")
70+
end
71+
return max_eig < 0
72+
end
73+
74+
# Used to determine the type of the steady states values, which is then used to set the tolerance's
75+
# type.
76+
ss_val_type(u::Vector{T}) where {T} = T
77+
ss_val_type(u::Vector{Pair{S,T}}) where {S,T} = T
78+
ss_val_type(u::Dict{S,T}) where {S,T} = T
79+
80+
"""
81+
steady_state_jac(rs::ReactionSystem; u0 = [])
82+
83+
Creates the Jacobian function which can be used as input to `steady_state_stability`. Useful when
84+
a large number of stability computation has to be carried out in a performant manner.
85+
86+
Arguments:
87+
- `rs`: The reaction system model for which we want to compute stability.
88+
- `u0 = []`: For systems with conservation laws, a `u` is required to compute the conserved quantities.
89+
90+
Example:
91+
```julia
92+
# Creates model.
93+
rn = @reaction_network begin
94+
(p,d), 0 <--> X
95+
end
96+
p = [:p => 1.0, :d => 0.5]
97+
98+
# Creates the steady state jacobian.
99+
ss_jac = steady_state_jacobian(rn)
100+
101+
# Finds (the trivial) steady state, and computes stability.
102+
steady_state = [2.0]
103+
steady_state_stability(steady_state, rn, p; ss_jac)
104+
105+
Notes:
106+
- In practise, this function returns an `ODEProblem` (with a computed Jacobian) set up in
107+
such a way that it can be used by the `steady_state_stability` function.
108+
```
109+
"""
110+
function steady_state_jac(rs::ReactionSystem; u0 = [sp => 0.0 for sp in unknowns(rs)],
111+
combinatoric_ratelaws = get_combinatoric_ratelaws(rs))
112+
# If u0 is a vector of values, must be converted to something MTK understands.
113+
114+
# Converts u0 to values MTK understands, and checks that potential conservation laws are accounted for.
115+
u0 = steady_state_u_conversion(u0, rs)
116+
conservationlaw_errorcheck(rs, u0)
117+
118+
# Creates an `ODEProblem` with a Jacobian. Dummy values for `u0` and `ps` must be provided.
119+
ps = [p => 0.0 for p in parameters(rs)]
120+
return ODEProblem(rs, u0, 0, ps; jac = true, remove_conserved = true,
121+
combinatoric_ratelaws = combinatoric_ratelaws)
122+
end
123+
124+
# Converts a `u` vector from a vector of values to a map.
125+
function steady_state_u_conversion(u, rs::ReactionSystem)
126+
if (u isa Vector{<:Number})
127+
if length(u) == length(unknowns(rs))
128+
u = [sp => v for (sp,v) in zip(unknowns(rs), u)]
129+
else
130+
error("You are trying to generate a stability Jacobian, 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.")
131+
end
132+
end
133+
return symmap_to_varmap(rs, u)
134+
end

0 commit comments

Comments
 (0)