Skip to content

Commit 1e96aa5

Browse files
authored
Merge pull request #1206 from SciML/fix_nlsys_conslaws
Re-enable conservation laws for `NonlinearSystem`s
2 parents 7d75a7b + 61ba9a8 commit 1e96aa5

File tree

11 files changed

+527
-114
lines changed

11 files changed

+527
-114
lines changed

HISTORY.md

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,47 @@
77
(at the time the release is made). If you need a dependency version increased,
88
please open an issue and we can update it and make a new Catalyst release once
99
testing against the newer dependency version is complete.
10-
- It is no longer recommended to install and use the full OrdinaryDiffEq library to access specific ODE solvers.
11-
Instead, only install the specific OrdinaryDiffEq sub-libraries that contain the desired
12-
solver. This significantly reduces installation and package loading times. I.e. to use the default
13-
solver that auto-switches between explicit and implicit methods, install `OrdinaryDiffEqDefault`. To
14-
use `Tsit5` install `OrdinaryDiffEqTsit5`, etc. The possible sub-libraries, each containing different solvers,
15-
can be viewed [here](https://github.com/SciML/OrdinaryDiffEq.jl/tree/master/lib).
16-
- It should now be safe to use `remake` on problems which have had conservation laws removed.
17-
The warning regarding this when eliminating conservation laws have been removed (in conjunction
18-
with the `remove_conserved_warn` argument for disabling this warning).
19-
- New formula for inferring variables from equations (declared using the `@equations` options) in the DSL. The order of inference of species/variables/parameters is now:
20-
(1) Every symbol explicitly declared using `@species`, `@variables`, and `@parameters` are assigned to the correct category.
21-
(2) Every symbol used as a reaction reactant is inferred as a species.
22-
(3) Every symbol not declared in (1) or (2) that occurs in an expression provided after `@equations` is inferred as a variable.
23-
(4) Every symbol not declared in (1), (2), or (3) that occurs either as a reaction rate or stoichiometric coefficient is inferred to be a parameter.
24-
E.g. in
25-
```julia
26-
@reaction_network begin
27-
@equations V1 + S ~ V2^2
28-
(p + S + V1), S --> 0
29-
end
30-
```
31-
`S` is inferred as a species, `V1` and `V2` as variables, and `p` as a parameter. The previous special cases for the `@observables`, `@compounds`, and `@differentials` options still hold. Finally, the `@require_declaration` options (described in more detail below) can now be used to require everything to be explicitly declared.
32-
- New formula for determining whether the default differentials have been used within an `@equations` option. Now, if any expression `D(...)` is encountered (where `...` can be anything), this is inferred as usage of the default differential D. E.g. in the following equations `D` is inferred as a differential with respect to the default independent variable:
10+
- It is no longer recommended to install and use the full OrdinaryDiffEq library
11+
to access specific ODE solvers. Instead, only install the specific
12+
OrdinaryDiffEq sub-libraries that contain the desired solver. This
13+
significantly reduces installation and package loading times. I.e. to use the
14+
default solver that auto-switches between explicit and implicit methods,
15+
install `OrdinaryDiffEqDefault`. To use `Tsit5` install `OrdinaryDiffEqTsit5`,
16+
etc. The possible sub-libraries, each containing different solvers, can be
17+
viewed [here](https://github.com/SciML/OrdinaryDiffEq.jl/tree/master/lib).
18+
- It should now be safe to use `remake` on problems which have had conservation
19+
laws removed with the exception of `NonlinearProblem`s or `NonlinearSystem`s.
20+
For NonlinearProblems it is safe to use `remake` if only updating `u0` values,
21+
but it is not safe to update the value of the conserved constant, `Γ`. See
22+
[the FAQ](https://docs.sciml.ai/Catalyst/stable/faqs/#faq_remake_nonlinprob)
23+
for details.
24+
- New formula for inferring variables from equations (declared using the
25+
`@equations` options) in the DSL. The order of inference of
26+
species/variables/parameters is now:
27+
1. Every symbol explicitly declared using `@species`, `@variables`, and
28+
`@parameters` are assigned to the correct category.
29+
2. Every symbol used as a reaction reactant is inferred as a species.
30+
3. Every symbol not declared in (1) or (2) that occurs in an expression
31+
provided after `@equations` is inferred as a variable.
32+
4. Every symbol not declared in (1), (2), or (3) that occurs either as a
33+
reaction rate or stoichiometric coefficient is inferred to be a
34+
parameter. E.g. in
35+
```julia
36+
@reaction_network begin
37+
@equations V1 + S ~ V2^2
38+
(p + S + V1), S --> 0
39+
end
40+
```
41+
`S` is inferred as a species, `V1` and `V2` as variables, and `p` as a
42+
parameter. The previous special cases for the `@observables`, `@compounds`,
43+
and `@differentials` options still hold. Finally, the `@require_declaration`
44+
options (described in more detail below) can now be used to require everything
45+
to be explicitly declared.
46+
- New formula for determining whether the default differentials have been used
47+
within an `@equations` option. Now, if any expression `D(...)` is encountered
48+
(where `...` can be anything), this is inferred as usage of the default
49+
differential D. E.g. in the following equations `D` is inferred as a
50+
differential with respect to the default independent variable:
3351
```julia
3452
@reaction_network begin
3553
@equations D(V) + V ~ 1
@@ -38,7 +56,9 @@
3856
@equations D(D(V)) ~ 1
3957
end
4058
```
41-
Please note that this cannot be used at the same time as `D` is used to represent a species, variable, or parameter (including if these are implicitly designated as such by e.g. appearing as a reaction reactant).
59+
Please note that this cannot be used at the same time as `D` is used to
60+
represent a species, variable, or parameter (including if these are implicitly
61+
designated as such by e.g. appearing as a reaction reactant).
4262
- Array symbolics support is more consistent with ModelingToolkit v9. Parameter
4363
arrays are no longer scalarized by Catalyst, while species and variables
4464
arrays still are (as in ModelingToolkit). As such, parameter arrays should now
@@ -51,7 +71,11 @@
5171
*not* to do this as it has signifcant performance costs with ModelingToolkit
5272
v9. Note, scalarized parameter arrays passed to the two-argument
5373
`ReactionSystem` constructor may become unscalarized.
54-
- Functional (e.g. time-dependent) parameters can now be used in Catalyst models. These can e.g. be used to incorporate arbitrary time-dependent functions (as a parameter) in a model. For more details on how to use these, please read: https://docs.sciml.ai/Catalyst/stable/model_creation/functional_parameters/.
74+
- Functional (e.g. time-dependent) parameters can now be used in Catalyst
75+
models. These can e.g. be used to incorporate arbitrary time-dependent
76+
functions (as a parameter) in a model. For more details on how to use these,
77+
please read:
78+
https://docs.sciml.ai/Catalyst/stable/model_creation/functional_parameters/.
5579
- Scoped species/variables/parameters are now treated similar to the latest MTK
5680
releases ( 9.49).
5781
- A tutorial on making interactive plot displays using Makie has been added.

docs/src/faqs.md

Lines changed: 138 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ so the reaction
271271
```@example faq7
272272
using Catalyst
273273
rn = @reaction_network begin
274-
k, X --> ∅
274+
k, X --> ∅
275275
end
276276
convert(ODESystem, rn)
277277
```
@@ -280,7 +280,7 @@ using any of the following non-filled arrows when declaring the reaction: `<=`,
280280
``, ``, `=>`, ``, ``, ``, ``. This means that the reaction
281281
```@example faq7
282282
rn = @reaction_network begin
283-
k, X => ∅
283+
k, X => ∅
284284
end
285285
convert(ODESystem, rn)
286286
```
@@ -290,7 +290,7 @@ will be degraded at a constant rate even when very small or equal to 0).
290290
Note, stoichiometric coefficients are still included, i.e. the reaction
291291
```@example faq7
292292
rn = @reaction_network begin
293-
k, 2*X ⇒ ∅
293+
k, 2*X ⇒ ∅
294294
end
295295
convert(ODESystem, rn)
296296
```
@@ -303,7 +303,7 @@ ModelingToolkit. e.g., this is should work
303303
using Catalyst
304304
myHill(x) = 2*x^3/(x^3+1.5^3)
305305
rn = @reaction_network begin
306-
myHill(X), ∅ --> X
306+
myHill(X), ∅ --> X
307307
end
308308
```
309309
In some cases, it may be necessary or desirable to register functions with
@@ -322,3 +322,137 @@ rn = @reaction_network begin
322322
(k1, k2), A <--> B
323323
end
324324
```
325+
326+
## [What to be aware of when using `remake` with conservation law elimination and NonlinearProblems?](@id faq_remake_nonlinprob)
327+
328+
When constructing `NonlinearSystem`s or `NonlinearProblem`s with `remove_conserved = true`, i.e.
329+
```julia
330+
# for rn a ReactionSystem
331+
nsys = convert(NonlinearSystem, rn; remove_conserved = true)
332+
333+
# or
334+
nprob = NonlinearProblem(rn, u0, p; remove_conserved = true)
335+
```
336+
`remake` is currently unable to correctly update all `u0` values when the
337+
conserved constant(s), `Γ`, are updated. As an example consider the following
338+
```@example faq_remake
339+
using Catalyst, NonlinearSolve
340+
rn = @reaction_network begin
341+
(k₁,k₂), X₁ <--> X₂
342+
(k₃,k₄), X₁ + X₂ --> 2X₃
343+
end
344+
u0 = [:X₁ => 1.0, :X₂ => 2.0, :X₃ => 3.0]
345+
ps = [:k₁ => 0.1, :k₂ => 0.2, :k₃ => 0.3, :k₄ => 0.4]
346+
nlsys = convert(NonlinearSystem, rn; remove_conserved = true, conseqs_remake_warn = false)
347+
nlsys = complete(nlsys)
348+
equations(nlsys)
349+
```
350+
If we generate a `NonlinearProblem` from this system the conservation constant,
351+
`Γ[1]`, is automatically set to `X₁ + X₂ + X₃ = 6` and the initial values are
352+
those in `u0`. i.e if
353+
```@example faq_remake
354+
nlprob1 = NonlinearProblem(nlsys, u0, ps)
355+
```
356+
then
357+
```@example faq_remake
358+
nlprob1[(:X₁, :X₂, :X₃)] == (1.0, 2.0, 3.0)
359+
```
360+
and
361+
```@example faq_remake
362+
nlprob1.ps[:Γ][1] == 6.0
363+
```
364+
If we now try to change a value of `X₁`, `X₂`, or `X₃` using `remake`, the
365+
conserved constant will be recalculated. i.e. if
366+
```@example faq_remake
367+
nlprob2 = remake(nlprob1; u0 = [:X₂ => 3.0])
368+
```
369+
compare
370+
```@example faq_remake
371+
println("Correct u0 is: ", (1.0, 3.0, 3.0), "\n", "remade value is: ", nlprob2[(:X₁, :X₂, :X₃)])
372+
```
373+
and
374+
```@example faq_remake
375+
println("Correct Γ is: ", 7.0, "\n", "remade value is: ", nlprob2.ps[:Γ][1])
376+
```
377+
However, if we try to directly change the value of `Γ` it is not always the case
378+
that a `u0` value will correctly update so that the conservation law is
379+
conserved. Consider
380+
```@example faq_remake
381+
nlprob3 = remake(nlprob1; u0 = [:X₂ => nothing], p = [:Γ => [4.0]])
382+
```
383+
Setting `[:X₂ => nothing]` for other problem types communicates that the
384+
`u0` value for `X₂` should be solved for. However, if we examine the values we
385+
find
386+
```@example faq_remake
387+
println("Correct u0 is: ", (1.0, 0.0, 3.0), "\n", "remade value is: ", nlprob3[(:X₁, :X₂, :X₃)])
388+
```
389+
and
390+
```@example faq_remake
391+
println("Correct Γ is: ", 4.0, "\n", "remade value is: ", nlprob3.ps[:Γ][1])
392+
```
393+
As such, the `u0` value for `X₂` has not updated, and the conservation law is
394+
now violated by the `u0` values, i.e,
395+
```@example faq_remake
396+
(nlprob3[:X₁] + nlprob3[:X₂] + nlprob3[:X₃]) == nlprob3.ps[:Γ][1]
397+
```
398+
Currently, the only way to avoid this issue is to manually specify updated
399+
values for the `u0` components, which will ensure that `Γ` updates appropriately
400+
as in the first example. i.e. we manually set `X₂` to the value it should be and
401+
`Γ` will be updated accordingly:
402+
```@example faq_remake
403+
nlprob4 = remake(nlprob1; u0 = [:X₂ => 0.0])
404+
```
405+
so that
406+
```@example faq_remake
407+
println("Correct u0 is: ", (1.0, 0.0, 3.0), "\n", "remade value is: ", nlprob4[(:X₁, :X₂, :X₃)])
408+
```
409+
and
410+
```@example faq_remake
411+
println("Correct Γ is: ", 4.0, "\n", "remade value is: ", nlprob4.ps[:Γ][1])
412+
```
413+
414+
Finally, we note there is one extra consideration to take into account if using
415+
`structural_simplify`. In this case one of `X₁`, `X₂`, or `X₃` will be moved to
416+
being an observed. It will then always correspond to the updated value if one
417+
tries to manually change `Γ`. Let's see what happens here directly
418+
```@example faq_remake
419+
nlsys = convert(NonlinearSystem, rn; remove_conserved = true, conseqs_remake_warn = false)
420+
nlsys = structural_simplify(nlsys)
421+
nlprob1 = NonlinearProblem(nlsys, u0, ps)
422+
```
423+
We can now try to change just `Γ` and implicitly the observed variable that was
424+
removed will be assumed to have changed its initial value to compensate for it.
425+
Let's confirm this. First we find the observed variable that was elminated.
426+
```@example faq_remake
427+
obs_unknown = only(observed(nlsys)).lhs
428+
```
429+
We can figure out its index in `u0` via
430+
```@example faq_remake
431+
obs_symbol = ModelingToolkit.getname(obs_unknown)
432+
obsidx = findfirst(p -> p[1] == obs_symbol, u0)
433+
```
434+
Let's now remake
435+
```@example faq_remake
436+
nlprob2 = remake(nlprob1; u0 = [obs_unknown => nothing], p = [:Γ => [8.0]])
437+
```
438+
Here we indicate that the observed variable should be treated as unspecified
439+
during initialization. Since the observed variable is not considered an unknown,
440+
everything now works, with the observed variable's assumed initial value
441+
adjusted to allow `Γ = 8`:
442+
```@example faq_remake
443+
correct_u0 = last.(u0)
444+
correct_u0[obsidx] = 8 - sum(correct_u0) + correct_u0[obsidx]
445+
println("Correct u0 is: ", (1.0, 2.0, 5.0), "\n", "remade value is: ", nlprob2[(:X₁, :X₂, :X₃)])
446+
```
447+
and `Γ` becomes
448+
```@example faq_remake
449+
println("Correct Γ is: ", 8.0, "\n", "remade value is: ", nlprob2.ps[:Γ][1])
450+
```
451+
Unfortunately, as with our first example, trying to enforce that a
452+
non-eliminated species should have its initial value updated instead of the
453+
observed species will not work.
454+
455+
*Summary:* it is not recommended to directly update `Γ` via `remake`, but to
456+
instead update values of the initial guesses in `u0` to obtain a desired `Γ`. At
457+
this time the behavior when updating `Γ` can result in `u0` values that do not
458+
satisfy the conservation law defined by `Γ` as illustrated above.

docs/src/model_creation/conservation_laws.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ Generally, for a conservation law where we have:
133133
If the value of the conservation law parameter $Γ$'s value *has never been specified*, its value will be updated to accommodate changes in species values. If the conservation law parameter ($Γ$)'s value *has been specified* (either when the `ODEProblem` was created, or using `remake`), then the eliminated species's value will be updated to accommodate changes in the conservation law parameter or non-eliminated species's values. Furthermore, in this case, the value of the eliminated species *cannot be updated*.
134134

135135
!!! warn
136-
When updating the values of problems with conservation laws it is additionally important to use `remake` (as opposed to direct indexing, e.g. setting `oprob[:X₁] = 16.0`).
136+
When updating the values of problems with conservation laws it is additionally important to use `remake` (as opposed to direct indexing, e.g. setting `oprob[:X₁] = 16.0`). Moreover, care is needed when using `remake` with `NonlinearProblem`s for which conserved equations have been removed. See [the FAQ](https://docs.sciml.ai/Catalyst/stable/faqs/#faq_remake_nonlinprob) for details on what is supported and what may lead to `u0` values that do not satisfy the conservation law in the special case of `NonlinearProblem`s.
137137

138138
### [Extracting the conservation law parameter's symbolic variable](@id conservation_laws_prob_updating_symvar)
139139
Catalyst represents its models using the [Symbolics.jl](https://github.com/JuliaSymbolics/Symbolics.jl) computer algebraic system, something which allows the user to [form symbolic expressions of model components](@ref simulation_structure_interfacing_symbolic_representation). If you wish to extract and use the symbolic variable corresponding to a model's conserved quantities, you can use the following syntax:

ext/CatalystBifurcationKitExtension/bifurcation_kit_extension.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@ function bkext_make_nsys(rs, u0)
3737
cons_default = [cons_eq.rhs for cons_eq in cons_eqs]
3838
cons_default = Catalyst.get_networkproperties(rs).conservedconst => cons_default
3939
defaults = Dict([u0; cons_default])
40-
nsys = convert(NonlinearSystem, rs; defaults, remove_conserved = true)
40+
nsys = convert(NonlinearSystem, rs; defaults, remove_conserved = true, conseqs_remake_warn = false)
4141
return complete(nsys)
4242
end

ext/CatalystHomotopyContinuationExtension/homotopy_continuation_extension.jl

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
### Homotopy Continuation Based Steady State Finding ###
22

33
"""
4-
hc_steady_states(rs::ReactionSystem, ps; filter_negative=true, neg_thres=-1e-20, u0=typeof(ps)(), kwargs...)
4+
hc_steady_states(rs::ReactionSystem, ps; filter_negative = true, neg_thres = -1e-16, u0 = typeof(ps)(), kwargs...)
55
66
Uses homotopy continuation via HomotopyContinuation.jl to find the steady states of the ODE system corresponding to the provided reaction system.
77
88
Arguments:
99
- `rs::ReactionSystem`: The reaction system for which we want to find the steady states.
1010
- `ps`: The parameter values for which we want to find the steady states.
11-
- `filter_negative=true`: If set to true, solutions with any species concentration <neg_thres is removed from the output.
12-
- `neg_thres=-1e-20`: Determine the minimum values for which a species concentration is to be considered non-negative. Species concentrations ``> neg_thres`` but `< 0.0` are set to `0.0`.
13-
- `u0=nothing`: Initial conditions for which we want to find the steady states. For systems with conservation laws this are required to compute conserved quantities. Initial conditions are not required for all species, only those involved in conserved quantities (if this set is unknown, it is recommended to provide initial conditions for all species).
11+
- `filter_negative = true`: If set to true, solutions with any species concentration <neg_thres is removed from the output.
12+
- `neg_thres = -1e-15`: Determine the minimum values for which a species concentration is to be considered non-negative. Species concentrations ``> neg_thres`` but `< 0.0` are set to `0.0`.
13+
- `u0 = nothing`: Initial conditions for which we want to find the steady states. For systems with conservation laws this are required to compute conserved quantities. Initial conditions are not required for all species, only those involved in conserved quantities (if this set is unknown, it is recommended to provide initial conditions for all species).
1414
- `kwargs...`: any additional arguments (like `show_progress= true`) are passed into HomotopyContinuation.jl's `solve` call.
1515
1616
Examples
@@ -36,7 +36,7 @@ Notes:
3636
```
3737
"""
3838
function Catalyst.hc_steady_states(rs::ReactionSystem, ps; filter_negative = true,
39-
neg_thres = -1e-20, u0 = [], kwargs...)
39+
neg_thres = -1e-15, u0 = [], kwargs...)
4040
if !isautonomous(rs)
4141
error("Attempting to compute steady state for a non-autonomous system (e.g. where some rate depend on $(get_iv(rs))). This is not possible.")
4242
end
@@ -51,7 +51,7 @@ function steady_state_polynomial(rs::ReactionSystem, ps, u0)
5151
# Creates the appropriate nonlinear system, and converts parameters to a form that can
5252
# be substituted in later.
5353
rs = Catalyst.expand_registered_functions(rs)
54-
ns = complete(convert(NonlinearSystem, rs; remove_conserved = true))
54+
ns = complete(convert(NonlinearSystem, rs; remove_conserved = true, conseqs_remake_warn = false))
5555
pre_varmap = [symmap_to_varmap(rs, u0)..., symmap_to_varmap(rs, ps)...]
5656
Catalyst.conservationlaw_errorcheck(rs, pre_varmap)
5757
p_dict = make_p_val_dict(pre_varmap, rs, ns)
@@ -164,7 +164,7 @@ function reorder_sols!(sols, ss_poly, rs::ReactionSystem)
164164
end
165165

166166
# Filters away solutions with negative species concentrations (and for neg_thres < val < 0.0, sets val=0.0).
167-
function filter_negative_f(sols; neg_thres = -1e-20)
167+
function filter_negative_f(sols; neg_thres = -1e-15)
168168
for sol in sols, idx in 1:length(sol)
169169
(neg_thres < sol[idx] < 0) && (sol[idx] = 0)
170170
end

0 commit comments

Comments
 (0)