diff --git a/docs/old_files/advanced.md b/docs/old_files/advanced.md
index 80388d12bb..d46af2afa7 100644
--- a/docs/old_files/advanced.md
+++ b/docs/old_files/advanced.md
@@ -1,28 +1,35 @@
# The Reaction DSL - Advanced
+
This section covers some of the more advanced syntax and features for building
chemical reaction network models (still not very complicated!).
#### User defined functions in reaction rates
+
The reaction network DSL can "see" user defined functions that work with
ModelingToolkit. E.g., this is should work
+
```julia
myHill(x) = 2.0*x^3/(x^3+1.5^3)
rn = @reaction_network begin
myHill(X), ∅ → X
end
```
+
In some cases, it may be necessary or desirable to register functions with
Symbolics.jl before their use in Catalyst, see the discussion
[here](https://symbolics.juliasymbolics.org/dev/manual/functions/).
#### Ignoring mass action kinetics
+
While generally one wants the reaction rate to use the law of mass action, so
the reaction
+
```julia
rn = @reaction_network begin
k, X → ∅
end
```
+
occurs at the rate ``d[X]/dt = -k[X]``, it is possible to ignore this by using
any of the following non-filled arrows when declaring the reaction: `<=`, `⇐`, `⟽`,
`⇒`, `⟾`, `=>`, `⇔`, `⟺` (`<=>` currently not possible due to Julia language technical reasons). This means that the reaction
diff --git a/docs/old_files/unused_tutorials/advanced_examples.md b/docs/old_files/unused_tutorials/advanced_examples.md
index cce12dd1f7..c1452c0bdd 100644
--- a/docs/old_files/unused_tutorials/advanced_examples.md
+++ b/docs/old_files/unused_tutorials/advanced_examples.md
@@ -1,4 +1,5 @@
# Advanced Chemical Reaction Network Examples
+
For additional flexibility, we can convert the generated `ReactionSystem` first
to another `ModelingToolkit.AbstractSystem`, e.g., an `ODESystem`, `SDESystem`,
`JumpSystem`, etc. These systems can then be used in problem generation. Please
@@ -11,6 +12,7 @@ Note, when generating problems from other system types, `u0` and `p` must
provide vectors, tuples or dictionaries of `Pair`s that map each the symbolic
variables for each species or parameter to their numerical value. E.g., for the
Michaelis-Menten example above we'd use
+
```julia
rs = @reaction_network begin
c1, X --> 2X
@@ -20,10 +22,9 @@ end
p = (:c1 => 1.0, :c2 => 2.0, :c3 => 50.)
pmap = symmap_to_varmap(rs,p) # convert Symbol map to symbolic variable map
tspan = (0.,4.)
-u0 = [:X => 5.]
+u0 = [:X => 5.]
u0map = symmap_to_varmap(rs,u0) # convert Symbol map to symbolic variable map
osys = convert(ODESystem, rs)
oprob = ODEProblem(osys, u0map, tspan, pmap)
sol = solve(oprob, Tsit5())
```
-
diff --git a/docs/old_files/unused_tutorials/models.md b/docs/old_files/unused_tutorials/models.md
index 7bffcf48c6..01e48d58df 100644
--- a/docs/old_files/unused_tutorials/models.md
+++ b/docs/old_files/unused_tutorials/models.md
@@ -1,16 +1,21 @@
# Model Simulation
+
Once created, a reaction network can be used as input to various problem types,
which can be solved by
[DifferentialEquations.jl](http://docs.sciml.ai/DiffEqDocs/stable/),
and more broadly used within [SciML](https://sciml.ai) packages.
#### Deterministic simulations using ODEs
+
A reaction network can be used as input to an `ODEProblem` instead of a
function, using
+
```julia
odeprob = ODEProblem(rn, args...; kwargs...)
```
+
E.g., a model can be created and solved using:
+
```julia
using DiffEqBase, OrdinaryDiffEq
rn = @reaction_network begin
@@ -23,6 +28,7 @@ tspan = (0.,1.)
prob = ODEProblem(rn,u0,tspan,p)
sol = solve(prob, Tsit5())
```
+
Here, the order of unknowns in `u0` and `p` matches the order that species and
parameters first appear within the DSL. They can also be determined by examining
the ordering within the `species(rn)` and `parameters` vectors, or accessed more
@@ -34,37 +40,47 @@ DSL](@ref)). Note, if no parameters are given in the
[`@reaction_network`](@ref), then `p` does not need to be provided.
We can then plot the solution using the solution plotting recipe:
+
```julia
using Plots
plot(sol, lw=2)
```
+

To solve for a steady state starting from the guess `u0`, one can use
+
```julia
using SteadyStateDiffEq
prob = SteadyStateProblem(rn,u0,p)
sol = solve(prob, SSRootfind())
```
+
or
+
```julia
prob = SteadyStateProblem(rn,u0,p)
sol = solve(prob, DynamicSS(Tsit5()))
```
#### Stochastic simulations using SDEs
+
In a similar way an SDE can be created using
+
```julia
using StochasticDiffEq
sdeprob = SDEProblem(rn, args...; kwargs...)
```
+
In this case the chemical Langevin equations (as derived in Gillespie, J. Chem.
Phys. 2000) will be used to generate stochastic differential equations.
#### Stochastic simulations using discrete stochastic simulation algorithms
+
Instead of solving SDEs, one can create a stochastic jump process model using
integer copy numbers and a discrete stochastic simulation algorithm (i.e.,
Gillespie Method or Kinetic Monte Carlo). This can be done using:
+
```julia
using JumpProcesses
rn = @reaction_network begin
@@ -78,30 +94,34 @@ discrete_prob = DiscreteProblem(rn, u0, tspan, p)
jump_prob = JumpProblem(rn, discrete_prob, Direct())
sol = solve(jump_prob, SSAStepper())
```
+
Here, we used Gillespie's `Direct` method as the underlying stochastic simulation
algorithm. We get:
+
```julia
plot(sol, lw=2)
```
+

### [`Reaction`](@ref) fields
Each `Reaction` within `reactions(rn)` has a number of subfields. For `rx` a
`Reaction` we have:
-* `rx.substrates`, a vector of ModelingToolkit expressions storing each
+
+- `rx.substrates`, a vector of ModelingToolkit expressions storing each
substrate variable.
-* `rx.products`, a vector of ModelingToolkit expressions storing each product
+- `rx.products`, a vector of ModelingToolkit expressions storing each product
variable.
-* `rx.substoich`, a vector storing the corresponding stoichiometry of each
+- `rx.substoich`, a vector storing the corresponding stoichiometry of each
substrate species in `rx.substrates`.
-* `rx.prodstoich`, a vector storing the corresponding stoichiometry of each
+- `rx.prodstoich`, a vector storing the corresponding stoichiometry of each
product species in `rx.products`.
-* `rx.rate`, a `Number`, `ModelingToolkit.Sym`, or ModelingToolkit expression
+- `rx.rate`, a `Number`, `ModelingToolkit.Sym`, or ModelingToolkit expression
representing the reaction rate. E.g., for a reaction like `k*X, Y --> X+Y`,
we'd have `rate = k*X`.
-* `rx.netstoich`, a vector of pairs mapping the ModelingToolkit expression for
+- `rx.netstoich`, a vector of pairs mapping the ModelingToolkit expression for
each species that changes numbers by the reaction to how much it changes. E.g.,
for `k, X + 2Y --> X + W`, we'd have `rx.netstoich = [Y(t) => -2, W(t) => 1]`.
-* `rx.only_use_rate`, a boolean that is `true` if the reaction was made with
+- `rx.only_use_rate`, a boolean that is `true` if the reaction was made with
non-filled arrows and should ignore mass action kinetics. `false` by default.
diff --git a/docs/src/api/core_api.md b/docs/src/api/core_api.md
index 849c6ac805..34f0ccb3f0 100644
--- a/docs/src/api/core_api.md
+++ b/docs/src/api/core_api.md
@@ -1,9 +1,11 @@
# [Catalyst.jl API](@id api)
+
```@meta
CurrentModule = Catalyst
```
## Reaction network generation and representation
+
Catalyst provides the [`@reaction_network`](@ref) macro for generating a
complete network, stored as a [`ReactionSystem`](@ref), which in turn is
composed of [`Reaction`](@ref)s. `ReactionSystem`s can be converted to other
@@ -16,11 +18,13 @@ appear as a substrate or product in some reaction will be treated as a species,
while all remaining symbols will be considered parameters (corresponding to
those symbols that only appear within rate expressions and/or as stoichiometric
coefficients). I.e. in
+
```julia
rn = @reaction_network begin
k*X, Y --> W
end
```
+
`Y` and `W` will all be classified as chemical species, while `k` and `X` will
be classified as parameters.
@@ -86,15 +90,17 @@ ReactionSystem
```
## [Options for the `@reaction_network` DSL](@id api_dsl_options)
+
We have [previously described](@ref dsl_advanced_options) how options permit the user to supply non-reaction information to [`ReactionSystem`](@ref) created through the DSL. Here follows a list
of all options currently available.
+
- [`parameters`](@ref dsl_advanced_options_declaring_species_and_parameters): Allows the designation of a set of symbols as system parameters.
- [`species`](@ref dsl_advanced_options_declaring_species_and_parameters): Allows the designation of a set of symbols as system species.
- [`variables`](@ref dsl_advanced_options_declaring_species_and_parameters): Allows the designation of a set of symbols as system non-species variables.
- [`ivs`](@ref dsl_advanced_options_ivs): Allows the designation of a set of symbols as system independent variables.
- [`compounds`](@ref chemistry_functionality_compounds): Allows the designation of compound species.
- [`observables`](@ref dsl_advanced_options_observables): Allows the designation of compound observables.
-- [`default_noise_scaling`](@ref simulation_intro_SDEs_noise_saling): Enables the setting of a default noise scaling expression.
+- [`default_noise_scaling`](@ref simulation_intro_SDEs_noise_scaling): Enables the setting of a default noise scaling expression.
- [`differentials`](@ref constraint_equations_coupling_constraints): Allows the designation of differentials.
- [`equations`](@ref constraint_equations_coupling_constraints): Allows the creation of algebraic and/or differential equations.
- [`continuous_events`](@ref constraint_equations_events): Allows the creation of continuous events.
@@ -102,6 +108,7 @@ of all options currently available.
- [`combinatoric_ratelaws`](@ref faq_combinatoric_ratelaws): Takes a single option (`true` or `false`), which sets whether to use combinatorial rate laws.
## [ModelingToolkit and Catalyst accessor functions](@id api_accessor_functions)
+
A [`ReactionSystem`](@ref) is an instance of a
`ModelingToolkit.AbstractTimeDependentSystem`, and has a number of fields that
can be accessed using the Catalyst API and the [ModelingToolkit.jl Abstract
@@ -117,22 +124,22 @@ retrieve info from just a base [`ReactionSystem`](@ref) `rn`, ignoring
sub-systems of `rn`, one can use the ModelingToolkit accessors (these provide
direct access to the corresponding internal fields of the `ReactionSystem`)
-* `ModelingToolkit.get_unknowns(rn)` is a vector that collects all the species
+- `ModelingToolkit.get_unknowns(rn)` is a vector that collects all the species
defined within `rn`, ordered by species and then non-species variables.
-* `Catalyst.get_species(rn)` is a vector of all the species variables in the system. The
+- `Catalyst.get_species(rn)` is a vector of all the species variables in the system. The
entries in `get_species(rn)` correspond to the first `length(get_species(rn))`
components in `get_unknowns(rn)`.
-* `ModelingToolkit.get_ps(rn)` is a vector that collects all the parameters
+- `ModelingToolkit.get_ps(rn)` is a vector that collects all the parameters
defined *within* reactions in `rn`.
-* `ModelingToolkit.get_eqs(rn)` is a vector that collects all the
+- `ModelingToolkit.get_eqs(rn)` is a vector that collects all the
[`Reaction`](@ref)s and `Symbolics.Equation` defined within `rn`, ordering all
`Reaction`s before `Equation`s.
-* `Catalyst.get_rxs(rn)` is a vector of all the [`Reaction`](@ref)s in `rn`, and
+- `Catalyst.get_rxs(rn)` is a vector of all the [`Reaction`](@ref)s in `rn`, and
corresponds to the first `length(get_rxs(rn))` entries in `get_eqs(rn)`.
-* `ModelingToolkit.get_iv(rn)` is the independent variable used in the system
+- `ModelingToolkit.get_iv(rn)` is the independent variable used in the system
(usually `t` to represent time).
-* `ModelingToolkit.get_systems(rn)` is a vector of all sub-systems of `rn`.
-* `ModelingToolkit.get_defaults(rn)` is a dictionary of all the default values
+- `ModelingToolkit.get_systems(rn)` is a vector of all sub-systems of `rn`.
+- `ModelingToolkit.get_defaults(rn)` is a dictionary of all the default values
for parameters and species in `rn`.
The preceding accessors do not allocate, directly accessing internal fields of
@@ -142,20 +149,20 @@ To retrieve information from the full reaction network represented by a system
`rn`, which corresponds to information within both `rn` and all sub-systems, one
can call:
-* `ModelingToolkit.unknowns(rn)` returns all species *and variables* across the
+- `ModelingToolkit.unknowns(rn)` returns all species *and variables* across the
system, *all sub-systems*, and all constraint systems. Species are ordered
before non-species variables in `unknowns(rn)`, with the first `numspecies(rn)`
entries in `unknowns(rn)` being the same as `species(rn)`.
-* [`species(rn)`](@ref) is a vector collecting all the chemical species within
+- [`species(rn)`](@ref) is a vector collecting all the chemical species within
the system and any sub-systems that are also `ReactionSystems`.
-* `ModelingToolkit.parameters(rn)` returns all parameters across the
+- `ModelingToolkit.parameters(rn)` returns all parameters across the
system, *all sub-systems*, and all constraint systems.
-* `ModelingToolkit.equations(rn)` returns all [`Reaction`](@ref)s and all
+- `ModelingToolkit.equations(rn)` returns all [`Reaction`](@ref)s and all
`Symbolics.Equations` defined across the system, *all sub-systems*, and all
constraint systems. `Reaction`s are ordered ahead of `Equation`s with the
first `numreactions(rn)` entries in `equations(rn)` being the same as
`reactions(rn)`.
-* [`reactions(rn)`](@ref) is a vector of all the `Reaction`s within the system
+- [`reactions(rn)`](@ref) is a vector of all the `Reaction`s within the system
and any sub-systems that are also `ReactionSystem`s.
These accessors will generally allocate new arrays to store their output unless
@@ -166,6 +173,7 @@ Below we list the remainder of the Catalyst API accessor functions mentioned
above.
## Basic system properties
+
See [Programmatic Construction of Symbolic Reaction Systems](@ref
programmatic_CRN_construction) for examples and [ModelingToolkit and Catalyst
Accessor Functions](@ref api_accessor_functions) for more details on the basic
@@ -187,8 +195,10 @@ isautonomous
```
## Coupled reaction/equation system properties
+
The following system property accessor functions are primarily relevant to reaction system [coupled
to differential and/or algebraic equations](@ref constraint_equations).
+
```@docs
ModelingToolkit.has_alg_equations
ModelingToolkit.alg_equations
@@ -197,7 +207,9 @@ ModelingToolkit.diff_equations
```
## Basic species properties
+
The following functions permits the querying of species properties.
+
```@docs
isspecies
Catalyst.isconstant
@@ -206,6 +218,7 @@ Catalyst.isvalidreactant
```
## Basic reaction properties
+
```@docs
ismassaction
dependents
@@ -217,7 +230,9 @@ reactionrates
```
## [Reaction metadata](@id api_rx_metadata)
+
The following functions permits the retrieval of [reaction metadata](@ref dsl_advanced_options_reaction_metadata).
+
```@docs
Catalyst.hasnoisescaling
Catalyst.getnoisescaling
@@ -228,6 +243,7 @@ Catalyst.getmisc
```
## [Functions to extend or modify a network](@id api_network_extension_and_modification)
+
`ReactionSystem`s can be programmatically extended using [`ModelingToolkit.extend`](@ref) and
[`ModelingToolkit.compose`](@ref).
@@ -239,6 +255,7 @@ Catalyst.flatten
```
## Network comparison
+
```@docs
==(rn1::Reaction, rn2::Reaction)
isequivalent
@@ -246,36 +263,45 @@ isequivalent
```
## [Network visualization](@id network_visualization)
+
[Latexify](https://korsbo.github.io/Latexify.jl/stable/) can be used to convert
networks to LaTeX equations by
+
```julia
using Latexify
latexify(rn)
```
+
An optional argument, `form` allows using `latexify` to display a reaction
network's ODE (as generated by the reaction rate equation) or SDE (as generated
by the chemical Langevin equation) form:
+
```julia
latexify(rn; form=:ode)
```
+
```julia
latexify(rn; form=:sde)
```
+
(As of writing this, an upstream bug causes the SDE form to be erroneously
displayed as the ODE form)
Finally, another optional argument (`expand_functions=true`) automatically expands functions defined by Catalyst (such as `mm`). To disable this, set `expand_functions=false`.
-Reaction networks can be plotted using the `GraphMakie` extension, which is loaded whenever all of `Catalyst`, `GraphMakie`, and `NetworkLayout` are loaded (note that a Makie backend, like `CairoMakie`, must be loaded as well). The two functions for plotting networks are `plot_network` and `plot_complexes`, which are two distinct representations.
+Reaction networks can be plotted using the `GraphMakie` extension, which is loaded whenever all of `Catalyst`, `GraphMakie`, and `NetworkLayout` are loaded (note that a Makie backend, like `CairoMakie`, must be loaded as well). The two functions for plotting networks are `plot_network` and `plot_complexes`, which are two distinct representations.
+
```@docs
plot_network(::ReactionSystem)
plot_complexes(::ReactionSystem)
```
## [Rate laws](@id api_rate_laws)
+
As the underlying [`ReactionSystem`](@ref) is comprised of `ModelingToolkit`
expressions, one can directly access the generated rate laws, and using
`ModelingToolkit` tooling generate functions or Julia `Expr`s from them.
+
```@docs
oderatelaw
jumpratelaw
@@ -287,6 +313,7 @@ hillar
```
## Transformations
+
```@docs
Base.convert
JumpInputs
@@ -295,7 +322,9 @@ set_default_noise_scaling
```
## Chemistry-related functionalities
+
Various functionalities primarily relevant to modelling of chemical systems (but potentially also in biology).
+
```@docs
@compound
@compounds
@@ -306,23 +335,28 @@ component_coefficients
```
## Unit validation
+
```@docs
validate(rx::Reaction; info::String = "")
validate(rs::ReactionSystem, info::String="")
```
## Utility functions
+
```@docs
symmap_to_varmap
```
## [Spatial modelling](@id api_lattice_simulations)
+
The first step of spatial modelling is to create a so-called `LatticeReactionSystem`:
+
```@docs
LatticeReactionSystem
```
The following functions can be used to querying the properties of `LatticeReactionSystem`s:
+
```@docs
reactionsystem
Catalyst.spatial_reactions
@@ -342,15 +376,18 @@ has_graph_lattice
grid_size
grid_dims
```
+
In addition, most accessor functions for normal `ReactionSystem`s (such as `species` and `parameters`) works when applied to `LatticeReactionSystem`s as well.
The following two helper functions can be used to create non-uniform parameter values.
+
```@docs
make_edge_p_values
make_directed_edge_values
```
The following functions can be used to access, or change, species or parameter values stored in problems, integrators, and solutions that are based on `LatticeReactionSystem`s.
+
```@docs
lat_getu
lat_setu!
@@ -360,6 +397,7 @@ rebuild_lat_internals!
```
Finally, we provide the following helper functions to plot and animate spatial lattice simulations.
+
```@docs
lattice_plot
lattice_animation
diff --git a/docs/src/api/network_analysis_api.md b/docs/src/api/network_analysis_api.md
index a4e3b21121..84ea584a2b 100644
--- a/docs/src/api/network_analysis_api.md
+++ b/docs/src/api/network_analysis_api.md
@@ -1,11 +1,12 @@
# [Network analysis and representations](@id api_network_analysis)
+
```@meta
CurrentModule = Catalyst
```
Note, currently API functions for network analysis and conservation law analysis
do not work with constant species (which are generated by SBML, and can be [declared
-in Catalyst as well](@ref dsl_advanced_options_constant_species).
+in Catalyst as well](@ref dsl_advanced_options_constant_species)).
For more information about these functions, please see the sections of the docs on
[network ODE representation](@ref network_analysis_odes) and [chemical reaction network theory](@ref network_analysis_structural_aspects).
diff --git a/docs/src/devdocs/dev_guide.md b/docs/src/devdocs/dev_guide.md
index e57937f8ba..7da574e257 100644
--- a/docs/src/devdocs/dev_guide.md
+++ b/docs/src/devdocs/dev_guide.md
@@ -1,6 +1,7 @@
# Catalyst Developer Documentation
## [Release Process](@id devdocs_releaseprocess)
+
Beginning with v15, Catalyst is using a new release process to try to ensure
continuing stability of releases. Before making a release one should
@@ -11,10 +12,10 @@ continuing stability of releases. Before making a release one should
- Do not cap the master branch as this can prevent upstream libraries from
properly testing against Catalyst, and hide breaking changes that impact
Catalyst.
-3. [Check docs build](@ref devdocs_advice_doc_inspection) with the capped dependencies.
+3. [Check docs build](@ref devdocs_advice_doc_inspection) with the capped dependencies.
Visually verify via checking the artifact in the doc build that the docs actually
look ok (since sometimes issues can arise that do not lead to actual errors in the doc CI).
-5. Release via the [registration
+4. Release via the [registration
issue](https://github.com/SciML/Catalyst.jl/issues/127) with the
command: `@JuliaRegistrator register branch=release-15.0.0`, modifying as appropriate
for the version you are releasing.
@@ -32,6 +33,7 @@ Catalyst release branch*.
## [Development advice](@id devdocs_advice)
### [Checking doc builds for errors](@id devdocs_advice_doc_error_checks)
+
When updating documentation, Catalyst will run any Julia code provided within example blocks to dynamically create figures and outputs. In addition to automatically creating these for us, it also provides an automatic check that all code in documentation is correct. Here, if any of the documentation code throws an error, the build job will fail. The documentation build job can be found at the bottom of a PRs conversation, here is an example of a failed one:

@@ -39,17 +41,21 @@ When updating documentation, Catalyst will run any Julia code provided within ex
To check what errors were produced, click on the "Details" link of the job. Next, any errors can be found at the bottom of the "Build and deploy" section (which should be opened automatically).
### [Inspecting the built documentation of a PR or branch](@id devdocs_advice_doc_inspection)
+
When updating documentation it is typically useful to view the updated documentation in HTML format (which is the format users will see). Here, some errors are much easier to spot in .html format as compared with the raw text files from which these are generated. There are two primary ways to view updated documentation, either by downloading them from the PR or by building the docs locally.
Whenever a PR to Catalyst is created, CI will create a corresponding documenter build job. If the build job passes, you can access the built documentation (which will be the new Catalyst documentation if the PR is merged) through the following steps:
-1. Click on "Details" in the documentation build job (at the bottom of the PR conversation tab).
+
+1. Click on "Details" in the documentation build job (at the bottom of the PR conversation tab).
2. Expand the "Upload site as artifact" section.
3. Click on the link at the end (which follows the "Artifact download URL: " text).
4. This will download a zip folder containing the documentation. Extract it to a location on your computer and then open the "index.html" file.
To build the Catalyst documentation locally:
+
1. Navigate to the ".julia/dev/Catalyst/docs/" folder, and run the "make.jl" file using ">julia --project=. make.jl". Alternatively, open a Julia session, activate the "docs" environment, and run the file using `include("make.jl").
2. Open the ".julia/dev/Catalyst/docs/build/index.html" file.
### [Spellchecking in your code](@id devdocs_advice_codespellchecker)
+
Especially when writing documentation, but also when writing normal code, it can be useful to have a spellchecker running through your texts. While code can be copied into a spellchecker and checked there (which is still useful to check grammar), it can also be very useful to (for users of VSCode) run the [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) extension. This will automatically provide simple spell-checking for code and documentation as you write it.
diff --git a/docs/src/faqs.md b/docs/src/faqs.md
index 723a50a34f..0caef2d70b 100644
--- a/docs/src/faqs.md
+++ b/docs/src/faqs.md
@@ -1,9 +1,11 @@
# FAQs
## How to index solution objects using symbolic variables and observables?
+
One can directly use symbolic variables to index into SciML solution objects.
Moreover, observables can also be evaluated in this way. For example,
consider the system
+
```@example faq1
using Catalyst, OrdinaryDiffEqTsit5, Plots
rn = @reaction_network ABtoC begin
@@ -11,68 +13,88 @@ rn = @reaction_network ABtoC begin
end
nothing # hide
```
+
Let's convert it to a system of ODEs, using the conservation laws of the system
to eliminate two of the species:
+
```@example faq1
osys = convert(ODESystem, rn; remove_conserved = true)
osys = complete(osys)
```
+
Notice the resulting ODE system has just one ODE, while algebraic observables
have been added for the two removed species (in terms of the conservation law
constants, `Γ[1]` and `Γ[2]`)
+
```@example faq1
observed(osys)
```
+
Let's solve the system and see how to index the solution using our symbolic
variables
+
```@example faq1
u0 = [osys.A => 1.0, osys.B => 2.0, osys.C => 0.0]
ps = [osys.k₊ => 1.0, osys.k₋ => 1.0]
oprob = ODEProblem(osys, u0, (0.0, 10.0), ps)
sol = solve(oprob, Tsit5())
```
+
Suppose we want to plot just species `C`, without having to know its integer
index in the unknown vector. We can do this using the symbolic variable `C`, which
we can get at in several ways
+
```@example faq1
sol[osys.C]
```
+
or
+
```@example faq1
@unpack C = osys
sol[C]
```
+
To evaluate `C` at specific times and plot it we can just do
+
```@example faq1
t = range(0.0, 10.0, length = 101)
plot(sol(t, idxs = C), label = "C(t)", xlabel = "t")
```
+
If we want to get multiple variables we can just do
+
```@example faq1
@unpack A, B = osys
sol(t, idxs = [A, B])
```
+
Plotting multiple variables using the SciML plot recipe can be achieved
like
+
```@example faq1
plot(sol; idxs = [A, B])
```
## [How to disable rescaling of reaction rates in rate laws?](@id faq_combinatoric_ratelaws)
+
As explained in the [Reaction rate laws used in simulations](@ref introduction_to_catalyst_ratelaws) section, for
a reaction such as `k, 2X --> 0`, the generated rate law will rescale the rate
constant, giving `k*X^2/2` instead of `k*X^2` for ODEs and `k*X*(X-1)/2` instead
of `k*X*(X-1)` for jumps. This can be disabled when directly `convert`ing a
[`ReactionSystem`](@ref). If `rn` is a generated [`ReactionSystem`](@ref), we can
do
+
```@example faq1
osys = convert(ODESystem, rn; combinatoric_ratelaws=false)
```
+
Disabling these rescalings should work for all conversions of `ReactionSystem`s
to other `ModelingToolkit.AbstractSystem`s.
-When creating a [`ReactionSystem`](@ref) using the DSL, combinatoric rate laws can be disabled (for
+When creating a [`ReactionSystem`](@ref) using the DSL, combinatoric rate laws can be disabled (for
the created system, and all systems derived from it) using the `@combinatoric_ratelaws` option (providing `false` as its only input):
+
```@example faq1
rn = @reaction_network begin
@combinatoric_ratelaws false
@@ -82,13 +104,16 @@ nothing # hide
```
## How to use non-integer stoichiometric coefficients?
+
```@example faq2
using Catalyst
rn = @reaction_network begin
k, 2.5*A --> 3*B
end
```
+
or directly via
+
```@example faq2
t = default_t()
@parameters k b
@@ -101,6 +126,7 @@ mixedsys = complete(mixedsys)
osys = convert(ODESystem, mixedsys; combinatoric_ratelaws = false)
osys = complete(osys)
```
+
Note, when using `convert(ODESystem, mixedsys; combinatoric_ratelaws=false)` the
`combinatoric_ratelaws=false` parameter must be passed. This is also true when
calling `ODEProblem(mixedsys,...; combinatoric_ratelaws=false)`. As described
@@ -110,13 +136,15 @@ simulations](@ref introduction_to_catalyst_ratelaws) section. Leaving this keywo
point stoichiometry will give an error message.
For a more extensive documentation of using non-integer stoichiometric
-coefficients, please see the [Symbolic Stochiometries](@ref
+coefficients, please see the [Symbolic Stoichiometries](@ref
parametric_stoichiometry) section.
## How to set default values for initial conditions and parameters?
+
How to set defaults when using the `@reaction_network` macro is described in
more detail [here](@ref dsl_advanced_options_default_vals). There are several ways to do
this. Using the DSL, one can use the `@species` and `@parameters` options:
+
```@example faq3
using Catalyst
sir = @reaction_network sir begin
@@ -131,6 +159,7 @@ show(stdout, MIME"text/plain"(), sir) # hide
When directly constructing a `ReactionSystem`, we can set the symbolic values to
have the desired default values, and this will automatically be propagated
through to the equation solvers:
+
```@example faq3
using Catalyst, Plots, OrdinaryDiffEqTsit5
t = default_t()
@@ -148,6 +177,7 @@ plot(sol)
One can also build a mapping from symbolic parameter/species to value/initial
condition and pass these to the `ReactionSystem` via the `defaults` keyword
argument:
+
```@example faq3
@parameters β ν
@species S(t) I(t) R(t)
@@ -160,6 +190,7 @@ nothing # hide
Finally, default values can also be added after creating the system via the
`setdefaults!` command and passing a `Symbol` based mapping, like
+
```@example faq3
sir = @reaction_network sir begin
β, S + I --> 2I
@@ -170,10 +201,12 @@ nothing # hide
```
## How to specify initial conditions and parameters values for `ODEProblem` and other problem types?
+
To explicitly pass initial conditions and parameters we can use mappings from
Julia `Symbol`s corresponding to each variable/parameter to their values, or
from ModelingToolkit symbolic variables/parameters to their values. Using
`Symbol`s we have
+
```@example faq4
using Catalyst, OrdinaryDiffEqTsit5
rn = @reaction_network begin
@@ -185,7 +218,9 @@ p = (:α => 1e-4, :β => .01)
op1 = ODEProblem(rn, u0, (0.0, 250.0), p)
nothing # hide
```
+
while using ModelingToolkit symbolic variables we have
+
```@example faq4
t = default_t()
u0 = [rn.S => 999.0, rn.I => 1.0, rn.R => 0.0]
@@ -195,11 +230,13 @@ nothing # hide
```
## How to include non-reaction terms in equations for a chemical species?
+
One method to add non-reaction terms into an ODE or algebraic equation for a
chemical species is to add a new (non-species) unknown variable that represents
those terms, let it be the rate of zero order reaction, and add a constraint
equation. I.e., to add a force of `(1 + sin(t))` to ``dA/dt`` in a system with
the reaction `k, A --> 0`, we can do
+
```@example faq5
using Catalyst
t = default_t()
@@ -211,19 +248,23 @@ eq = f ~ (1 + sin(t))
rs = complete(rs)
osys = convert(ODESystem, rs)
```
+
In the final ODE model, `f` can be eliminated by using
`ModelingToolkit.structural_simplify`
+
```@example faq5
osyss = structural_simplify(osys)
full_equations(osyss)
```
## How to modify generated ODEs?
+
Conversion to other `ModelingToolkit.AbstractSystem`s allows the possibility to
modify the system with further terms that are difficult to encode as a chemical
reaction or a constraint equation. For example, an alternative method to the
previous question for adding a forcing function, $1 + \sin(t)$, to the ODE for
`dA/dt` is
+
```@example faq6
using Catalyst
rn = @reaction_network begin
@@ -239,8 +280,10 @@ dAdteq = Equation(dAdteq.lhs, dAdteq.rhs + 1 + sin(t))
```
## How to override mass action kinetics rate laws?
+
While generally one wants the reaction rate law to use the law of mass action,
so the reaction
+
```@example faq7
using Catalyst
rn = @reaction_network begin
@@ -248,30 +291,37 @@ rn = @reaction_network begin
end
convert(ODESystem, rn)
```
+
occurs at the (ODE) rate ``d[X]/dt = -k[X]``, it is possible to override this by
using any of the following non-filled arrows when declaring the reaction: `<=`,
`⇐`, `⟽`, `=>`, `⇒`, `⟾`, `⇔`, `⟺`. This means that the reaction
+
```@example faq7
rn = @reaction_network begin
k, X => ∅
end
convert(ODESystem, rn)
```
+
will occur at rate ``d[X]/dt = -k`` (which might become a problem since ``[X]``
will be degraded at a constant rate even when very small or equal to 0).
Note, stoichiometric coefficients are still included, i.e. the reaction
+
```@example faq7
rn = @reaction_network begin
k, 2*X ⇒ ∅
end
convert(ODESystem, rn)
```
+
has rate ``d[X]/dt = -2 k``.
## [How to specify user-defined functions as reaction rates?](@id user_functions)
+
The reaction network DSL can "see" user-defined functions that work with
ModelingToolkit. e.g., this is should work
+
```@example faq8
using Catalyst
myHill(x) = 2*x^3/(x^3+1.5^3)
@@ -279,19 +329,24 @@ rn = @reaction_network begin
myHill(X), ∅ --> X
end
```
+
In some cases, it may be necessary or desirable to register functions with
Symbolics.jl before their use in Catalyst, see the discussion
[here](https://symbolics.juliasymbolics.org/stable/manual/functions/).
## [How does the Catalyst DSL (`@reaction_network`) infer what different symbols represent?](@id faq_dsl_sym_inference)
+
When declaring a model using the Catalyst DSL, e.g.
+
```@example faq_dsl_inference
using Catalyst
rn = @reaction_network begin
(p,d), 0 <--> X
end
```
+
Catalyst can automatically infer that `X` is a species and `p` and `d` are parameters. In total, Catalyst can infer the following quantities:
+
- Species (from reaction reactants).
- Parameters (from reaction rates and stoichiometries).
- (non-species) Variables (from the `@equations` option).
@@ -300,12 +355,14 @@ Catalyst can automatically infer that `X` is a species and `p` and `d` are param
- Compound species (from the [`@compounds` option](@ref chemistry_functionality_compounds_DSL)).
Inference of species, variables, and parameters follows the following steps:
+
1. Every symbol [explicitly declared](@ref dsl_advanced_options_declaring_species_and_parameters) using the `@species`, `@variables`, and `@parameters` options are assigned to the corresponding category.
2. Every symbol not declared in (1) that occurs as a reaction reactant is inferred as a species.
3. Every symbol not declared in (1) or (2) that occurs in an expression provided after `@equations` is inferred as a variable.
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.
-Here, in
+Here, in
+
```@example faq_dsl_inference
using Catalyst
rn = @reaction_network begin
@@ -314,6 +371,7 @@ rn = @reaction_network begin
X + V + p1 + p2, 0 --> X
end
```
+
`p` is first set as a parameter (as it is explicitly declared as such). Next, `X` is inferred as a species. Next, `V` is inferred as a variable. Finally, `p2` is inferred as a parameter.
Next, if any expression `D(...)` (where `...` can be anything) is encountered within the `@equations` option, `D` is inferred to be the differential with respect to the default independent variable (typically `t`). Note that using `D` in this way, while also using it in another form (e.g. in a reaction rate) will produce an error.
@@ -323,19 +381,22 @@ Any symbol used as the left-hand side within the `@observables` option is inferr
Any symbol declared as a compound using the `@compound` option is automatically inferred to be a system species.
Symbols occurring within other expressions will not be inferred as anything. These must either occur in one of the forms described above (which enables Catalyst to infer what they are) or be explicitly declared. E.g. having a parameter which only occurs in an event:
+
```julia
using Catalyst
rn_error = @reaction_network begin
- @discrete_events 1.0 => [X ~ X + Xadd]
+ @discrete_events 1.0 => [X ~ X + Xadd]
d, X --> 0
end
```
+
is not permitted. E.g. here `Xadd` must be explicitly declared as a parameter using `@parameters`:
+
```@example faq_dsl_inference
using Catalyst
rn = @reaction_network begin
@parameters Xadd
- @discrete_events 1.0 => [X ~ X + Xadd]
+ @discrete_events 1.0 => [X ~ X + Xadd]
d, X --> 0
end
```
@@ -343,6 +404,7 @@ end
It is possible to turn off all inference (requiring all symbols to be declared using `@parameters`, `@species`, and `@variables`) through the [`@require_declaration` option](@ref faq_require_declaration).
## [How can I turn off automatic inferring of species and parameters when using the DSL?](@id faq_require_declaration)
+
This option can be set using the `@require_declaration` option inside `@reaction_network`. In this case all the species, parameters, and variables in the system must be pre-declared using one of the `@species`, `@parameters`, or `@variables` macros. For more information about what is inferred automatically and not, please see the section on [`@require_declaration`](@ref dsl_advanced_options_require_dec).
```@example faq9
@@ -358,15 +420,18 @@ end
## [What to be aware of when using `remake` with conservation law elimination and NonlinearProblems?](@id faq_remake_nonlinprob)
When constructing `NonlinearSystem`s or `NonlinearProblem`s with `remove_conserved = true`, i.e.
+
```julia
# for rn a ReactionSystem
nsys = convert(NonlinearSystem, rn; remove_conserved = true)
-# or
+# or
nprob = NonlinearProblem(rn, u0, p; remove_conserved = true)
```
+
`remake` is currently unable to correctly update all `u0` values when the
conserved constant(s), `Γ`, are updated. As an example consider the following
+
```@example faq_remake
using Catalyst, NonlinearSolve
rn = @reaction_network begin
@@ -379,66 +444,92 @@ nlsys = convert(NonlinearSystem, rn; remove_conserved = true, conseqs_remake_war
nlsys = complete(nlsys)
equations(nlsys)
```
+
If we generate a `NonlinearProblem` from this system the conservation constant,
`Γ[1]`, is automatically set to `X₁ + X₂ + X₃ = 6` and the initial values are
those in `u0`. i.e if
+
```@example faq_remake
nlprob1 = NonlinearProblem(nlsys, u0, ps)
```
+
then
+
```@example faq_remake
nlprob1[(:X₁, :X₂, :X₃)] == (1.0, 2.0, 3.0)
```
+
and
+
```@example faq_remake
nlprob1.ps[:Γ][1] == 6.0
```
+
If we now try to change a value of `X₁`, `X₂`, or `X₃` using `remake`, the
conserved constant will be recalculated. i.e. if
+
```@example faq_remake
nlprob2 = remake(nlprob1; u0 = [:X₂ => 3.0])
```
-compare
+
+compare
+
```@example faq_remake
-println("Correct u0 is: ", (1.0, 3.0, 3.0), "\n", "remade value is: ", nlprob2[(:X₁, :X₂, :X₃)])
+println("Correct u0 is: ", (1.0, 3.0, 3.0), "\n", "remade value is: ", nlprob2[(:X₁, :X₂, :X₃)])
```
+
and
+
```@example faq_remake
println("Correct Γ is: ", 7.0, "\n", "remade value is: ", nlprob2.ps[:Γ][1])
```
+
However, if we try to directly change the value of `Γ` it is not always the case
that a `u0` value will correctly update so that the conservation law is
conserved. Consider
+
```@example faq_remake
nlprob3 = remake(nlprob1; u0 = [:X₂ => nothing], p = [:Γ => [4.0]])
```
+
Setting `[:X₂ => nothing]` for other problem types communicates that the
`u0` value for `X₂` should be solved for. However, if we examine the values we
find
+
```@example faq_remake
-println("Correct u0 is: ", (1.0, 0.0, 3.0), "\n", "remade value is: ", nlprob3[(:X₁, :X₂, :X₃)])
+println("Correct u0 is: ", (1.0, 0.0, 3.0), "\n", "remade value is: ", nlprob3[(:X₁, :X₂, :X₃)])
```
+
and
+
```@example faq_remake
println("Correct Γ is: ", 4.0, "\n", "remade value is: ", nlprob3.ps[:Γ][1])
```
+
As such, the `u0` value for `X₂` has not updated, and the conservation law is
now violated by the `u0` values, i.e,
+
```@example faq_remake
-(nlprob3[:X₁] + nlprob3[:X₂] + nlprob3[:X₃]) == nlprob3.ps[:Γ][1]
+(nlprob3[:X₁] + nlprob3[:X₂] + nlprob3[:X₃]) == nlprob3.ps[:Γ][1]
```
+
Currently, the only way to avoid this issue is to manually specify updated
values for the `u0` components, which will ensure that `Γ` updates appropriately
as in the first example. i.e. we manually set `X₂` to the value it should be and
`Γ` will be updated accordingly:
+
```@example faq_remake
nlprob4 = remake(nlprob1; u0 = [:X₂ => 0.0])
```
+
so that
+
```@example faq_remake
-println("Correct u0 is: ", (1.0, 0.0, 3.0), "\n", "remade value is: ", nlprob4[(:X₁, :X₂, :X₃)])
+println("Correct u0 is: ", (1.0, 0.0, 3.0), "\n", "remade value is: ", nlprob4[(:X₁, :X₂, :X₃)])
```
+
and
+
```@example faq_remake
println("Correct Γ is: ", 4.0, "\n", "remade value is: ", nlprob4.ps[:Γ][1])
```
@@ -447,39 +538,51 @@ Finally, we note there is one extra consideration to take into account if using
`structural_simplify`. In this case one of `X₁`, `X₂`, or `X₃` will be moved to
being an observed. It will then always correspond to the updated value if one
tries to manually change `Γ`. Let's see what happens here directly
+
```@example faq_remake
nlsys = convert(NonlinearSystem, rn; remove_conserved = true, conseqs_remake_warn = false)
nlsys = structural_simplify(nlsys)
nlprob1 = NonlinearProblem(nlsys, u0, ps)
```
+
We can now try to change just `Γ` and implicitly the observed variable that was
removed will be assumed to have changed its initial value to compensate for it.
Let's confirm this. First we find the observed variable that was eliminated.
+
```@example faq_remake
obs_unknown = only(observed(nlsys)).lhs
```
+
We can figure out its index in `u0` via
+
```@example faq_remake
obs_symbol = ModelingToolkit.getname(obs_unknown)
obsidx = findfirst(p -> p[1] == obs_symbol, u0)
```
-Let's now remake
+
+Let's now remake
+
```@example faq_remake
nlprob2 = remake(nlprob1; u0 = [obs_unknown => nothing], p = [:Γ => [8.0]])
```
+
Here we indicate that the observed variable should be treated as unspecified
during initialization. Since the observed variable is not considered an unknown,
everything now works, with the observed variable's assumed initial value
adjusted to allow `Γ = 8`:
+
```@example faq_remake
correct_u0 = last.(u0)
correct_u0[obsidx] = 8 - sum(correct_u0) + correct_u0[obsidx]
-println("Correct u0 is: ", (1.0, 2.0, 5.0), "\n", "remade value is: ", nlprob2[(:X₁, :X₂, :X₃)])
+println("Correct u0 is: ", (1.0, 2.0, 5.0), "\n", "remade value is: ", nlprob2[(:X₁, :X₂, :X₃)])
```
+
and `Γ` becomes
+
```@example faq_remake
println("Correct Γ is: ", 8.0, "\n", "remade value is: ", nlprob2.ps[:Γ][1])
```
+
Unfortunately, as with our first example, trying to enforce that a
non-eliminated species should have its initial value updated instead of the
observed species will not work.
@@ -487,4 +590,4 @@ observed species will not work.
*Summary:* it is not recommended to directly update `Γ` via `remake`, but to
instead update values of the initial guesses in `u0` to obtain a desired `Γ`. At
this time the behavior when updating `Γ` can result in `u0` values that do not
-satisfy the conservation law defined by `Γ` as illustrated above.
\ No newline at end of file
+satisfy the conservation law defined by `Γ` as illustrated above.
diff --git a/docs/src/index.md b/docs/src/index.md
index f7ed725b99..936c2854d5 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -18,6 +18,7 @@ etc).
## [Features](@id doc_index_features)
#### [Features of Catalyst](@id doc_index_features_catalyst)
+
- [The Catalyst DSL](@ref dsl_description) provides a simple and readable format for manually specifying reaction network models using chemical reaction notation.
- Catalyst `ReactionSystem`s provides a symbolic representation of reaction networks, built on [ModelingToolkit.jl](https://docs.sciml.ai/ModelingToolkit/stable/) and [Symbolics.jl](https://docs.sciml.ai/Symbolics/stable/).
- The [Catalyst.jl API](@ref api) provides functionality for building networks programmatically and for composing multiple networks together.
@@ -32,12 +33,13 @@ etc).
- [Steady states](@ref homotopy_continuation) (and their [stabilities](@ref steady_state_stability)) can be computed for model ODE representations.
#### [Features of Catalyst composing with other packages](@id doc_index_features_composed)
+
- [OrdinaryDiffEq.jl](https://github.com/SciML/OrdinaryDiffEq.jl) Can be used to numerically solve generated reaction rate equation ODE models.
- [StochasticDiffEq.jl](https://github.com/SciML/StochasticDiffEq.jl) can be used to numerically solve generated Chemical Langevin Equation SDE models.
- [JumpProcesses.jl](https://github.com/SciML/JumpProcesses.jl) can be used to numerically sample generated Stochastic Chemical Kinetics Jump Process models.
- Support for [parallelization of all simulations](@ref ode_simulation_performance_parallelisation), including parallelization of [ODE](@ref ode_simulation_performance_parallelisation_GPU) and [SDE](@ref sde_simulation_performance_parallelisation_GPU) simulations on GPUs using [DiffEqGPU.jl](https://github.com/SciML/DiffEqGPU.jl).
- [Latexify](https://korsbo.github.io/Latexify.jl/stable/) can be used to [generate LaTeX expressions](@ref visualisation_latex) corresponding to generated mathematical models or the underlying set of reactions.
-- [GraphMakie](https://docs.makie.org/stable/) recipes are provided that can be used to generate and [visualize reaction network graphs](@ref visualisation_graphs)
+- [GraphMakie](https://docs.makie.org/stable/) recipes are provided that can be used to generate and [visualize reaction network graphs](@ref visualisation_graphs)
- Model steady states can be [computed through homotopy continuation](@ref homotopy_continuation) using [HomotopyContinuation.jl](https://github.com/JuliaHomotopyContinuation/HomotopyContinuation.jl) (which can find *all* steady states of systems with multiple ones), by [forward ODE simulations](@ref steady_state_solving_simulation) using [SteadyStateDiffEq.jl](https://github.com/SciML/SteadyStateDiffEq.jl), or by [numerically solving steady-state nonlinear equations](@ref steady_state_solving_nonlinear) using [NonlinearSolve.jl](https://github.com/SciML/NonlinearSolve.jl).
- [BifurcationKit.jl](https://github.com/bifurcationkit/BifurcationKit.jl) can be used to compute bifurcation diagrams of model steady states (including finding periodic orbits).
- [DynamicalSystems.jl](https://github.com/JuliaDynamics/DynamicalSystems.jl) can be used to compute model [basins of attraction](@ref dynamical_systems_basins_of_attraction), [Lyapunov spectrums](@ref dynamical_systems_lyapunov_exponents), and other dynamical system properties.
@@ -47,12 +49,14 @@ etc).
- [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) can be used to perform structural identifiability analysis.
#### [Features of packages built upon Catalyst](@id doc_index_features_other_packages)
+
- Catalyst [`ReactionSystem`](@ref)s can be [imported from SBML files](@ref model_file_import_export_sbml) via [SBMLImporter.jl](https://github.com/sebapersson/SBMLImporter.jl) and [SBMLToolkit.jl](https://github.com/SciML/SBMLToolkit.jl), and [from BioNetGen .net files](@ref model_file_import_export_sbml_rni_net) and various stoichiometric matrix network representations using [ReactionNetworkImporters.jl](https://github.com/SciML/ReactionNetworkImporters.jl).
- [MomentClosure.jl](https://github.com/augustinas1/MomentClosure.jl) allows generation of symbolic ModelingToolkit `ODESystem`s that represent moment closure approximations to moments of the Chemical Master Equation, from reaction networks defined in Catalyst.
- [FiniteStateProjection.jl](https://github.com/kaandocal/FiniteStateProjection.jl) allows the construction and numerical solution of Chemical Master Equation models from reaction networks defined in Catalyst.
- [DelaySSAToolkit.jl](https://github.com/palmtree2013/DelaySSAToolkit.jl) can augment Catalyst reaction network models with delays, and can simulate the resulting stochastic chemical kinetics with delays models.
## [How to read this documentation](@id doc_index_documentation)
+
The Catalyst documentation is separated into sections describing Catalyst's various features. Where appropriate, some sections will also give advice on best practices for various modeling workflows, and provide links with further reading. Each section also contains a set of relevant example workflows. Finally, the [API](@ref api) section contains a list of all functions exported by Catalyst (as well as descriptions of them and their inputs and outputs).
New users are recommended to start with either the [Introduction to Catalyst and Julia for New Julia users](@ref catalyst_for_new_julia_users) or [Introduction to Catalyst](@ref introduction_to_catalyst) sections (depending on whether they are familiar with Julia programming or not). This should be enough to carry out many basic Catalyst workflows.
@@ -60,22 +64,29 @@ New users are recommended to start with either the [Introduction to Catalyst and
This documentation contains code which is dynamically run whenever it is built. If you copy the code and run it in your Julia environment it should work. The exact Julia environment that is used in this documentation can be found [here](@ref doc_index_reproducibility).
For most code blocks in this documentation, the output of the last line of code is printed at the of the block, e.g.
+
```@example home_display
1 + 2
```
+
and
+
```@example home_display
using Catalyst # hide
@reaction_network begin
(p,d), 0 <--> X
end
```
+
However, in some situations (e.g. when output is extensive, or irrelevant to what is currently being described) we have disabled this, e.g. like here:
+
```@example home_display
1 + 2
nothing # hide
```
+
and here:
+
```@example home_display
@reaction_network begin
(p,d), 0 <--> X
@@ -84,45 +95,54 @@ nothing # hide
```
## [Installation](@id doc_index_installation)
+
Catalyst is an officially registered Julia package, which can be installed through the Julia package manager:
+
```julia
using Pkg
Pkg.add("Catalyst")
```
Many Catalyst features require the installation of additional packages. E.g. for ODE-solving and simulation plotting
+
```julia
Pkg.add("OrdinaryDiffEqDefault")
Pkg.add("Plots")
```
+
is also needed.
It is **strongly** recommended to install and use Catalyst in its own environment with the
minimal set of needed packages. For example, to install Catalyst and Plots in a
new environment named `catalyst_project` (saved in the current directory) one
can say
+
```julia
Pkg.activate("catalyst_project")
Pkg.add("Catalyst")
Pkg.add("Plots")
```
+
If one would rather just create a temporary environment that is not saved when
exiting Julia you can say
+
```julia
Pkg.activate(; temp = true)
Pkg.add("Catalyst")
Pkg.add("Plots")
```
-After installation, we suggest running
+After installation, we suggest running
+
```julia
Pkg.status("Catalyst")
```
-to confirm that the latest version of Catalyst was installed (and not an older version).
-If you have installed into a new environment this should always be the case. However, if you
+
+to confirm that the latest version of Catalyst was installed (and not an older version).
+If you have installed into a new environment this should always be the case. However, if you
installed into an existing environment, such as the default Julia global environment, the presence
-of incompatible versions of other pre-installed packages could lead to older versions of Catalyst
-being installed. In this case we again recommend creating a new environment and installing Catalyst
+of incompatible versions of other pre-installed packages could lead to older versions of Catalyst
+being installed. In this case we again recommend creating a new environment and installing Catalyst
there to obtain the latest version.
A more thorough guide for setting up Catalyst and installing Julia packages can be found [here](@ref catalyst_for_new_julia_users_packages).
@@ -130,6 +150,7 @@ A more thorough guide for setting up Catalyst and installing Julia packages can
## [Illustrative example](@id doc_index_example)
#### [Deterministic ODE simulation of Michaelis-Menten enzyme kinetics](@id doc_index_example_ode)
+
Here we show a simple example where a model is created using the Catalyst DSL, and then simulated as
an ordinary differential equation.
@@ -156,7 +177,9 @@ plot(sol; lw = 5)
```
#### [Stochastic jump simulations](@id doc_index_example_jump)
+
The same model can be used as input to other types of simulations. E.g. here we instead generate and simulate a stochastic chemical kinetics jump process model.
+
```@example home_simple_example
# Create and simulate a jump process (here using Gillespie's direct algorithm).
# The initial conditions are now integers as we track exact populations for each species.
@@ -170,14 +193,16 @@ plot(jump_sol; lw = 2)
```
## [More elaborate example](@id doc_index_elaborate_example)
+
In the above example, we used basic Catalyst workflows to simulate a simple
model. Here we instead show how various Catalyst features can compose to create
a much more advanced model. Our model describes how the volume of a cell ($V$)
is affected by a growth factor ($G$). The growth factor only promotes growth
while in its phosphorylated form ($G^P$). The phosphorylation of $G$ ($G \to G^P$)
is promoted by sunlight, which is modeled as the cyclic sinusoid $k_a (\sin(t) + 1)$.
-When the cell reaches a critical volume ($V_m$) it undergoes cell division. First, we
+When the cell reaches a critical volume ($V_m$) it undergoes cell division. First, we
declare our model:
+
```@example home_elaborate_example
using Catalyst
cell_model = @reaction_network begin
@@ -192,14 +217,18 @@ cell_model = @reaction_network begin
kᵢ/V, Gᴾ --> G
end
```
+
We now study the system as a Chemical Langevin Dynamics SDE model, which can be generated as follows
+
```@example home_elaborate_example
u0 = [:V => 25.0, :G => 50.0, :Gᴾ => 0.0]
tspan = (0.0, 20.0)
ps = [:Vₘ => 50.0, :g => 0.3, :kₚ => 100.0, :kᵢ => 60.0]
sprob = SDEProblem(cell_model, u0, tspan, ps)
```
+
This problem encodes the following stochastic differential equation model:
+
```math
\begin{align*}
dG(t) &= - \left( \frac{k_p(\sin(t)+1)}{V(t)} G(t) + \frac{k_i}{V(t)} G^P(t) \right) dt - \sqrt{\frac{k_p (\sin(t)+1)}{V(t)} G(t)} \, dW_1(t) + \sqrt{\frac{k_i}{V(t)} G^P(t)} \, dW_2(t) \\
@@ -207,7 +236,9 @@ dG^P(t) &= \left( \frac{k_p(\sin(t)+1)}{V(t)} G(t) - \frac{k_i}{V(t)} G^P(t) \ri
dV(t) &= \left(g \, G^P(t)\right) dt
\end{align*}
```
+
where the $dW_1(t)$ and $dW_2(t)$ terms represent independent Brownian Motions, encoding the noise added by the Chemical Langevin Equation. Finally, we can simulate and plot the results.
+
```@example home_elaborate_example
using StochasticDiffEq, Plots
sol = solve(sprob, EM(); dt = 0.05)
@@ -216,49 +247,58 @@ plot(sol; xguide = "Time (au)", lw = 2)
```
## [Getting Help](@id doc_index_help)
+
Catalyst developers are active on the [Julia Discourse](https://discourse.julialang.org/) and
the [Julia Slack](https://julialang.slack.com) channels \#sciml-bridged and \#sciml-sysbio.
For bugs or feature requests, [open an issue](https://github.com/SciML/Catalyst.jl/issues).
## [Supporting and Citing Catalyst.jl](@id doc_index_citation)
+
The software in this ecosystem was developed as part of academic research. If you would like to help
support it, please star the repository as such metrics may help us secure funding in the future. If
you use Catalyst as part of your research, teaching, or other activities, we would be grateful if you
could cite our work:
-```
+
+```bibtex
@article{CatalystPLOSCompBio2023,
- doi = {10.1371/journal.pcbi.1011530},
- author = {Loman, Torkel E. AND Ma, Yingbo AND Ilin, Vasily AND Gowda, Shashi AND Korsbo, Niklas AND Yewale, Nikhil AND Rackauckas, Chris AND Isaacson, Samuel A.},
- journal = {PLOS Computational Biology},
- publisher = {Public Library of Science},
- title = {Catalyst: Fast and flexible modeling of reaction networks},
- year = {2023},
- month = {10},
- volume = {19},
- url = {https://doi.org/10.1371/journal.pcbi.1011530},
- pages = {1-19},
- number = {10},
+ doi = {10.1371/journal.pcbi.1011530},
+ author = {Loman, Torkel E. AND Ma, Yingbo AND Ilin, Vasily AND Gowda, Shashi AND Korsbo, Niklas AND Yewale, Nikhil AND Rackauckas, Chris AND Isaacson, Samuel A.},
+ journal = {PLOS Computational Biology},
+ publisher = {Public Library of Science},
+ title = {Catalyst: Fast and flexible modeling of reaction networks},
+ year = {2023},
+ month = {10},
+ volume = {19},
+ url = {https://doi.org/10.1371/journal.pcbi.1011530},
+ pages = {1-19},
+ number = {10},
}
```
## [Reproducibility](@id doc_index_reproducibility)
+
```@raw html
The documentation of this SciML package was built using these direct dependencies,
```
+
```@example
using Pkg # hide
Pkg.status() # hide
```
+
```@raw html
```
+
```@raw html
and using this machine and Julia version.
```
+
```@example
using InteractiveUtils # hide
versioninfo() # hide
```
+
```@raw html
```
diff --git a/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md b/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md
index d7a2493109..a816654715 100644
--- a/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md
+++ b/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md
@@ -1,4 +1,5 @@
# [Introduction to Catalyst and Julia for New Julia users](@id catalyst_for_new_julia_users)
+
The Catalyst tool for the modelling of chemical reaction networks is based in the Julia programming language[^1][^2]. While experience in Julia programming is advantageous for using Catalyst, it is not necessary for accessing most of its basic features. This tutorial serves as an introduction to Catalyst for those unfamiliar with Julia, while also introducing some basic Julia concepts. Anyone who plans on using Catalyst extensively is recommended to familiarise oneself more thoroughly with the Julia programming language. A collection of resources for learning Julia can be found [here](https://julialang.org/learning/), and a full documentation is available [here](https://docs.julialang.org/en/v1/). A more practical (but also extensive) guide to Julia programming can be found [here](https://modernjuliaworkflows.github.io/writing/).
Julia can be downloaded [here](https://julialang.org/downloads/). Generally, it is recommended to use the [*juliaup*](https://github.com/JuliaLang/juliaup) tool to install and update Julia. Furthermore, *Visual Studio Code* is a good IDE with [extensive Julia support](https://code.visualstudio.com/docs/languages/julia), and a good default choice.
@@ -6,9 +7,11 @@ Julia can be downloaded [here](https://julialang.org/downloads/). Generally, it
*Users who are already familiar with Julia can skip to the [Introduction to Catalyst](@ref introduction_to_catalyst) tutorial.*
## Basic Julia usage
+
On the surface, Julia has many similarities to languages like MATLAB, Python, and R.
*Values* can be assigned to *variables* through `=` sign. Values (possibly stored in variables) can be used for most basic computations.
+
```@example ex1
length = 2.0
width = 4.0
@@ -16,61 +19,80 @@ area = length * width
```
*Functions* take one or more inputs (enclosed by `()`) and return some output. E.g. the `min` function returns the minimum of two values.
+
```@example ex1
min(1.0, 3.0)
```
+
Julia has a specific *help mode*, which can be [queried for information about any function](https://docs.julialang.org/en/v1/stdlib/REPL/#Help-mode) (including those defined by Catalyst).
Each Julia variable has a specific *type*, designating what type of value it contains. While not directly required to use Catalyst, this is useful to be aware of. To learn the type of a specific variable, use the `typeof` function. More information about types can be [found here](https://docs.julialang.org/en/v1/manual/types/).
+
```@example ex1
typeof(1.0)
```
+
Here, `Float64` denotes decimal-valued numbers. Integer-valued numbers instead have the `Int64` type.
+
```@example ex1
typeof(1)
```
+
There exists a large number of Julia types (with even more being defined by various packages). Additional examples include `String`s (defined by enclosing text within `" "`):
+
```@example ex1
"Hello world!"
```
+
and `Symbol`s (defined by pre-appending an expression with `:`):
+
```@example ex1
:Julia
```
Finally, we note that the first time some code is run in Julia, it has to be [*compiled*](https://en.wikipedia.org/wiki/Just-in-time_compilation). However, this is only required once per Julia session. Hence, the second time the same code is run, it runs much faster. E.g. try running this line of code first one time, and then one additional time. You will note that the second run is much faster.
+
```@example ex1
rand(100, 100)^3.5
nothing # hide
```
-(This code creates a random 100x100 matrix, and takes it to the power of 3.5)
-This is useful to know when you e.g. declare, simulate, or plot, a Catalyst model. The first time you run a command there might be a slight delay. However, subsequent runs will be much quicker. This holds even if you make minor adjustments before the second run (such as changing simulation initial conditions).
+(This code creates a random 100x100 matrix, and takes it to the power of 3.5.)
+
+This is useful to know when you e.g. declare, simulate, or plot a Catalyst model. The first time you run a command there might be a slight delay. However, subsequent runs will be much quicker. This holds even if you make minor adjustments before the second run (such as changing simulation initial conditions).
## [Installing and activating packages](@id catalyst_for_new_julia_users_packages_intro)
+
Due to its native package manager (Pkg), and a registry of almost all packages of relevancy, package management in Julia is unusually easy. Here, we will briefly describe how to install and activate Catalyst (and two additional packages relevant to this tutorial).
To import a Julia package into a session, you can use the `using PackageName` command (where `PackageName` is the name of the package you wish to import). However, before you can do so, it must first be installed on your computer. This is done through the `Pkg.add("PackageName")` command:
+
```julia
using Pkg
Pkg.add("Catalyst")
```
-Here, the Julia package manager package (`Pkg`) is by default installed on your computer when Julia is installed, and can be activated directly. Next, we install an ODE solver from a sub-library of the larger `OrdinaryDiffEq` package, and install the `Plots` package for making graphs. We will import the recommended default solver from the `OrdinaryDiffEqDefault` sub-library. A full list of `OrdinaryDiffEq` solver sublibraries can be found on the sidebar of [this page](https://docs.sciml.ai/OrdinaryDiffEq/stable/).
+
+Here, the Julia package manager package (`Pkg`) is by default installed on your computer when Julia is installed, and can be activated directly. Next, we install an ODE solver from a sub-library of the larger `OrdinaryDiffEq` package, and install the `Plots` package for making graphs. We will import the recommended default solver from the `OrdinaryDiffEqDefault` sub-library. A full list of `OrdinaryDiffEq` solver sub-libraries can be found on the sidebar of [this page](https://docs.sciml.ai/OrdinaryDiffEq/stable/).
+
```julia
Pkg.add("OrdinaryDiffEqDefault")
Pkg.add("Plots")
```
+
Once a package has been installed through the `Pkg.add` command, this command does not have to be repeated if we restart our Julia session. We can now import all three packages into our current session with:
+
```@example ex2
using Catalyst
using OrdinaryDiffEqDefault
using Plots
```
+
Here, if we restart Julia, these `using` commands *must be rerun*.
A more comprehensive (but still short) introduction to package management in Julia is provided at [the end of this documentation page](@ref catalyst_for_new_julia_users_packages). It contains some useful information and is hence highly recommended reading. For a more detailed introduction to Julia package management, please read [the Pkg documentation](https://docs.julialang.org/en/v1/stdlib/Pkg/).
## Simulating a basic Catalyst model
+
Now that we have some basic familiarity with Julia, and have installed and imported the required packages, we will create and simulate a basic chemical reaction network model using Catalyst.
Catalyst models are created through the `@reaction_network` *macro*. For more information on macros, please read [the Julia documentation on macros](https://docs.julialang.org/en/v1/manual/metaprogramming/#man-macros). This documentation is, however, rather advanced (and not required to use Catalyst). We instead recommend that you simply familiarise yourself with the Catalyst syntax, without studying in detail how macros work and what they are.
@@ -78,47 +100,56 @@ Catalyst models are created through the `@reaction_network` *macro*. For more in
The `@reaction_network` command is followed by the `begin` keyword, which is followed by one line for each *reaction* of the model. Each reaction consists of a *reaction rate*, followed by the reaction itself. The reaction contains a set of *substrates* and a set of *products* (what is consumed and produced by the reaction, respectively). These are separated by a `-->` arrow. Finally, the model ends with the `end` keyword.
Here, we create a simple [*birth-death* model](@ref basic_CRN_library_bd), where a single species ($X$) is created at rate $b$, and degraded at rate $d$. The model is stored in the variable `rn`.
+
```@example ex2
rn = @reaction_network begin
b, 0 --> X
d, X --> 0
end
```
-For more information on how to use the Catalyst model creator (also known as *the Catalyst DSL*), please read [the corresponding documentation](https://docs.sciml.ai/Catalyst/stable/catalyst_functionality/dsl_description/).
-Next, we wish to simulate our model. To do this, we need to provide some additional information to the simulator. This is
-* The initial condition. That is, the concentration (or copy numbers) of each species at the start of the simulation.
-* The time span. That is, the time frame over which we wish to run the simulation.
-* The parameter values. That is, the values of the model's parameters for this simulation.
+For more information on how to use the Catalyst model creator (also known as *the Catalyst DSL*), please read [the corresponding documentation](https://docs.sciml.ai/Catalyst/stable/model_creation/dsl_basics/).
+
+Next, we wish to simulate our model. To do this, we need to provide some additional information to the simulator. This is:
+
+- The initial condition. That is, the concentration (or copy numbers) of each species at the start of the simulation.
+- The time span. That is, the time frame over which we wish to run the simulation.
+- The parameter values. That is, the values of the model's parameters for this simulation.
+
+The initial condition is given as a *Vector*. This is a type which collects several different values. To declare a vector, the values are specified within brackets, `[]`, and separated by `,`. Since we only have one species, the vector holds a single element. In this element, we set the value of $X$ using the `:X => 1.0` syntax. Here, we first denote the name of the species (with a `:` pre-appended, which creates a `Symbol`), next follows a `=>` and then the value of $X$. Since we wish to simulate the *concentration* of $X$ over time, we will let the initial condition be decimal valued.
-The initial condition is given as a *Vector*. This is a type which collects several different values. To declare a vector, the values are specific within brackets, `[]`, and separated by `,`. Since we only have one species, the vector holds a single element. In this element, we set the value of $X$ using the `:X => 1.0` syntax. Here, we first denote the name of the species (with a `:` pre-appended, which creates a `Symbol`), next follows a `=>` and then the value of $X$. Since we wish to simulate the *concentration* of X over time, we will let the initial condition be decimal valued.
```@example ex2
u0 = [:X => 1.0]
```
-The timespan sets the time point at which we start the simulation (typically `0.0` is used) and the final time point of the simulation. These are combined into a two-valued *tuple*. Tuples are similar to vectors, but are enclosed by `()` and not `[]`. Again, we will let both time points be decimal valued.
+The time span sets the time point at which we start the simulation (typically `0.0` is used) and the final time point of the simulation. These are combined into a two-valued *tuple*. Tuples are similar to vectors, but are enclosed by `()` and not `[]`. Again, we will let both time points be decimal valued.
+
```@example ex2
tspan = (0.0, 10.0)
```
Finally, the parameter values are, like the initial conditions, given in a vector. Since we have two parameters ($b$ and $d$), the parameter vector has two values. We use a similar notation for setting the parameter values as the initial condition (first the parameter, then an arrow, then the value).
+
```@example ex2
params = [:b => 1.0, :d => 0.2]
```
-Please read here for more information on [vectors](https://docs.julialang.org/en/v1/manual/arrays/) and [tuples](https://docs.julialang.org/en/v1/manual/types/#Tuple-Types).
+Please read the corresponding manual entries on [vectors](https://docs.julialang.org/en/v1/manual/arrays/) and [tuples](https://docs.julialang.org/en/v1/manual/types/#Tuple-Types) for more information.
+
+Next, before we can simulate our model, we bundle all the required information together in a so-called `ODEProblem`. Note that the order in which the input (the model, the initial condition, the time span, and the parameter values) is provided to the `ODEProblem` matters. E.g. the parameter values cannot be provided as the first argument, but have to be the fourth argument. Here, we save our `ODEProblem` in the `oprob` variable.
-Next, before we can simulate our model, we bundle all the required information together in a so-called `ODEProblem`. Note that the order in which the input (the model, the initial condition, the timespan, and the parameter values) is provided to the `ODEProblem` matters. E.g. the parameter values cannot be provided as the first argument, but have to be the fourth argument. Here, we save our `ODEProblem` in the `oprob` variable.
```@example ex2
oprob = ODEProblem(rn, u0, tspan, params)
```
We can now simulate our model. We do this by providing the `ODEProblem` to the `solve` function. We save the output to the `sol` variable.
+
```@example ex2
sol = solve(oprob)
```
Finally, we can plot the solution through the `plot` function.
+
```@example ex2
plot(sol)
```
@@ -128,37 +159,45 @@ Here, the plot shows the time evolution of the concentration of the species $X$
For more information about the numerical simulation package, please see the [DifferentialEquations documentation](https://docs.sciml.ai/DiffEqDocs/stable/). For more information about the plotting package, please see the [Plots documentation](https://docs.juliaplots.org/stable/).
## Additional modelling example
+
To make this introduction more comprehensive, we here provide another example, using a more complicated model. Instead of simulating our model as concentrations evolve over time, we will now simulate the individual reaction events through the [Gillespie algorithm](https://en.wikipedia.org/wiki/Gillespie_algorithm) (a common approach for adding *noise* to models).
-Remember (unless we have restarted Julia) we do not need to activate our packages (through the `using` command) again. However, we do need to install, and then import, the JumpProcesses package (just to perform Gillespie, and other jump, simulations)
+Remember (unless we have restarted Julia) we do not need to activate our packages (through the `using` command) again. However, we do need to install, and then import, the [`JumpProcesses`](https://docs.sciml.ai/JumpProcesses/stable/) package (just to perform Gillespie, and other jump, simulations).
+
```julia
Pkg.add("JumpProcesses")
using JumpProcesses
```
This time, we will declare a so-called [SIR model for an infectious disease](@ref basic_CRN_library_sir). Note that even if this model does not describe a set of chemical reactions, it can be modelled using the same framework. The model consists of 3 species:
-* *S*, the amount of *susceptible* individuals.
-* *I*, the amount of *infected* individuals.
-* *R*, the amount of *recovered* (or *removed*) individuals.
+
+- *S*, the amount of *susceptible* individuals.
+- *I*, the amount of *infected* individuals.
+- *R*, the amount of *recovered* (or *removed*) individuals.
It also has 2 reaction events:
-* Infection, where a susceptible individual meets an infected individual and also becomes infected.
-* Recovery, where an infected individual recovers from the infection.
+
+- Infection, where a susceptible individual meets an infected individual and also becomes infected.
+- Recovery, where an infected individual recovers from the infection.
Each reaction is also associated with a specific rate (corresponding to a parameter).
-* *b*, the infection rate.
-* *k*, the recovery rate.
+
+- *b*, the infection rate.
+- *k*, the recovery rate.
We declare the model using the `@reaction_network` macro, and store it in the `sir_model` variable.
+
```@example ex2
sir_model = @reaction_network begin
b, S + I --> 2I
k, I --> R
end
```
+
Note that the first reaction contains two different substrates (separated by a `+` sign). While there is only a single product (*I*), two copies of *I* are produced. The *2* in front of the product *I* denotes this.
Next, we declare our initial condition, time span, and parameter values. Since we want to simulate the individual reaction events that discretely change the state of our model, we want our initial conditions to be integer-valued. We will start with a mostly susceptible population, but to which a single infected individual has been introduced.
+
```@example ex2
u0 = [:S => 50, :I => 1, :R => 0]
tspan = (0.0, 10.0)
@@ -167,13 +206,16 @@ nothing # hide
```
Previously we have bundled this information into an `ODEProblem` (denoting a deterministic *ordinary differential equation*). Now we wish to simulate our model as a jump process (where each reaction event corresponds to a discrete change in the state of the system). We do this by first processing the inputs to work in a jump model -- an extra step needed for jump models that can be avoided for ODE/SDE models -- and then creating a `JumpProblem` from the inputs:
+
```@example ex2
using JumpProcesses # hide
jinput = JumpInputs(sir_model, u0, tspan, params)
jprob = JumpProblem(jinput)
nothing # hide
```
+
Finally, we can now simulate our model using the `solve` function, and plot the solution using the `plot` function.
+
```@example ex2
sol = solve(jprob)
sol = solve(jprob; seed=1234) # hide
@@ -183,17 +225,22 @@ plot(sol)
**Exercise:** Try simulating the model several times. Note that the epidemic doesn't always take off, but sometimes dies out without spreading through the population. Try changing the infection rate (*b*), determining how this value affects the probability that the epidemic goes through the population.
## [Package management in Julia](@id catalyst_for_new_julia_users_packages)
+
We have previously introduced how to install and activate Julia packages. While this is enough to get started with Catalyst, for long-term users, there are some additional considerations for a smooth experience. These are described here.
### [Setting up a new Julia environment](@id catalyst_for_new_julia_users_packages_environments)
+
Whenever you run Julia, it will run in a specific *environment*. You can specify any folder on your computer as a Julia environment. Some modes of running Julia will automatically use the environment corresponding to the folder you start Julia in. Others (or if you start Julia in a folder without an environment), will use your *default* environment. In these cases you can, during your session, switch to another environment. While it is possible to not worry about environments (and always use the default one), this can lead to long-term problems as more packages are installed.
To activate your current folder as an environment, run the following commands:
+
```julia
using Pkg
Pkg.activate(".")
```
+
This will:
+
1. If your current folder (which can be displayed using the `pwd()` command) is not designated as a possible Julia environment, designate it as such.
2. Switch your current Julia session to use the current folder's environment.
@@ -201,31 +248,43 @@ This will:
If you check any folder which has been designated as a Julia environment, it contains a Project.toml and a Manifest.toml file. These store all information regarding the corresponding environment. For non-advanced users, it is recommended to never touch these files directly (and instead do so using various functions from the Pkg package, the important ones which are described in the next two subsections).
### [Installing and importing packages in Julia](@id catalyst_for_new_julia_users_packages_installing)
+
Package installation and import have been described [previously](@ref catalyst_for_new_julia_users_packages_intro). However, for the sake of this extended tutorial, let us repeat the description by demonstrating how to install the [Latexify.jl](https://github.com/korsbo/Latexify.jl) package (which enables e.g. displaying Catalyst models in Latex format). First, we import the Julia Package manager ([Pkg](https://github.com/JuliaLang/Pkg.jl)) (which is required to install Julia packages):
+
```@example ex3
using Pkg
```
+
Latexify is a registered package, so it can be installed directly using:
+
```julia
Pkg.add("Latexify")
```
+
Finally, to import Latexify into our current Julia session we use:
+
```@example ex3
using Latexify
```
+
Here, `using Latexify` must be rerun whenever you restart a Julia session. However, you only need to run `Pkg.add("Latexify")` once to install it on your computer (but possibly additional times to add it to new environments, see the next section).
### [Why environments are important](@id catalyst_for_new_julia_users_packages_environment_importance)
+
We have previously described how to set up new Julia environments, how to install Julia packages, and how to import them into a current session. Let us say that you were to restart Julia in a new folder and activate this as a separate environment. If you then try to import Latexify through `using Latexify` you will receive an error claiming that Latexify was not found. The reason is that the `Pkg.add("Latexify")` command actually carries out two separate tasks:
+
1. If Latexify is not already installed on your computer, install it.
2. Add Latexify as an available package to your current environment.
Here, while Catalyst has previously been installed on your computer, it has not been added to the new environment you created. To do so, simply run
+
```julia
using Pkg
Pkg.add("Latexify")
```
+
after which Catalyst can be imported through `using Latexify`. You can get a list of all packages available in your current environment using:
+
```julia
Pkg.status()
```
@@ -238,6 +297,7 @@ The reason why all this is important is that it is *highly recommended* to, for
A not-infrequent cause for reported errors with Catalyst (typically the inability to replicate code in tutorials) is package incompatibilities in large environments preventing the latest version of Catalyst from being installed. Hence, whenever an issue is encountered, it is useful to run `Pkg.status()` to check whenever the latest version of Catalyst is being used.
Some additional useful Pkg commands are:
+
- `Pk.rm("PackageName")` removes a package from the current environment.
- `Pkg.update("PackageName")`: updates the designated package.
- `Pkg.update()`: updates all packages.
@@ -245,12 +305,15 @@ Some additional useful Pkg commands are:
!!! note
A useful feature of Julia's environment system is that enables the exact definition of what packages and versions were used to execute a script. This supports e.g. reproducibility in academic research. Here, by providing the corresponding Project.toml and Manifest.toml files, you can enable someone to reproduce the exact program used to perform some set of analyses.
-
---
+
## Feedback
+
If you are a new Julia user who has used this tutorial, and there was something you struggled with or would have liked to have explained better, please [raise an issue](https://github.com/SciML/Catalyst.jl/issues). That way, we can continue improving this tutorial. The same goes for any part of the Catalyst documentation: It is written to help new users understand how to use the package, and if it is not doing so successfully we would like to know!
---
+
## References
+
[^1]: [Torkel E. Loman, Yingbo Ma, Vasily Ilin, Shashi Gowda, Niklas Korsbo, Nikhil Yewale, Chris Rackauckas, Samuel A. Isaacson, *Catalyst: Fast and flexible modeling of reaction networks*, PLOS Computational Biology (2023).](https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1011530)
[^2]: [Jeff Bezanson, Alan Edelman, Stefan Karpinski, Viral B. Shah, *Julia: A Fresh Approach to Numerical Computing*, SIAM Review (2017).](https://epubs.siam.org/doi/abs/10.1137/141000671)
diff --git a/docs/src/introduction_to_catalyst/introduction_to_catalyst.md b/docs/src/introduction_to_catalyst/introduction_to_catalyst.md
index d63e85c3a3..73e8b4828b 100644
--- a/docs/src/introduction_to_catalyst/introduction_to_catalyst.md
+++ b/docs/src/introduction_to_catalyst/introduction_to_catalyst.md
@@ -1,7 +1,8 @@
# [Introduction to Catalyst](@id introduction_to_catalyst)
+
In this tutorial we provide an introduction to using Catalyst to specify
chemical reaction networks, and then to solve ODE, jump, and SDE models
-generated from them [1]. At the end we show what mathematical rate laws and
+generated from them[^1]. At the end we show what mathematical rate laws and
transition rate functions (i.e. intensities or propensities) are generated by
Catalyst for ODE, SDE and jump process models. The [Mathematical Models Catalyst
can Generate](@ref math_models_in_catalyst) documentation illustrates the
@@ -12,6 +13,7 @@ prerequisite.
We begin by installing Catalyst and any needed packages into a new environment.
This step can be skipped if you have already installed them in your current,
active environment:
+
```julia
using Pkg
@@ -28,6 +30,7 @@ Pkg.add("StochasticDiffEq")
```
We next load the basic packages we'll need for our first example:
+
```@example tut1
using Catalyst, OrdinaryDiffEqTsit5, Plots, Latexify
```
@@ -39,6 +42,7 @@ use are discussed in detail within the tutorial, [The Reaction DSL](@ref
dsl_description). Here, we use a mix of first order, zero order, and repressive
Hill function rate laws. Note, $\varnothing$ corresponds to the empty state, and
is used for zeroth order production and first order degradation reactions:
+
```@example tut1
rn = @reaction_network Repressilator begin
hillr(P₃,α,K,n), ∅ --> m₁
@@ -56,6 +60,7 @@ rn = @reaction_network Repressilator begin
end
show(stdout, MIME"text/plain"(), rn) # hide
```
+
showing that we've created a new network model named `Repressilator` with the
listed chemical species and unknowns. [`@reaction_network`](@ref) returns a
[`ReactionSystem`](@ref), which we saved in the `rn` variable. It can
@@ -63,28 +68,37 @@ be converted to a variety of other mathematical models represented as
`ModelingToolkit.AbstractSystem`s, or analyzed in various ways using the
[Catalyst.jl API](@ref api). For example, to see the chemical species, parameters,
and reactions we can use
+
```@example tut1
species(rn)
```
+
```@example tut1
parameters(rn)
```
+
and
+
```@example tut1
reactions(rn)
```
+
We can also use Latexify to see the corresponding reactions in Latex, which shows what
the `hillr` terms mathematically correspond to
+
```julia
latexify(rn)
```
+
```@example tut1
rn #hide
```
+
Catalyst also has functionality for visualizing networks using the [Makie](https://docs.makie.org/stable/)
plotting ecosystem. The relevant packages to load are Catalyst, GraphMakie, NetworkLayout, and a Makie backend
-such as CairoMakie. Doing so and then using the `plot_network` function allows us to
-visualize the network:
+such as CairoMakie. Doing so and then using the `plot_network` function allows us to
+visualize the network:
+
```@example tut1
using Catalyst
import CairoMakie, GraphMakie, NetworkLayout
@@ -98,27 +112,33 @@ Similarly, black arrows from reactions to species indicate products, and are
labelled with their output stoichiometry. In contrast, red arrows from a species
to reactions indicate the species is used within the reactions' rate
expressions. For the repressilator, the reactions
+
```julia
hillr(P₃,α,K,n), ∅ --> m₁
hillr(P₁,α,K,n), ∅ --> m₂
hillr(P₂,α,K,n), ∅ --> m₃
```
+
have rates that depend on the proteins, and hence lead to red arrows from each
`Pᵢ`.
Note, from the REPL or scripts one can always use Makie's `save` function to save
the graph.
+
```julia
save("repressilator_graph.png", g)
```
## [Mass action ODE models](@id introduction_to_catalyst_massaction_ode)
+
Let's now use our `ReactionSystem` to generate and solve a corresponding mass
action ODE model. We first convert the system to a `ModelingToolkit.ODESystem`
by
+
```@example tut1
odesys = convert(ODESystem, rn)
```
+
(Here Latexify is used automatically to display `odesys` in Latex within Markdown
documents or notebook environments like Pluto.jl.)
@@ -128,14 +148,17 @@ To do this we need to build mappings from the symbolic parameters and the
species to the corresponding numerical values for parameters and initial
conditions. We can build such mappings in several ways. One is to use Julia
`Symbols` to specify the values like
+
```@example tut1
pmap = (:α => .5, :K => 40, :n => 2, :δ => log(2)/120,
:γ => 5e-3, :β => log(2)/6, :μ => log(2)/60)
u₀map = [:m₁ => 0., :m₂ => 0., :m₃ => 0., :P₁ => 20., :P₂ => 0., :P₃ => 0.]
nothing # hide
```
+
Alternatively, we can use ModelingToolkit-based symbolic species variables to
specify these mappings like
+
```@example tut1
psymmap = (rn.α => .5, rn.K => 40, rn.n => 2, rn.δ => log(2)/120,
rn.γ => 5e-3, rn.β => 20*log(2)/120, rn.μ => log(2)/60)
@@ -143,6 +166,7 @@ u₀symmap = [rn.m₁ => 0., rn.m₂ => 0., rn.m₃ => 0., rn.P₁ => 20.,
rn.P₂ => 0., rn.P₃ => 0.]
nothing # hide
```
+
Knowing one of the preceding mappings we can set up the `ODEProblem` we want to
solve:
@@ -154,14 +178,17 @@ tspan = (0., 10000.)
oprob = ODEProblem(rn, u₀map, tspan, pmap)
nothing # hide
```
+
By passing `rn` directly to the `ODEProblem`, Catalyst has to
(internally) call `convert(ODESystem, rn)` again to generate the
symbolic ODEs. We could instead pass `odesys` directly like
+
```@example tut1
odesys = complete(odesys)
oprob2 = ODEProblem(odesys, u₀map, tspan, pmap)
nothing # hide
```
+
`oprob` and `oprob2` are functionally equivalent, each representing the same
underlying problem.
@@ -169,12 +196,13 @@ At this point we are all set to solve the ODEs. We can now use any ODE solver
from within the
[OrdinaryDiffEq.jl](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/)
package. We'll use the recommended default explicit solver, `Tsit5()`, and then
-plot the solutions:
+plot the solutions:
```@example tut1
sol = solve(oprob, Tsit5(), saveat=10.0)
plot(sol)
```
+
We see the well-known oscillatory behavior of the repressilator! For more on the
choices of ODE solvers, see the [OrdinaryDiffEq.jl
documentation](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/).
@@ -185,6 +213,7 @@ documentation](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/).
---
## Stochastic simulation algorithms (SSAs) for stochastic chemical kinetics
+
Let's now look at a stochastic chemical kinetics model of the repressilator,
modeling it with jump processes. Here, we will construct a
[JumpProcesses](https://docs.sciml.ai/JumpProcesses/stable/) `JumpProblem` that uses
@@ -217,6 +246,7 @@ JumpProcesses instead of letting JumpProcesses auto-select a solver, see the
list of SSAs (i.e., constant rate jump aggregators) in the
[documentation](https://docs.sciml.ai/JumpProcesses/stable/jump_types/#Jump-Aggregators-for-Exact-Simulation).
For example, to choose the `SortingDirect` method we would instead say
+
```@example tut1
jprob = JumpProblem(jinputs, SortingDirect())
sol = solve(jprob)
@@ -228,7 +258,9 @@ Common questions that arise in using the JumpProcesses SSAs (i.e. Gillespie meth
are collated in the [JumpProcesses FAQ](https://docs.sciml.ai/JumpProcesses/stable/faq/).
---
+
## Chemical Langevin equation (CLE) stochastic differential equation (SDE) models
+
At an intermediate physical scale between macroscopic ODE models and microscopic
stochastic chemical kinetics models lies the CLE, given by a system of SDEs that
add to each ODE above a noise term. As the repressilator has species that get
@@ -276,12 +308,15 @@ StochasticDiffEq.jl SDE solvers, see the
[documentation](https://docs.sciml.ai/stable/modules/DiffEqDocs/solvers/sde_solve/).
---
+
## Specifying a complete model via the DSL
+
In the previous examples we specified initial conditions and parameter values
via mappings that were constructed after building our [`ReactionSystem`](@ref).
Catalyst also supports specifying default values for these during
`ReactionSystem` construction. For example, for the last SDE example we
could have also built and simulated the complete model using the DSL like
+
```@example tut1
bdp2 = @reaction_network begin
@parameters c₁ = 1.0 c₂ = 2.0 c₃ = 50.0
@@ -293,8 +328,10 @@ end
tspan = (0., 4.)
sprob2 = SDEProblem(bdp2, [], tspan)
```
+
Let's now simulate both models, starting from the same random number generator
seed, and check we get the same solutions
+
```@example tut1
using Random
Random.seed!(1)
@@ -310,25 +347,34 @@ For details on what information can be specified via the DSL see the [The
Reaction DSL](@ref dsl_description) tutorial.
---
+
## [Reaction rate laws used in simulations](@id introduction_to_catalyst_ratelaws)
+
In generating mathematical models from a [`ReactionSystem`](@ref), reaction
rates are treated as *microscopic* rates. That is, for a general mass action
reaction of the form $n_1 S_1 + n_2 S_2 + \dots n_M S_M \to \dots$ with
stoichiometric substrate coefficients $\{n_i\}_{i=1}^M$ and rate constant $k$,
the corresponding ODE and SDE rate laws are taken to be
+
```math
k \prod_{i=1}^M \frac{(S_i)^{n_i}}{n_i!},
```
+
while the jump process transition rate (i.e., the propensity or intensity
function) is
+
```math
k \prod_{i=1}^M \frac{S_i (S_i-1) \dots (S_i-n_i+1)}{n_i!}.
```
+
For example, the rate law of the reaction $2X + 3Y \to Z$ with rate constant $k$ would be
+
```math
k \frac{X^2}{2!} \frac{Y^3}{3!} \\
```
+
giving the ODE model
+
```math
\begin{align*}
\frac{dX}{dt} &= -2 k \frac{X^2}{2!} \frac{Y^3}{3!}, &
@@ -336,20 +382,25 @@ giving the ODE model
\frac{dZ}{dt} &= k \frac{X^2}{2!} \frac{Y^3}{3!}.
\end{align*}
```
+
This implicit rescaling of rate constants can be disabled through explicit
conversion of a [`ReactionSystem`](@ref) to another system via
[`Base.convert`](@ref) using the `combinatoric_ratelaws=false` keyword
argument, i.e.
+
```julia
rn = @reaction_network ...
convert(ODESystem, rn; combinatoric_ratelaws=false)
```
For the previous example using this keyword argument would give the rate law
+
```math
k X^2 Y^3
```
+
and the ODE model
+
```math
\begin{align*}
\frac{dX}{dt} &= -2 k X^2 Y^3, &
@@ -361,7 +412,9 @@ and the ODE model
A more detailed summary of the precise mathematical equations Catalyst can generate is available in the [Mathematical Models Catalyst can Generate](@ref math_models_in_catalyst) documentation.
---
+
## Notes
+
1. For each of the preceding models we converted the `ReactionSystem` to, i.e.,
ODEs, jumps, or SDEs, we had two paths for conversion:
@@ -381,5 +434,7 @@ A more detailed summary of the precise mathematical equations Catalyst can gener
[`SDEProblem`s](https://mtk.sciml.ai/dev/systems/SDESystem/#DiffEqBase.SDEProblem).
---
+
## References
-1. [Torkel E. Loman, Yingbo Ma, Vasily Ilin, Shashi Gowda, Niklas Korsbo, Nikhil Yewale, Chris Rackauckas, Samuel A. Isaacson, *Catalyst: Fast and flexible modeling of reaction networks*, PLOS Computational Biology (2023).](https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1011530)
+
+[^1]: [Torkel E. Loman, Yingbo Ma, Vasily Ilin, Shashi Gowda, Niklas Korsbo, Nikhil Yewale, Chris Rackauckas, Samuel A. Isaacson, *Catalyst: Fast and flexible modeling of reaction networks*, PLOS Computational Biology (2023).](https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1011530)
diff --git a/docs/src/introduction_to_catalyst/math_models_intro.md b/docs/src/introduction_to_catalyst/math_models_intro.md
index 569d2cee72..cccf25da7a 100644
--- a/docs/src/introduction_to_catalyst/math_models_intro.md
+++ b/docs/src/introduction_to_catalyst/math_models_intro.md
@@ -1,4 +1,5 @@
# [Mathematical Models Catalyst can Generate](@id math_models_in_catalyst)
+
We now describe the types of mathematical models that Catalyst can generate from
chemical reaction networks (CRNs), corresponding to reaction rate equation (RRE)
ordinary differential equation (ODE) models, Chemical Langevin equation (CLE)
@@ -14,40 +15,55 @@ documentation for more details on how Catalyst supports such functionality.
This documentation assumes you have already read the [Introduction to Catalyst](@ref introduction_to_catalyst) tutorial.
## General Chemical Reaction Notation
+
Suppose we have a reaction network with ``K`` reactions and ``M`` species, with the species labeled by $S_1$, $S_2$, $\dots$, $S_M$. We denote by
+
```math
\mathbf{X}(t) = \begin{pmatrix} X_1(t) \\ \vdots \\ X_M(t)) \end{pmatrix}
```
+
the state vector for the amount of each species, i.e. $X_m(t)$ represents the amount of species $S_m$ at time $t$. This could be either a concentration or a count (i.e. "number of molecules" units), but for consistency between modeling representations we will assume the latter in the remainder of this introduction.
The $k$th chemical reaction is given by
+
```math
\alpha_1^k S_1 + \alpha_2^k S_2 + \dots \alpha_M^k S_M \to \beta_1^k S_1 + \beta_2^k S_2 + \dots \beta_M^k S_M
```
+
with $\alpha^k = (\alpha_1^k,\dots,\alpha_M^k)$ its substrate stoichiometry vector, $\beta^k = (\beta_1^k,\dots,\beta_M^k)$ its product stoichiometry vector, and $\nu^k = \beta^k - \alpha^k$ its net stoichiometry vector. $\nu^k$ corresponds to the change in $\mathbf{X}(t)$ when reaction $k$ occurs, i.e. $\mathbf{X}(t) \to \mathbf{X}(t) + \nu^k$. Along with the stoichiometry vectors, we assume each reaction has a reaction rate law (ODEs/SDEs) or propensity (jump process) function, $a_k(\mathbf{X}(t),t)$.
As explained in [the Catalyst introduction](@ref introduction_to_catalyst), for a mass action reaction where the preceding reaction has a fixed rate constant, $k$, this function would be the rate law
+
```math
a_k(\mathbf{X}(t)) = k \prod_{m=1}^M \frac{(X_m(t))^{\alpha_m^k}}{\alpha_m^k!},
```
+
for RRE ODE and CLE SDE models, and the propensity function
+
```math
a_k(\mathbf{X}(t)) = k \prod_{m=1}^M \frac{X_m(t) (X_m(t)-1) \dots (X_m(t)-\alpha_m^k+1)}{\alpha_m^k!},
```
+
for stochastic chemical kinetics jump process models.
-### Rate Law vs. Propensity Example:
+### Rate Law vs. Propensity Example
+
For the reaction $2A + B \overset{k}{\to} 3 C$ we would have
+
```math
\mathbf{X}(t) = (A(t), B(t), C(t))
```
+
with $\alpha_1 = 2$, $\alpha_2 = 1$, $\alpha_3 = 0$, $\beta_1 = 0$, $\beta_2 =
0$, $\beta_3 = 3$, $\nu_1 = -2$, $\nu_2 = -1$, and $\nu_3 = 3$. For an ODE/SDE
model we would have the rate law
+
```math
a(\mathbf{X}(t)) = \frac{k}{2} A^2 B
```
+
while for a jump process model we would have the propensity function
+
```math
a(\mathbf{X}(t)) = \frac{k}{2} A (A-1) B.
```
@@ -56,6 +72,7 @@ Note, if the combinatoric factors are already included in one's rate constants,
the implicit rescaling of rate constants can be disabled through use of the
`combinatoric_ratelaws = false` argument to [`Base.convert`](@ref) or whatever
Problem is being generated, i.e.
+
```julia
rn = @reaction_network ...
osys = convert(ODESystem, rn; combinatoric_ratelaws = false)
@@ -64,16 +81,22 @@ oprob = ODEProblem(osys, ...)
# or
oprob = ODEProblem(rn, ...; combinatoric_ratelaws = false)
```
+
In this case our ODE/SDE rate law would be
+
```math
a(\mathbf{X}(t)) = k A^2 B
```
+
while the jump process propensity function is
+
```math
a(\mathbf{X}(t)) = k A (A-1) B.
```
+
One can also specify during system construction that by default combinatoric
scalings should be disabled, i.e.
+
```@example math_examples
using Catalyst
rn = @reaction_network begin
@@ -84,18 +107,25 @@ osys = convert(ODESystem, rn)
```
## [Reaction Rate Equation (RRE) ODE Models](@id math_models_in_catalyst_rre_odes)
+
The RRE ODE models Catalyst creates for a general system correspond to the coupled system of ODEs given by
+
```math
\frac{d X_m}{dt} =\sum_{k=1}^K \nu_m^k a_k(\mathbf{X}(t),t), \quad m = 1,\dots,M.
```
+
These models can be generated by creating `ODEProblem`s from Catalyst `ReactionSystem`s, and solved using the solvers in [OrdinaryDiffEq.jl](https://github.com/SciML/OrdinaryDiffEq.jl). Similarly, creating `NonlinearProblem`s or `SteadyStateProblem`s will generate the coupled algebraic system of steady-state equations associated with a RRE ODE model, i.e.
+
```math
0 =\sum_{k=1}^K \nu_m^k a_k(\bar{\mathbf{X}}), \quad m = 1,\dots,M
```
+
for a steady-state $\bar{\mathbf{X}}$. Note, here we have assumed the rate laws are [autonomous](https://en.wikipedia.org/wiki/Autonomous_system_(mathematics)) so that the equations are well-defined.
### RRE ODE Example
+
Let's see the generated ODEs for the following network
+
```@example math_examples
using Catalyst, ModelingToolkit, Latexify
rn = @reaction_network begin
@@ -105,20 +135,27 @@ rn = @reaction_network begin
end
osys = convert(ODESystem, rn)
```
+
Likewise, the following drops the combinatoric scaling factors, giving unscaled ODEs
+
```@example math_examples
osys = convert(ODESystem, rn; combinatoric_ratelaws = false)
```
## [Chemical Langevin Equation (CLE) SDE Models](@id math_models_in_catalyst_cle_sdes)
+
The CLE SDE models Catalyst creates for a general system correspond to the coupled system of SDEs given by
+
```math
d X_m = \sum_{k=1}^K \nu_m^k a_k(\mathbf{X}(t),t) dt + \sum_{k=1}^K \nu_m^k \sqrt{a_k(\mathbf{X}(t),t)} dW_k(t), \quad m = 1,\dots,M,
```
+
where each $W_k(t)$ represents an independent, standard Brownian Motion. Realizations of these processes can be generated by creating `SDEProblem`s from Catalyst `ReactionSystem`s, and sampling the processes using the solvers in [StochasticDiffEq.jl](https://github.com/SciML/StochasticDiffEq.jl).
### CLE SDE Example
+
Consider the same network as above,
+
```julia
rn = @reaction_network begin
k₁, 2A + B --> 3C
@@ -126,7 +163,9 @@ rn = @reaction_network begin
k₃, 0 --> A
end
```
+
We obtain the CLE SDEs
+
```math
\begin{align}
dA(t) &= \left(- k_1 A^{2} B - k_2 A + k_3 \right) dt
@@ -138,20 +177,27 @@ dC(t) &= \frac{3}{2} k_1 A^{2} B \, dt + 3 \sqrt{\frac{k_1}{2} A^{2} B} \, dW_1(
```
## [Stochastic Chemical Kinetics Jump Process Models](@id math_models_in_catalyst_sck_jumps)
+
The stochastic chemical kinetics jump process models Catalyst creates for a general system correspond to the coupled system of jump processes, in the time change representation, given by
+
```math
X_m(t) = X_m(0) + \sum_{k=1}^K \nu_m^k Y_k\left( \int_{0}^t a_k(\mathbf{X}(s^-),s) \, ds \right), \quad m = 1,\dots,M.
```
+
Here each $Y_k(t)$ denotes an independent unit rate Poisson counting process with $Y_k(0) = 0$, which counts the number of times the $k$th reaction has occurred up to time $t$. Realizations of these processes can be generated by creating `JumpProblem`s from Catalyst `ReactionSystem`s, and sampling the processes using the solvers in [JumpProcesses.jl](https://github.com/SciML/JumpProcesses.jl).
Let $P(\mathbf{x},t) = \operatorname{Prob}[\mathbf{X}(t) = \mathbf{x}]$ represent the probability the state of the system, $\mathbf{X}(t)$, has the concrete value $\mathbf{x}$ at time $t$. The forward equation, i.e. Chemical Master Equation (CME), associated with $\mathbf{X}(t)$ is then the coupled system of ODEs over all possible values for $\mathbf{x}$ given by
+
```math
\frac{dP}{dt}(\mathbf{x},t) = \sum_{k=1}^k \left[ a_k(\mathbf{x} - \nu^k,t) P(\mathbf{x} - \nu^k,t) - a_k(\mathbf{x},t) P(\mathbf{x},t) \right].
```
+
While Catalyst does not currently support generating and solving for $P(\mathbf{x},t)$, for sufficiently small models the [FiniteStateProjection.jl](https://github.com/SciML/FiniteStateProjection.jl) package can be used to generate and solve such models directly from Catalyst [`ReactionSystem`](@ref)s.
### Stochastic Chemical Kinetics Jump Process Example
+
Consider the same network as above,
+
```julia
rn = @reaction_network begin
k₁, 2A + B --> 3C
@@ -159,7 +205,9 @@ rn = @reaction_network begin
k₃, 0 --> A
end
```
+
The time change process representation would be
+
```math
\begin{align*}
A(t) &= A(0) - 2 Y_1\left( \frac{k_1}{2} \int_0^t A(s^-)(A(s^-)-1) B(s^-) \, ds \right) - Y_2 \left( k_2 \int_0^t A(s^-) \, ds \right) + Y_3 \left( k_3 t \right) \\
@@ -167,7 +215,9 @@ B(t) &= B(0) - Y_1\left( \frac{k_1}{2} \int_0^t A(s^-)(A(s^-)-1) B(s^-) \, ds \r
C(t) &= C(0) + 3 Y_1\left( \frac{k_1}{2} \int_0^t A(s^-)(A(s^-)-1) B(s^-) \, ds \right),
\end{align*}
```
+
while the CME would be the coupled (infinite) system of ODEs over all realizable values of the non-negative integers $a$, $b$, and $c$ given by
+
```math
\begin{align*}
\frac{dP}{dt}(a,b,c,t) &= \left[\tfrac{k_1}{2} (a+2) (a+1) (b+1) P(a+2,b+1,c-3,t) - \tfrac{k_1}{2} a (a-1) b P(a,b,c,t)\right] \\
@@ -175,4 +225,5 @@ while the CME would be the coupled (infinite) system of ODEs over all realizable
&\phantom{=} + \left[k_3 P(a-1,b,c,t) - k_3 P(a,b,c,t)\right].
\end{align*}
```
-If we initially have $A(0) = a_0$, $B(0) = b_0$, and $C(0) = c_0$ then we would have one ODE for each of possible state $(a,b,c)$ where $a \in \{0,1,\dots\}$ (i.e. $a$ can be any non-negative integer), $b \in \{0,1,\dots,b_0\}$, and $c \in \{c_0, c_0 + 1,\dots, c_0 + 3 b_0\}$. Other initial conditions would lead to different possible ranges for $a$, $b$, and $c$.
\ No newline at end of file
+
+If we initially have $A(0) = a_0$, $B(0) = b_0$, and $C(0) = c_0$ then we would have one ODE for each of possible state $(a,b,c)$ where $a \in \{0,1,\dots\}$ (i.e. $a$ can be any non-negative integer), $b \in \{0,1,\dots,b_0\}$, and $c \in \{c_0, c_0 + 1,\dots, c_0 + 3 b_0\}$. Other initial conditions would lead to different possible ranges for $a$, $b$, and $c$.
diff --git a/docs/src/inverse_problems/behaviour_optimisation.md b/docs/src/inverse_problems/behaviour_optimisation.md
index 0fb073ce62..bc3f864d50 100644
--- a/docs/src/inverse_problems/behaviour_optimisation.md
+++ b/docs/src/inverse_problems/behaviour_optimisation.md
@@ -1,10 +1,13 @@
# [Optimization for Non-data Fitting Purposes](@id behaviour_optimisation)
+
In previous tutorials we have described how to use [PEtab.jl](https://github.com/sebapersson/PEtab.jl) and [Optimization.jl](@ref optimization_parameter_fitting) for parameter fitting. This involves solving an optimisation problem (to find the parameter set yielding the best model-to-data fit). There are, however, other situations that require solving optimisation problems. Typically, these involve the creation of a custom objective function, which minimizer can then be found using Optimization.jl. In this tutorial we will describe this process, demonstrating how parameter space can be searched to find values that achieve a desired system behaviour. Many options used here are described in more detail in [the tutorial on using Optimization.jl for parameter fitting](@ref optimization_parameter_fitting). A more throughout description of how to solve these problems is provided by [Optimization.jl's documentation](https://docs.sciml.ai/Optimization/stable/) and the literature[^1].
## [Maximising the pulse amplitude of an incoherent feed forward loop](@id behaviour_optimisation_IFFL_example)
+
Incoherent feedforward loops (network motifs where a single component both activates and deactivates a downstream component) are able to generate pulses in response to step inputs[^2]. In this tutorial we will consider such an incoherent feedforward loop, attempting to generate a system with as prominent a response pulse as possible.
Our model consists of 3 species: $X$ (the input node), $Y$ (an intermediary), and $Z$ (the output node). In it, $X$ activates the production of both $Y$ and $Z$, with $Y$ also deactivating $Z$. When $X$ is activated, there will be a brief time window where $Y$ is still inactive, and $Z$ is activated. However, as $Y$ becomes active, it will turn $Z$ off. This creates a pulse of $Z$ activity. To trigger the system, we create [an event](@ref constraint_equations_events), which increases the production rate of $X$ ($pX$) by a factor of $10$ at time $t = 10$.
+
```@example behaviour_optimization
using Catalyst
incoherent_feed_forward = @reaction_network begin
@@ -15,7 +18,9 @@ incoherent_feed_forward = @reaction_network begin
1.0, (X,Y,Z) --> 0
end
```
+
To demonstrate this pulsing behaviour we will simulate the system for an example parameter set. We select an initial condition (`u0`) so the system begins in a steady state.
+
```@example behaviour_optimization
using OrdinaryDiffEqDefault, Plots
example_p = [:pX => 0.1, :pY => 1.0, :pZ => 1.0]
@@ -26,9 +31,11 @@ oprob = ODEProblem(incoherent_feed_forward, example_u0, tspan, example_p)
sol = solve(oprob)
plot(sol)
```
+
Here we note that, while $X$ and $Y$ reach new steady state levels in response to the increase in $pX$, $Z$ resumes to its initial concentration after the pulse.
We will now attempt to find the parameter set $(pX,pY,pZ)$ which maximises the response pulse amplitude (defined by the maximum activity of $Z$ subtracted by its steady state activity). To do this, we create a custom objective function:
+
```@example behaviour_optimization
function pulse_amplitude(p, _)
p = Dict([:pX => p[1], :pY => p[2], :pZ => p[2]])
@@ -40,27 +47,33 @@ function pulse_amplitude(p, _)
end
nothing # hide
```
+
This objective function takes two arguments (a parameter value `p`, and an additional one which we will ignore but is discussed in a note [here](@ref optimization_parameter_fitting_basics)). It first calculates the new initial steady state concentration for the given parameter set. Next, it creates an updated `ODEProblem` using the steady state as initial conditions and the, to the objective function provided, input parameter set. Finally, Optimization.jl finds the function's *minimum value*, so to find the *maximum* relative pulse amplitude, we make our objective function return the negative pulse amplitude.
As described [in our tutorial on parameter fitting using Optimization.jl](@ref optimization_parameter_fitting_basics) we use `remake`, `verbose = false`, `maxiters = 10000`, and a check on the simulations return code, all providing various advantages to the optimisation procedure (as explained in that tutorial).
Just like for [parameter fitting](@ref optimization_parameter_fitting_basics), we create an `OptimizationProblem` using our objective function, and some initial guess of the parameter values. We also [set upper and lower bounds](@ref optimization_parameter_fitting_constraints) for each parameter using the `lb` and `ub` optional arguments (in this case limiting each parameter's value to the interval $(0.1,10.0)$).
+
```@example behaviour_optimization
using Optimization
initial_guess = [1.0, 1.0, 1.0]
opt_prob = OptimizationProblem(pulse_amplitude, initial_guess; lb = [1e-1, 1e-1, 1e-1], ub = [1e1, 1e1, 1e1])
nothing # hide
```
+
!!! note
As described in a [previous section on Optimization.jl](@ref optimization_parameter_fitting), `OptimizationProblem`s do not support setting parameter values using maps. We must instead set `initial_guess` values using a vector. Next, in the first line of our objective function, we reshape the parameter values to the common form used across Catalyst (e.g. `[:pX => p[1], :pY => p[2], :pZ => p[2]]`, however, here we use a dictionary to easier compute the steady state initial condition). We also note that the order used in this array corresponds to the order we give each parameter's bounds in `lb` and `ub`, and the order in which their values occur in the output solution.
As [previously described](@ref optimization_parameter_fitting), Optimization.jl supports a wide range of optimisation algorithms. Here we use one from [BlackBoxOptim.jl](https://github.com/robertfeldt/BlackBoxOptim.jl):
+
```@example behaviour_optimization
using OptimizationBBO
opt_sol = solve(opt_prob, BBO_adaptive_de_rand_1_bin_radiuslimited())
nothing # hide
```
+
Finally, we plot a simulation using the found parameter set (stored in `opt_sol.u`):
+
```@example behaviour_optimization
ps_res = Dict([:pX => opt_sol.u[1], :pY => opt_sol.u[2], :pZ => opt_sol.u[2]])
u0_res = [:X => ps_res[:pX], :Y => ps_res[:pX]*ps_res[:pY], :Z => ps_res[:pZ]/ps_res[:pY]^2]
@@ -68,28 +81,35 @@ oprob_res = remake(oprob; u0 = u0_res, p = ps_res)
sol_res = solve(oprob_res)
plot(sol_res; idxs = :Z)
```
+
For this model, it turns out that $Z$'s maximum pulse amplitude is equal to twice its steady state concentration. Hence, the maximisation of its pulse amplitude is equivalent to maximising its steady state concentration.
## [Other optimisation options](@id behaviour_optimisation_options)
+
How to use Optimization.jl is discussed in more detail in [this tutorial](@ref optimization_parameter_fitting). This includes options such as using [automatic differentiation](@ref optimization_parameter_fitting_AD), [setting constraints](@ref optimization_parameter_fitting_constraints), and setting [optimisation solver options](@ref optimization_parameter_fitting_solver_options). Finally, it discusses the advantages of [carrying out the fitting in logarithmic space](@ref optimization_parameter_fitting_log_scale), something which can be advantageous for the problem described above as well.
---
+
## [Citation](@id structural_identifiability_citation)
+
If you use this functionality in your research, please cite the following paper to support the authors of the Optimization.jl package:
-```
+
+```bibtex
@software{vaibhav_kumar_dixit_2023_7738525,
- author = {Vaibhav Kumar Dixit and Christopher Rackauckas},
- month = mar,
- publisher = {Zenodo},
- title = {Optimization.jl: A Unified Optimization Package},
- version = {v3.12.1},
- doi = {10.5281/zenodo.7738525},
- url = {https://doi.org/10.5281/zenodo.7738525},
- year = 2023
+ author = {Vaibhav Kumar Dixit and Christopher Rackauckas},
+ month = mar,
+ publisher = {Zenodo},
+ title = {Optimization.jl: A Unified Optimization Package},
+ version = {v3.12.1},
+ doi = {10.5281/zenodo.7738525},
+ url = {https://doi.org/10.5281/zenodo.7738525},
+ year = 2023
}
```
---
+
## References
+
[^1]: [Mykel J. Kochenderfer, Tim A. Wheeler *Algorithms for Optimization*, The MIT Press (2019).](https://algorithmsbook.com/optimization/files/optimization.pdf)
[^2]: [Lea Goentoro, Oren Shoval, Marc W Kirschner, Uri Alon *The incoherent feedforward loop can provide fold-change detection in gene regulation*, Molecular Cell (2009).](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2896310/)
diff --git a/docs/src/inverse_problems/examples/ode_fitting_oscillation.md b/docs/src/inverse_problems/examples/ode_fitting_oscillation.md
index f6140a167b..6a3c40158a 100644
--- a/docs/src/inverse_problems/examples/ode_fitting_oscillation.md
+++ b/docs/src/inverse_problems/examples/ode_fitting_oscillation.md
@@ -1,7 +1,9 @@
# [Fitting Parameters for an Oscillatory System](@id parameter_estimation)
+
In this example we will use [Optimization.jl](https://github.com/SciML/Optimization.jl) to fit the parameters of an oscillatory system (the [Brusselator](@ref basic_CRN_library_brusselator)) to data. Here, special consideration is taken to avoid reaching a local minimum. Instead of fitting the entire time series directly, we will start with fitting parameter values for the first period, and then use those as an initial guess for fitting the next (and then these to find the next one, and so on). Using this procedure is advantageous for oscillatory systems, and enables us to reach the global optimum. For more information on fitting ODE parameters to data, please see [the main documentation page](@ref optimization_parameter_fitting) on this topic.
First, we fetch the required packages.
+
```@example pe_osc_example
using Catalyst
using OrdinaryDiffEqRosenbrock
@@ -11,6 +13,7 @@ using SciMLSensitivity # Required for `Optimization.AutoZygote()` automatic diff
```
Next, we declare our model, the Brusselator oscillator.
+
```@example pe_osc_example
brusselator = @reaction_network begin
A, ∅ --> X
@@ -24,6 +27,7 @@ nothing # hide
We simulate our model, and from the simulation generate sampled data points
(to which we add noise). We will use this data to fit the parameters of our model.
+
```@example pe_osc_example
u0 = [:X => 1.0, :Y => 1.0]
tend = 30.0
@@ -37,6 +41,7 @@ nothing # hide
```
We can plot the real solution, as well as the noisy samples.
+
```@example pe_osc_example
using Plots
default(; lw = 3, framestyle = :box, size = (800, 400))
@@ -49,8 +54,9 @@ Next, we create a function to fit the parameters using the `ADAM` optimizer. For
a given initial estimate of the parameter values, `pinit`, this function will
fit parameter values, `p`, to our data samples. We use `tend` to indicate the
time interval over which we fit the model. We use an out of place [`set_p` function](@ref simulation_structure_interfacing_functions)
-to update the parameter set in each iteration. We also provide the `set_p`, `prob`,
+to update the parameter set in each iteration. We also provide the `set_p`, `prob`,
`sample_times`, and `sample_vals` variables as parameters to our optimization problem.
+
```@example pe_osc_example
set_p = ModelingToolkit.setp_oop(prob, [:A, :B])
function optimize_p(pinit, tend,
@@ -76,11 +82,13 @@ nothing # hide
```
Next, we will fit a parameter set to the data on the interval `(0, 10)`.
+
```@example pe_osc_example
p_estimate = optimize_p([5.0, 5.0], 10.0)
```
We can compare this to the real solution, as well as the sample data
+
```@example pe_osc_example
function plot_opt_fit(p, tend)
p = set_p(prob, p)
@@ -97,6 +105,7 @@ plot_opt_fit(p_estimate, 10.0)
Next, we use this parameter estimate as the input to the next iteration of our
fitting process, this time on the interval `(0, 20)`.
+
```@example pe_osc_example
p_estimate = optimize_p(p_estimate, 20.0)
plot_opt_fit(p_estimate, 20.0)
@@ -104,23 +113,28 @@ plot_opt_fit(p_estimate, 20.0)
Finally, we use this estimate as the input to fit a parameter set on the full
time interval of the sampled data.
+
```@example pe_osc_example
p_estimate = optimize_p(p_estimate, 30.0)
plot_opt_fit(p_estimate, 30.0)
```
The final parameter estimate is then
+
```@example pe_osc_example
p_estimate
```
+
which is close to the actual parameter set of `[1.0, 2.0]`.
## Why we fit the parameters in iterations
+
As previously mentioned, the reason we chose to fit the model on a smaller interval to begin with, and
then extend the interval, is to avoid getting stuck in a local minimum. Here
specifically, we chose our initial interval to be smaller than a full cycle of
the oscillation. If we had chosen to fit a parameter set on the full interval
immediately we would have obtained poor fit and an inaccurate estimate for the parameters.
+
```@example pe_osc_example
p_estimate = optimize_p([5.0,5.0], 30.0)
plot_opt_fit(p_estimate, 30.0)
diff --git a/docs/src/inverse_problems/global_sensitivity_analysis.md b/docs/src/inverse_problems/global_sensitivity_analysis.md
index 037ce9fd74..6d134e0b39 100644
--- a/docs/src/inverse_problems/global_sensitivity_analysis.md
+++ b/docs/src/inverse_problems/global_sensitivity_analysis.md
@@ -1,17 +1,22 @@
# [Global Sensitivity Analysis](@id global_sensitivity_analysis)
+
*Global sensitivity analysis* (GSA) is used to study the sensitivity of a function's outputs with respect to its input[^1]. Within the context of chemical reaction network modelling it is primarily used for two purposes:
+
- When fitting a model's parameters to data, it can be applied to the cost function of the optimisation problem. Here, GSA helps determine which parameters do, and do not, affect the model's fit to the data. This can be used to identify parameters that are less relevant to the observed data.
- [When measuring some system behaviour or property](@ref behaviour_optimisation), it can help determine which parameters influence that property. E.g. for a model of a biofuel-producing circuit in a synthetic organism, GSA could determine which system parameters have the largest impact on the total rate of biofuel production.
GSA can be carried out using the [GlobalSensitivity.jl](https://github.com/SciML/GlobalSensitivity.jl) package. This tutorial contains a brief introduction of how to use it for GSA on Catalyst models, with [GlobalSensitivity providing a more complete documentation](https://docs.sciml.ai/GlobalSensitivity/stable/).
### [Global vs local sensitivity](@id global_sensitivity_analysis_global_vs_local_sensitivity)
+
A related concept to global sensitivity is *local sensitivity*. This, rather than measuring a function's sensitivity (with regards to its inputs) across its entire (or large part of its) domain, measures it at a specific point. This is equivalent to computing the function's gradients at a specific point in phase space, which is an important routine for most gradient-based optimisation methods (typically carried out through [*automatic differentiation*](https://en.wikipedia.org/wiki/Automatic_differentiation)). For most Catalyst-related functionalities, local sensitivities are computed using the [SciMLSensitivity.jl](https://github.com/SciML/SciMLSensitivity.jl) package. While certain GSA methods can utilise local sensitivities, this is not necessarily the case.
While local sensitivities are primarily used as a subroutine of other methodologies (such as optimisation schemes), it also has direct uses. E.g., in the context of fitting parameters to data, local sensitivity analysis can be used to, at the parameter set of the optimal fit, determine the cost function's sensitivity to the system parameters.
## [Basic example](@id global_sensitivity_analysis_basic_example)
+
We will consider a simple [SEIR model of an infectious disease](https://en.wikipedia.org/wiki/Compartmental_models_in_epidemiology). This is an expansion of the classic [SIR model](@ref basic_CRN_library_sir) with an additional *exposed* state, $E$, denoting individuals who are latently infected but currently unable to transmit their infection to others.
+
```@example gsa_1
using Catalyst
seir_model = @reaction_network begin
@@ -20,7 +25,9 @@ seir_model = @reaction_network begin
10^γ, I --> R
end
```
+
We will study the peak number of infected cases's ($max(I(t))$) sensitivity to the system's three parameters. We create a function which simulates the system from a given initial condition and measures this property:
+
```@example gsa_1
using OrdinaryDiffEqDefault
@@ -37,17 +44,21 @@ function peak_cases(p)
end
nothing # hide
```
+
Now, GSA can be applied to our `peak_cases` function using GlobalSensitivity's `gsa` function. It takes 3 mandatory inputs:
+
- The function for which we wish to carry out GSA.
- A method with which we wish to carry out GSA.
- A domain on which we carry out GSA. This is defined by a vector, which contains one two-valued Tuple for each parameter. These Tuples contain a lower and an upper bound for their respective parameter's value.
E.g., here we carry out GSA using [Morris's method](https://en.wikipedia.org/wiki/Morris_method):
+
```@example gsa_1
using GlobalSensitivity
global_sens = gsa(peak_cases, Morris(), [(-3.0,-1.0), (-2.0,0.0), (-2.0,0.0)])
nothing # hide
```
+
on the domain $10^β ∈ (-3.0,-1.0)$, $10^a ∈ (-2.0,0.0)$, $10^γ ∈ (-2.0,0.0)$ (which corresponds to $β ∈ (0.001,0.1)$, $a ∈ (0.01,1.0)$, $γ ∈ (0.01,1.0)$). The output of `gsa` varies depending on which GSA approach is used. GlobalSensitivity implements a range of methods for GSA. Below, we will describe the most common ones, as well as how to apply them and interpret their outputs.
!!! note
@@ -57,54 +68,67 @@ on the domain $10^β ∈ (-3.0,-1.0)$, $10^a ∈ (-2.0,0.0)$, $10^γ ∈ (-2.0,0
- Again, as [previously described in other inverse problem tutorials](@ref optimization_parameter_fitting_basics), when exploring a function over large parameter spaces, we will likely simulate our model for unsuitable parameter sets. To reduce time spent on these, and to avoid excessive warning messages, we provide the `maxiters = 100000` and `verbose = false` arguments to `solve`.
- As we have encountered in [a few other cases](@ref optimization_parameter_fitting_basics), the `gsa` function is not able to take parameter inputs of the map form usually used for Catalyst. Hence, as a first step in `peak_cases` we convert the parameter vector to this form. Next, we remember that the order of the parameters when we e.g. evaluate the GSA output, or set the parameter bounds, corresponds to the order used in `ps = [:β => p[1], :a => p[2], :γ => p[3]]`.
-
## [Sobol's method-based global sensitivity analysis](@id global_sensitivity_analysis_sobol)
+
The most common method for GSA is [Sobol's method](https://en.wikipedia.org/wiki/Variance-based_sensitivity_analysis). This can be carried out using:
+
```@example gsa_1
global_sens = gsa(peak_cases, Sobol(), [(-3.0,-1.0), (-2.0,0.0), (-2.0,0.0)]; samples = 500)
nothing # hide
```
+
Note: when `Sobol()` is used as the method, the `samples` argument must also be used.
Sobol's method computes so-called *Sobol indices*, each measuring some combination of input's effect on the output. Here, when `Sobol()` is used, the *first order*, *second order*, and *total order* Sobol indices are computed. These can be accessed through the following fields:
+
- `global_sens.S1`: A vector where the i'th element is the output's sensitivity to variations in the i'th input.
- `global_sens.S2`: A matrix where element i-j contains the output's sensitivity to simultaneous variations in the i'th and j'th inputs.
- `global_sens.ST`: A vector where the i'th element is the output's sensitivity to any simultaneous variation of any combination of inputs that contain the i'th input. While only the first and second-order (and the total) Sobol indices are computed, the total order index compounds the information contained in Sobol indices across all orders.
We can plot the first-order Sobol indices to analyse their content:
+
```@example gsa_1
using Plots
bar(["β", "a", "γ"], global_sens.S1; group = ["β", "a", "γ"], fillrange = 1e-3)
```
+
Here, we see that $β$ has a relatively low effect on the peak in infected cases, as compared to $a$ and $γ$. Plotting the total order indices suggests the same:
+
```@example gsa_1
bar(["β", "a", "γ"], global_sens.ST; group = ["β", "a", "γ"], fillrange = 1e-3)
```
GlobalSensitivity implements several versions of Sobol's method, and also provides several options. These are described [here](https://docs.sciml.ai/GlobalSensitivity/stable/methods/sobol/). Specifically, it is often recommended to, due to its quick computation time, use the related extended Fourier amplitude sensitivity test (EFAST) version. We can run this using:
+
```@example gsa_1
global_sens = gsa(peak_cases, eFAST(), [(-3.0,-1.0), (-2.0,0.0), (-2.0,0.0)]; samples = 500)
nothing # hide
```
+
It should be noted that when EFAST is used, only the first and total-order Sobol indices are computed (and not the second-order ones).
## [Morris's method-based global sensitivity analysis](@id global_sensitivity_analysis_morris)
+
An alternative to using Sobol's method is to use [Morris's method](https://en.wikipedia.org/wiki/Morris_method). The syntax is similar to previously (however, the `samples` argument is no longer required):
+
```@example gsa_1
global_sens = gsa(peak_cases, Morris(), [(-3.0,-1.0), (-2.0,0.0), (-2.0,0.0)])
nothing # hide
```
Morris's method computes, for parameter samples across parameter space, their *elementary effect* on the output. Next, the output's sensitivity with respect to each parameter is assessed through various statistics on these elementary effects. In practice, the following two fields are considered:
-- `global_sens.means_star` (called $μ*$): Measures each parameter's influence on the output. A large $μ*$ indicates a parameter to which the output is sensitive.
+
+- `global_sens.means_star` (called $μ*$): Measures each parameter's influence on the output. A large $μ*$ indicates a parameter to which the output is sensitive.
- `global_sens.variances`: Measures the variance of each parameter's influence on the output. A large variance suggests that a parameter's influence on the output is highly dependent on other parameter values.
We can check these values for our example:
+
```@example gsa_1
mean_star_plot = bar(["β" "a" "γ"], global_sens.means_star; labels=["β" "a" "γ"], title="μ*")
variances_plot = bar(["β" "a" "γ"], global_sens.variances; labels=["β" "a" "γ"], title="σ²")
plot(mean_star_plot, variances_plot)
```
+
As previously, we note that the peak number of infected cases is more sensitive to $a$ and $γ$ than to $β$.
!!! note
@@ -112,12 +136,14 @@ As previously, we note that the peak number of infected cases is more sensitive
Generally, Morris's method is computationally less intensive, and has easier to interpret output, as compared to Sobol's method. However, if computational resources are available, Sobol's method is more comprehensive.
-
## [Other global sensitivity analysis methods](@id global_sensitivity_analysis_other_methods)
+
GlobalSensitivity also implements additional methods for GSA, more details on these can be found in the [package's documentation](https://docs.sciml.ai/GlobalSensitivity/stable/).
## [Global sensitivity analysis for non-scalar outputs](@id global_sensitivity_analysis_nonscalars)
+
Previously, we have demonstrated GSA on functions with scalar outputs. However, it is also possible to apply it to functions with vector outputs. Let us consider our previous function, but where it provides both the peak number of exposed *and* infected individuals:
+
```@example gsa_1
function peak_cases_2(p)
ps = [:β => p[1], :a => p[2], :γ => p[3]]
@@ -130,20 +156,27 @@ nothing # hide
```
We can apply `gsa` to this function as previously:
+
```@example gsa_1
global_sens = gsa(peak_cases_2, Morris(), [(-3.0,-1.0), (-2.0,0.0), (-2.0,0.0)])
nothing # hide
```
+
however, each output field is now a multi-row matrix, containing one row for each of the outputs. E.g., we have
+
```@example gsa_1
global_sens.means_star
```
+
Here, the function's sensitivity is evaluated with respect to each output independently. Hence, GSA on `peak_cases_2` is equivalent to first carrying out GSA on a function returning the peak number of exposed individuals, and then on one returning the peak number of infected individuals.
---
+
## [Citations](@id global_sensitivity_analysis_citations)
+
If you use this functionality in your research, [in addition to Catalyst](@ref doc_index_citation), please cite the following paper to support the authors of the GlobalSensitivity.jl package:
-```
+
+```bibtex
@article{dixit2022globalsensitivity,
title={GlobalSensitivity. jl: Performant and Parallel Global Sensitivity Analysis with Julia},
author={Dixit, Vaibhav Kumar and Rackauckas, Christopher},
@@ -156,5 +189,7 @@ If you use this functionality in your research, [in addition to Catalyst](@ref d
```
---
+
## References
+
[^1]: [Saltelli, A et al. *Global Sensitivity Analysis. The Primer*, Wiley (2008).](http://www.andreasaltelli.eu/file/repository/A_Saltelli_Marco_Ratto_Terry_Andres_Francesca_Campolongo_Jessica_Cariboni_Debora_Gatelli_Michaela_Saisana_Stefano_Tarantola_Global_Sensitivity_Analysis_The_Primer_Wiley_Interscience_2008_.pdf)
diff --git a/docs/src/inverse_problems/optimization_ode_param_fitting.md b/docs/src/inverse_problems/optimization_ode_param_fitting.md
index 94db51d878..1ca55f611f 100644
--- a/docs/src/inverse_problems/optimization_ode_param_fitting.md
+++ b/docs/src/inverse_problems/optimization_ode_param_fitting.md
@@ -1,7 +1,9 @@
# [Parameter Fitting for ODEs using Optimization.jl](@id optimization_parameter_fitting)
+
Fitting parameters to data involves solving an optimisation problem (that is, finding the parameter set that optimally fits your model to your data, typically by minimising an objective function)[^1]. The SciML ecosystem's primary package for solving optimisation problems is [Optimization.jl](https://github.com/SciML/Optimization.jl). It provides access to a variety of solvers via a single common interface by wrapping a large number of optimisation libraries that have been implemented in Julia.
-This tutorial demonstrates how to
+This tutorial demonstrates how to
+
1. Create a custom objective function which minimiser corresponds to the parameter set optimally fitting the data.
2. Use Optimization.jl to minimize this objective function and find the parameter set providing the optimal fit.
@@ -10,7 +12,8 @@ For simple parameter fitting problems (such as the one outlined below), [PEtab.j
## [Basic example](@id optimization_parameter_fitting_basics)
Let us consider a [Michaelis-Menten enzyme kinetics model](@ref basic_CRN_library_mm), where an enzyme ($E$) converts a substrate ($S$) into a product ($P$):
-```@example optimization_paramfit_1
+
+```@example optimization_paramfit_1
using Catalyst
rn = @reaction_network begin
kB, S + E --> SE
@@ -18,8 +21,10 @@ rn = @reaction_network begin
kP, SE --> P + E
end
```
+
From some known initial condition, and a true parameter set (which we later want to recover from the data) we generate synthetic data (on which we will demonstrate the fitting process).
-```@example optimization_paramfit_1
+
+```@example optimization_paramfit_1
# Define initial conditions and parameters.
u0 = [:S => 1.0, :E => 1.0, :SE => 0.0, :P => 0.0]
ps_true = [:kB => 1.0, :kD => 0.1, :kP => 0.5]
@@ -42,7 +47,8 @@ Catalyst.PNG(plot(plt; fmt = :png, dpi = 200)) # hide
```
Next, we will formulate an objective function which, for a single parameter set, simulates our model and computes the sum-of-square distance between the data and the simulation (non-sum-of-square approaches can be used, but this is the most straightforward one).
-```@example optimization_paramfit_1
+
+```@example optimization_paramfit_1
ps_init = [:kB => 1.0, :kD => 1.0, :kP => 1.0]
oprob_base = ODEProblem(rn, u0, (0.0, 10.0), ps_init)
function objective_function(p, _)
@@ -53,7 +59,9 @@ function objective_function(p, _)
return sum((sol .- data_vals) .^2)
end
```
+
When our optimisation algorithm searches parameter space it will likely consider many highly non-plausible parameter sets. To better handle this we:
+
1. Add `maxiters = 10000` to our `solve` command. As most well-behaved ODEs can be solved in relatively few timesteps, this speeds up the optimisation procedure by preventing us from spending too much time trying to simulate (for the model) unsuitable parameter sets.
2. Add `verbose = false` to our `solve` command. This prevents (potentially a very large number of) warnings from being printed to our output as unsuitable parameter sets are simulated.
3. Add the line `SciMLBase.successful_retcode(sol) || return Inf`, which returns an infinite value for parameter sets which does not lead to successful simulations.
@@ -61,29 +69,34 @@ When our optimisation algorithm searches parameter space it will likely consider
To improve optimisation performance, rather than creating a new `ODEProblem` in each iteration, we pre-declare one which we [apply `remake` to](@ref simulation_structure_interfacing_problems_remake). We also use the `saveat = data_ts, save_idxs = :P` arguments to only save the values of the measured species at the measured time points.
We can now create an `OptimizationProblem` using our `objective_function` and some initial guess of parameter values from which the optimiser will start:
+
```@example optimization_paramfit_1
using Optimization
p_guess = [1.0, 1.0, 1.0]
optprob = OptimizationProblem(objective_function, p_guess)
nothing # hide
```
+
!!! note
`OptimizationProblem`s cannot currently accept parameter values in the form of a map (e.g. `[:kB => 1.0, :kD => 1.0, :kP => 1.0]`). These must be provided as individual values (using the same order as the parameters occur in in the `parameters(rs)` vector). This should hopefully be remedied in future Optimization releases.
!!! note
Especially if you check Optimization.jl's documentation, you will note that objective functions have the `f(u,p)` form. This is because `OptimizationProblem`s (like e.g. `ODEProblem`s) can take both variables (which are varied during the optimisation procedure), but also parameters that are fixed. In our case, the *optimisation variables* correspond to our *model parameters*. Hence, our model parameter values (`p`) are the first argument (`u`). This is also why we find the optimisation solution (our optimised parameter set) in `opt_sol`'s `u` field. Our optimisation problem does not actually have any parameters, hence, the second argument of `objective_function` is unused (that is why we call it `_`, a name commonly indicating unused function arguments).
+
There are several modifications to our problem where it would actually have parameters. E.g. we might want to run the optimisation where one parameter has a known fixed value. If we then would like to rerun this for alternative fixed values, this value could be encoded as an `OptimizationProblem` parameter.
Finally, we can solve `optprob` to find the parameter set that best fits our data. Optimization.jl only provides a few optimisation methods natively. However, for each supported optimisation package, it provides a corresponding wrapper package to import that optimisation package for use with Optimization.jl. E.g., if we wish to use [NLopt.jl](https://github.com/JuliaOpt/NLopt.jl)'s [Nelder-Mead](https://en.wikipedia.org/wiki/Nelder%E2%80%93Mead_method) method, we must install and import the OptimizationNLopt package. A summary of all, by Optimization.jl supported, optimisation packages can be found [here](https://docs.sciml.ai/Optimization/stable/#Overview-of-the-Optimizers). Here, we import the NLopt.jl package and uses it to minimise our objective function (thus finding a parameter set that fits the data):
-```@example optimization_paramfit_1
+
+```@example optimization_paramfit_1
using OptimizationNLopt
optsol = solve(optprob, NLopt.LN_NELDERMEAD())
nothing # hide
```
We can now simulate our model for the found parameter set (stored in `optsol.u`), checking that it fits our data.
-```@example optimization_paramfit_1
+
+```@example optimization_paramfit_1
oprob_fitted = remake(oprob_base; p = Pair.([:kB, :kD, :kP], optsol.u))
fitted_sol = solve(oprob_fitted)
plot!(fitted_sol; idxs = :P, label = "Fitted solution", linestyle = :dash, lw = 6, color = :lightblue)
@@ -92,34 +105,42 @@ Catalyst.PNG(plot(plt; fmt = :png, dpi = 200)) # hide
```
!!! note
- Here, a good exercise is to check the resulting parameter set and note that, while it creates a good fit to the data, it does not actually correspond to the original parameter set. Identifiability is a concept that studies how to deal with this problem.
+ Here, a good exercise is to check the resulting parameter set and note that, while it creates a good fit to the data, it does not actually correspond to the original parameter set. Identifiability is a concept that studies how to deal with this problem.
Say that we instead would like to use a [genetic algorithm](https://en.wikipedia.org/wiki/Genetic_algorithm) approach, as implemented by the [Evolutionary.jl](https://github.com/wildart/Evolutionary.jl) package. In this case we can run:
-```@example optimization_paramfit_1
+
+```@example optimization_paramfit_1
using OptimizationEvolutionary
sol = solve(optprob, Evolutionary.GA())
nothing # hide
```
+
to solve `optprob` for this combination of solve and implementation.
## [Utilising automatic differentiation](@id optimization_parameter_fitting_AD)
+
Optimisation methods can be divided into differentiation-free and differentiation-based optimisation methods. E.g. consider finding the minimum of the function $f(x) = x^2$, given some initial guess of $x$. Here, we can simply compute the differential and descend along it until we find $x=0$ (admittedly, for this simple problem the minimum can be computed directly). This principle forms the basis of optimisation methods such as [gradient descent](https://en.wikipedia.org/wiki/Gradient_descent), which utilises information of a function's differential to minimise it. When attempting to find a global minimum, to avoid getting stuck in local minimums, these methods are often augmented by additional routines. While the differentiation of most algebraic functions is trivial, it turns out that even complicated functions (such as the one we used above) can be differentiated computationally through the use of [*automatic differentiation* (AD)](https://en.wikipedia.org/wiki/Automatic_differentiation).
Through packages such as [ForwardDiff.jl](https://github.com/JuliaDiff/ForwardDiff.jl), [ReverseDiff.jl](https://github.com/JuliaDiff/ReverseDiff.jl), and [Zygote.jl](https://github.com/FluxML/Zygote.jl), Julia supports AD for most code. Specifically for code including simulation of differential equations, differentiation is supported by [SciMLSensitivity.jl](https://github.com/SciML/SciMLSensitivity.jl). Generally, AD can be used without specific knowledge from the user, however, it requires an additional step in the construction of our `OptimizationProblem`. Here, we create a [specialised `OptimizationFunction` from our objective function](https://docs.sciml.ai/Optimization/stable/API/optimization_function/#optfunction). To it, we will also provide our choice of AD method. There are [several alternatives](https://docs.sciml.ai/Optimization/stable/API/optimization_function/#Automatic-Differentiation-Construction-Choice-Recommendations), and in our case we will use `AutoForwardDiff()` (a good choice for small optimisation problems). We can then create a new `OptimizationProblem` using our updated objective function:
+
```@example optimization_paramfit_1
opt_func = OptimizationFunction(objective_function, AutoForwardDiff())
opt_prob = OptimizationProblem(opt_func, p_guess)
nothing # hide
```
+
Finally, we can find the optimum using some differentiation-based optimisation methods. Here we will use [Optim.jl](https://github.com/JuliaNLSolvers/Optim.jl)'s [Broyden–Fletcher–Goldfarb–Shanno algorithm](https://en.wikipedia.org/wiki/Broyden%E2%80%93Fletcher%E2%80%93Goldfarb%E2%80%93Shanno_algorithm) implementation:
+
```@example optimization_paramfit_1
using OptimizationOptimJL
opt_sol = solve(opt_prob, OptimizationOptimJL.BFGS())
```
## [Optimisation problems with data for multiple species](@id optimization_parameter_fitting_multiple_species)
+
Imagine that, in our previous example, we had measurements of the concentration of both $S$ and $P$:
-```@example optimization_paramfit_1
+
+```@example optimization_paramfit_1
data_vals_S = (0.8 .+ 0.4*rand(10)) .* data_sol[:S][2:end]
data_vals_P = (0.8 .+ 0.4*rand(10)) .* data_sol[:P][2:end]
@@ -133,7 +154,8 @@ Catalyst.PNG(plot(plt2; fmt = :png, dpi = 200)) # hide
```
In this case we simply modify our objective function to take this into account:
-```@example optimization_paramfit_1
+
+```@example optimization_paramfit_1
function objective_function_S_P(p, _)
p = Pair.([:kB, :kD, :kP], p)
oprob = remake(oprob_base; p)
@@ -142,10 +164,12 @@ function objective_function_S_P(p, _)
return sum((sol[:S] .- data_vals_S) .^2 + (sol[:P] .- data_vals_P) .^2)
end
```
+
Here we do not normalise the contribution from each species to the objective function. However, if species are present at different concentration levels this might be necessary (or you might essentially only take the highest concentration species(s) into account).
We can now fit our model to data and plot the results:
-```@example optimization_paramfit_1
+
+```@example optimization_paramfit_1
optprob_S_P = OptimizationProblem(objective_function_S_P, p_guess)
optsol_S_P = solve(optprob_S_P, NLopt.LN_NELDERMEAD())
p = Pair.([:kB, :kD, :kP], optsol_S_P.u)
@@ -157,8 +181,10 @@ Catalyst.PNG(plot(plt2; fmt = :png, dpi = 200)) # hide
```
## [Setting parameter constraints and boundaries](@id optimization_parameter_fitting_constraints)
+
Sometimes, it is desirable to set boundaries on parameter values. Indeed, this can speed up the optimisation process (by preventing searching through unfeasible parts of parameter space), and can also be a requirement for some optimisation methods. This can be done by passing the `lb` (lower bounds) and `up` (upper bounds) arguments to `OptimizationProblem`. These are vectors (of the same length as the number of parameters), with each argument corresponding to the boundary value of the parameter with the same index. If we wish to constrain each parameter to the interval $(0.1, 10.0)$ this can be done through:
-```@example optimization_paramfit_1
+
+```@example optimization_paramfit_1
optprob = OptimizationProblem(objective_function, [1.0, 1.0, 1.0]; lb = [1e-1, 1e-1, 1e-1], ub = [1e1, 1e1, 1e1])
nothing # hide
```
@@ -166,8 +192,10 @@ nothing # hide
In addition to boundaries, Optimization.jl also supports setting [linear and non-linear constraints](https://docs.sciml.ai/Optimization/stable/tutorials/constraints/#constraints) on its output solution (only available for some optimisers).
## [Parameter fitting with known parameters](@id optimization_parameter_fitting_known_parameters)
+
If we from previous knowledge know that $kD = 0.1$, and only want to fit the values of $kB$ and $kP$, this can be achieved by making corresponding changes to our objective function.
-```@example optimization_paramfit_1
+
+```@example optimization_paramfit_1
function objective_function_known_kD(p, _)
p = Pair.([:kB, :kD, :kP], [p[1], 0.1, p[2]])
oprob = remake(oprob_base; p)
@@ -176,23 +204,30 @@ function objective_function_known_kD(p, _)
return sum((sol .- data_vals) .^2)
end
```
+
We can now create and solve the corresponding `OptimizationProblem`, but with only two parameters in the initial guess.
-```@example optimization_paramfit_1
+
+```@example optimization_paramfit_1
optprob_known_kD = OptimizationProblem(objective_function_known_kD, [1.0, 1.0])
optsol_known_kD = solve(optprob_known_kD, NLopt.LN_NELDERMEAD())
nothing # hide
```
## [Optimisation solver options](@id optimization_parameter_fitting_solver_options)
+
Optimization.jl supports various [optimisation solver options](https://docs.sciml.ai/Optimization/stable/API/solve/) that can be supplied to the `solve` command. For example, to set a maximum number of seconds (after which the optimisation process is terminated), you can use the `maxtime` argument:
-```@example optimization_paramfit_1
+
+```@example optimization_paramfit_1
optsol_fixed_kD = solve(optprob, NLopt.LN_NELDERMEAD(); maxtime = 100)
nothing # hide
```
+
It should be noted that not all solver options are available to all optimisation solvers.
## [Fitting parameters on the logarithmic scale](@id optimization_parameter_fitting_log_scale)
+
Often it can be advantageous to fit parameters on a [logarithmic scale (rather than on a linear scale)](https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1008646). The most straightforward way to do this is to simply replace each parameter in the model definition by its logarithmic version:
+
```@example optimization_paramfit_2
using Catalyst
rn = @reaction_network begin
@@ -201,13 +236,17 @@ rn = @reaction_network begin
10^kP, SE --> P + E
end
```
+
And then going forward, by keeping in mind that parameter values are logarithmic. Here, setting
+
```@example optimization_paramfit_2
p_true = [:kB => 0.0, :kD => -1.0, :kP => 10^(0.5)]
nothing # hide
```
+
corresponds to the same true parameter values as used previously (`[:kB => 1.0, :kD => 0.1, :kP => 0.5]`). Alternatively, we can provide the log-transform in the objective function:
-```@example optimization_paramfit_1
+
+```@example optimization_paramfit_1
function objective_function_logtransformed(p, _)
p = Pair.([:kB, :kD, :kP], 10.0 .^ p)
oprob = remake(oprob_base; p)
@@ -218,21 +257,26 @@ end
```
---
+
## [Citation](@id optimization_parameter_fitting_citation)
+
If you use this functionality in your research, please cite the following paper to support the authors of the Optimization.jl package:
-```
+
+```bibtex
@software{vaibhav_kumar_dixit_2023_7738525,
- author = {Vaibhav Kumar Dixit and Christopher Rackauckas},
- month = mar,
- publisher = {Zenodo},
- title = {Optimization.jl: A Unified Optimization Package},
- version = {v3.12.1},
- doi = {10.5281/zenodo.7738525},
- url = {https://doi.org/10.5281/zenodo.7738525},
- year = 2023
+ author = {Vaibhav Kumar Dixit and Christopher Rackauckas},
+ month = mar,
+ publisher = {Zenodo},
+ title = {Optimization.jl: A Unified Optimization Package},
+ version = {v3.12.1},
+ doi = {10.5281/zenodo.7738525},
+ url = {https://doi.org/10.5281/zenodo.7738525},
+ year = 2023
}
```
---
+
## References
+
[^1]: [Alejandro F. Villaverde, Dilan Pathirana, Fabian Fröhlich, Jan Hasenauer, Julio R. Banga, *A protocol for dynamic model calibration*, Briefings in Bioinformatics (2023).](https://academic.oup.com/bib/article/23/1/bbab387/6383562?login=false)
diff --git a/docs/src/inverse_problems/petab_ode_param_fitting.md b/docs/src/inverse_problems/petab_ode_param_fitting.md
index 8f80c30dc5..4fc4b0e55c 100644
--- a/docs/src/inverse_problems/petab_ode_param_fitting.md
+++ b/docs/src/inverse_problems/petab_ode_param_fitting.md
@@ -1,10 +1,13 @@
# [Parameter Fitting for ODEs using PEtab.jl](@id petab_parameter_fitting)
-The [PEtab.jl package](https://github.com/sebapersson/PEtab.jl) implements the [PEtab format](https://petab.readthedocs.io/en/latest/) for fitting the parameters of deterministic CRN models to data [^1]. PEtab.jl both implements methods for creating cost functions (determining how well parameter sets fit to data), and for minimizing these cost functions. The PEtab approach covers most cases of fitting deterministic (ODE) models to data and is a good default choice when fitting reaction rate equation ODE models. This page describes how to combine PEtab.jl and Catalyst for parameter fitting, with the PEtab.jl package providing [a more extensive documentation](https://sebapersson.github.io/PEtab.jl/stable/) (this tutorial is partially an adaptation of this documentation).
+
+The [PEtab.jl package](https://github.com/sebapersson/PEtab.jl) implements the [PEtab format](https://petab.readthedocs.io/en/latest/) for fitting the parameters of deterministic CRN models to data[^1]. PEtab.jl both implements methods for creating cost functions (determining how well parameter sets fit to data), and for minimizing these cost functions. The PEtab approach covers most cases of fitting deterministic (ODE) models to data and is a good default choice when fitting reaction rate equation ODE models. This page describes how to combine PEtab.jl and Catalyst for parameter fitting, with the PEtab.jl package providing [a more extensive documentation](https://sebapersson.github.io/PEtab.jl/stable/) (this tutorial is partially an adaptation of this documentation).
While PEtab's interface generally is very flexible, there might be specific use-cases where it cannot create an appropriate cost-function. Here, it is recommended to instead look at using [Optimization.jl](@ref optimization_parameter_fitting).
## Introductory example
+
Let us consider a simple catalysis network, where an enzyme ($E$) turns a substrate ($S$) into a product ($P$):
+
```@example petab1
using Catalyst, PEtab
@@ -14,7 +17,9 @@ rn = @reaction_network begin
kP, SE --> P + E
end
```
+
From some known initial condition, and a true parameter set (which we later want to recover from the data) we generate synthetic data (on which we will demonstrate the fitting process).
+
```@example petab1
# Define initial conditions and parameters.
u0 = [:S => 1.0, :E => 1.0, :SE => 0.0, :P => 0.0]
@@ -36,6 +41,7 @@ plot!(data_ts, data_vals; label = "Measurements", seriestype = :scatter, ms = 6,
```
Generally, PEtab takes five different inputs to define an optimisation problem (the minimiser of which generates a fitted parameter set):
+
1. **Model**: The model which we want to fit to the data (a `ReactionSystem`).
2. **Observables**: The possible observables that can be measured (a `Dict{String,PEtabObservable}`).
3. **Estimation parameters**: The parameters which we want to fit to the data (a `Vector{PEtabParameter}`).
@@ -43,6 +49,7 @@ Generally, PEtab takes five different inputs to define an optimisation problem (
5. **Measurements**: The measurements to which the model is fitted (a `DataFrame`).
### Observables
+
The observables define the quantities that we may measure in our experiments. Typically, each corresponds to a single species, however, [more complicated observables are possible](@ref petab_observables_observables). For each observable, we also need a noise formula, defining the uncertainty in its measurements. By default, PEtab assumes normally distributed noise, with a mean equal to the true value and a standard deviation which we have to define. It is also possible to use [more advanced noise formulas](@ref petab_observables_noise_formula).
In our example, we only have a single possible observable, the `P` species. We will assume that the noise is normally distributed with a standard deviation `0.5` (in our case this is not true, however, typically the noise distribution is unknown and a guess must be made). We combine this information in a `PEtabObservable` struct (to access the `P` species we must use [`@unpack`](@ref simulation_structure_interfacing_symbolic_representation)). Finally, we store all our observables in a dictionary, giving each an id tag (which is later used in the measurements input).
@@ -55,12 +62,15 @@ nothing # hide
```
### Parameters
+
Each parameter of the system can either be
+
1. Known (described [here](@ref petab_parameters_known)).
2. Depend on experimental/simulation conditions (described [here](@ref petab_simulation_conditions)).
3. Be an unknown that we wish to fit to data.
In our case, we wish to fit all three system parameters ($kB$, $kD$, and $kP$). For each, we create a single `PEtabParameter`, and then gather these into a single vector.
+
```@example petab1
par_kB = PEtabParameter(:kB)
par_kD = PEtabParameter(:kD)
@@ -68,13 +78,17 @@ par_kP = PEtabParameter(:kP)
params = [par_kB, par_kD, par_kP]
nothing # hide
```
+
For each parameter, it is also possible to set [a lower and/or upper bound](@ref petab_parameters_bounds) (by default, $(0.001,1000)$ is used), set whether to use [logarithmic or linear scale](@ref petab_parameters_scales), or add a [prior distribution of its value](@ref petab_parameters_priors).
### Simulation conditions
+
Sometimes, several different experiments are performed on a system (each potentially generating several measurements). An experiment could e.g. be the time development of a system from a specific initial condition. Since each experimental condition (during the optimisation procedure, for a guess of the unknown parameters) generates a distinct simulation, these are also called simulation conditions. In our example, all data comes from a single experiment, and the simulation condition input is not required. How to define and use different experimental conditions is described [here](@ref petab_simulation_conditions).
### Measurements
+
Finally, we need to define the system measurements to which the parameters will be fitted. Each measurement combines:
+
1. The observable which is observed (here we use the id tag defined in the `observables` dictionary).
2. The time point of the measurement.
3. The measured value.
@@ -86,28 +100,35 @@ For cases where several simulation conditions are given, we also need to provide
*Note also, when [pre-equilibration](https://sebapersson.github.io/PEtab.jl/stable/Brannmark/) is used to initiate the system in a steady state, a fifth field is also required.*
Each individual measurement is provided as a row of a `DataFrame`. The values are provided in the `obs_id`, `time`, `measurement`, and `simulation_id` columns. In our case we only need to fill in the first three:
+
```@example petab1
using DataFrames
measurements = DataFrame(obs_id = "obs_P", time = data_ts, measurement = data_vals)
```
-Since, in our example, all measurements are of the same observable, we can set `obs_id="obs_P"`. However, it is also possible to [include measurements from several different observables](@ref petab_simulation_measurements_several_observables).
+Since, in our example, all measurements are of the same observable, we can set `obs_id="obs_P"`. However, it is also possible to [include measurements from several different observables](@ref petab_simulation_measurements_several_observables).
### Creating a PEtabModel
+
Finally, we combine all inputs in a single `PEtabModel`. To it, we also pass the initial conditions of our simulations (using the `speciemap` argument). It is also possible to have [initial conditions with uncertainty](@ref petab_simulation_initial_conditions_uncertainty), [that vary between different simulations](@ref petab_simulation_conditions), or [that we attempt to fit to the data](@ref petab_simulation_initial_conditions_fitted).
+
```@example petab1
petab_model = PEtabModel(rn, observables, measurements, params; speciemap = u0)
nothing # hide
```
### Fitting parameters
+
We are now able to fit our model to the data. First, we create a `PEtabODEProblem`. Here, we use `petab_model` as the only input, but it is also possible to set various [numeric solver and automatic differentiation options](@ref petab_simulation_options) (such as method or tolerance).
+
```@example petab1
petab_problem = PEtabODEProblem(petab_model)
```
+
Since no additional input is given, default options are selected by PEtab.jl (and generally, its choices are good).
To fit a parameter set we use the `calibrate` function. In addition to our `PEtabODEProblem`, we must also provide an initial guess (which can be generated with the `generate_startguesses` function) and an optimisation algorithm (which needs to be imported specifically). PEtab.jl supports various [optimisation methods and options](@ref petab_optimisation_optimisers).
+
```@example petab1
using Optim
p0 = get_startguesses(petab_problem, 1)
@@ -117,6 +138,7 @@ res = calibrate(petab_problem, p0, IPNewton())
```
We can now simulate our model for the fitted parameter set, and compare the result to the measurements and true solution.
+
```@example petab1
oprob_fitted = remake(oprob_true; p = get_ps(res, petab_problem))
fitted_sol = solve(oprob_fitted)
@@ -126,12 +148,15 @@ plot!(fitted_sol; idxs = :P, label = "Fitted solution", linestyle = :dash, lw =
Here we use the `get_ps` function to retrieve a full parameter set using the optimal parameters. Alternatively, the `ODEProblem` or fitted simulation can be retrieved directly using the `get_odeproblem` or `get_odesol` [functions](https://sebapersson.github.io/PEtab.jl/stable/API/), respectively (and the initial condition using the `get_u0` function). The calibration result can also be found in `res.xmin`, however, note that PEtab automatically ([unless a linear scale is selected](@ref petab_parameters_scales)) converts parameters to logarithmic scale, so typically `10 .^res.xmin` are the values of interest. If you investigate the result from this example you might note, that even if PEtab.jl has found the global optimum (which fits the data well), this does not actually correspond to the true parameter set. This phenomenon is related to the concept of *identifiability*, which is very important for parameter fitting.
### Final notes
+
PEtab.jl also supports [multistart optimisation](@ref petab_multistart_optimisation), [automatic pre-equilibration before simulations](https://sebapersson.github.io/PEtab.jl/stable/petab_preeq_simulations/), and [events](@ref petab_events). Various [plot recipes](@ref petab_plotting) exist for investigating the optimisation process. Please read the [PEtab.jl documentation](https://sebapersson.github.io/PEtab.jl/stable/) for a more complete description of the package's features. Below follows additional details of various options and features (generally, PEtab is able to find good default values for most options that are not specified).
## [Additional features: Observables](@id petab_observables)
### [Defining non-trivial observables](@id petab_observables_observables)
+
It is possible for observables to be any algebraic expression of species concentrations and parameters. E.g. in this example the total amount of `X` in the system is an observable:
+
```@example petab2
using Catalyst, PEtab # hide
two_state_model = @reaction_network begin
@@ -144,18 +169,24 @@ obs_X = PEtabObservable(X1 + X2, 0.5)
A common application for this is to define an [*offset* and a *scale* for each observable](https://sebapersson.github.io/PEtab.jl/stable/petab_obs_noise/).
### [Advanced observables noise formulas](@id petab_observables_noise_formula)
+
In our basic example we assumed that the normally distributed noise had a standard deviation of `0.5`. However, this value may be a parameter (or indeed any algebraic expression). E.g, we could set
+
```@example petab1
@parameters σ
obs_P = PEtabObservable(P, σ)
```
+
and then let the parameter $σ$ vary between different [simulation conditions](@ref petab_simulation_conditions). Alternatively we could let the noise scale linearly with the species' concentration:
+
```@example petab1
obs_P = PEtabObservable(P, 0.05P)
```
+
It would also be possible to make $σ$ a *parameter that is fitted as a part of the parameter fitting process*.
PEtab.jl assumes that noise is normally distributed (with a standard deviation equal to the second argument provided to `PEtabObservable`). The only other (currently implemented) noise distribution is log-normally distributed noise, which is designated through the `transformation=:log` argument:
+
```@example petab1
obs_P = PEtabObservable(P, σ; transformation = :log)
```
@@ -163,24 +194,31 @@ obs_P = PEtabObservable(P, σ; transformation = :log)
## [Additional features: Parameters](@id petab_parameters)
### [Known parameters](@id petab_parameters_known)
+
In our previous example, all parameters were unknowns that we wished to fit to the data. If any parameters have known values, it is possible to provide these to `PEtabModel` through the `parameter_map` argument. E.g if we had known that $kB = 1.0$, then we would only define $kD$ and $kP$ as parameters we wish to fit:
+
```@example petab1
par_kD = PEtabParameter(:kD)
par_kP = PEtabParameter(:kP)
params = [par_kD, par_kP]
nothing # hide
```
+
We then provide `parameter_map=[:kB => 1.0]` when we assembly our model:
+
```@example petab1
petab_model_known_param = PEtabModel(rn, observables, measurements, params; speciemap = u0, parametermap = [:kB => 1.0])
nothing # hide
```
### [Parameter bounds](@id petab_parameters_bounds)
+
By default, when fitted, potential parameter values are assumed to be in the interval $(1e-3, 1e3)$. When declaring a `PEtabParameter` it is possible to change these values through the `lb` and `ub` arguments. E.g. we could use
+
```@example petab1
par_kB = PEtabParameter(:kB; lb = 1e-2, ub = 1e2)
```
+
to achieve the more conservative bound $(1e-2, 1e2)$ for the parameter $kB$.
### [Parameter scales](@id petab_parameters_scales)
@@ -192,35 +230,45 @@ par_kB = PEtabParameter(:kB; scale = :lin)
```
### [Parameter priors](@id petab_parameters_priors)
+
If we have prior knowledge about the distribution of a parameter, it is possible to incorporate this into the model. The prior can be any continuous, univariate, distribution from the [Distributions.jl package](https://github.com/JuliaStats/Distributions.jl). E.g we can use:
```@example petab1
using Distributions
par_kB = PEtabParameter(:kB; prior = Normal(1.0,0.2))
```
+
to set a normally distributed prior (with mean `1.0` and standard deviation `0.2`) on the value of $kB$. By default, the prior is assumed to be on the linear scale of the parameter (before any potential log transform). To specify that the prior is on the logarithmic scale, the `prior_on_linear_scale=false` argument can be provided:
+
```@example petab1
par_kB = PEtabParameter(:kB; prior = Normal(1.0,0.2), prior_on_linear_scale = false)
```
+
In this example, setting `prior_on_linear_scale=false` makes sense as a (linear) normal distribution is non-zero for negative values (an alternative is to use a log-normal distribution, e.g. `prior=LogNormal(3.0, 3.0)`).
## [Simulation conditions](@id petab_simulation_conditions)
+
Sometimes, we have data from different experimental conditions. Here, when a potential parameter set is evaluated during the fitting process, each experimental condition corresponds to one simulation condition (which produces one simulation). To account for this, PEtab permits the user to define different simulation conditions, with each condition being defined by specific values for some initial conditions and/or parameters.
If, for our previous catalysis example, we had measured the system for two different initial values of $S$ ($S(0)=1.0$ and $S(0)=\tfrac{1}{2}$), these would correspond to two different simulation conditions. For each condition we define a `Dict` mapping the species to their initial condition (here, $S$ is the only species in each `Dict`):
+
```@example petab1
c1 = Dict(:S => 1.0)
c2 = Dict(:S => 0.5)
nothing # hide
```
+
Similarly as for observables, we then gather the conditions in another `Dict`, giving each an id tag:
+
```@example petab1
simulation_conditions = Dict("c1" => c1, "c2" => c2)
nothing # hide
```
+
Again (like for observables), each measurement in the measurements `DataFrame` needs to be associated with a simulation condition id tag (describing which condition those measurements were taken from). Parameters, just like initial conditions, may vary between different conditions. If an initial condition (or parameter) occurs in one condition, it must occur in all of them.
Here follows a complete version of our basic example, but with measurements both for $S(0)=1.0$ and $S(0)=\tfrac{1}{2}$.
+
```@example petab3
using Catalyst, PEtab
@@ -264,23 +312,28 @@ m1 = DataFrame(simulation_id = "c1", obs_id = "obs_P", time = t1, measurement =
m2 = DataFrame(simulation_id = "c2", obs_id = "obs_P", time = t2, measurement = d2)
measurements = vcat(m1,m2)
-petab_model = PEtabModel(rn, observables, measurements, params; speciemap = u0,
+petab_model = PEtabModel(rn, observables, measurements, params; speciemap = u0,
simulation_conditions = simulation_conditions)
nothing # hide
```
+
Note that the `u0` we pass into `PEtabModel` through the `speciemap` argument no longer contains the value of $S$ (as it is provided by the conditions).
## [Additional features: Measurements](@id petab_simulation_measurements)
### [Measurements of several observables](@id petab_simulation_measurements_several_observables)
+
In our previous example, all our measurements were from a single observable, `obs_P`. If we also had collected measurements of both $S$ and $P$:
+
```@example petab1
data_ts = data_sol.t[2:end]
data_vals_S = (0.8 .+ 0.4*rand(10)) .* data_sol[:S][2:end]
data_vals_P = (0.8 .+ 0.4*rand(10)) .* data_sol[:P][2:end]
nothing # hide
```
+
and then corresponding observables:
+
```@example petab1
@unpack S, P = rn
obs_S = PEtabObservable(S, 0.5)
@@ -288,21 +341,27 @@ obs_P = PEtabObservable(P, 0.5)
observables = Dict("obs_S" => obs_P, "obs_P" => obs_P)
nothing # hide
```
+
we are able to include all these measurements in the same `measurements` `DataFrame`:
+
```@example petab1
m1 = DataFrame(obs_id = "obs_P", time = data_ts, measurement = data_vals_S)
m2 = DataFrame(obs_id = "obs_S", time = data_ts, measurement = data_vals_P)
measurements = vcat(m1,m2)
```
+
which then can be used as input to `PEtabModel`.
### Varying parameters between different simulation conditions
+
Sometimes, the parameters that are used vary between the different conditions. Consider our catalysis example, if we had performed the experiment twice, using two different enzymes with different catalytic properties, this could have generated such conditions. The two enzymes could e.g. yield different rates ($kP_1$ and $kP_2$) for the `SE --> P + E` reaction, but otherwise be identical. Here, the parameters $kP_1$ and $kP_2$ are unique to their respective conditions. PEtab.jl provides support for cases such as this, and [its documentation](https://sebapersson.github.io/PEtab.jl/stable/petab_cond_specific/) provided instructions of how to handle them.
## [Additional features: Initial conditions](@id petab_simulation_initial_conditions)
### [Fitting initial conditions](@id petab_simulation_initial_conditions_fitted)
+
Sometimes, initial conditions are uncertain quantities which we wish to fit to the data. This is possible [by defining an initial condition as a parameter](@ref dsl_advanced_options_parametric_initial_conditions):
+
```@example petab4
using Catalyst, PEtab # hide
rn = @reaction_network begin
@@ -314,12 +373,16 @@ rn = @reaction_network begin
end
nothing # hide
```
+
Here, the initial value of `E` is equal to the parameter `E0`. We modify our `u0` vector by removing `E` (which is no longer known):
+
```@example petab4
u0 = [:S => 1.0, :SE => 0.0, :P => 0.0]
nothing # hide
```
+
Next, we add `E0` to the parameters we wish to fit:
+
```@example petab4
par_kB = PEtabParameter(:kB)
par_kD = PEtabParameter(:kD)
@@ -328,14 +391,17 @@ par_E0 = PEtabParameter(:E0)
params = [par_kB, par_kD, par_kP, par_E0]
nothing # hide
```
+
and we can use our updated `rn`, `u0`, and `params` as input to our `PEtabModel`.
### [Uncertain initial conditions](@id petab_simulation_initial_conditions_uncertainty)
+
Often, while an initial condition has been reported for an experiment, its exact value is uncertain. This can be modelled by making the initial condition a [parameter that is fitted to the data](@ref petab_simulation_initial_conditions_fitted) and attaching a prior to it corresponding to our certainty about its value.
Let us consider our initial example, but where we want to add uncertainty to the initial conditions of `S` and `E`. We will add priors on these, assuming normal distributions with mean `1.0` and standard deviation `0.1`. For the synthetic measured data we will use the true values $S(0) = E(0) = 1.0$.
+
```@example petab5
-using Catalyst, Distributions, PEtab
+using Catalyst, Distributions, PEtab
rn = @reaction_network begin
@parameters S0 E0
@@ -372,6 +438,7 @@ measurements = DataFrame(obs_id = "obs_P", time = data_ts, measurement = data_va
petab_model = PEtabModel(rn, observables, measurements, params; speciemap = u0)
nothing # hide
```
+
Here, when we fit our data we will also gain values for `S0` and `E0`, however, unless we are explicitly interested in these, they can be ignored.
## [Additional features: Simulation options](@id petab_simulation_options)
@@ -379,20 +446,24 @@ Here, when we fit our data we will also gain values for `S0` and `E0`, however,
While in our basic example, we do not provide any additional information to our `PEtabODEProblem`, this is an opportunity to specify how the model should be simulated, and what automatic differentiation techniques to use for the optimisation procedure (if none are provided, appropriate defaults are selected).
Here is an example, adapted from the [more detailed PEtab.jl documentation](https://sebapersson.github.io/PEtab.jl/stable/default_options/)
+
```@example petab1
using OrdinaryDiffEqRosenbrock
-PEtabODEProblem(petab_model,
+PEtabODEProblem(petab_model,
odesolver = ODESolver(Rodas5P(), abstol = 1e-8, reltol = 1e-8),
- gradient_method = :ForwardDiff,
+ gradient_method = :ForwardDiff,
hessian_method = :ForwardDiff)
nothing # hide
```
+
where we simulate our ODE model using the `Rodas5P` method (with absolute and relative tolerance both equal `1e-8`) and use [forward automatic differentiation](https://github.com/JuliaDiff/ForwardDiff.jl) for both gradient and hessian computation. More details on available ODE solver options can be found in [the PEtab.jl documentation](https://sebapersson.github.io/PEtab.jl/stable/API/#PEtabODEProblem).
## [Additional features: Optimisation](@id petab_optimisation)
### [Optimisation methods and options](@id petab_optimisation_optimisers)
+
For our examples, we have used the `Optim.IPNewton` optimisation method. PEtab.jl supports [several additional optimisation methods](https://sebapersson.github.io/PEtab.jl/stable/pest_algs/). Furthermore, `calibrate`'s `options` argument permits the customisation of the options for any used optimiser. E.g. to designate the maximum number of iterations of the `Optim.IPNewton` method we would use:
+
```@example petab1
res = calibrate(petab_problem, p0, IPNewton(); options = Optim.Options(iterations = 10000))
nothing # hide
@@ -403,27 +474,35 @@ Please read the [PEtab.jl documentation](https://sebapersson.github.io/PEtab.jl/
### [Optimisation path recording](@id petab_optimisation_path_recording)
To record all the parameter sets evaluated (and their objective values) during the optimisation procedure, provide the `save_trace=true` argument to `calibrate` (or `calibrate_multistart`):
+
```@example petab1
res = calibrate(petab_problem, p0, IPNewton(); save_trace = true)
nothing # hide
```
+
This is required for the various [optimisation evaluation plots](@ref petab_plotting) provided by PEtab.jl. If desired, this information can be accessed in the calibration output's `.xtrace` and `.ftrace` fields.
## Objective function extraction
+
While PEtab.jl provides various tools for analysing the objective function generated by `PEtabODEProblem`, it is also possible to extract this function for customised analysis. Given a `PEtabODEProblem`
+
```@example petab1
petab_problem = PEtabODEProblem(petab_model)
nothing # hide
```
+
```julia
petab_problem = PEtabODEProblem(petab_model)
```
+
We can find the:
+
1. Objective function (negative log-likelihood) as the `petab_problem.nllh`. It takes a single argument (`p`) and returns the objective value.
2. Gradient as the `petab_problem.grad!` field. It takes two arguments (`g` and `p`) with the updated gradient values being written to `g`.
3. Hessian as the `petab_problem.hess!` field. It takes two arguments (`H` and `p`) with the updated hessian values being written to `H`.
## [Multi-start optimisation](@id petab_multistart_optimisation)
+
To avoid the optimisation process returning a local minimum, it is often advised to run it multiple times, using different initial guesses. PEtab.jl supports this through the `calibrate_multistart` function. This is identical to the `calibrate` function, but takes one additional arguments:
1. `nmultistarts`: The number of runs to perform.
@@ -436,6 +515,7 @@ And two additional optional argument:
Because `calibrate_multistart` handles initial guess sampling, unlike for `calibrate`, no initial guess has to be provided.
Here, we fit parameters through 10 independent optimisation runs, using QuasiMonteCarlo's `SobolSample` method, and save the result to the OptimisationRuns folder:
+
```@example petab1
using Optim
using QuasiMonteCarlo
@@ -445,39 +525,51 @@ res_ms = calibrate_multistart(petab_problem, IPNewton(), 10; dirsave = "Optimisa
res_ms = calibrate_multistart(petab_problem, IPNewton(), 10; dirsave = "OptimisationRuns", sampling_method = QuasiMonteCarlo.SobolSample()) # hide
nothing # hide
```
+
The best result across all runs can still be retrieved using `get_ps(res_ms, petab_problem)`, with the results of the individual runs being stored in the `res_ms.runs` field.
To load the result in a later session, we can call:
+
```@example petab1
res_ms = PEtabMultistartResult("OptimisationRuns")
nothing # hide
```
+
where `"OptimisationRuns"` is the name of the save directory (specified in `calibrate_multistart`). If the OptimisationRuns folder contains the output from several runs, we can designate which to load using the `which_run` argument. Here we load the second run to be saved in that folder:
+
```@example petab1
res_ms = PEtabMultistartResult("OptimisationRuns"; which_run = 2)
rm("OptimisationRuns", recursive = true) # hide
nothing # hide
```
+
By default, `which_run` loads the first run saved to that directory.
## [Events](@id petab_events)
+
So far, we have assumed that all experiments, after initiation, run without interference. Experiments where conditions change, or where species are added/removed during the time course, can be represented through events. In PEtab, an event is represented through the `PEtabEvent` structure. It takes three arguments:
+
1. The condition for triggering the event. This can either indicate a point in time, or a boolean condition.
2. A rule for updating the event's target
3. The event's target (either a species or parameter).
Here we create an event which adds `0.5` units of `S` to the system at time `5.0`:
+
```@example petab1
@unpack S = rn
event1 = PEtabEvent(5.0, S + 0.5, S)
```
+
Here, the first argument is evaluated to a scalar value, in which case it is interpreted as a time point at which the event happens. If we instead want the event to happen whenever the concentration of `S` falls below `0.5` we set the first argument to a boolean condition indicating this:
+
```@example petab1
event2 = PEtabEvent(S < 0.5, S + 0.5, S)
```
+
Here, the event only triggers whenever the condition changes from `false` to `true`, and not while it remains `true` (or when changing from `true` to `false`). E.g. this event only triggers when `S` concentration passes from more than $5.0$ to less that $5.0$.
Whenever we have several events or not, we bundle them together in a single vector which is later passed to the `PEtabODEProblem` using the `events` argument:
+
```@example petab1
params = [par_kB, par_kD, par_kP] # hide
events = [event1, event2]
@@ -491,15 +583,18 @@ More details on how to use events, including how to create events with multiple
PEtab currently ignores events [created as a part of a Catalyst `ReactionSystem` model](@ref constraint_equations_events), and does not support SciML-style events. Instead, events have to use the preceding interface.
## [Plot recipes](@id petab_plotting)
+
There exist various types of graphs that can be used to evaluate the parameter fitting process. These can be plotted using the `plot` command, where the input is either the result of a `calibrate` or a `calibrate_multistart` run. To be able to use this functionality, you have to ensure that PEtab.jl [records the optimisation process](@ref petab_optimisation_path_recording) by providing the `save_trace=true` argument to the calibration functions.
To, for a single start calibration run, plot, for each iteration of the optimization process, the best objective value achieved so far, run:
+
```@example petab1
res = calibrate(petab_problem, p0, IPNewton(); save_trace = true) # hide
plot(res)
```
For a multi-start calibration run, the default output is instead a so-called waterfall plot:
+
```@example petab1
res_ms = PEtabMultistartResult("../assets/boehm___for_petab_tutorial") # hide
plot(res_ms)
@@ -510,6 +605,7 @@ plot(res_ms)
In the waterfall plot, each dot shows the final objective value for a single run in the multi-start process. The runs are ordered by their objective values, and colours designate runs in the same local minimum. A common use of waterfall plots is to check whether a sufficient number of runs (typically $>5$) has converged to the same best local minimum (in which case it is assumed to be the *global* minimum).
To instead use the best objective value plot for a multi-start run (with one curve for each run), the `plot_type` argument is used:
+
```@example petab1
plot(res_ms; plot_type = :best_objective)
```
@@ -517,9 +613,12 @@ plot(res_ms; plot_type = :best_objective)
There exist several types of plots for both types of calibration results. More details of the types of available plots, and how to customise them, can be found [here](https://sebapersson.github.io/PEtab.jl/stable/optimisation_output_plotting/).
---
+
## [Citations](@id petab_citations)
+
If you use this functionality in your research, [in addition to Catalyst](@ref doc_index_citation), please cite the following papers to support the authors of the PEtab.jl package (currently there is no article associated with this package) and the PEtab standard:
-```
+
+```bibtex
@misc{2023Petabljl,
author = {Ognissanti, Damiano AND Arutjunjan, Rafael AND Persson, Sebastian AND Hasselgren, Viktor},
title = {{2023Petabljl.jl}},
@@ -527,7 +626,8 @@ If you use this functionality in your research, [in addition to Catalyst](@ref d
year = {2023}
}
```
-```
+
+```bibtex
@article{SchmiesterSch2021,
author = {Schmiester, Leonard AND Schälte, Yannik AND Bergmann, Frank T. AND Camba, Tacio AND Dudkin, Erika AND Egert, Janine AND Fröhlich, Fabian AND Fuhrmann, Lara AND Hauber, Adrian L. AND Kemmer, Svenja AND Lakrisenko, Polina AND Loos, Carolin AND Merkt, Simon AND Müller, Wolfgang AND Pathirana, Dilan AND Raimúndez, Elba AND Refisch, Lukas AND Rosenblatt, Marcus AND Stapor, Paul L. AND Städter, Philipp AND Wang, Dantong AND Wieland, Franz-Georg AND Banga, Julio R. AND Timmer, Jens AND Villaverde, Alejandro F. AND Sahle, Sven AND Kreutz, Clemens AND Hasenauer, Jan AND Weindl, Daniel},
journal = {PLOS Computational Biology},
@@ -544,6 +644,8 @@ If you use this functionality in your research, [in addition to Catalyst](@ref d
```
---
+
## References
+
[^1]: [Schmiester, L et al. *PEtab—Interoperable specification of parameter estimation problems in systems biology*, PLOS Computational Biology (2021).](https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1008646)
[^2]: [Hass, H et al. *Benchmark problems for dynamic modeling of intracellular processes*, Bioinformatics (2019).](https://academic.oup.com/bioinformatics/article/35/17/3073/5280731?login=false)
diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md
index 48a15df07b..8ea86933fd 100644
--- a/docs/src/inverse_problems/structural_identifiability.md
+++ b/docs/src/inverse_problems/structural_identifiability.md
@@ -1,6 +1,6 @@
# [Structural Identifiability Analysis](@id structural_identifiability)
-During parameter fitting, parameter values are inferred from data. Parameter identifiability refers to whether inferring parameter values for a given model is mathematically feasible. Ideally, parameter fitting should always be accompanied with an identifiability analysis of the problem.
+During parameter fitting, parameter values are inferred from data. Parameter identifiability refers to whether inferring parameter values for a given model is mathematically feasible. Ideally, parameter fitting should always be accompanied with an identifiability analysis of the problem.
Identifiability can be divided into *structural* and *practical* identifiability[^1]. Structural identifiability considers only the mathematical model, and which parameters are and are not inherently identifiable due to model structure. Practical identifiability also considers the available data, and determines what system quantities can be inferred from it. In the idealised case of an infinite amount of non-noisy data, practical identifiability converges to structural identifiability. Generally, structural identifiability is assessed before parameters are fitted, while practical identifiability is assessed afterwards.
@@ -8,14 +8,15 @@ Structural identifiability (which is what this tutorial considers) can be illust
${dx \over dt} = p1*p2*x(t)$
where, however much data is collected on $x$, it is impossible to determine the distinct values of $p1$ and $p2$. Hence, these parameters are non-identifiable (however, their product, $p1*p2$, *is* identifiable).
-Catalyst contains a special extension for carrying out structural identifiability analysis of generated reaction rate equation ODE models using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability`, `assess_local_identifiability`, and `find_identifiable_functions` functions to be called directly on Catalyst `ReactionSystem`s. It also implements specialised routines to make these more efficient when applied to reaction network models (e.g. by improving runtimes). How to use these functions is described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/).
+Catalyst contains a special extension for carrying out structural identifiability analysis of generated reaction rate equation ODE models using the [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) package. This enables StructuralIdentifiability's `assess_identifiability`, `assess_local_identifiability`, and `find_identifiable_functions` functions to be called directly on Catalyst `ReactionSystem`s. It also implements specialised routines to make these more efficient when applied to reaction network models (e.g. by improving runtimes). How to use these functions is described in the following tutorial, with [StructuralIdentifiability providing a more extensive documentation](https://docs.sciml.ai/StructuralIdentifiability/stable/).
Structural identifiability can be divided into *local* and *global* identifiability. If a model quantity is locally identifiable, it means that its true value can be determined down to a finite-number of possible options. This also means that there is some limited region around the quantity's true value where this true value is the only possible value (and hence, within this region, the quantity is fully identifiable). Globally identifiable quantities' values, on the other hand, can be uniquely determined. Again, while identifiability can be confirmed structurally for a quantity, it does not necessarily mean that it is practically identifiable for some given data.
Generally, there are three types of quantities for which identifiability can be assessed.
+
- Parameters (e.g. $p1$ and $p2$).
- Full variable trajectories (e.g. $x(t)$).
-- Variable initial conditions (e.g. $x(0)$).
+- Variable initial conditions (e.g. $x(0)$).
StructuralIdentifiability currently assesses identifiability for the first two only (however, if $x(t)$ is identifiable, then $x(0)$ will be as well).
@@ -27,11 +28,13 @@ StructuralIdentifiability currently assesses identifiability for the first two o
### [Basic example](@id structural_identifiability_gi_example)
Global identifiability can be assessed using the `assess_identifiability` function. For each model quantity (parameters and variables), it will assess whether they are:
+
- Globally identifiable.
- Locally identifiable.
- Unidentifiable.
-
+
To it, we provide our `ReactionSystem` model and a list of quantities that we are able to measure. Here, we consider a Goodwind oscillator (a simple 3-component model, where the three species $M$, $E$, and $P$ are produced and degraded, which may exhibit oscillations)[^2]. Let us say that we are able to measure the concentration of $M$, we then designate this using the `measured_quantities` argument. We can now assess identifiability in the following way:
+
```@example structural_identifiability
using Catalyst, Logging, StructuralIdentifiability
gwo = @reaction_network begin
@@ -41,32 +44,42 @@ gwo = @reaction_network begin
end
assess_identifiability(gwo; measured_quantities = [:M], loglevel = Logging.Error)
```
+
From the output, we find that `E(t)`, `pₑ`, and `pₚ` (the trajectory of $E$, and the production rates of $E$ and $P$, respectively) are non-identifiable. Next, `dₑ` and `dₚ` (the degradation rates of $E$ and $P$, respectively) are locally identifiable. Finally, `P(t)`, `M(t)`, `pₘ`, and `dₘ` (the trajectories of `P` and `M`, and the production and degradation rate of `M`, respectively) are all globally identifiable. We note that we also imported the Logging.jl package, and provided the `loglevel = Logging.Error` input argument. StructuralIdentifiability functions generally provide a large number of output messages. Hence, we will use this argument (which requires the Logging package) throughout this tutorial to decrease the amount of printed text.
Next, we also assess identifiability in the case where we can measure all three species concentrations:
+
```@example structural_identifiability
assess_identifiability(gwo; measured_quantities = [:M, :P, :E], loglevel = Logging.Error)
```
+
in which case all species trajectories and parameters become identifiable.
### [Indicating known parameters](@id structural_identifiability_gi_known_ps)
+
In the previous case we assumed that all parameters are unknown, however, this is not necessarily true. If there are parameters with known values, we can supply these using the `known_p` argument. Providing this additional information might also make other, previously unidentifiable, parameters identifiable. Let us consider the previous example, where we measure the concentration of $M$ only, but now assume we also know the production rate of $E$ ($pₑ$):
+
```@example structural_identifiability
assess_identifiability(gwo; measured_quantities = [:M], known_p = [:pₑ], loglevel = Logging.Error)
```
+
Not only does this turn the previously non-identifiable `pₑ` (globally) identifiable (which is obvious, given that its value is now known), but this additional information improve identifiability for several other network components.
To, in a similar manner, indicate that certain initial conditions are known is a work in progress. Hopefully this feature should be an available in the near future.
### [Providing non-trivial measured quantities](@id structural_identifiability_gi_nontrivial_mq)
+
Sometimes, ones may not have measurements of species, but rather some combinations of species (or possibly parameters). To account for this, `measured_quantities` accepts any algebraic expression (and not just single species). To form such expressions, species and parameters have to first be `@unpack`'ed from the model. Say that we have a model where an enzyme ($E$) is converted between an active and inactive form, which in turns activates the production of a product, $P$:
+
```@example structural_identifiability
rs = @reaction_network begin
(kA,kD), Eᵢ <--> Eₐ
(Eₐ, d), 0 <-->P
end
```
+
If we can measure the total amount of $E$ ($=Eᵢ+Eₐ$), as well as the amount of $P$, we can use the following to assess identifiability:
+
```@example structural_identifiability
@unpack Eᵢ, Eₐ = rs
assess_identifiability(rs; measured_quantities = [Eᵢ + Eₐ, :P], loglevel = Logging.Error)
@@ -74,7 +87,9 @@ nothing # hide
```
### [Assessing identifiability for specified quantities only](@id structural_identifiability_gi_ftc)
+
By default, StructuralIdentifiability assesses identifiability for all parameters and variables. It is, however, possible to designate precisely which quantities you want to check using the `funcs_to_check` option. This both includes selecting a smaller subset of parameters and variables to check, or defining customised expressions. Let us consider the Goodwind from previously, and say that we would like to check whether the production parameters ($pₘ$, $pₑ$, and $pₚ$) and the total amount of the three species ($P + M + E$) are identifiable quantities. Here, we would first unpack these (allowing us to form algebraic expressions) and then use the following code:
+
```@example structural_identifiability
@unpack pₘ, pₑ, pₚ, M, E, P = gwo
assess_identifiability(gwo; measured_quantities = [:M], funcs_to_check = [pₘ, pₑ, pₚ, M + E + P], loglevel = Logging.Error)
@@ -82,65 +97,87 @@ nothing # hide
```
### [Probability of correctness](@id structural_identifiability_gi_probs)
+
The identifiability methods used can, in theory, produce erroneous results. However, it is possible to adjust the lower bound for the probability of correctness using the argument `prob_threshold` (by default set to `0.99`, that is, at least a $99\%$ chance of correctness). We can e.g. increase the bound through:
+
```@example structural_identifiability
assess_identifiability(gwo; measured_quantities=[:M], prob_threshold = 0.999, loglevel = Logging.Error)
nothing # hide
```
+
giving a minimum bound of $99.9\%$ chance of correctness. In practise, the bounds used by StructuralIdentifiability are very conservative, which means that while the minimum guaranteed probability of correctness in the default case is $99\%$, in practise it is much higher. While increasing the value of `prob_threshold` increases the certainty of correctness, it will also increase the time required to assess identifiability.
## [Local identifiability analysis](@id structural_identifiability_lit)
+
Local identifiability can be assessed through the `assess_local_identifiability` function. While this is already determined by `assess_identifiability`, assessing local identifiability only has the advantage that it is easier to compute. Hence, there might be models where global identifiability analysis fails (or takes a prohibitively long time), where instead `assess_local_identifiability` can be used. This function takes the same inputs as `assess_identifiability` and returns, for each quantity, `true` if it is locally identifiable (or `false` if it is not). Here, for the Goodwind oscillator, we assesses it for local identifiability only:
+
```@example structural_identifiability
assess_local_identifiability(gwo; measured_quantities = [:M], loglevel = Logging.Error)
```
+
We note that the results are consistent with those produced by `assess_identifiability` (with globally or locally identifiable quantities here all being assessed as at least locally identifiable).
-## [Finding identifiable functions](@id structural_identifiability_identifiabile_funcs)
+## [Finding identifiable functions](@id structural_identifiability_identifiable_funcs)
+
Finally, StructuralIdentifiability provides the `find_identifiable_functions` function. Rather than determining the identifiability of each parameter and unknown of the model, it finds a set of identifiable functions, such as any other identifiable expression of the model can be generated by these. Let us again consider the Goodwind oscillator, using the `find_identifiable_functions` function we find that identifiability can be reduced to five globally identifiable expressions:
+
```@example structural_identifiability
find_identifiable_functions(gwo; measured_quantities = [:M], loglevel = Logging.Error)
```
+
Again, these results are consistent with those produced by `assess_identifiability`. There, `pₑ` and `pₚ` where found to be globally identifiable. Here, they correspond directly to identifiable expressions. The remaining four parameters (`pₘ`, `dₘ`, `dₑ`, and `dₚ`) occur as part of more complicated composite expressions.
`find_identifiable_functions` tries to simplify its output functions to create nice expressions. The degree to which it does this can be adjusted using the `simplify` keywords. Using the `:weak`, `:standard` (default), and `:strong` arguments, increased simplification can be forced (at the expense of longer runtime).
## [Creating StructuralIdentifiability compatible ODE models from Catalyst `ReactionSystem`s](@id structural_identifiability_si_odes)
+
While the functionality described above covers the vast majority of analysis that user might want to perform, the StructuralIdentifiability package supports several additional features. While these does not have inherent Catalyst support, we do provide the `make_si_ode` function to simplify their use. Similar to the previous functions, it takes a `ReactionSystem`, lists of measured quantities, and known parameter values. The output is a [ODE of the standard form supported by StructuralIdentifiability](https://docs.sciml.ai/StructuralIdentifiability/stable/tutorials/creating_ode/#Defining-the-model-using-@ODEmodel-macro). It can be created using the following syntax:
+
```@example structural_identifiability
si_ode = make_si_ode(gwo; measured_quantities = [:M])
nothing # hide
```
+
and then used as input to various StructuralIdentifiability functions. In the following example we use StructuralIdentifiability's `print_for_DAISY` function, printing the model as an expression that can be used by the [DAISY](https://daisy.dei.unipd.it/) software for identifiability analysis[^3].
+
```@example structural_identifiability
print_for_DAISY(si_ode)
nothing # hide
```
## [Notes on systems with conservation laws](@id structural_identifiability_conslaws)
+
Several reaction network models, such as
+
```@example structural_identifiability
using Catalyst, Logging, StructuralIdentifiability # hide
rs = @reaction_network begin
(k1,k2), X1 <--> X2
end
```
+
contain [conservation laws](@ref conservation_laws) (in this case $Γ = X1 + X2$, where $Γ = X1(0) + X2(0)$ is a constant). Because the presence of such conservation laws makes structural identifiability analysis prohibitively computationally expensive (for all but the simplest of cases), these are automatically eliminated by Catalyst. This is handled internally, and should not be noticeable to the user. The exception is the `make_si_ode` function. For each conservation law, its output will have one ODE removed, and instead have a conservation parameter (of the form `Γ[i]`) added to its equations. This feature can be disabled through the `remove_conserved = false` option.
## [Systems with exponent parameters](@id structural_identifiability_exp_params)
+
Structural identifiability cannot currently be applied to systems with parameters (or species) in exponents. E.g. this
+
```julia
rn_si_impossible = @reaction_network begin
(hill(X,v,K,n),d), 0 <--> X
end
assess_identifiability(rn; measured_quantities = [:X])
```
-is currently not possible. Hopefully this will be a supported feature in the future. For now, such expressions will have to be rewritten to not include such exponents. For some cases, e.g. `10^k` this is trivial. However, it is also possible generally (but more involved and often includes introducing additional variables).
+
+is currently not possible. Hopefully this will be a supported feature in the future. For now, such expressions will have to be rewritten to not include such exponents. For some cases, e.g. `10^k` this is trivial. However, it is also possible generally (but more involved and often includes introducing additional variables).
---
+
## [Citation](@id structural_identifiability_citation)
+
If you use this functionality in your research, please cite the following paper to support the authors of the StructuralIdentifiability package:
-```
+
+```bibtex
@article{structidjl,
author = {Dong, R. and Goodbrake, C. and Harrington, H. and Pogudin G.},
title = {Differential Elimination for Dynamical Models via Projections with Applications to Structural Identifiability},
@@ -154,7 +191,9 @@ If you use this functionality in your research, please cite the following paper
```
---
+
## References
+
[^1]: [Guillaume H.A. Joseph et al., *Introductory overview of identifiability analysis: A guide to evaluating whether you have the right type of data for your modeling purpose*, Environmental Modelling & Software (2019).](https://www.sciencedirect.com/science/article/pii/S1364815218307278)
[^2]: [Goodwin B.C., *Oscillatory Behavior in Enzymatic Control Processes*, Advances in Enzyme Regulation (1965).](https://www.sciencedirect.com/science/article/pii/0065257165900671?via%3Dihub)
-[^3]: [Bellu G., et al., *DAISY: A new software tool to test global identifiability of biological and physiological systems*, Computer Methods and Programs in Biomedicine (2007).](https://www.sciencedirect.com/science/article/abs/pii/S0169260707001605)
\ No newline at end of file
+[^3]: [Bellu G., et al., *DAISY: A new software tool to test global identifiability of biological and physiological systems*, Computer Methods and Programs in Biomedicine (2007).](https://www.sciencedirect.com/science/article/abs/pii/S0169260707001605)
diff --git a/docs/src/model_creation/chemistry_related_functionality.md b/docs/src/model_creation/chemistry_related_functionality.md
index f1086f556e..fc8d1254bb 100644
--- a/docs/src/model_creation/chemistry_related_functionality.md
+++ b/docs/src/model_creation/chemistry_related_functionality.md
@@ -1,45 +1,59 @@
# [Chemistry-related Functionality](@id chemistry_functionality)
While Catalyst has primarily been designed around the modelling of biological systems, reaction network models are also common in chemistry. This section describes two types of functionality, that while of general interest, should be especially useful in the modelling of chemical systems.
+
- The `@compound` option, which enables the user to designate that a specific species is composed of certain subspecies.
- The `balance_reaction` function, which enables the user to balance a reaction so the same number of components occur on both sides.
-
## [Modelling with compound species](@id chemistry_functionality_compounds)
### [Creating compound species programmatically](@id chemistry_functionality_compounds_programmatic)
+
We will first show how to create compound species through [programmatic model construction](@ref programmatic_CRN_construction), and then demonstrate using the DSL. To create a compound species, use the `@compound` macro, first designating the compound, followed by its components (and their stoichiometries). In this example, we will create a CO₂ molecule, consisting of one C atom and two O atoms. First, we create species corresponding to the components:
+
```@example chem1
using Catalyst
t = default_t()
@species C(t) O(t)
nothing # hide
```
+
Next, we create the `CO2` compound species:
+
```@example chem1
@compound CO2 ~ C + 2O
```
+
Here, the compound is the first argument to the macro, followed by its component (with the left-hand and right-hand sides separated by a `~` sign). While non-compound species (such as `C` and `O`) have their independent variable (in this case `t`) designated, independent variables are generally not designated for compounds (these are instead directly inferred from their components). Components with non-unitary stoichiometries have this value written before the component (generally, the rules for designating the components of a compound are identical to those of designating the substrates or products of a reaction). The created compound, `CO2`, is also a species, and can be used wherever e.g. `C` can be used:
+
```@example chem1
isspecies(CO2)
```
+
In its metadata, however, is stored information of its components, which can be retrieved using the `components` (returning a vector of its component species) and `coefficients` (returning a vector with each component's stoichiometry) functions:
+
```@example chem1
components(CO2)
```
+
```@example chem1
coefficients(CO2)
```
+
Alternatively, we can retrieve the components and their stoichiometric coefficients as a single vector using the `component_coefficients` function:
+
```@example chem1
component_coefficients(CO2)
```
+
Finally, it is possible to check whether a species is a compound using the `iscompound` function:
+
```@example chem1
iscompound(CO2)
```
Compound components that are also compounds are allowed, e.g. we can create a carbonic acid compound (H₂CO₃) that consists of CO₂ and H₂O:
+
```@example chem1
@species H(t)
@compound H2O ~ 2H + O
@@ -47,6 +61,7 @@ Compound components that are also compounds are allowed, e.g. we can create a ca
```
When multiple compounds are created, they can be created simultaneously using the `@compounds` macro, e.g. the previous code-block can be re-written as:
+
```@example chem1
@species H(t)
@compounds begin
@@ -56,7 +71,9 @@ end
```
### [Creating compound species within the DSL](@id chemistry_functionality_compounds_DSL)
+
It is also possible to declare species as compound species within the `@reaction_network` DSL, using the `@compounds` options:
+
```@example chem1
rn = @reaction_network begin
@species C(t) H(t) O(t)
@@ -68,8 +85,10 @@ rn = @reaction_network begin
(k1,k2), H2O + CO2 <--> H2CO3
end
```
+
When creating compound species using the DSL, it is important to note that *every component must be known to the system as a species, either by being declared using the `@species` or `@compound` options, or by appearing in a reaction*. E.g. the following is not valid
-```julia
+
+```julia
rn = @reaction_network begin
@compounds begin
C2O ~ C + 2O
@@ -79,52 +98,69 @@ rn = @reaction_network begin
(k1,k2), H2O+ CO2 <--> H2CO3
end
```
+
as the components `C`, `H`, and `O` are not declared as species anywhere. Please also note that only `@compounds` can be used as an option in the DSL, not `@compound`.
### [Designating metadata and default values for compounds](@id chemistry_functionality_compounds_metadata)
+
Just like for normal species, it is possible to designate metadata and default values for compounds. Metadata is provided after the compound name, but separated from it by a `,`:
+
```@example chem1
@compound (CO2, [unit="mol"]) ~ C + 2O
nothing # hide
```
+
Default values are designated using `=`, and provided directly after the compound name.:
+
```@example chem1
@compound (CO2 = 2.0) ~ C + 2O
nothing # hide
```
+
If both default values and meta data are provided, the metadata is provided after the default value:
+
```@example chem1
@compound (CO2 = 2.0, [unit="mol"]) ~ C + 2O
nothing # hide
```
+
In all of these cases, the left-hand side must be enclosed within `()`.
### [Compounds with multiple independent variables](@id chemistry_functionality_compounds_mult_ivs)
+
While we generally do not need to specify independent variables for compound, if the components (together) have more than one independent variable, this *must be done*:
+
```@example chem1
t = default_t()
@parameters s
-@species N(s) O(t)
+@species N(s) O(t)
@compound NO2(t,s) ~ N + 2O
```
+
Here, `NO2` depend both on a spatial independent variable (`s`) and a time one (`t`). This is required since, while multiple independent variables can be inferred, their internal order cannot (and must hence be provided by the user).
## Balancing chemical reactions
+
One use of defining a species as a compound is that they can be used to balance reactions so that the number of components are the same on both sides. Catalyst provides the `balance_reaction` function, which takes a reaction, and returns a balanced version. E.g. let us consider a reaction when carbon dioxide is formed from carbon and oxide `C + O --> CO2`. Here, `balance_reaction` enables us to find coefficients creating a balanced reaction (in this case, where the number of carbon and oxygen atoms are the same on both sides). To demonstrate, we first created the unbalanced reactions:
+
```@example chem1
rx = @reaction k, C + O --> $CO2
```
+
Here, the reaction rate (`k`) is not involved in the reaction balancing. We use interpolation for `CO2`, ensuring that the `CO2` used in the reaction is the same one we previously defined as a compound of `C` and `O`. Next, we call the `balance_reaction` function
+
```@example chem1
balance_reaction(rx)
```
+
which correctly finds the (rather trivial) solution `C + 2O --> CO2`. Here we note that `balance_reaction` actually returns a vector. The reason is that, in some cases, the reaction balancing problem does not have a single obvious solution. Typically, a single solution is the obvious candidate (in which case this is the vector's only element). However, when this is not the case, the vector instead contain several reactions (from which a balanced reaction cab be generated).
Let us consider a more elaborate example, the reaction between ammonia (NH₃) and oxygen (O₂) to form nitrogen monoxide (NO) and water (H₂O). Let us first create the components and the unbalanced reaction:
+
```@example chem2
using Catalyst # hide
t = default_t()
-@species N(t) H(t) O(t)
+@species N(t) H(t) O(t)
@compounds begin
NH3 ~ N + 3H
O2 ~ 2O
@@ -133,7 +169,9 @@ t = default_t()
end
unbalanced_reaction = @reaction k, $NH3 + $O2 --> $NO + $H2O
```
+
We can now create a balanced version (where the amount of H, N, and O is the same on both sides):
+
```@example chem2
balanced_reaction = balance_reaction(unbalanced_reaction)[1]
```
@@ -144,7 +182,9 @@ Reactions declared as a part of a `ReactionSystem` (e.g. using the DSL) can be r
Reaction balancing is currently not supported for reactions involving compounds of compounds.
### Balancing full systems
+
It is possible to balance all the reactions of a reaction system simultaneously using the `balance_system` function. Here, the output is a new system, where all reactions are balanced. E.g. We can use it to balance this system of methane formation/combustion:
+
```@example chem2
rs = @reaction_network begin
@species C(t) O(t) H(t)
@@ -160,4 +200,5 @@ rs = @reaction_network begin
end
rs_balanced = balance_system(rs)
```
-Except for the modified reaction stoichiometries, the new system is identical to the previous one.
\ No newline at end of file
+
+Except for the modified reaction stoichiometries, the new system is identical to the previous one.
diff --git a/docs/src/model_creation/compositional_modeling.md b/docs/src/model_creation/compositional_modeling.md
index f565b096c0..2a84688c3c 100644
--- a/docs/src/model_creation/compositional_modeling.md
+++ b/docs/src/model_creation/compositional_modeling.md
@@ -1,4 +1,5 @@
# [Compositional Modeling of Reaction Systems](@id compositional_modeling)
+
Catalyst supports the construction of models in a compositional fashion, based
on ModelingToolkit's subsystem functionality. In this tutorial we'll see how we
can construct the earlier repressilator model by composing together three
@@ -6,16 +7,20 @@ identically repressed genes, and how to use compositional modeling to create
compartments.
## [A note on *completeness*](@id completeness_note)
+
Catalyst `ReactionSystem` can either be *complete* or *incomplete*. When created using the `@reaction_network` DSL they are *created as complete*. Here, only complete `ReactionSystem`s can be used to create the various problem types (e.g. `ODEProblem`). However, only incomplete `ReactionSystem`s can be composed using the features described below. Hence, for compositional modeling, `ReactionSystem` must be created as incomplete, and later set to complete before simulation.
To create a `ReactionSystem`s for use in compositional modeling via the DSL, simply use the `@network_component` macro instead of `@reaction_network`:
+
```@example ex0
using Catalyst
degradation_component = @network_component begin
d, X --> 0
end
```
+
Alternatively one can just build the `ReactionSystem` via the symbolic interface.
+
```@example ex0
@parameters d
t = default_t()
@@ -23,22 +28,28 @@ t = default_t()
rx = Reaction(d, [X], nothing)
@named degradation_component = ReactionSystem([rx], t)
```
+
We can test whether a system is complete using the `ModelingToolkit.iscomplete` function:
+
```@example ex0
ModelingToolkit.iscomplete(degradation_component)
```
+
To mark a system as complete, after which it should be considered as
representing a finalized model, use the `complete` function
+
```@example ex0
degradation_component_complete = complete(degradation_component)
ModelingToolkit.iscomplete(degradation_component_complete)
```
## Compositional modeling tooling
+
Catalyst supports two ModelingToolkit interfaces for composing multiple
[`ReactionSystem`](@ref)s together into a full model. The first mechanism allows
for extending an existing system by merging in a second system via the `extend`
command
+
```@example ex1
using Catalyst
basern = @network_component rn1 begin
@@ -49,6 +60,7 @@ newrn = @network_component rn2 begin
end
@named rn = extend(newrn, basern)
```
+
Here we extended `basern` with `newrn` giving a system with all the
reactions. Note, if a name is not specified via `@named` or the `name` keyword
then `rn` will have the same name as `newrn`.
@@ -56,30 +68,37 @@ then `rn` will have the same name as `newrn`.
The second main compositional modeling tool is the use of subsystems. Suppose we
now add to `basern` two subsystems, `newrn` and `newestrn`, we get a
different result:
+
```@example ex1
newestrn = @network_component rn3 begin
v, A + D --> 2D
end
@named rn = compose(basern, [newrn, newestrn])
```
+
Here we have created a new `ReactionSystem` that adds `newrn` and `newestrn` as
subsystems of `basern`. The variables and parameters in the sub-systems are
considered distinct from those in other systems, and so are namespaced (i.e.
prefaced) by the name of the system they come from.
We can see the subsystems of a given system by
+
```@example ex1
ModelingToolkit.get_systems(rn)
```
+
They naturally form a tree-like structure
+
```julia
using Plots, GraphRecipes
plot(TreePlot(rn), method=:tree, fontsize=12, nodeshape=:ellipse)
```
+

We could also have directly constructed `rn` using the same reaction as in
`basern` as
+
```@example ex1
t = default_t()
@parameters k
@@ -91,30 +110,41 @@ rxs = [Reaction(k, [A,B], [C])]
Catalyst provides several different accessors for getting information from a
single system, or all systems in the tree. To get the species, parameters, and
reactions *only* within a given system (i.e. ignoring subsystems), we can use
+
```@example ex1
Catalyst.get_species(rn)
```
+
```@example ex1
Catalyst.get_ps(rn)
```
+
```@example ex1
Catalyst.get_rxs(rn)
```
+
To see all the species, parameters and reactions in the tree we can use
+
```@example ex1
species(rn) # or unknowns(rn)
```
+
```@example ex1
parameters(rn)
```
+
```@example ex1
reactions(rn) # or equations(rn)
```
+
If we want to collapse `rn` down to a single system with no subsystems we can use
+
```@example ex1
flatrn = Catalyst.flatten(rn)
```
+
where
+
```@example ex1
ModelingToolkit.get_systems(flatrn)
```
@@ -123,9 +153,11 @@ More about ModelingToolkit's interface for compositional modeling can be found
in the [ModelingToolkit docs](http://docs.sciml.ai/ModelingToolkit/stable/).
## Compositional model of the repressilator
+
Let's apply the tooling we've just seen to create the repressilator in a more
modular fashion. We start by defining a function that creates a negatively
repressed gene, taking the repressor as input
+
```@example ex1
function repressed_gene(; R, name)
@network_component $name begin
@@ -137,11 +169,13 @@ function repressed_gene(; R, name)
end
nothing # hide
```
+
Here we assume the user will pass in the repressor species as a ModelingToolkit
variable, and specify a name for the network. We use Catalyst's interpolation
ability to substitute the value of these variables into the DSL (see
[Interpolation of Julia Variables](@ref dsl_advanced_options_symbolics_and_DSL_interpolation)). To make the repressilator we now make
three genes, and then compose them together
+
```@example ex1
t = default_t()
@species G3₊P(t)
@@ -150,10 +184,13 @@ t = default_t()
@named G3 = repressed_gene(; R=ParentScope(G2.P))
@named repressilator = ReactionSystem(t; systems=[G1,G2,G3])
```
+
Notice, in this system each gene is a child node in the system graph of the repressilator
+
```julia
plot(TreePlot(repressilator), method=:tree, fontsize=12, nodeshape=:ellipse)
```
+

In building the repressilator we needed to use two new features. First, we
@@ -166,6 +203,7 @@ that have the same parent as the system being constructed (in this case the
top-level `repressilator` system).
## Compartment-based models
+
Finally, let's see how we can make a compartment-based model. Let's create a
simple eukaryotic gene expression model with negative feedback by protein
dimers. Transcription and gene inhibition by the protein dimer occurs in the
@@ -175,7 +213,7 @@ parameters for the nucleus and cytosol, and assume we are working with species
having units of number of molecules. Rate constants will have their common
concentration units, i.e. if ``V`` denotes the volume of a compartment then
-| Reaction Type | Example | Rate Constant Units | Effective rate constant (units of per time)
+| Reaction Type | Example | Rate Constant Units | Effective rate constant (units of per time) |
|:----------: | :----------: | :----------: |:------------:|
| Zero order | ``\varnothing \overset{\alpha}{\to} A`` | concentration / time | ``\alpha V`` |
| First order | ``A \overset{\beta}{\to} B`` | (time)⁻¹ | ``\beta`` |
@@ -183,6 +221,7 @@ concentration units, i.e. if ``V`` denotes the volume of a compartment then
In our model we'll therefore add the conversions of the last column to properly
account for compartment volumes:
+
```@example ex1
# transcription and regulation
nuc = @network_component nuc begin
@@ -206,8 +245,11 @@ model = @network_component model begin
end
@named model = compose(model, [nuc, cyto])
```
+
A graph of the resulting network is
+
```julia
Graph(model)
```
+

diff --git a/docs/src/model_creation/conservation_laws.md b/docs/src/model_creation/conservation_laws.md
index 5bb77eb649..56c65a7249 100644
--- a/docs/src/model_creation/conservation_laws.md
+++ b/docs/src/model_creation/conservation_laws.md
@@ -1,14 +1,18 @@
# [Working with Conservation Laws](@id conservation_laws)
+
Catalyst can detect, and eliminate for differential-equation based models, *conserved quantities*, i.e. linear combinations of species which are conserved via their chemistry. This functionality is both automatically utilised by Catalyst (e.g. to [remove singular Jacobians during steady state computations](@ref homotopy_continuation_conservation_laws)), but is also available for users to utilise directly (e.g. to potentially [improve simulation performance](@ref ode_simulation_performance_conservation_laws)).
To illustrate conserved quantities, let us consider the following [two-state](@ref basic_CRN_library_two_states) model:
+
```@example conservation_laws
using Catalyst
rs = @reaction_network begin
(k₁,k₂), X₁ <--> X₂
end
```
+
If we simulate it, we note that while the concentrations of $X₁$ and $X₂$ change throughout the simulation, the total concentration of $X$ ($= X₁ + X₂$) is constant:
+
```@example conservation_laws
using OrdinaryDiffEqDefault, Plots
u0 = [:X₁ => 80.0, :X₂ => 20.0]
@@ -17,35 +21,49 @@ oprob = ODEProblem(rs, u0, (0.0, 1.0), ps)
sol = solve(oprob)
plot(sol; idxs = [rs.X₁, rs.X₂, rs.X₁ + rs.X₂], label = ["X₁" "X₂" "X₁ + X₂ (a conserved quantity)"])
```
+
This makes sense, as while $X$ is converted between two different forms ($X₁$ and $X₂$), it is neither produced nor degraded. That is, it is a *conserved quantity*. Next, if we consider the ODE that our model generates:
+
```@example conservation_laws
using Latexify
latexify(rs; form = :ode)
```
+
we note that it essentially generates the same equation twice (i.e. $\frac{dX₁(t)}{dt} = -\frac{dX₂(t)}{dt}$). By designating our conserved quantity $X₁ + X₂ = Γ$, we can rewrite our differential equation model as a [differential-algebraic equation](https://en.wikipedia.org/wiki/Differential-algebraic_system_of_equations) (with a single differential equation and a single algebraic equation):
+
```math
\frac{dX₁(t)}{dt} = - k₁X₁(t) + k₂(-X₁(t) + Γ) \\
X₂(t) = -X₁(t) + Γ
```
+
Using Catalyst, it is possible to detect any such conserved quantities and eliminate them from the system. Here, when we convert our `ReactionSystem` to an `ODESystem`, we provide the `remove_conserved = true` argument to instruct Catalyst to perform this elimination:
+
```@example conservation_laws
osys = convert(ODESystem, rs; remove_conserved = true)
```
+
We note that the output system only contains a single (differential) equation and can hence be solved with an ODE solver. The second (algebraic) equation is stored as an [*observable*](@ref dsl_advanced_options_observables), and can be retrieved using the `observed` function:
+
```@example conservation_laws
observed(osys)
```
+
Using the `unknowns` function we can confirm that the ODE only has a single unknown variable:
+
```@example conservation_laws
unknowns(osys)
```
+
Next, using `parameters` we note that an additional (vector) parameter, `Γ` has been added to the system:
+
```@example conservation_laws
parameters(osys)
```
+
Here, Catalyst encodes all conserved quantities in a single, [vector-valued](@ref dsl_advanced_options_vector_variables), parameter called `Γ`.
Practically, the `remove_conserved = true` argument can be provided when a `ReactionSystem` is converted to an `ODEProblem`:
+
```@example conservation_laws
using OrdinaryDiffEqDefault, Plots
u0 = [:X₁ => 80.0, :X₂ => 20.0]
@@ -53,18 +71,23 @@ ps = [:k₁ => 10.0, :k₂ => 2.0]
oprob = ODEProblem(rs, u0, (0.0, 1.0), ps; remove_conserved = true)
nothing # hide
```
+
Here, while `Γ` becomes a parameter of the new system, it has a [default value](@ref dsl_advanced_options_default_vals) equal to the corresponding conservation law. Hence, its value is computed from the initial condition `[:X₁ => 80.0, :X₂ => 20.0]`, and does not need to be provided in the parameter vector. Next, we can simulate and plot our model using normal syntax:
+
```@example conservation_laws
sol = solve(oprob)
plot(sol)
```
+
!!! note
Any species eliminated using `remove_conserved = true` will not be plotted by default. However, it can be added to the plot using [the `idxs` plotting option](@ref simulation_plotting_options). E.g. here we would use `plot(sol; idxs = [:X₁, :X₂])` to plot both species.
While `X₂` is an observable (and not unknown) of the ODE, we can [access it](@ref simulation_structure_interfacing_problems) just like if `remove_conserved = true` had not been used:
+
```@example conservation_laws
sol[:X₂]
```
+
!!! note
Generally, `remove_conserved = true` should not change any modelling workflows. I.e. anything that works without this option should also work when an `ODEProblem` is created using `remove_conserved = true`.
@@ -77,25 +100,35 @@ sol[:X₂]
## [Conservation law accessor functions](@id conservation_laws_accessors)
For any given `ReactionSystem` model, we can use `conservationlaw_constants` to compute all of a system's conserved quantities:
+
```@example conservation_laws
conservationlaw_constants(rs)
```
+
Next, the `conservedequations` function can be used to compute the algebraic equations produced when a system's conserved quantities are eliminated:
+
```@example conservation_laws
conservedequations(rs)
```
+
Finally, the `conservationlaws` function yields a $m \times n$ matrix, where $n$ is a system's number of species, $m$ its number of conservation laws, and element $(i,j)$ corresponds to the copy number of the $j$th species that occurs in the $i$th conserved quantity:
+
```@example conservation_laws
conservationlaws(rs)
```
+
I.e. in this case we have a single conserved quantity, which contains a single copy each of the system's two species.
## [Updating conservation law values directly](@id conservation_laws_prob_updating)
+
Previously we noted that conservation law elimination adds the conservation law as a system parameter (named $Γ$). E.g. here we find it among the parameters of the ODE model generated by the previously used two-state system:
+
```@example conservation_laws
parameters(convert(ODESystem, rs; remove_conserved = true))
```
+
An advantage of this is that we can set conserved quantities's values directly in simulations. Here we simulate the model while specifying that $X₁ + X₂ = 10.0$ (and while also specifying an initial condition for $X₁$, but none for $X₂$).
+
```@example conservation_laws
u0 = [:X₁ => 6.0]
ps = [:k₁ => 1.0, :k₂ => 2.0, :Γ => [10.0]]
@@ -103,40 +136,48 @@ oprob = ODEProblem(rs, u0, 1.0, ps; remove_conserved = true)
sol = solve(oprob)
nothing # hide
```
-Generally, for each conservation law, one can omit specifying either the conservation law constant, or one initial condition (whichever quantity is missing will be computed from the other ones).
+
+Generally, for each conservation law, one can omit specifying either the conservation law constant, or one initial condition (whichever quantity is missing will be computed from the other ones).
!!! note
As previously mentioned, the conservation law parameter $Γ$ is a [*vector-valued* parameter](@ref dsl_advanced_options_vector_variables) with one value for each conservation law. That is why we above provide its value as a vector (`:Γ => [5.0]`). If there had been multiple conservation laws, we would provide the `:Γ` vector with one value for each of them (e.g. `:Γ => [10.0, 15.0]`).
!!! warn
If you specify the value of a conservation law parameter, you *must not* specify the value of all species of that conservation law (this can result in an error). Instead, the value of exactly one species must be left unspecified.
Just like when we create a problem, if we [update the species (or conservation law parameter) values of `oprob`](@ref simulation_structure_interfacing_problems), the remaining ones will be recomputed to generate an accurate conservation law. E.g. here we create an `ODEProblem`, check the value of the conservation law, and then confirm that its value is updated with $X₁$.
+
```@example conservation_laws
u0 = [:X₁ => 6.0, :X₂ => 4.0]
ps = [:k₁ => 1.0, :k₂ => 2.0]
oprob = ODEProblem(rs, u0, 10.0, ps; remove_conserved = true)
oprob.ps[:Γ][1]
```
+
```@example conservation_laws
oprob = remake(oprob; u0 = [:X₁ => 16.0])
oprob.ps[:Γ][1]
```
+
It is also possible to update the value of $Γ$. Here, as $X₂$ is the species eliminated by the conservation law (which can be checked using `conservedequations`), $X₂$'s value will be modified to ensure that $Γ$'s new value is correct:
+
```@example conservation_laws
oprob = remake(oprob; p = [:Γ => [30.0]])
oprob[:X₂]
```
Generally, for a conservation law where we have:
+
- One (or more) non-eliminated species.
- One eliminated species.
- A conservation law parameter.
-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*.
+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*.
!!! warn
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.
### [Extracting the conservation law parameter's symbolic variable](@id conservation_laws_prob_updating_symvar)
+
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:
+
```@example conservation_laws
Γ = Catalyst.get_networkproperties(rs).conservedconst
-```
\ No newline at end of file
+```
diff --git a/docs/src/model_creation/constraint_equations.md b/docs/src/model_creation/constraint_equations.md
index eeeca03e5f..356ce2eb3b 100644
--- a/docs/src/model_creation/constraint_equations.md
+++ b/docs/src/model_creation/constraint_equations.md
@@ -1,4 +1,5 @@
# [Coupled ODEs, Algebraic Equations, and Events](@id constraint_equations)
+
In many applications one has additional algebraic or differential equations for
non-chemical species that can be coupled to a chemical reaction network model.
Catalyst supports coupled differential and algebraic equations, and currently
@@ -17,9 +18,11 @@ grow indefinitely. We'll also keep track of one protein $P(t)$, which is
produced at a rate proportional $V$ and can be degraded.
## [Coupling ODE constraints via the DSL](@id constraint_equations_dsl)
+
The easiest way to include ODEs and algebraic equations is to just include them
when using the DSL to specify a model. Here we include an ODE for $V(t)$ along
with degradation and production reactions for $P(t)$:
+
```@example ceq1
using Catalyst, OrdinaryDiffEqTsit5, Plots
@@ -43,8 +46,10 @@ rn = @reaction_network growing_cell begin
end
end
```
+
We can now create an `ODEProblem` from our model and solve it to see how $V(t)$
and $P(t)$ evolve in time:
+
```@example ceq1
oprob = ODEProblem(rn, [], (0.0, 1.0))
sol = solve(oprob, Tsit5())
@@ -52,8 +57,10 @@ plot(sol)
```
## Coupling ODE constraints via directly building a `ReactionSystem`
+
As an alternative to the previous approach, we could have also constructed our
`ReactionSystem` all at once using the symbolic interface:
+
```@example ceq2
using Catalyst, OrdinaryDiffEqTsit5, Plots
@@ -74,7 +81,6 @@ sol = solve(oprob, Tsit5())
plot(sol)
```
-
## [Coupling ODE constraints via extending a system](@id constraint_equations_coupling_constraints)
Finally, we could also construct our model by using compositional modeling. Here
@@ -110,6 +116,7 @@ rn = @network_component begin
1.0, P --> 0
end
```
+
Notice, here we interpolated the variable `V` with `$V` to ensure we use the
same symbolic unknown variable in the `rn` as we used in building `osys`. See
the doc section on [interpolation of variables](@ref
@@ -120,6 +127,7 @@ systems together Catalyst requires that the systems have not been marked as
We can now merge the two systems into one complete `ReactionSystem` model using
[`ModelingToolkit.extend`](@ref):
+
```@example ceq2b
@named growing_cell = extend(osys, rn)
growing_cell = complete(growing_cell)
@@ -127,14 +135,15 @@ growing_cell = complete(growing_cell)
We see that the combined model now has both the reactions and ODEs as its
equations. To solve and plot the model we proceed like normal
+
```@example ceq2b
oprob = ODEProblem(growing_cell, [], (0.0, 1.0))
sol = solve(oprob, Tsit5())
plot(sol)
```
-
## [Adding events](@id constraint_equations_events)
+
Our current model is unrealistic in assuming the cell will grow exponentially
forever. Let's modify it such that the cell divides in half every time its
volume reaches a size of `2`. We also assume we lose half of the protein upon
@@ -170,26 +179,30 @@ rn = @reaction_network growing_cell begin
end
# every 1.0 time unit we half the volume of the cell and the number of proteins
- @continuous_events begin
+ @continuous_events begin
[V ~ 2.0] => [V ~ V/2, P ~ P/2]
end
end
```
+
We can now create and simulate our model
+
```@example ceq3a
oprob = ODEProblem(rn, [], (0.0, 10.0))
sol = solve(oprob, Tsit5())
plot(sol)
Catalyst.PNG(plot(sol; fmt = :png, dpi = 200)) # hide
```
+
We can also model discrete events. Here at a time `switch_time` we will set the parameter `k_on` to be
zero:
+
```@example ceq3a
rn = @reaction_network param_off_ex begin
@parameters switch_time
k_on, A --> B
k_off, B --> A
-
+
@discrete_events begin
(t == switch_time) => [k_on ~ 0.0]
end
@@ -202,6 +215,7 @@ oprob = ODEProblem(rn, u0, tspan, p)
sol = solve(oprob, Tsit5(); tstops = 2.0)
plot(sol)
```
+
Note that for discrete events we need to set a stop time via `tstops` so that
the ODE solver can step exactly to the specific time of our event. In the
previous example we just manually set the numeric value of the parameter in the
@@ -209,20 +223,24 @@ previous example we just manually set the numeric value of the parameter in the
the value of the parameter from `oprob` and pass this numeric value. This helps
ensure consistency between the value passed via `p` and/or symbolic defaults and
what we pass as a `tstop` to `solve`. We can do this as
+
```@example ceq3a
oprob = ODEProblem(rn, u0, tspan, p)
switch_time_val = oprob.ps[:switch_time]
sol = solve(oprob, Tsit5(); tstops = switch_time_val)
plot(sol)
```
+
For a detailed discussion on how to directly use the lower-level but more
flexible DifferentialEquations.jl event/callback interface, see the
[tutorial](https://docs.sciml.ai/Catalyst/stable/catalyst_applications/advanced_simulations/#Event-handling-using-callbacks)
on event handling using callbacks.
## [Adding events via the symbolic interface](@id constraint_equations_events_symbolic)
+
Let's repeat the previous two models using the symbolic interface. We first
create our equations and unknowns/species again
+
```@example ceq3
using Catalyst, OrdinaryDiffEqTsit5, Plots
t = default_t()
@@ -235,12 +253,16 @@ eq = D(V) ~ λ * V
rx1 = @reaction $V, 0 --> $P
rx2 = @reaction 1.0, $P --> 0
```
+
Before creating our `ReactionSystem` we make the event.
+
```@example ceq3
# every 1.0 time unit we half the volume of the cell and the number of proteins
continuous_events = [V ~ 2.0] => [V ~ V/2, P ~ P/2]
```
+
We can now create and simulate our model
+
```@example ceq3
@named rs = ReactionSystem([rx1, rx2, eq], t; continuous_events)
rs = complete(rs)
@@ -250,9 +272,11 @@ sol = solve(oprob, Tsit5())
plot(sol)
Catalyst.PNG(plot(sol; fmt = :png, dpi = 200)) # hide
```
+
We can again also model discrete events. Similar to our example with continuous
events, we start by creating reaction equations, parameters, variables, and
unknowns.
+
```@example ceq3
t = default_t()
@parameters k_on switch_time k_off
@@ -260,7 +284,9 @@ t = default_t()
rxs = [(@reaction k_on, A --> B), (@reaction k_off, B --> A)]
```
+
Now we add an event such that at time `t` (`switch_time`), `k_on` is set to zero.
+
```@example ceq3
discrete_events = (t == switch_time) => [k_on ~ 0.0]
@@ -268,7 +294,9 @@ u0 = [:A => 10.0, :B => 0.0]
tspan = (0.0, 4.0)
p = [k_on => 100.0, switch_time => 2.0, k_off => 10.0]
```
+
Simulating our model,
+
```@example ceq3
@named rs2 = ReactionSystem(rxs, t, [A, B], [k_on, k_off, switch_time]; discrete_events)
rs2 = complete(rs2)
diff --git a/docs/src/model_creation/dsl_advanced.md b/docs/src/model_creation/dsl_advanced.md
index 3aeb468132..b3e2266046 100644
--- a/docs/src/model_creation/dsl_advanced.md
+++ b/docs/src/model_creation/dsl_advanced.md
@@ -1,25 +1,33 @@
# [The Catalyst DSL - Advanced Features and Options](@id dsl_advanced_options)
-Within the Catalyst DSL, each line can represent either *a reaction* or *an option*. The [previous DSL tutorial](@ref dsl_description) described how to create reactions. This one will focus on options. These are typically used to supply a model with additional information. Examples include the declaration of initial condition/parameter default values, or the creation of observables.
-All option designations begin with a declaration starting with `@`, followed by its input. E.g. the `@observables` option allows for the generation of observables. Each option can only be used once within each use of `@reaction_network`. This tutorial will also describe some additional advanced DSL features that do not involve using an option.
+Within the Catalyst DSL, each line can represent either *a reaction* or *an option*. The [previous DSL tutorial](@ref dsl_description) described how to create reactions. This one will focus on options. These are typically used to supply a model with additional information. Examples include the declaration of initial condition/parameter default values, or the creation of observables.
+
+All option designations begin with a declaration starting with `@`, followed by its input. E.g. the `@observables` option allows for the generation of observables. Each option can only be used once within each use of `@reaction_network`. This tutorial will also describe some additional advanced DSL features that do not involve using an option.
As a first step, we import Catalyst (which is required to run the tutorial):
+
```@example dsl_advanced_explicit_definitions
using Catalyst
```
## [Explicit specification of network species and parameters](@id dsl_advanced_options_declaring_species_and_parameters)
+
Previously, we mentioned that the DSL automatically determines which symbols correspond to species and which to parameters. This is done by designating everything that appears as either a substrate or a product as a species, and all remaining quantities as parameters (i.e. those only appearing within rates or [stoichiometric constants](@ref dsl_description_stoichiometries_parameters)). Sometimes, one might want to manually override this default behaviour for a given symbol. I.e. consider the following model, where the conversion of a protein `P` from its inactive form (`Pᵢ`) to its active form (`Pₐ`) is catalysed by an enzyme (`E`). Using the most natural description:
+
```@example dsl_advanced_explicit_definitions
catalysis_sys = @reaction_network begin
k*E, Pᵢ --> Pₐ
end
```
+
`E` (as well as `k`) will be considered a parameter, something we can confirm directly:
+
```@example dsl_advanced_explicit_definitions
parameters(catalysis_sys)
```
+
If we want `E` to be considered a species, we can designate this using the `@species` option:
+
```@example dsl_advanced_explicit_definitions
catalysis_sys = @reaction_network begin
@species E(t)
@@ -27,24 +35,28 @@ catalysis_sys = @reaction_network begin
end
parameters(catalysis_sys)
```
+
!!! note
When declaring species using the `@species` option, the species symbol must be followed by `(t)`. The reason is that species are time-dependent variables, and this time-dependency must be explicitly specified ([designation of non-`t` dependant species is also possible](@ref dsl_advanced_options_ivs)).
Similarly, the `@parameters` option can be used to explicitly designate something as a parameter:
+
```@example dsl_advanced_explicit_definitions
catalysis_sys = @reaction_network begin
@parameters k
k*E, Pᵢ --> Pₐ
end
```
+
Here, while `k` is explicitly defined as a parameter, no information is provided about `E`. Hence, the default case will be used (setting `E` to a parameter). The `@species` and `@parameter` options can be used simultaneously (although a quantity cannot be declared *both* as a species and a parameter). They may be followed by a full list of all species/parameters, or just a subset.
While designating something which would default to a parameter as a species is straightforward, the reverse (creating a parameter which occurs as a substrate or product) is more involved. This is, however, possible, and described [here](@ref dsl_advanced_options_constant_species).
Rather than listing all species/parameters on a single line after the options, a `begin ... end` block can be used (listing one species/parameter on each line). E.g. in the following example we use this notation to explicitly designate all species and parameters of the system:
+
```@example dsl_advanced_explicit_definitions
catalysis_sys = @reaction_network begin
- @species begin
+ @species begin
E(t)
Pᵢ(t)
Pₐ(t)
@@ -57,6 +69,7 @@ end
```
A side-effect of using the `@species` and `@parameter` options is that they specify *the order in which the species and parameters are stored*. I.e. lets check the order of the parameters in the parameters in the following dimerisation model:
+
```@example dsl_advanced_explicit_definitions
dimerisation = @reaction_network begin
(p,d), 0 <--> X
@@ -64,7 +77,9 @@ dimerisation = @reaction_network begin
end
parameters(dimerisation)
```
+
The default order is typically equal to the order with which the parameters (or species) are encountered in the DSL (this is, however, not guaranteed). If we specify the parameters using `@parameters`, the order used within the option is used instead:
+
```@example dsl_advanced_explicit_definitions
dimerisation = @reaction_network begin
@parameters kB kD p d
@@ -73,13 +88,15 @@ dimerisation = @reaction_network begin
end
parameters(dimerisation)
```
+
!!! danger
- Generally, Catalyst and the SciML ecosystem *do not* guarantee that parameter and species order are preserved throughout various operations on a model. Writing programs that depend on these orders is *strongly discouraged*. There are, however, some legacy packages which still depend on order (one example can be found [here](@ref optimization_parameter_fitting_basics)). In these situations, this might be useful. However, in these cases, it is recommended that the user is extra wary, and also checks the order manually.
+ Generally, Catalyst and the SciML ecosystem *do not* guarantee that parameter and species order are preserved throughout various operations on a model. Writing programs that depend on these orders is *strongly discouraged*. There are, however, some legacy packages which still depend on order (one example can be found [here](@ref optimization_parameter_fitting_basics)). In these situations, this might be useful. However, in these cases, it is recommended that the user is extra wary, and also checks the order manually.
!!! note
The syntax of the `@species` and `@parameters` options is identical to that used by the `@species` and `@parameters` macros [used in programmatic modelling in Catalyst](@ref programmatic_CRN_construction) (for e.g. designating metadata or initial conditions). Hence, if one has learnt how to specify species/parameters using either approach, that knowledge can be transferred to the other one.
Generally, there are four main reasons for specifying species/parameters using the `@species` and `@parameters` options:
+
1. To designate a quantity, that would otherwise have defaulted to a parameter, as a species.
2. To designate default values for parameters/species initial conditions (described [here](@ref dsl_advanced_options_default_vals)).
3. To designate metadata for species/parameters (described [here](@ref dsl_advanced_options_species_and_parameters_metadata)).
@@ -89,7 +106,9 @@ Generally, there are four main reasons for specifying species/parameters using t
Catalyst's DSL automatically infer species and parameters from the input. However, it only does so for *quantities that appear in reactions*. Until now this has not been relevant. However, this tutorial will demonstrate cases where species/parameters that are not part of reactions are used. These *must* be designated using either the `@species` or `@parameters` options (or the `@variables` option, which is described [later](@ref constraint_equations)).
### [Setting default values for species and parameters](@id dsl_advanced_options_default_vals)
+
When declaring species/parameters using the `@species` and `@parameters` options, one can also assign them default values (by appending them with `=` followed by the desired default value). E.g here we set `X`'s default initial condition value to $1.0$, and `p` and `d`'s default values to $1.0$ and $0.2$, respectively:
+
```@example dsl_advanced_defaults
using Catalyst # hide
rn = @reaction_network begin
@@ -98,7 +117,9 @@ rn = @reaction_network begin
(p,d), 0 <--> X
end
```
+
Next, if we simulate the model, we do not need to provide values for species or parameters that have default values. In this case all have default values, so both `u0` and `ps` can be empty vectors:
+
```@example dsl_advanced_defaults
using OrdinaryDiffEqDefault, Plots
u0 = []
@@ -108,7 +129,9 @@ oprob = ODEProblem(rn, u0, tspan, p)
sol = solve(oprob)
plot(sol)
```
+
It is still possible to provide values for some (or all) initial conditions/parameters in `u0`/`ps` (in which case these overrides the default values):
+
```@example dsl_advanced_defaults
u0 = [:X => 4.0]
p = [:d => 0.5]
@@ -116,7 +139,9 @@ oprob = ODEProblem(rn, u0, tspan, p)
sol = solve(oprob)
plot(sol)
```
+
It is also possible to declare a model with default values for only some initial conditions/parameters:
+
```@example dsl_advanced_defaults
using Catalyst # hide
rn = @reaction_network begin
@@ -132,7 +157,9 @@ plot(sol)
```
### [Setting parametric initial conditions](@id dsl_advanced_options_parametric_initial_conditions)
-In the previous section, we designated default values for initial conditions and parameters. However, the right-hand side of the designation accepts any valid expression (not only numeric values). While this can be used to set up some advanced default values, the most common use case is to designate a species's initial condition as a parameter. E.g. in the following example we represent the initial condition of `X` using the parameter `X₀`.
+
+In the previous section, we designated default values for initial conditions and parameters. However, the right-hand side of the designation accepts any valid expression (not only numeric values). While this can be used to set up some advanced default values, the most common use case is to designate a species's initial condition as a parameter. E.g. in the following example we represent the initial condition of `X` using the parameter `X₀`.
+
```@example dsl_advanced_defaults
rn = @reaction_network begin
@species X(t)=X₀
@@ -140,7 +167,9 @@ rn = @reaction_network begin
(p,d), 0 <--> X
end
```
+
Please note that as the parameter `X₀` does not occur as part of any reactions, Catalyst's DSL cannot infer whether it is a species or a parameter. This must hence be explicitly declared. We can now simulate our model while providing `X`'s value through the `X₀` parameter:
+
```@example dsl_advanced_defaults
using OrdinaryDiffEqTsit5
u0 = []
@@ -149,7 +178,9 @@ oprob = ODEProblem(rn, u0, tspan, p)
sol = solve(oprob, Tsit5())
plot(sol)
```
+
It is still possible to designate $X$'s value in `u0`, in which case this overrides the default value.
+
```@example dsl_advanced_defaults
u0 = [:X => 0.5]
p = [:X₀ => 1.0, :p => 1.0, :d => 0.5]
@@ -157,12 +188,15 @@ oprob = ODEProblem(rn, u0, tspan, p)
sol = solve(oprob, Tsit5())
plot(sol)
```
+
Please note that `X₀` is still a parameter of the system, and as such its value must still be designated to simulate the model (even if it is not actually used).
### [Designating metadata for species and parameters](@id dsl_advanced_options_species_and_parameters_metadata)
-Catalyst permits the user to define *metadata* for species and parameters. This permits the user to assign additional information to these, which can be used for a variety of purposes. Some Catalyst features depend on using metadata (with each such case describing specifically how this is done). Here we will introduce how to set metadata, and describe some common metadata types.
+
+Catalyst permits the user to define *metadata* for species and parameters. This permits the user to assign additional information to these, which can be used for a variety of purposes. Some Catalyst features depend on using metadata (with each such case describing specifically how this is done). Here we will introduce how to set metadata, and describe some common metadata types.
Whenever a species/parameter is declared using the `@species`/`@parameters` options, it can be followed by a `[]` within which the metadata is given. Each metadata entry consists of the metadata's name, followed by a `=`, followed by its value. E.g. the `description` metadata allows you to attach a [`String`](https://docs.julialang.org/en/v1/base/strings/) to a species/parameter. Here we create a simple model where we add descriptions to all species and parameters.
+
```@example dsl_advanced_metadata
using Catalyst # hide
two_state_system = @reaction_network begin
@@ -171,7 +205,9 @@ two_state_system = @reaction_network begin
(ka,kD), Xᵢ <--> Xₐ
end
```
+
A metadata can be given to only a subset of a system's species/parameters, and a quantity can be given several metadata entries. To give several metadata, separate each by a `,`. Here we only provide a description for `kA`, for which we also provide a [`bounds` metadata](https://docs.sciml.ai/ModelingToolkit/dev/basics/Variable_metadata/#Bounds),
+
```@example dsl_advanced_metadata
two_state_system = @reaction_network begin
@parameters kA [description="Activation rate", bounds=(0.01,10.0)]
@@ -180,6 +216,7 @@ end
```
It is possible to add both default values and metadata to a parameter/species. In this case, first provide the default value, and next the metadata. I.e. to in the above example set $kA$'s default value to $1.0$ we use
+
```@example dsl_advanced_metadata
two_state_system = @reaction_network begin
@parameters kA=1.0 [description="Activation rate", bounds=(0.01,10.0)]
@@ -188,6 +225,7 @@ end
```
When designating metadata for species/parameters in `begin ... end` blocks the syntax changes slightly. Here, a `,` must be inserted before the metadata (but after any potential default value). I.e. a version of the previous example can be written as
+
```@example dsl_advanced_metadata
two_state_system = @reaction_network begin
@parameters begin
@@ -199,12 +237,15 @@ end
```
Each metadata has its own getter functions. E.g. we can get the description of the parameter `kA` using `ModelingToolkit.getdescription`:
+
```@example dsl_advanced_metadata
ModelingToolkit.getdescription(two_state_system.kA)
```
### [Designating constant-valued/fixed species parameters](@id dsl_advanced_options_constant_species)
+
Catalyst enables the designation of parameters as `constantspecies`. These parameters can be used as species in reactions, however, their values are not changed by the reaction and remain constant throughout the simulation (unless changed by e.g. the [occurrence of an event](@ref constraint_equations_events). Practically, this is done by setting the parameter's `isconstantspecies` metadata to `true`. Here, we create a simple reaction where the species `X` is converted to `Xᴾ` at rate `k`. By designating `X` as a constant species parameter, we ensure that its quantity is unchanged by the occurrence of the reaction.
+
```@example dsl_advanced_constant_species
using Catalyst # hide
rn = @reaction_network begin
@@ -212,11 +253,15 @@ rn = @reaction_network begin
k, X --> Xᴾ
end
```
+
We can confirm that $Xᴾ$ is the only species of the system:
+
```@example dsl_advanced_constant_species
species(rn)
```
+
Here, the produced model is actually identical to if $X$ had simply been a parameter in the reaction's rate:
+
```@example dsl_advanced_constant_species
rn = @reaction_network begin
k*X, 0 --> Xᴾ
@@ -226,7 +271,9 @@ end
A common use-case for constant species is when modelling systems where some species are present in such surplus that their amounts the reactions' effect on it is negligible. A system which is commonly modelled this way is the [Brusselator](https://en.wikipedia.org/wiki/Brusselator).
### [Designating parameter types](@id dsl_advanced_options_parameter_types)
+
Sometimes it is desired to designate that a parameter should have a specific [type](https://docs.julialang.org/en/v1/manual/types/). When supplying this parameter's value to e.g. an `ODEProblem`, that parameter will then be restricted to that specific type. Designating a type is done by appending the parameter with `::` followed by its type. E.g. in the following example we specify that the parameter `n` (the number of `X` molecules in the `Xn` polymer) must be an integer (`Int64`)
+
```@example dsl_advanced_parameter_types
using Catalyst # hide
polymerisation_network = @reaction_network begin
@@ -235,13 +282,16 @@ polymerisation_network = @reaction_network begin
end
nothing # hide
```
+
Generally, when simulating models with mixed parameter types, it is recommended to [declare parameter values as tuples, rather than vectors](@ref simulation_intro_ODEs_input_forms), e.g.:
+
```@example dsl_advanced_parameter_types
ps = (:kB => 0.2, :kD => 1.0, :n => 2)
nothing # hide
```
If a parameter has a type, metadata, and a default value, they are designated in the following order:
+
```@example dsl_advanced_parameter_types
polymerisation_network = @reaction_network begin
@parameters n::Int64 = 2 [description="Parameter n, an integer with defaults value 2."]
@@ -251,7 +301,9 @@ nothing # hide
```
### [Vector-valued species or parameters](@id dsl_advanced_options_vector_variables)
+
Sometimes, one wishes to declare a large number of similar parameters or species. This can be done by *creating them as vectors*. E.g. below we create a [two-state system](@ref basic_CRN_library_two_states). However, instead of declaring `X1` and `X2` (and `k1` and `k2`) as separate entities, we declare them as vectors:
+
```@example dsl_advanced_vector_variables
using Catalyst # hide
two_state_model = @reaction_network begin
@@ -260,7 +312,9 @@ two_state_model = @reaction_network begin
(k[1],k[2]), X[1] <--> X[2]
end
```
+
Now, we can also declare our initial conditions and parameter values as vectors as well:
+
```@example dsl_advanced_vector_variables
using OrdinaryDiffEqDefault, Plots # hide
u0 = [:X => [0.0, 2.0]]
@@ -272,7 +326,8 @@ plot(sol)
```
### [Turning off species, parameter, and variable inference](@id dsl_advanced_options_require_dec)
-In some cases it may be desirable for Catalyst to not infer species and parameters from the DSL, as in the case of reaction networks with very many variables, or as a sanity check that variable names are written correctly. To turn off inference, simply use the `@require_declaration` option when using the `@reaction_network` DSL. This will require every single variable, species, or parameter used within the DSL to be explicitly declared using the `@variable`, `@species`, or `@parameter` options. In the case that the DSL parser encounters an undeclared symbolic, it will error with an `UndeclaredSymbolicError` and print the reaction or equation that the undeclared symbolic was found in.
+
+In some cases it may be desirable for Catalyst to not infer species and parameters from the DSL, as in the case of reaction networks with very many variables, or as a sanity check that variable names are written correctly. To turn off inference, simply use the `@require_declaration` option when using the `@reaction_network` DSL. This will require every single variable, species, or parameter used within the DSL to be explicitly declared using the `@variable`, `@species`, or `@parameter` options. In the case that the DSL parser encounters an undeclared symbolic, it will error with an `UndeclaredSymbolicError` and print the reaction or equation that the undeclared symbolic was found in.
```julia
using Catalyst
@@ -281,13 +336,17 @@ rn = @reaction_network begin
(k1, k2), A <--> B
end
```
-Running the code above will yield the following error:
-```
+
+Running the code above will yield the following error:
+
+```julia
LoadError: UndeclaredSymbolicError: Unrecognized variables A detected in reaction expression: "((k1, k2), A <--> B)". Since the flag @require_declaration is declared, all species must be explicitly declared with the @species macro.
```
+
In order to avoid the error in this case all the relevant species and parameters will have to be declared.
+
```@example dsl_advanced_require_dec
-# The following case will not error.
+# The following case will not error.
using Catalyst
t = default_t()
rn = @reaction_network begin
@@ -299,6 +358,7 @@ end
```
The following cases in which the DSL would normally infer variables will all throw errors if `@require_declaration` is set and the variables are not explicitly declared.
+
- Occurrence of an undeclared species in a reaction, as in the example above.
- Occurrence of an undeclared parameter in a reaction rate expression, as in the reaction line `k*n, A --> B`.
- Occurrence of an undeclared parameter in the stoichiometry of a species, as in the reaction line `k, n*A --> B`.
@@ -307,7 +367,9 @@ The following cases in which the DSL would normally infer variables will all thr
- Occurrence of an undeclared [observable](@ref dsl_advanced_options_observables) in an `@observables` expression, such as `@observables Xtot ~ X1 + X2`.
## [Naming reaction networks](@id dsl_advanced_options_naming)
+
Each reaction network model has a name. It can be accessed using the `nameof` function. By default, some generic name is used:
+
```@example dsl_advanced_names
using Catalyst # hide
rn = @reaction_network begin
@@ -315,7 +377,9 @@ rn = @reaction_network begin
end
nameof(rn)
```
+
A specific name can be given as an argument between the `@reaction_network` and the `begin`. E.g. to name a network `my_network` we can use:
+
```@example dsl_advanced_names
rn = @reaction_network my_network begin
(p,d), 0 <--> X
@@ -324,6 +388,7 @@ nameof(rn)
```
A consequence of generic names being used by default is that networks, even if seemingly identical, by default are not. E.g.
+
```@example dsl_advanced_names
rn1 = @reaction_network begin
(p,d), 0 <--> X
@@ -333,11 +398,15 @@ rn2 = @reaction_network begin
end
rn1 == rn2
```
+
The reason can be confirmed by checking that their respective (randomly generated) names are different:
+
```@example dsl_advanced_names
nameof(rn1) == nameof(rn2)
```
+
By designating the networks to have the same name, however, identity is achieved.
+
```@example dsl_advanced_names
rn1 = @reaction_network my_network begin
(p,d), 0 <--> X
@@ -347,14 +416,17 @@ rn2 = @reaction_network my_network begin
end
rn1 == rn2
```
+
If you wish to check for identity, and wish that models that have different names but are otherwise identical, should be considered equal, you can use the [`isequivalent`](@ref) function.
Setting model names is primarily useful for [hierarchical modelling](@ref compositional_modeling), where network names are appended to the display names of subnetworks' species and parameters.
## [Creating observables](@id dsl_advanced_options_observables)
+
Sometimes one might want to use observable variables. These are variables with values that can be computed directly from a system's state (rather than having their values implicitly given by reactions or equations). Observables can be designated using the `@observables` option. Here, the `@observables` option is followed by a `begin ... end` block with one line for each observable. Each line first gives the observable, followed by a `~` (*not* a `=`!), followed by an expression describing how to compute it.
Let us consider a model where two species (`X` and `Y`) can bind to form a complex (`XY`, which also can dissociate back into `X` and `Y`). If we wish to create a representation for the total amount of `X` and `Y` in the system, we can do this by creating observables `Xtot` and `Ytot`:
+
```@example dsl_advanced_observables
using Catalyst # hide
rn = @reaction_network begin
@@ -365,7 +437,9 @@ rn = @reaction_network begin
(kB,kD), X + Y <--> XY
end
```
+
We can now simulate our model using normal syntax (initial condition values for observables should not, and can not, be provided):
+
```@example dsl_advanced_observables
using OrdinaryDiffEqTsit5
u0 = [:X => 1.0, :Y => 2.0, :XY => 0.0]
@@ -376,20 +450,25 @@ sol = solve(oprob, Tsit5())
nothing # hide
```
-Next, we can use [symbolic indexing](@ref simulation_structure_interfacing) of our solution object, but with the observable as input. E.g. we can use
+Next, we can use [symbolic indexing](@ref simulation_structure_interfacing) of our solution object, but with the observable as input. E.g. we can use
+
```@example dsl_advanced_observables
sol[:Xtot]
nothing # hide
```
+
to get a vector with `Xtot`'s value throughout the simulation. We can also use
+
```@example dsl_advanced_observables
using Plots
plot(sol; idxs = :Xtot)
plot!(ylimit = (minimum(sol[:Xtot])*0.95, maximum(sol[:Xtot])*1.05)) # hide
```
+
to plot the observables (rather than the species).
Observables can be defined using complicated expressions containing species, parameters, and [variables](@ref constraint_equations) (but not other observables). In the following example (which uses a [parametric stoichiometry](@ref dsl_description_stoichiometries_parameters)) `X` polymerises to form a complex `Xn` containing `n` copies of `X`. Here, we create an observable describing the total number of `X` molecules in the system:
+
```@example dsl_advanced_observables
rn = @reaction_network begin
@observables Xtot ~ X + n*Xn
@@ -397,10 +476,12 @@ rn = @reaction_network begin
end
nothing # hide
```
+
!!! note
If only a single observable is declared, the `begin ... end` block is not required and the observable can be declared directly after the `@observables` option.
[Metadata](@ref dsl_advanced_options_species_and_parameters_metadata) can be supplied to an observable directly after its declaration (but before its formula). If so, the metadata must be separated from the observable with a `,`, and the observable plus the metadata encapsulated by `()`. E.g. to add a [description metadata](@ref dsl_advanced_options_species_and_parameters_metadata) to our observable we can use
+
```@example dsl_advanced_observables
rn = @reaction_network begin
@observables (Xtot, [description="The total amount of X in the system."]) ~ X + n*Xn
@@ -410,16 +491,18 @@ nothing # hide
```
Observables are by default considered [variables](@ref constraint_equations) (not species). To designate them as a species, they can be pre-declared using the `@species` option. I.e. Here `Xtot` becomes a species:
+
```@example dsl_advanced_observables
rn = @reaction_network begin
@species Xtot(t)
- @observables Xtot ~ X + n*Xn
+ @observables Xtot ~ X + n*Xn
(kB,kD), n*X <--> Xn
end
nothing # hide
```
Some final notes regarding observables:
+
- The left-hand side of the observable declaration must contain a single symbol only (with the exception of metadata, which can also be supplied).
- All quantities appearing on the right-hand side must be declared elsewhere within the `@reaction_network` call (either by being part of a reaction, or through the `@species`, `@parameters`, or `@variables` options).
- Observables may not depend on other observables.
@@ -428,6 +511,7 @@ Some final notes regarding observables:
## [Specifying non-time independent variables](@id dsl_advanced_options_ivs)
Catalyst's `ReactionSystem` models depend on a *time independent variable*, and potentially one or more *spatial independent variables*. By default, the independent variable `t` is used. We can declare another independent variable (which is automatically used as the default one) using the `@ivs` option. E.g. to use `τ` instead of `t` we can use
+
```@example dsl_advanced_ivs
using Catalyst # hide
rn = @reaction_network begin
@@ -436,12 +520,15 @@ rn = @reaction_network begin
end
nothing # hide
```
+
We can confirm that `Xᵢ` and `Xₐ` depend on `τ` (and not `t`):
+
```@example dsl_advanced_ivs
species(rn)
```
It is possible to designate several independent variables using `@ivs`. If so, the first one is considered the default (time) independent variable, while the following one(s) are considered spatial independent variable(s). If we want some species to depend on a non-default independent variable, this has to be explicitly declared:
+
```@example dsl_advanced_ivs
rn = @reaction_network begin
@ivs τ x
@@ -451,7 +538,9 @@ rn = @reaction_network begin
end
species(rn)
```
+
It is also possible to have species which depends on several independent variables:
+
```@example dsl_advanced_ivs
rn = @reaction_network begin
@ivs t x
@@ -464,9 +553,10 @@ species(rn)
!!! note
Setting spatial independent variables is primarily intended for the modelling of spatial systems on continuous domains. Catalyst's support for this is currently under development. Hence, the utility of specifying spatial independent variables is limited.
-
## [Setting reaction metadata](@id dsl_advanced_options_reaction_metadata)
+
It is possible to supply reactions with *metadata*, containing some additional information of the reaction. A reaction's metadata follows after its declaration (first using the metadata's name, then a `=`, then its value) and is encapsulated by `[]` (where individual entries are separated by `,`). Here, we add a `description` metadata to the reactions of a birth-death process:
+
```@example dsl_advanced_reaction_metadata
using Catalyst # hide
bd_model = @reaction_network begin
@@ -477,6 +567,7 @@ nothing # hide
```
When [bundling reactions](@ref dsl_description_reaction_bundling), reaction metadata can be bundled using the same rules as rates. Bellow we re-declare our birth-death process, but on a single line:
+
```@example dsl_advanced_reaction_metadata
bd_model = @reaction_network begin
(p,d), 0 <--> X, ([description="Production reaction"], [description="Degradation reaction"])
@@ -485,6 +576,7 @@ nothing # hide
```
Here we declare a model where we also provide a `misc` metadata (which can hold any quantity we require) to our birth reaction:
+
```@example dsl_advanced_reaction_metadata
bd_model = @reaction_network begin
p, 0 --> X, [description="Production reaction", misc=:value]
@@ -494,6 +586,7 @@ nothing # hide
```
A reaction's metadata can be accessed using specific functions, e.g. `Catalyst.hasdescription` and `Catalyst.getdescription` can be used to check if a reaction have a description metadata, and to retrieve it, respectively:
+
```@example dsl_advanced_reaction_metadata
rx = @reaction p, 0 --> X, [description="A production reaction"]
Catalyst.getdescription(rx)
@@ -502,10 +595,13 @@ Catalyst.getdescription(rx)
A list of all available reaction metadata can be found [in the api](@ref api_rx_metadata).
## [Working with symbolic variables and the DSL](@id dsl_advanced_options_symbolics_and_DSL)
+
We have previously described how Catalyst represents its models symbolically (enabling e.g. symbolic differentiation of expressions stored in models). While Catalyst utilises this for many internal operation, these symbolic representations can also be accessed and harnessed by the user. Primarily, doing so is much easier during programmatic (as opposed to DSL-based) modelling. Indeed, the section on [programmatic modelling](@ref programmatic_CRN_construction) goes into more details about symbolic representation in models, and how these can be used. It is, however, also ways to utilise these methods during DSL-based modelling. Below we briefly describe two methods for doing so.
### [Using `@unpack` to extract symbolic variables from `ReactionSystem`s](@id dsl_advanced_options_symbolics_and_DSL_unpack)
+
Let us consider a simple [birth-death process](@ref basic_CRN_library_bd) created using the DSL:
+
```@example dsl_advanced_programmatic_unpack
using Catalyst # hide
bd_model = @reaction_network begin
@@ -513,16 +609,22 @@ bd_model = @reaction_network begin
end
nothing # hide
```
+
Since we have not explicitly declared `p`, `d`, and `X` using `@parameters` and `@species`, we cannot represent these symbolically (only using `Symbol`s). If we wish to do so, however, we can fetch these into our current scope using the `@unpack` macro:
+
```@example dsl_advanced_programmatic_unpack
@unpack p, d, X = bd_model
nothing # hide
```
+
This lists first the quantities we wish to fetch (does not need to be the model's full set of parameters and species), then `=`, followed by the model variable. `p`, `d` and `X` are now symbolic variables in the current scope, just as if they had been declared using `@parameters` or `@species`. We can confirm this:
+
```@example dsl_advanced_programmatic_unpack
X
```
+
Next, we can now use these to e.g. designate initial conditions and parameter values for model simulations:
+
```@example dsl_advanced_programmatic_unpack
using OrdinaryDiffEqDefault, Plots # hide
u0 = [X => 0.1]
@@ -537,9 +639,11 @@ plot(sol)
Just like when using `@parameters` and `@species`, `@unpack` will overwrite any variables in the current scope which share name with the imported quantities.
### [Interpolating variables into the DSL](@id dsl_advanced_options_symbolics_and_DSL_interpolation)
-Catalyst's DSL allows Julia variables to be interpolated for the network name, within rate constant expressions, or for species/stoichiometries within reactions. Using the lower-level symbolic interface we can then define symbolic variables and parameters outside of `@reaction_network`, which can then be used within expressions in the DSL.
+
+Catalyst's DSL allows Julia variables to be interpolated for the network name, within rate constant expressions, or for species/stoichiometries within reactions. Using the lower-level symbolic interface we can then define symbolic variables and parameters outside of `@reaction_network`, which can then be used within expressions in the DSL.
Interpolation is carried out by pre-appending the interpolating variable with a `$`. For example, here we declare the parameters and species of a birth-death model, and interpolate these into the model:
+
```@example dsl_advanced_programmatic_interpolation
using Catalyst # hide
t = default_t()
@@ -549,7 +653,9 @@ bd_model = @reaction_network begin
($p, $d), 0 <--> $X
end
```
+
Additional information (such as default values or metadata) supplied to `p`, `d`, and `X` is carried through to the DSL. However, interpolation for this purpose is of limited value, as such information [can be declared within the DSL](@ref dsl_advanced_options_declaring_species_and_parameters). However, it is possible to interpolate larger algebraic expressions into the DSL, e.g. here
+
```@example dsl_advanced_programmatic_interpolation
@species X1(t) X2(t) X3(t) E(t)
@parameters d
@@ -560,33 +666,40 @@ degradation_model = @reaction_network begin
$d_rate, X3 --> 0
end
```
+
we declare an expression `d_rate`, which then can be inserted into the DSL via interpolation.
It is also possible to use interpolation in combination with the `@reaction` macro. E.g. the reactions of the above network can be declared individually using
+
```@example dsl_advanced_programmatic_interpolation
rxs = [
@reaction $d_rate, $X1 --> 0
@reaction $d_rate, $X2 --> 0
@reaction $d_rate, $X3 --> 0
]
-nothing # hide
+nothing # hide
```
!!! note
When using interpolation, expressions like `2$spec` won't work; the multiplication symbol must be explicitly included like `2*$spec`.
## [Creating individual reactions using the `@reaction` macro](@id dsl_advanced_options_reaction_macro)
+
Catalyst exports a macro `@reaction`, which can be used to generate a singular [`Reaction`](@ref) object of the same type which is stored within the [`ReactionSystem`](@ref) structure (which in turn can be generated by `@reaction_network`). Generally, `@reaction` follows [identical rules to those of `@reaction_network`](@ref dsl_description_reactions) for writing and interpreting reactions (however, bi-directional reactions are not permitted). E.g. here we create a simple dimerisation reaction:
+
```@example dsl_advanced_reaction_macro
using Catalyst # hide
rx_dimerisation = @reaction kD, 2X --> X2
```
+
Here, `@reaction` is followed by a single line consisting of three parts:
+
- A rate (at which the reaction occurs).
- Any number of substrates (which are consumed by the reaction).
- Any number of products (which are produced by the reaction).
In the next example, we first create a simple [SIR model](@ref basic_CRN_library_sir). Then, we specify the same model by instead creating its individual reaction components using the `@reaction` macro. Finally, we confirm that these are identical to those stored in the initial model (using the [`reactions`](@ref) function).
+
```@example dsl_advanced_reaction_macro
sir_model = @reaction_network begin
α, S + I --> 2I
@@ -597,13 +710,16 @@ recovery_rx = @reaction β, I --> R
sir_rxs = [infection_rx, recovery_rx]
issetequal(reactions(sir_model), sir_rxs)
```
+
One of the primary uses of the `@reaction` macro is to provide some of the convenience of the DSL to [*programmatic modelling](@ref programmatic_CRN_construction). E.g. here we can combine our reactions to create a `ReactionSystem` directly, and also confirm that this is identical to the model created through the DSL:
+
```@example dsl_advanced_reaction_macro
sir_programmatic = complete(ReactionSystem(sir_rxs, default_t(); name = :sir))
sir_programmatic == sir_model
```
During programmatic modelling, it can be good to keep in mind that already declared symbolic variables can be [*interpolated*](@ref dsl_advanced_options_symbolics_and_DSL_interpolation). E.g. here we create two production reactions both depending on the same Michaelis-Menten function:
+
```@example dsl_advanced_reaction_macro
t = default_t()
@species X(t)
@@ -617,33 +733,39 @@ nothing # hide
## [Disabling mass action for reactions](@id dsl_advanced_options_disable_ma)
As [described previously](@ref math_models_in_catalyst_rre_odes), Catalyst uses *mass action kinetics* to generate ODEs from reactions. Here, each reaction generates a term for each of its reactants, which consists of the reaction's rate, substrates, and the reactant's stoichiometry. E.g. the following reaction:
+
```@example dsl_advanced_disable_ma
using Catalyst # hide
rn = @reaction_network begin
k, X --> ∅
end
```
+
generates a single term $-k*[X]$:
+
```@example dsl_advanced_disable_ma
using Latexify
latexify(rn; form = :ode)
```
It is possible to remove the substrate contribution by using any of the following non-filled arrows when declaring the reaction: `<=`, `⇐`, `⟽`, `=>`, `⇒`, `⟾`, `⇔`, `⟺`. This means that the reaction
+
```@example dsl_advanced_disable_ma
rn = @reaction_network begin
k, X => ∅
end
latexify(rn; form = :ode)
```
-will occur at rate $d[X]/dt = -k$ (which might become a problem since $[X]$ will be degraded at a constant rate even when very small or equal to 0). This functionality allows the user to fully customise the ODEs generated by their models.
+
+will occur at rate $d[X]/dt = -k$ (which might become a problem since $[X]$ will be degraded at a constant rate even when very small or equal to 0). This functionality allows the user to fully customise the ODEs generated by their models.
Note, stoichiometric coefficients are still included, i.e. the reaction
+
```@example dsl_advanced_disable_ma
rn = @reaction_network begin
k, 2*X ⇒ ∅
end
latexify(rn; form = :ode)
```
-has rate $d[X]/dt = -2 k$.
+has rate $d[X]/dt = -2 k$.
diff --git a/docs/src/model_creation/dsl_basics.md b/docs/src/model_creation/dsl_basics.md
index 5ea700aff1..b2d52ad031 100644
--- a/docs/src/model_creation/dsl_basics.md
+++ b/docs/src/model_creation/dsl_basics.md
@@ -1,25 +1,32 @@
# [The Catalyst DSL - Introduction](@id dsl_description)
+
In the [introduction to Catalyst](@ref introduction_to_catalyst) we described how the `@reaction_network` [macro](https://docs.julialang.org/en/v1/manual/metaprogramming/#man-macros) can be used to create chemical reaction network (CRN) models. This macro enables a so-called [domain-specific language](https://en.wikipedia.org/wiki/Domain-specific_language) (DSL) for creating CRN models. This tutorial will give a basic introduction on how to create Catalyst models using this macro (from now onwards called "*the Catalyst DSL*"). A [follow-up tutorial](@ref dsl_advanced_options) will describe some of the DSL's more advanced features.
The Catalyst DSL generates a [`ReactionSystem`](@ref) (the [julia structure](https://docs.julialang.org/en/v1/manual/types/#Composite-Types) Catalyst uses to represent CRN models). These can be created through alternative methods (e.g. [programmatically](@ref programmatic_CRN_construction) or [compositionally](@ref compositional_modeling)). [Previous](@ref introduction_to_catalyst) and [following](@ref simulation_intro) tutorials describe how to simulate models once they have been created using the DSL. This tutorial will solely focus on model creation.
Before we begin, we will first load the Catalyst package (which is required to run the code).
+
```@example dsl_basics_intro
using Catalyst
```
### [Quick-start summary](@id dsl_description_quick_start)
+
The DSL is initiated through the `@reaction_network` macro, which is followed by one line for each reaction. Each reaction consists of a *rate*, followed lists first of the substrates and next of the products. E.g. a [Michaelis-Menten enzyme kinetics system](@ref basic_CRN_library_mm) can be written as
+
```@example dsl_basics_intro
rn = @reaction_network begin
(kB,kD), S + E <--> SE
kP, SE --> P + E
end
```
+
Here, `<-->` is used to create a bi-directional reaction (with forward rate `kP` and backward rate `kD`). Next, the model (stored in the variable `rn`) can be used as input to various types of [simulations](@ref simulation_intro).
## [Basic syntax](@id dsl_description_basic_syntax)
+
The basic syntax of the DSL is
+
```@example dsl_basics
using Catalyst # hide
rn = @reaction_network begin
@@ -27,17 +34,21 @@ rn = @reaction_network begin
1.0, Y --> X
end
```
+
Here, you start with `@reaction_network begin`, next list all of the model's reactions, and finish with `end`. Each reaction consists of
+
- A *rate*.
- A (potentially empty) set of *substrates*.
- A (potentially empty) set of *products*.
Each reaction line declares, in order, the rate, the substrate(s), and the product(s). The rate is separated from the substrate(s) by a `,`, and the substrate(s) from the production by a `-->` (other arrows, however, are [also possible](@ref dsl_description_symbols_arrows)). In the above example, our model consists of two reactions. In the first one, `X` (the single substrate) becomes `Y` (the single product) at rate `2.0`. In the second reaction, `Y` becomes `X` at rate `1.0`.
-Finally, `rn = ` is used to store the model in the variable `rn` (a normal Julia variable, which does not need to be called `rn`).
+Finally, `rn =` is used to store the model in the variable `rn` (a normal Julia variable, which does not need to be called `rn`).
## [Defining parameters and species in the DSL](@id dsl_description_parameters_basics)
+
Typically, the rates are not constants, but rather parameters (which values can be set e.g. at [the beginning of each simulation](@ref simulation_intro_ODEs)). To set parametric rates, simply use whichever symbol you wish to represent your parameter with. E.g. to set the above rates to `a` and `b`, we use:
+
```@example dsl_basics
rn = @reaction_network begin
a, X --> Y
@@ -46,25 +57,31 @@ end
```
Here we have used single-character symbols to designate all species and parameters. Multi-character symbols, however, are also permitted. E.g. we could call the rates `kX` and `kY`:
+
```@example dsl_basics
rn = @reaction_network begin
kX, X --> Y
kY, Y --> X
end
```
+
Generally, anything that is a [permitted Julia variable name](https://docs.julialang.org/en/v1/manual/variables/#man-allowed-variable-names) can be used to designate a species or parameter in Catalyst.
## [Different types of reactions](@id dsl_description_reactions)
### [Reactions with multiple substrates or products](@id dsl_description_reactions_multiples)
+
Previously, our reactions have had a single substrate and a single product. However, reactions with multiple substrates and/or products are possible. Here, all the substrates (or products) are listed and separated by a `+`. E.g. to create a model where `X` and `Y` bind (at rate `kB`) to form `XY` (which then can dissociate, at rate `kD`, to form `XY`) we use:
+
```@example dsl_basics
rn = @reaction_network begin
kB, X + Y --> XY
kD, XY --> X + Y
end
```
+
Reactions can have any number of substrates and products, and their names do not need to have any relationship to each other, as demonstrated by the following mock model:
+
```@example dsl_basics
rn = @reaction_network begin
k, X + Y + Z --> A + B + C + D
@@ -72,7 +89,9 @@ end
```
### [Reactions with degradation or production](@id dsl_description_reactions_degradation_and_production)
+
Some reactions have no products, in which case the substrate(s) are degraded (i.e. removed from the system). To denote this, set the reaction's right-hand side to `0`. Similarly, some reactions have no substrates, in which case the product(s) are produced (i.e. added to the system). This is denoted by setting the left-hand side to `0`. E.g. to create a model where a single species `X` is both created (in the first reaction) and degraded (in a second reaction), we use:
+
```@example dsl_basics
rn = @reaction_network begin
p, 0 --> X
@@ -81,16 +100,20 @@ end
```
### [Reactions with non-unitary stoichiometries](@id dsl_description_reactions_stoichiometries)
+
Reactions may include multiple copies of the same reactant (i.e. a substrate or a product). To specify this, the reactant is preceded by a number indicating its number of copies (also called the reactant's *stoichiometry*). E.g. to create a model where two copies of `X` dimerise to form `X2` (which then dissociate back to two `X` copies) we use:
+
```@example dsl_basics
rn = @reaction_network begin
kB, 2X --> X2
kD, X2 --> 2X
end
```
+
Reactants whose stoichiometries are not defined are assumed to have stoichiometry `1`. Any integer number can be used, furthermore, [decimal numbers and parameters can also be used as stoichiometries](@ref dsl_description_stoichiometries). A discussion of non-unitary (i.e. not equal to `1`) stoichiometries affecting the created model can be found [here](@ref introduction_to_catalyst_ratelaws).
Stoichiometries can be combined with `()` to define them for multiple reactants. Here, the following (mock) model declares the same reaction twice, both with and without this notation:
+
```@example dsl_basics
rn = @reaction_network begin
k, 2X + 3(Y + 2Z) --> 5(V + W)
@@ -101,57 +124,74 @@ end
## [Bundling of similar reactions](@id dsl_description_reaction_bundling)
### [Bi-directional (or reversible) reactions](@id dsl_description_reaction_bundling_reversible)
+
As is the case for the following two-state model:
+
```@example dsl_basics
rn_bidir = @reaction_network begin
k1, X1 --> X2
k2, X2 --> X1
end
```
+
it is common that reactions occur in both directions (so-called *bi-directional* reactions). Here, it is possible to bundle the reactions into a single line by using the `<-->` arrow. When we do this, the rate term must include two separate rates (one for each direction, these are enclosed by a `()` and separated by a `,`). I.e. the two-state model can be declared using:
+
```@example dsl_basics
rn_bidir = @reaction_network begin
(k1,k2), X1 <--> X2
end
```
+
Here, the first rate (`k1`) denotes the *forward rate* and the second rate (`k2`) the *backwards rate*.
Catalyst also permits writing pure backwards reactions. These use identical syntax to forward reactions, but with the `<--` arrow:
+
```@example dsl_basics
rn_ytox = @reaction_network begin
k, X <-- Y
end
```
+
Here, the substrate(s) are on the right-hand side and the product(s) are on the left-hand side. Hence, the above model can be written identically using:
+
```@example dsl_basics
rn_ytox = @reaction_network begin
k, Y --> X
end
```
+
Generally, using forward reactions is clearer than backwards ones, with the latter typically never being used.
### [Bundling similar reactions on a single line](@id dsl_description_reaction_bundling_similar)
+
There exist additional situations where models contain similar reactions (e.g. systems where all system components degrade at identical rates). Reactions which share either rates, substrates, or products can be bundled into a single line. Here, the parts which are different for the reactions are written using `(,)` (containing one separate expression for each reaction). E.g., let us consider the following model where species `X` and `Y` both degrade at the rate `d`:
+
```@example dsl_basics
rn_deg = @reaction_network begin
d, X --> 0
d, Y --> 0
end
```
+
These share both their rates (`d`) and products (`0`), however, the substrates are different (`X` and `Y`). Hence, the reactions can be bundled into a single line using the common rate and product expression while providing separate substrate expressions:
+
```@example dsl_basics
rn_deg = @reaction_network begin
d, (X,Y) --> 0
end
```
+
This declaration of the model is identical to the previous one. Reactions can share any subset of the rate, substrate, and product expression (the cases where they share all or none, however, do not make sense to use). I.e. if the two reactions also have different degradation rates:
+
```@example dsl_basics
rn_deg2 = @reaction_network begin
dX, X --> 0
dY, Y --> 0
end
```
+
This can be represented using:
+
```@example dsl_basics
rn_deg2 = @reaction_network begin
(dX,dY), (X,Y) --> 0
@@ -159,6 +199,7 @@ end
```
It is possible to use bundling for any number of reactions. E.g. in the following model we bundle the conversion of a species $X$ between its various forms (where all reactions use the same rate $k$):
+
```@example dsl_basics
rn = @reaction_network begin
k, (X0,X1,X2,X3) --> (X1,X2,X3,X4)
@@ -166,6 +207,7 @@ end
```
It is possible to combine bundling with bi-directional reactions. In this case, the rate is first split into the forward and backwards rates. These may then (or may not) indicate several rates. We exemplify this using the two following two (identical) networks, created with and without bundling.
+
```@example dsl_basics
rn_sp = @reaction_network begin
kf, S --> P1
@@ -174,6 +216,7 @@ rn_sp = @reaction_network begin
kb_2, P2 --> S
end
```
+
```@example dsl_basics
rn_sp = @reaction_network begin
(kf, (kb_1, kb_2)), S <--> (P1,P2)
@@ -181,49 +224,60 @@ end
```
Like when we designated stoichiometries, reaction bundling can be applied very generally to create some truly complicated reactions:
+
```@example dsl_basics
rn = @reaction_network begin
((pX, pY, pZ),d), (0, Y0, Z0) <--> (X, Y, Z1+Z2)
end
```
+
However, like for the above model, bundling reactions too zealously can reduce (rather than improve) a model's readability.
The one exception to reaction bundling is that we do not permit the user to provide multiple rates but only set one set each for the substrates and products. I.e.
+
```julia
rn_erroneous = @reaction_network begin
(k1,k2), X --> Y
end
```
-is not permitted (due to this notation's similarity to a bidirectional reaction). However, if multiples are provided for substrates and/or products, like `(k1,k2), (X1,X2) --> Y`, then bundling works.
+is not permitted (due to this notation's similarity to a bidirectional reaction). However, if multiples are provided for substrates and/or products, like `(k1,k2), (X1,X2) --> Y`, then bundling works.
## [Non-constant reaction rates](@id dsl_description_nonconstant_rates)
+
So far we have assumed that all reaction rates are constant (being either a number of a parameter). Non-constant rates that depend on one (or several) species are also possible. More generally, the rate can be any valid expression of parameters and species.
Let us consider a model with an activator (`A`, which degraded at a constant rate) and a protein (`P`). The production rate of `P` depends both on `A` and a parameter (`kP`). We model this through:
+
```@example dsl_basics
rn_ap = @reaction_network begin
d, A --> 0
kP*A, 0 --> P
end
```
+
Here, `P`'s production rate will be reduced as `A` decays. We can [print the ODE this model produces with `Latexify`](@ref visualisation_latex):
+
```@example dsl_basics
using Latexify
latexify(rn_ap; form = :ode)
```
In this case, we can generate an equivalent model by instead adding `A` as both a substrate and a product to `P`'s production reaction:
+
```@example dsl_basics
rn_ap_alt = @reaction_network begin
d, A --> 0
kp, A --> A + P
end
```
+
We can confirm that this generates the same ODE:
+
```@example dsl_basics
latexify(rn_ap_alt; form = :ode)
```
+
Here, while these models will generate identical ODE, SDE, and jump simulations, the chemical reaction network models themselves are not equivalent. Generally, as pointed out in the two notes below, using the second form is preferable.
!!! warning
While `rn_ap` and `rn_ap_alt` will generate equivalent simulations, for jump simulations, the first model will have reduced performance as it generates a less performant representation of the system in JumpProcesses. It is generally recommended to write pure mass action reactions such that there is just a single constant within the rate constant expression for optimal performance of jump process simulations.
@@ -232,6 +286,7 @@ Here, while these models will generate identical ODE, SDE, and jump simulations,
Catalyst automatically infers whether quantities appearing in the DSL are species or parameters (as described [here](@ref dsl_advanced_options_declaring_species_and_parameters)). Generally, anything that does not appear as a reactant is inferred to be a parameter. This means that if you want to model a reaction activated by a species (e.g. `kp*A, 0 --> P`), but that species does not occur as a reactant, it will be interpreted as a parameter. This can be handled by [manually declaring the system species](@ref dsl_advanced_options_declaring_species_and_parameters).
Above we used a simple example where the rate was the product of a species and a parameter. However, any valid Julia expression of parameters, species, and values can be used. E.g the following is a valid model:
+
```@example dsl_basics
rn = @reaction_network begin
2.0 + X^2, 0 --> X + Y
@@ -241,14 +296,18 @@ end
```
### [Using functions in rates](@id dsl_description_nonconstant_rates_functions)
+
It is possible for the rate to contain Julia functions. These can either be functions from Julia's standard library:
+
```@example dsl_basics
rn = @reaction_network begin
d, A --> 0
kp*sqrt(A), 0 --> P
end
```
+
or ones defined by the user:
+
```@example dsl_basics
custom_function(p1, p2, X) = (p1 + X) / (p2 + X)
rn = @reaction_network begin
@@ -258,7 +317,9 @@ end
```
### [Pre-defined functions](@id dsl_description_nonconstant_rates_available_functions)
+
Two functions frequently used within systems biology are the [*Michaelis-Menten*](https://en.wikipedia.org/wiki/Michaelis%E2%80%93Menten_kinetics) and [*Hill*](https://en.wikipedia.org/wiki/Hill_equation_(biochemistry)) functions. These are pre-defined in Catalyst and can be called using `mm(X,v,K)` and `hill(X,v,K,n)`. E.g. a self-activation loop where `X` activates its own production through a Hill function can be created using:
+
```@example dsl_basics
rn = @reaction_network begin
hill(X,v,K,n), 0 --> P
@@ -267,6 +328,7 @@ end
```
Catalyst comes with the following predefined functions:
+
- The Michaelis-Menten function: $mm(X,v,K) = v * X/(X + K)$.
- The repressive Michaelis-Menten function: $mmr(X,v,K) = v * K/(X + K)$.
- The Hill function: $hill(X,v,K,n) = v * (X^n)/(X^n + K^n)$.
@@ -274,7 +336,9 @@ Catalyst comes with the following predefined functions:
- The activating/repressive Hill function: $hillar(X,Y,v,K,n) = v * (X^n)/(X^n + Y^n + K^n)$.
### [Registration of non-algebraic functions](@id dsl_description_nonconstant_rates_function_registration)
+
Previously we showed how user-defined functions [can be used in rates directly](@ref dsl_description_nonconstant_rates_available_functions). For functions containing more complicated syntax (e.g. `for` loops or `if` statements), we must add an additional step: registering it using the `@register_symbolic` macro. Below we define a non-standard function of one variable. Next, we register it using `@register_symbolic`, after which we can use it within the DSL.
+
```@example dsl_basics
weirdfunc(x) = round(x) + 2.0
@register_symbolic weirdfunc(X)
@@ -285,7 +349,9 @@ end
```
### [Time-dependant rates](@id dsl_description_nonconstant_rates_time)
+
Previously we have assumed that the rates are independent of the time variable, $t$. However, time-dependent reactions are also possible. Here, simply use `t` to represent the time variable. E.g., to create a production/degradation model where the production rate decays as time progresses, we can use:
+
```@example dsl_basics
rn = @reaction_network begin
kp/(1 + t), 0 --> P
@@ -294,6 +360,7 @@ end
```
Like previously, `t` can be part of any valid expression. E.g. to create a reaction with a cyclic rate (e.g. to represent a [circadian system](https://en.wikipedia.org/wiki/Circadian_rhythm)) we can use:
+
```@example dsl_basics
rn = @reaction_network begin
A*(sin(2π*f*t - ϕ)+1)/2, 0 --> P
@@ -311,23 +378,30 @@ end
## [Non-standard stoichiometries](@id dsl_description_stoichiometries)
### [Non-integer stoichiometries](@id dsl_description_stoichiometries_decimal)
+
Previously all stoichiometric constants have been integer numbers, however, decimal numbers are also permitted. Here we create a birth-death model where each production reaction produces 1.5 units of `X`:
+
```@example dsl_basics
rn = @reaction_network begin
p, 0 --> 1.5X
d, X --> 0
end
```
+
It is also possible to have non-integer stoichiometric coefficients for substrates. However, in this case the `combinatoric_ratelaw = false` option must be used. We note that non-integer stoichiometric coefficients do not make sense in most fields, however, this feature is available for use for models where it does make sense.
### [Parametric stoichiometries](@id dsl_description_stoichiometries_parameters)
+
It is possible for stoichiometric coefficients to be parameters. E.g. here we create a generic polymerisation system where `n` copies of `X` bind to form `Xn`:
+
```@example dsl_basics
rn = @reaction_network begin
(kB,kD), n*X <--> Xn
end
```
+
Now we can designate the value of `n` through a parameter when we e.g. create an `ODEProblem`:
+
```@example dsl_basics
u0 = [:X => 5.0, :Xn => 1.0]
ps = [:kB => 1.0, :kD => 0.1, :n => 4]
@@ -336,10 +410,13 @@ nothing # hide
```
## [Using special symbols](@id dsl_description_symbols)
+
Julia permits any Unicode characters to be used in variable names, thus Catalyst can use these as well. Below we describe some cases where this can be useful. No functionality is, however, tied to this.
### [Using ∅ in degradation/production reactions](@id dsl_description_symbols_empty_set)
+
Previously, we described how `0` could be used to [create degradation or production reactions](@ref dsl_description_reactions_degradation_and_production). Catalyst permits the user to instead use the `∅` symbol. E.g. the production/degradation system can alternatively be written as:
+
```@example dsl_basics
rn_pd = @reaction_network begin
p, ∅ --> X
@@ -348,12 +425,15 @@ end
```
### [Using special arrow symbols](@id dsl_description_symbols_arrows)
+
Catalyst uses `-->`, `<-->`, and `<--` to denote forward, bi-directional, and backwards reactions, respectively. Several unicode representations of these arrows are available. Here,
+
- `>`, `→`, `↣`, `↦`, `⇾`, `⟶`, `⟼`, `⥟`, `⥟`, `⇀`, and `⇁` can be used to represent forward reactions.
- `↔`, `⟷`, `⇄`, `⇆`, `⇌`, `⇋`, , and `⇔` can be used to represent bi-directional reactions.
- `<`, `←`, `↢`, `↤`, `⇽`, `⟵`, `⟻`, `⥚`, `⥞`, `↼`, , and `↽` can be used to represent backwards reactions.
E.g. the production/degradation system can alternatively be written as:
+
```@example dsl_basics
rn_pd = @reaction_network begin
p, ∅ → X
@@ -362,13 +442,16 @@ end
```
### [Using special symbols to denote species or parameters](@id dsl_description_symbols_special)
+
A range of possible characters are available which can be incorporated into species and parameter names. This includes, but is not limited to:
+
- Greek letters (e.g `α`, `σ`, `τ`, and `Ω`).
- Superscript and subscript characters (to create e.g. `k₁`, `k₂`, `Xₐ`, and `Xᴾ`).
- Non-latin, non-greek, letters (e.g. `ä`, `Д`, `س`, and `א`).
- Other symbols (e.g. `£`, `ℂ`, `▲`, and `♠`).
An example of how this can be used to create a neat-looking model can be found in [Schwall et al. (2021)](https://www.embopress.org/doi/full/10.15252/msb.20209832) where it was used to model a sigma factor V circuit in the bacteria *Bacillus subtilis*:
+
```@example dsl_basics
σᵛ_model = @reaction_network begin
v₀ + hill(σᵛ,v,K,n), ∅ → σᵛ + A
@@ -380,6 +463,7 @@ nothing # hide
```
This functionality can also be used to create less serious models:
+
```@example dsl_basics
rn = @reaction_network begin
🍦, 😢 --> 😃
@@ -387,6 +471,7 @@ end
```
It should be noted that the following symbols are *not permitted* to be used as species or parameter names:
+
- `pi` and `π` (used in Julia to denote [`3.1415926535897...`](https://en.wikipedia.org/wiki/Pi)).
- `ℯ` (used in Julia to denote [Euler's constant](https://en.wikipedia.org/wiki/Euler%27s_constant)).
- `t` (used to denote the [time variable](@ref dsl_description_nonconstant_rates_time)).
diff --git a/docs/src/model_creation/examples/basic_CRN_library.md b/docs/src/model_creation/examples/basic_CRN_library.md
index 3cb5fe9c54..5715f27044 100644
--- a/docs/src/model_creation/examples/basic_CRN_library.md
+++ b/docs/src/model_creation/examples/basic_CRN_library.md
@@ -1,8 +1,11 @@
# [Library of Basic Chemical Reaction Network Models](@id basic_CRN_library)
+
```@raw html
Environment setup and package installation
```
+
The following code sets up an environment for running the code on this page.
+
```julia
using Pkg
Pkg.activate(".")
@@ -12,36 +15,46 @@ Pkg.add("StochasticDiffEq")
Pkg.add("JumpProcesses")
Pkg.add("Plots")
```
+
```@raw html
```
+
\
-
+
Below we will present various simple and established chemical reaction network (CRN) models. Each model is given some brief background, implemented using the `@reaction_network` DSL, and basic simulations are performed.
## [Birth-death process](@id basic_CRN_library_bd)
+
The birth-death process is one of the simplest possible CRN models. It consists of a single component ($X$) which is both produced and degraded at linear rates:
+
```@example crn_library_birth_death
using Catalyst
bd_process = @reaction_network begin
(p,d), ∅ <--> X
end
```
+
Next, we define simulation conditions. Note that the initial condition is integer-valued (more natural than decimal numbers for jump simulations).
+
```@example crn_library_birth_death
u0 = [:X => 1]
tspan = (0.0, 10.0)
ps = [:p => 1.0, :d => 0.2]
nothing # hide
```
+
We can now simulate our model using all three interpretations. First, we perform a reaction rate equation-based ODE simulation:
+
```@example crn_library_birth_death
using OrdinaryDiffEqDefault
oprob = ODEProblem(bd_process, u0, tspan, ps)
osol = solve(oprob)
nothing # hide
```
+
Next, a chemical Langevin equation-based SDE simulation:
+
```@example crn_library_birth_death
using StochasticDiffEq
sprob = SDEProblem(bd_process, u0, tspan, ps)
@@ -49,7 +62,9 @@ ssol = solve(sprob, STrapezoid())
ssol = solve(sprob, STrapezoid(); seed = 12) # hide
nothing # hide
```
+
Next, a stochastic chemical kinetics-based jump simulation:
+
```@example crn_library_birth_death
using JumpProcesses
jinput = JumpInputs(bd_process, u0, tspan, ps)
@@ -58,7 +73,9 @@ jsol = solve(jprob)
jsol = solve(jprob; seed = 12) # hide
nothing # hide
```
+
Finally, we plot the results:
+
```@example crn_library_birth_death
using Plots
oplt = plot(osol; title = "Reaction rate equation (ODE)")
@@ -68,7 +85,9 @@ plot(oplt, splt, jplt; lw = 3, size=(800,700), layout = (3,1))
```
## [Two-state model](@id basic_CRN_library_two_states)
+
The two-state model describes a component (here called $X$) which can exist in two different forms (here called $X₁$ and $X₂$). It switches between these forms at linear rates. First, we simulate the model using both ODEs and SDEs:
+
```@example crn_library_two_states
using Catalyst, OrdinaryDiffEqDefault, StochasticDiffEq, Plots
two_state_model = @reaction_network begin
@@ -90,7 +109,9 @@ splt = plot(ssol; title = "Chemical Langevin equation (SDE)")
plot(oplt, splt; lw = 3, size = (800,550), layout = (2,1))
```
+
What is interesting about this model is that it has a *conserved quantity*, where $X₁ + X₂$ remains constant throughout the simulation (both in deterministic and stochastic cases). We can show this by instead plotting this conserved quantity.
+
```@example crn_library_two_states
@unpack X₁, X₂ = two_state_model
oplt = plot(osol; idxs = X₁ + X₂, title = "Reaction rate equation (ODE)")
@@ -99,7 +120,9 @@ plot(oplt, splt; lw = 3, ylimit = (99,101), size = (800,450), layout = (2,1))
```
## [Michaelis-Menten enzyme kinetics](@id basic_CRN_library_mm)
+
[Michaelis-Menten enzyme kinetics](https://en.wikipedia.org/wiki/Michaelis%E2%80%93Menten_kinetics) is a simple description of an enzyme ($E$) transforming a substrate ($S$) into a product ($P$). Under certain assumptions, it can be simplified to a single function (a Michaelis-Menten function) and used as a reaction rate. Here we instead present the full system model:
+
```@example crn_library_michaelis_menten
using Catalyst
mm_system = @reaction_network begin
@@ -108,7 +131,9 @@ mm_system = @reaction_network begin
kP, SE --> P + E
end
```
+
Next, we perform ODE, SDE, and jump simulations of the model:
+
```@example crn_library_michaelis_menten
u0 = [:S => 301, :E => 100, :SE => 0, :P => 0]
tspan = (0., 100.)
@@ -140,7 +165,9 @@ Catalyst.PNG(fullplt) # hide
```
## [SIR infection model](@id basic_CRN_library_sir)
+
The [SIR model](https://en.wikipedia.org/wiki/Compartmental_models_in_epidemiology#The_SIR_model) is the simplest model of the spread of an infectious disease. While the real system is very different from the chemical and cellular processes typically modelled with CRNs, it (and several other epidemiological systems) can be modelled using the same CRN formalism. The SIR model consists of three species: susceptible ($S$), infected ($I$), and removed ($R$) individuals, and two reaction events: infection and recovery.
+
```@example crn_library_sir
using Catalyst
sir_model = @reaction_network begin
@@ -148,7 +175,9 @@ sir_model = @reaction_network begin
β, I --> R
end
```
+
First, we perform a deterministic ODE simulation:
+
```@example crn_library_sir
using OrdinaryDiffEqDefault, Plots
u0 = [:S => 99, :I => 1, :R => 0]
@@ -160,7 +189,9 @@ oprob = ODEProblem(sir_model, u0, tspan, ps)
osol = solve(oprob)
plot(osol; title = "Reaction rate equation (ODE)", size=(800,350))
```
+
Next, we perform 3 different Jump simulations. Note that for the stochastic model, the occurrence of an outbreak is not certain. Rather, there is a possibility that it fizzles out without a noteworthy peak.
+
```@example crn_library_sir
using JumpProcesses
jinput = JumpInputs(sir_model, u0, tspan, ps)
@@ -182,7 +213,9 @@ Catalyst.PNG(fullplt) # hide
```
## [Chemical cross-coupling](@id basic_CRN_library_cc)
+
In chemistry, [cross-coupling](https://en.wikipedia.org/wiki/Cross-coupling_reaction) is when a catalyst combines two substrates to form a product. In this example, the catalyst ($C$) first binds one substrate ($S₁$) to form an intermediary complex ($S₁C$). Next, the complex binds the second substrate ($S₂$) to form another complex ($CP$). Finally, the catalyst releases the now-formed product ($P$). This system is an extended version of the [Michaelis-Menten system presented earlier](@ref basic_CRN_library_mm).
+
```@example crn_library_cc
using Catalyst
cc_system = @reaction_network begin
@@ -191,11 +224,14 @@ cc_system = @reaction_network begin
k₃, CP --> C + P
end
```
+
Below, we perform a simple deterministic ODE simulation of the system. Next, we plot both:
+
- The concentration of the substrates and the product.
- The concentration of the catalyst and the intermediaries.
In two separate plots.
+
```@example crn_library_cc
using OrdinaryDiffEqDefault, Plots
u0 = [:S₁ => 1.0, :C => 0.05, :S₂ => 1.2, :S₁C => 0.0, :CP => 0.0, :P => 0.0]
@@ -212,7 +248,9 @@ plot(plt1, plt2; lw = 3, size = (800,600), layout = (2,1))
```
## [The Wilhelm model](@id basic_CRN_library_wilhelm)
+
The Wilhelm model was introduced in [*Wilhelm (2009)*](https://bmcsystbiol.biomedcentral.com/articles/10.1186/1752-0509-3-90) as the smallest CRN model (with constant rates) that exhibits bistability.
+
```@example crn_library_wilhelm
using Catalyst
wilhelm_model = @reaction_network begin
@@ -222,7 +260,9 @@ wilhelm_model = @reaction_network begin
k4, X --> 0
end
```
+
We can simulate the model for two different initial conditions, demonstrating the existence of two different stable steady states.
+
```@example crn_library_wilhelm
using OrdinaryDiffEqDefault, Plots
u0_1 = [:X => 1.5, :Y => 0.5]
@@ -240,7 +280,9 @@ plot!(bottom_margin = 3Plots.Measures.mm) # hide
```
## [Simple self-activation loop](@id basic_CRN_library_self_activation)
+
The simplest self-activation loop consists of a single species (here called $X$) which activates its own production. If its production rate is modelled as a [Hill function](https://en.wikipedia.org/wiki/Hill_equation_(biochemistry)) with $n>1$, the system may exhibit bistability.
+
```@example crn_library_self_activation
using Catalyst
sa_loop = @reaction_network begin
@@ -248,9 +290,11 @@ sa_loop = @reaction_network begin
d, X --> ∅
end
```
+
A simple example of such a loop is a transcription factor which activates its own gene. Here, $v₀$ represents a basic transcription rate (leakage) in the absence of the transcription factor.
We simulate the self-activation loop from a single initial condition using both deterministic (ODE) and stochastic (jump) simulations. We note that while the deterministic simulation reaches a single steady state, the stochastic one switches between two different states.
+
```@example crn_library_self_activation
using JumpProcesses, OrdinaryDiffEqDefault, Plots
u0 = [:X => 4]
@@ -273,7 +317,9 @@ Catalyst.PNG(fplt) # hide
```
## [The Brusselator](@id basic_CRN_library_brusselator)
+
The [Brusselator](https://en.wikipedia.org/wiki/Brusselator) is a well-known (theoretical) CRN model able to produce oscillations (its name is a portmanteau of "Brussels" and "oscillator").
+
```@example crn_library_brusselator
using Catalyst
brusselator = @reaction_network begin
@@ -283,7 +329,9 @@ brusselator = @reaction_network begin
1, X --> ∅
end
```
+
It is generally known to (for reaction rate equation-based ODE simulations) produce oscillations when $B > 1 + A^2$. However, this result is based on models generated when *combinatorial adjustment of rates is not performed*. Since Catalyst [automatically perform these adjustments](@ref introduction_to_catalyst_ratelaws), and one reaction contains a stoichiometric constant $>1$, the threshold will be different. Here, we trial two different values of $B$. In both cases, $B < 1 + A^2$, however, in the second case the system can generate oscillations.
+
```@example crn_library_brusselator
using OrdinaryDiffEqDefault, Plots
u0 = [:X => 1.0, :Y => 1.0]
@@ -302,7 +350,9 @@ plot(oplt1, oplt2; lw = 3, size = (800,600), layout = (2,1))
```
## [The Repressilator](@id basic_CRN_library_repressilator)
+
The Repressilator was introduced in [*Elowitz & Leibler (2000)*](https://www.nature.com/articles/35002125) as a simple system that can generate oscillations (most notably, they demonstrated this both in a model and in a synthetic in vivo implementation in *Escherichia col*). It consists of three genes, repressing each other in a cycle. Here, we will implement it using three species ($X$, $Y$, and $Z$) whose production rates are (repressing) [Hill functions](https://en.wikipedia.org/wiki/Hill_equation_(biochemistry)).
+
```@example crn_library_brusselator
using Catalyst
repressilator = @reaction_network begin
@@ -312,7 +362,9 @@ repressilator = @reaction_network begin
d, (X, Y, Z) --> ∅
end
```
+
Whether the Repressilator oscillates or not depends on its parameter values. Here, we will perform deterministic (ODE) simulations for two different values of $K$, showing that it oscillates for one value and not the other one. Next, we will perform stochastic (SDE) simulations for both $K$ values, showing that the stochastic model can sustain oscillations in both cases. This is an example of the phenomena of *noise-induced oscillation*.
+
```@example crn_library_brusselator
using OrdinaryDiffEqDefault, StochasticDiffEq, Plots
u0 = [:X => 50.0, :Y => 15.0, :Z => 15.0]
@@ -340,7 +392,9 @@ plot(oplt1, oplt2, splt1, splt2; lw = 2, layout = (2,2), size = (800,600))
```
## [The Willamowski–Rössler model](@id basic_CRN_library_wr)
+
The Willamowski–Rössler model was introduced in [*Willamowski & Rössler (1979)*](https://www.degruyter.com/document/doi/10.1515/zna-1980-0308/html?lang=en) as an example of a simple CRN model which exhibits [*chaotic behaviours*](https://en.wikipedia.org/wiki/Chaos_theory). This means that small changes in initial conditions can produce relatively large changes in the system's trajectory.
+
```@example crn_library_chaos
using Catalyst
wr_model = @reaction_network begin
@@ -353,7 +407,9 @@ wr_model = @reaction_network begin
k7, Z --> ∅
end
```
+
Here we simulate the model for a single initial condition, showing both time-state space and phase space how it reaches a [*strange attractor*](https://www.dynamicmath.xyz/strange-attractors/).
+
```@example crn_library_chaos
using OrdinaryDiffEqDefault, Plots
u0 = [:X => 1.5, :Y => 1.5, :Z => 1.5]
diff --git a/docs/src/model_creation/examples/hodgkin_huxley_equation.md b/docs/src/model_creation/examples/hodgkin_huxley_equation.md
index 5d433e80e2..fb201afa75 100644
--- a/docs/src/model_creation/examples/hodgkin_huxley_equation.md
+++ b/docs/src/model_creation/examples/hodgkin_huxley_equation.md
@@ -17,11 +17,13 @@ chemistry and the dynamics of the transmembrane potential can be combined into a
complete model.
We begin by importing some necessary packages:
+
```@example hh1
using ModelingToolkit, Catalyst, NonlinearSolve, Plots, OrdinaryDiffEqRosenbrock
```
## Building the model via the Catalyst DSL
+
Let's build a simple Hodgkin-Huxley model for a single neuron, with the voltage,
$V(t)$, included as a coupled ODE. We first specify the transition rates for
three gating variables, $m(t)$, $n(t)$ and $h(t)$.
@@ -53,7 +55,8 @@ nothing # hide
```
We also declare a function to represent an applied current in our model, which we
-will use to perturb the system and create action potentials.
+will use to perturb the system and create action potentials.
+
```@example hh1
Iapp(t,I₀) = I₀ * sin(2*pi*t/30)^2
```
@@ -66,13 +69,13 @@ maps.
```@example hh1
hhmodel = @reaction_network hhmodel begin
@parameters begin
- C = 1.0
- ḡNa = 120.0
- ḡK = 36.0
- ḡL = .3
- ENa = 45.0
- EK = -82.0
- EL = -59.0
+ C = 1.0
+ ḡNa = 120.0
+ ḡK = 36.0
+ ḡL = .3
+ ENa = 45.0
+ EK = -82.0
+ EL = -59.0
I₀ = 0.0
end
@@ -81,12 +84,13 @@ hhmodel = @reaction_network hhmodel begin
(αₙ(V), βₙ(V)), n′ <--> n
(αₘ(V), βₘ(V)), m′ <--> m
(αₕ(V), βₕ(V)), h′ <--> h
-
+
@equations begin
D(V) ~ -1/C * (ḡK*n^4*(V-EK) + ḡNa*m^3*h*(V-ENa) + ḡL*(V-EL)) + Iapp(t,I₀)
end
end
```
+
For now we turn off the applied current by setting its amplitude, `I₀`, to zero.
`hhmodel` is now a `ReactionSystem` that is coupled to an internal constraint
@@ -97,7 +101,7 @@ action potential.
```@example hh1
tspan = (0.0, 50.0)
u₀ = [:V => -70, :m => 0.0, :h => 0.0, :n => 0.0,
- :m′ => 1.0, :n′ => 1.0, :h′ => 1.0]
+ :m′ => 1.0, :n′ => 1.0, :h′ => 1.0]
oprob = ODEProblem(hhmodel, u₀, tspan)
hhsssol = solve(oprob, Rosenbrock23())
nothing # hide
@@ -133,7 +137,7 @@ plot(sol, idxs = V, legend = :outerright)
We observe three action potentials due to the steady applied current.
-## Building the model via composition of separate systems for the ion channel and transmembrane voltage dynamics
+## Building the model via composition of separate systems for the ion channel and transmembrane voltage dynamics
As an illustration of how one can construct models from individual components,
we now separately construct and compose the model components.
@@ -141,24 +145,25 @@ we now separately construct and compose the model components.
We start by defining systems to model each ionic current. Note we now use
`@network_component` instead of `@reaction_network` as we want the models to be
composable and not marked as finalized.
+
```@example hh1
IKmodel = @network_component IKmodel begin
- @parameters ḡK = 36.0 EK = -82.0
+ @parameters ḡK = 36.0 EK = -82.0
@variables V(t) Iₖ(t)
(αₙ(V), βₙ(V)), n′ <--> n
@equations Iₖ ~ ḡK*n^4*(V-EK)
end
INamodel = @network_component INamodel begin
- @parameters ḡNa = 120.0 ENa = 45.0
+ @parameters ḡNa = 120.0 ENa = 45.0
@variables V(t) Iₙₐ(t)
(αₘ(V), βₘ(V)), m′ <--> m
(αₕ(V), βₕ(V)), h′ <--> h
- @equations Iₙₐ ~ ḡNa*m^3*h*(V-ENa)
+ @equations Iₙₐ ~ ḡNa*m^3*h*(V-ENa)
end
ILmodel = @network_component ILmodel begin
- @parameters ḡL = .3 EL = -59.0
+ @parameters ḡL = .3 EL = -59.0
@variables V(t) Iₗ(t)
@equations Iₗ ~ ḡL*(V-EL)
end
@@ -166,6 +171,7 @@ nothing # hide
```
We next define the voltage dynamics with unspecified values for the currents
+
```@example hh1
hhmodel2 = @network_component hhmodel2 begin
@parameters C = 1.0 I₀ = 0.0
@@ -174,13 +180,16 @@ hhmodel2 = @network_component hhmodel2 begin
end
nothing # hide
```
+
Finally, we extend the `hhmodel` with the systems defining the ion channel currents
+
```@example hh1
@named hhmodel2 = extend(IKmodel, hhmodel2)
@named hhmodel2 = extend(INamodel, hhmodel2)
@named hhmodel2 = extend(ILmodel, hhmodel2)
hhmodel2 = complete(hhmodel2)
```
+
Let's again solve the system starting from the previously calculated resting
state, using the same applied current as above (to verify we get the same
figure). Note, we now run `structural_simplify` from ModelingToolkit to
diff --git a/docs/src/model_creation/examples/noise_modelling_approaches.md b/docs/src/model_creation/examples/noise_modelling_approaches.md
index 39f6d94088..4f5d5176fa 100644
--- a/docs/src/model_creation/examples/noise_modelling_approaches.md
+++ b/docs/src/model_creation/examples/noise_modelling_approaches.md
@@ -1,5 +1,6 @@
# [Approaches for modelling system noise](@id noise_modelling_approaches)
-Catalyst's primary tools for modelling stochasticity include the creation of `SDEProblem`s or `JumpProblem`s from reaction network models. However, other approaches for incorporating model noise exist, some of which will be discussed here. We will first consider *intrinsic* and *extrinsic* noise. These are well-established terms, both of which we will describe below (however, to our knowledge, no generally agreed-upon definition of these terms exists)[^1]. Finally, we will demonstrate a third approach, the utilisation of a noisy input process to an otherwise deterministic system. This approach is infrequently used, however, as it is encountered in the literature, we will demonstrate it here as well.
+
+Catalyst's primary tools for modelling stochasticity include the creation of `SDEProblem`s or `JumpProblem`s from reaction network models. However, other approaches for incorporating model noise exist, some of which will be discussed here. We will first consider *intrinsic* and *extrinsic* noise. These are well-established terms, both of which we will describe below (however, to our knowledge, no generally agreed-upon definition of these terms exists)[^1]. Finally, we will demonstrate a third approach, the utilisation of a noisy input process to an otherwise deterministic system. This approach is infrequently used, however, as it is encountered in the literature, we will demonstrate it here as well.
We note that these approaches can all be combined. E.g. an intrinsic noise model (using an SDE) can be combined with extrinsic noise (using randomised parameter values), while also feeding a noisy input process into the system.
@@ -7,7 +8,9 @@ We note that these approaches can all be combined. E.g. an intrinsic noise model
Here we use intrinsic and extrinsic noise as descriptions of two of our modelling approaches. It should be noted that while these are established terminologies for noisy biological systems[^1], our use of these terms to describe different approaches for modelling noise is only inspired by this terminology, and nothing that is established in the field. Please consider the [references](@ref noise_modelling_approaches_references) for more information on intrinsic and extrinsic noise.
## [The repressilator model](@id noise_modelling_approaches_model_intro)
+
For this tutorial we will use the oscillating [repressilator](@ref basic_CRN_library_repressilator) model.
+
```@example noise_modelling_approaches
using Catalyst
repressilator = @reaction_network begin
@@ -19,9 +22,11 @@ end
```
## [Using intrinsic noise](@id noise_modelling_approaches_model_intrinsic)
-Generally, intrinsic noise is randomness inherent to a system itself. This means that it cannot be controlled for, or filtered out by, experimental settings. Low-copy number cellular systems, were reaction occurs due to the encounters of molecules due to random diffusion, is an example of intrinsic noise. In practise, this can be modelled exactly through [SDE](@ref simulation_intro_SDEs) (chemical Langevin equations) or [jump](@ref simulation_intro_jumps) (stochastic chemical kinetics) simulations.
+
+Generally, intrinsic noise is randomness inherent to a system itself. This means that it cannot be controlled for, or filtered out by, experimental settings. Low-copy number cellular systems, were reaction occurs due to the encounters of molecules due to random diffusion, is an example of intrinsic noise. In practise, this can be modelled exactly through [SDE](@ref simulation_intro_SDEs) (chemical Langevin equations) or [jump](@ref simulation_intro_jumps) (stochastic chemical kinetics) simulations.
In Catalyst, intrinsic noise is accounted for whenever an `SDEProblem` or `JumpProblem` is created and simulated. Here we will model intrinsic noise through SDEs, which means creating an `SDEProblem` using the standard approach.
+
```@example noise_modelling_approaches
u0 = [:X => 45.0, :Y => 20.0, :Z => 20.0]
tend = 200.0
@@ -29,21 +34,26 @@ ps = [:v => 10.0, :K => 20.0, :n => 3, :d => 0.1]
sprob = SDEProblem(repressilator, u0, tend, ps)
nothing # hide
```
+
Next, to illustrate the system's noisiness, we will perform multiple simulations. We do this by [creating an `EnsembleProblem`](@ref ensemble_simulations_monte_carlo). From it, we perform, and plot, 4 simulations.
+
```@example noise_modelling_approaches
using StochasticDiffEq, Plots
eprob_intrinsic = EnsembleProblem(sprob)
sol_intrinsic = solve(eprob_intrinsic, ImplicitEM(); trajectories = 4)
plot(sol_intrinsic; idxs = :X)
```
+
Here, each simulation is performed from the same system using the same settings. Despite this, due to the noise, the individual trajectories are different.
## [Using extrinsic noise](@id noise_modelling_approaches_model_extrinsic)
+
Next, we consider extrinsic noise. This is randomness caused by stochasticity external to, yet affecting, a system. Examples could be different bacteria experiencing different microenvironments or cells being in different parts of the cell cycle. This is noise which (in theory) can be controlled for experimentally (e.g. by ensuring a uniform environment). Whenever a specific source of noise is intrinsic and extrinsic to a system may depend on how one defines the system itself (this is a reason why giving an exact definition of these terms is difficult).
In Catalyst we can model extrinsic noise by letting the model parameters be probability distributions. Here, at the beginning of each simulation, random parameter values are drawn from their distributions. Let us imagine that our repressilator circuit was inserted into a bacterial population. Here, while each bacteria would have the same circuit, their individual number of e.g. ribosomes (which will be random) might affect the production rates (which while constant within each bacteria, might differ between the individuals).
Again we will perform ensemble simulation. Instead of creating an `SDEProblem`, we will create an `ODEProblem`, as well as a [problem function](@ref ensemble_simulations_varying_conditions) which draws random parameter values for each simulation. Here we have implemented the parameter's probability distributions as [normal distributions](https://en.wikipedia.org/wiki/Normal_distribution) using the [Distributions.jl](https://github.com/JuliaStats/Distributions.jl) package.
+
```@example noise_modelling_approaches
using Distributions
p_dists = Dict([:v => Normal(10.0, 2.0), :K => Normal(20.0, 5.0), :n => Normal(3, 0.2), :d => Normal(0.1, 0.02)])
@@ -53,7 +63,9 @@ function prob_func(prob, i, repeat)
end
nothing # hide
```
+
Next, we again perform 4 simulations. While the individual trajectories are performed using deterministic simulations, the randomised parameter values create heterogeneity across the ensemble.
+
```@example noise_modelling_approaches
using OrdinaryDiffEqDefault
oprob = ODEProblem(repressilator, u0, tend, ps)
@@ -61,12 +73,15 @@ eprob_extrinsic = EnsembleProblem(oprob; prob_func)
sol_extrinsic = solve(eprob_extrinsic; trajectories = 4)
plot(sol_extrinsic; idxs = :X)
```
+
We note that a similar approach can be used to also randomise the initial conditions. In a very detailed model, the parameter values could fluctuate during a single simulation, something which could be implemented using the approach from the next section.
## [Using a noisy input process](@id noise_modelling_approaches_model_input_noise)
+
Finally, we will consider the case where we have a deterministic system, but which is exposed to a noisy input process. One example could be a [light sensitive system, where the amount of experienced sunlight is stochastic due to e.g. variable cloud cover](@ref functional_parameters_circ_rhythm). Practically, this can be considered as extrinsic noise, however, we will generate the noise using a different approach from in the previous section. Here, we pre-simulate a random process in time, which we then feed into the system as a functional, time-dependent, parameter. A more detailed description of functional parameters can be found [here](@ref time_dependent_parameters).
-We assume that our repressilator has an input, which corresponds to the $K$ value that controls $X$'s production. First we create a function, `make_K_series`, which creates a randomised time series representing $K$'s value over time.
+We assume that our repressilator has an input, which corresponds to the $K$ value that controls $X$'s production. First we create a function, `make_K_series`, which creates a randomised time series representing $K$'s value over time.
+
```@example noise_modelling_approaches
using DataInterpolations
function make_K_series(; K_mean = 20.0, n = 500, θ = 0.01)
@@ -79,7 +94,9 @@ function make_K_series(; K_mean = 20.0, n = 500, θ = 0.01)
end
plot(make_K_series())
```
+
Next, we create an updated repressilator model, where the input $K$ value is modelled as a time-dependent parameter.
+
```@example noise_modelling_approaches
@parameters (K_in::typeof(make_K_series()))(..)
K_in = K_in(default_t())
@@ -91,7 +108,9 @@ repressilator_Kin = @reaction_network begin
end
nothing # hide
```
+
Finally, we will again perform ensemble simulations of our model. This time, at the beginning of each simulation, we will use `make_K_series` to generate a new $K$, and set this as the `K_in` parameter's value.
+
```@example noise_modelling_approaches
function prob_func_Kin(prob, i, repeat)
p = [ps; :K_in => make_K_series()]
@@ -102,31 +121,39 @@ eprob_inputnoise = EnsembleProblem(oprob; prob_func = prob_func_Kin)
sol_inputnoise = solve(eprob_inputnoise; trajectories = 4)
plot(sol_inputnoise; idxs = :X)
```
-Like in the previous two cases, this generates heterogeneous trajectories across our ensemble.
+Like in the previous two cases, this generates heterogeneous trajectories across our ensemble.
## [Investigating the mean of noisy oscillations](@id noise_modelling_approaches_model_noisy_oscillation_mean)
+
Finally, we will observe an interesting phenomenon for ensembles of stochastic oscillators. First, we create ensemble simulations with a larger number of trajectories.
+
```@example noise_modelling_approaches
sol_intrinsic = solve(eprob_intrinsic, ImplicitEM(); trajectories = 200)
sol_extrinsic = solve(eprob_extrinsic; trajectories = 200)
nothing # hide
```
+
Next, we can use the `EnsembleSummary` interface to plot each ensemble's mean activity (as well as 5% and 95% quantiles) over time:
+
```@example noise_modelling_approaches
e_summary_intrinsic = EnsembleAnalysis.EnsembleSummary(sol_intrinsic, 0.0:1.0:tend)
e_summary_extrinsic = EnsembleAnalysis.EnsembleSummary(sol_extrinsic, 0.0:1.0:tend)
plot(e_summary_intrinsic; label = "Intrinsic noise", idxs = 1)
plot!(e_summary_extrinsic; label = "Extrinsic noise", idxs = 1)
```
+
Here we can see that, over time, the systems' mean $X$ activity reaches a constant level around $30$.
This is a well-known phenomenon (especially in circadian biology[^2]). Here, as stochastic oscillators evolve from a common initial condition the mean behaves as a damped oscillator. This can be caused by two different phenomena:
+
- The individual trajectories are themselves damped.
- The individual trajectories's phases get de-synchronised.
However, if we only observe the mean behaviour (and not the single trajectories), we cannot know which of these cases we are encountering. Here, by checking the single-trajectory plots from the previous sections, we note that this is due to trajectory de-synchronisation. Stochastic oscillators have often been cited as a reason for the importance to study cellular systems at the *single-cell* level, and not just in bulk.
---
+
## [References](@id noise_modelling_approaches_references)
+
[^1]: [Michael B. Elowitz, Arnold J. Levine, Eric D. Siggia, Peter S. Swain, *Stochastic Gene Expression in a Single Cell*, Science (2002).](https://www.science.org/doi/10.1126/science.1070919)
-[^2]: [Qiong Yang, Bernardo F. Pando, Guogang Dong, Susan S. Golden, Alexander van Oudenaarden, *Circadian Gating of the Cell Cycle Revealed in Single Cyanobacterial Cells*, Science (2010).](https://www.science.org/doi/10.1126/science.1181759)
\ No newline at end of file
+[^2]: [Qiong Yang, Bernardo F. Pando, Guogang Dong, Susan S. Golden, Alexander van Oudenaarden, *Circadian Gating of the Cell Cycle Revealed in Single Cyanobacterial Cells*, Science (2010).](https://www.science.org/doi/10.1126/science.1181759)
diff --git a/docs/src/model_creation/examples/programmatic_generative_linear_pathway.md b/docs/src/model_creation/examples/programmatic_generative_linear_pathway.md
index 1007131ec2..dbfd1797dd 100644
--- a/docs/src/model_creation/examples/programmatic_generative_linear_pathway.md
+++ b/docs/src/model_creation/examples/programmatic_generative_linear_pathway.md
@@ -1,34 +1,48 @@
# [Programmatic, Generative, Modelling of a Linear Pathway](@id programmatic_generative_linear_pathway)
+
This example will show how to use programmatic, generative, modelling to model a system implicitly. I.e. rather than listing all system reactions explicitly, the reactions are implicitly generated from a simple set of rules. This example is specifically designed to show how [programmatic modelling](@ref programmatic_CRN_construction) enables *generative workflows* (demonstrating one of its advantages as compared to [DSL-based modelling](@ref dsl_description)). In our example, we will model linear pathways, so we will first introduce these. Next, we will model them first using the DSL, and then using a generative programmatic workflow.
## [Linear pathways](@id programmatic_generative_linear_pathway_intro)
+
Linear pathways consists of a series of species ($X_0$, $X_1$, $X_2$, ..., $X_n$) where each activates the subsequent one[^1]. These are often modelled through the following reaction system:
+
```math
X_{i-1}/\tau,\hspace{0.33cm} ∅ \to X_{i}\\
1/\tau,\hspace{0.33cm} X_{i} \to ∅
```
-for $i = 1, ..., n$, where the activation of $X_1$ depends on some input species $X_0$.
+
+for $i = 1, ..., n$, where the activation of $X_1$ depends on some input species $X_0$.
A common use of these linear pathways is the implementation of *time delays*. Consider a species $X(t)$ which is activated by species $X_0(t)$. This can be modelled by making the production rate of $X(t)$ a function of the *time-delayed* value of $X_0(t)$:
+
```math
f(X_0(t-\tau)),\hspace{0.33cm} ∅ \to X
```
+
This is a so-called *discrete-delay* (which will generate a *delay differential equation*). However, in reality, $X(t)$ probably does not depend on only $f(X_0(t-\tau))$, but rather *a distribution of previous $X_0(t)$ values*. This can be modelled through a *distributed delay*s
+
```math
f(\int_{0}^{\inf} X_0(t-\tau)g(\tau) d\tau),\hspace{0.33cm} ∅ \to X
```
+
for some kernel $g(\tau)$. Here, a common kernel is a [gamma distribution](https://en.wikipedia.org/wiki/Gamma_distribution), which generates a gamma-distributed delay:
+
```math
g(\tau; \alpha, \beta) = \frac{\beta^{\alpha}\tau^{\alpha-1}}{\Gamma(\alpha)}e^{-\beta\tau}
```
+
When this is converted to an ODE, this generates an integro-differential equation. These (as well as the simpler delay differential equations) can be difficult to solve and analyse (especially when SDE or jump simulations are desired). Here, *the linear chain trick* can be used to instead model the delay as a linear pathway of the form described above[^2]. A result by Fargue shows that this is equivalent to a gamma-distributed delay, where $\alpha$ is equivalent to $n$ (the number of species in our linear pathway) and $\beta$ to %\tau$ (the delay length term)[^3]. While modelling time delays using the linear chain trick introduces additional system species, it is often advantageous as it enables simulations using standard ODE, SDE, and Jump methods.
## [Modelling linear pathways using the DSL](@id programmatic_generative_linear_pathway_dsl)
+
It is known that two linear pathways have similar delays if the following equality holds:
+
```math
\frac{1}{\tau_1 n_1} = \frac{1}{\tau_2 n_2}
```
-However, the shape of the delay depends on the number of intermediaries ($n$). Here we wish to investigate this shape for two choices of $n$ ($n = 3$ and $n = 10$). We do so by implementing two models using the DSL, one for each $n$.
+
+However, the shape of the delay depends on the number of intermediaries ($n$). Here we wish to investigate this shape for two choices of $n$ ($n = 3$ and $n = 10$). We do so by implementing two models using the DSL, one for each $n$.
+
```@example programmatic_generative_linear_pathway_dsl
using Catalyst
@@ -54,7 +68,9 @@ lp_n10 = @reaction_network begin
end
nothing # hide
```
+
Next, we prepare an ODE for each model (scaling the initial concentration of $X_0$ and the value of $\tau$ appropriately for each model).
+
```@example programmatic_generative_linear_pathway_dsl
using OrdinaryDiffEqDefault, Plots
u0_n3 = [:X0 => 3*1.0, :X1 => 0.0, :X2 => 0.0, :X3 => 0.0]
@@ -66,7 +82,9 @@ ps_n10 = [:τ => 1.0/10.0]
oprob_n10 = ODEProblem(lp_n10, u0_n10, (0.0, 5.0), ps_n10)
nothing # hide
```
+
Finally, we plot the concentration of the final species in each linear pathway, noting that while the two pulses both peak at $t = 1.0$, their shapes depend on $n$.
+
```@example programmatic_generative_linear_pathway_dsl
sol_n3 = solve(oprob_n3)
sol_n10 = solve(oprob_n10)
@@ -75,9 +93,11 @@ plot!(sol_n10; idxs = :X10, label = "n = 10")
```
## [Modelling linear pathways using programmatic, generative, modelling](@id programmatic_generative_linear_pathway_generative)
+
Above, we investigated the impact of linear pathways' lengths on their behaviours. Since the models were implemented using the DSL, we had to implement a new model for each pathway (in each case writing out all reactions). Here, we will instead show how [programmatic modelling](@ref programmatic_CRN_construction) can be used to generate pathways of arbitrary lengths.
First, we create a function, `generate_lp`, which creates a linear pathway model of length `n`. It utilises *vector variables* to create an arbitrary number of species, and also creates an observable for the final species of the chain.
+
```@example programmatic_generative_linear_pathway_generative
using Catalyst # hide
t = default_t()
@@ -94,7 +114,7 @@ function generate_lp(n)
rxs = [
Reaction(1/τ, [X[1]], []);
[Reaction(X[i]/τ, [], [X[i+1]]) for i in 1:n];
- [Reaction(1/τ, [X[i+1]], []) for i in 1:n]
+ [Reaction(1/τ, [X[i+1]], []) for i in 1:n]
]
# Assembly and return a complete `ReactionSystem` (including an observable for the final species).
@@ -103,7 +123,9 @@ function generate_lp(n)
end
nothing # hide
```
+
Next, we create a function that generates an `ODEProblem` (with appropriate initial conditions and parameter values) for arbitrarily lengthed linear pathway models.
+
```@example programmatic_generative_linear_pathway_generative
function generate_oprob(n)
lp = generate_lp(n)
@@ -115,7 +137,9 @@ function generate_oprob(n)
end
nothing # hide
```
+
We can now simulate linear pathways of arbitrary lengths using a simple syntax. We use this to recreate our previous result from the DSL:
+
```@example programmatic_generative_linear_pathway_generative
using OrdinaryDiffEqDefault, Plots # hide
sol_n3 = solve(generate_oprob(3))
@@ -123,15 +147,18 @@ sol_n10 = solve(generate_oprob(10))
plot(sol_n3; idxs = :Xend, label = "n = 3")
plot!(sol_n10; idxs = :Xend, label = "n = 10")
```
+
If we wish to investigate the behaviour of a pathway with a different length, we can easily add this to the plot
+
```@example programmatic_generative_linear_pathway_generative
sol_n20 = solve(generate_oprob(20))
plot!(sol_n20; idxs = :Xend, label = "n = 20")
```
-
---
+
## References
+
[^1]: [N. Korsbo, H. Jönsson *It’s about time: Analysing simplifying assumptions for modelling multi-step pathways in systems biology*, PLoS Computational Biology (2020).](https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1007982)
[^2]: [J. Metz, O. Diekmann *The Abstract Foundations of Linear Chain Trickery* (1991).](https://ir.cwi.nl/pub/1559/1559D.pdf)
[^3]: D. Fargue *Reductibilite des systemes hereditaires a des systemes dynamiques (regis par des equations differentielles aux derivees partielles)*, Comptes rendus de l'Académie des Sciences (1973).
diff --git a/docs/src/model_creation/examples/smoluchowski_coagulation_equation.md b/docs/src/model_creation/examples/smoluchowski_coagulation_equation.md
index 7ed5a96bff..09fce9b8cd 100644
--- a/docs/src/model_creation/examples/smoluchowski_coagulation_equation.md
+++ b/docs/src/model_creation/examples/smoluchowski_coagulation_equation.md
@@ -1,15 +1,19 @@
# [Smoluchowski Coagulation Equation](@id smoluchowski_coagulation_equation)
+
This tutorial shows how to programmatically construct a [`ReactionSystem`](@ref) corresponding to the chemistry underlying the [Smoluchowski coagulation model](https://en.wikipedia.org/wiki/Smoluchowski_coagulation_equation) using [ModelingToolkit](http://docs.sciml.ai/ModelingToolkit/stable/)/[Catalyst](http://docs.sciml.ai/Catalyst/stable/). A jump process version of the model is then constructed from the [`ReactionSystem`](@ref), and compared to the model's analytical solution obtained by the [method of Scott](https://journals.ametsoc.org/view/journals/atsc/25/1/1520-0469_1968_025_0054_asocdc_2_0_co_2.xml) (see also [3](https://doi.org/10.1006/jcph.2002.7017)).
The Smoluchowski coagulation equation describes a system of reactions in which monomers may collide to form dimers, monomers and dimers may collide to form trimers, and so on. This models a variety of chemical/physical processes, including polymerization and flocculation.
We begin by importing some necessary packages.
+
```@example smcoag1
using ModelingToolkit, Catalyst, LinearAlgebra
using JumpProcesses
using Plots, SpecialFunctions
```
+
Suppose the maximum cluster size is `N`. We assume an initial concentration of monomers, `Nₒ`, and let `uₒ` denote the initial number of monomers in the system. We have `nr` total reactions, and label by `V` the bulk volume of the system (which plays an important role in the calculation of rate laws since we have bimolecular reactions). Our basic parameters are then
+
```@example smcoag1
# maximum cluster size
N = 10
@@ -31,6 +35,7 @@ n = floor(Int, N / 2)
nr = ((N % 2) == 0) ? (n*(n + 1) - n) : (n*(n + 1))
nothing #hide
```
+
The [Smoluchowski coagulation equation](https://en.wikipedia.org/wiki/Smoluchowski_coagulation_equation) Wikipedia page illustrates the set of possible reactions that can occur. We can easily enumerate the `pair`s of multimer reactants that can combine when allowing a maximal cluster size of `N` monomers. We initialize the volumes of the reactant multimers as `volᵢ` and `volⱼ`
```@example smcoag1
@@ -48,7 +53,9 @@ volⱼ = Vₒ * vⱼ # cm⁻³
sum_vᵢvⱼ = @. vᵢ + vⱼ # Product index
nothing #hide
```
+
We next specify the rates (i.e. kernel) at which reactants collide to form products. For simplicity, we allow a user-selected additive kernel or constant kernel. The constants(`B` and `C`) are adopted from Scott's paper [2](https://journals.ametsoc.org/view/journals/atsc/25/1/1520-0469_1968_025_0054_asocdc_2_0_co_2.xml)
+
```@example smcoag1
# set i to 1 for additive kernel, 2 for constant
i = 1
@@ -63,7 +70,9 @@ elseif i==2
end
nothing #hide
```
+
We'll set the parameters and the initial condition that only monomers are present at ``t=0`` in `u₀map`.
+
```@example smcoag1
# k is a vector of the parameters, with values given by the vector kv
@parameters k[1:nr] = kv
@@ -72,7 +81,7 @@ We'll set the parameters and the initial condition that only monomers are presen
t = default_t()
@species (X(t))[1:N]
-# time-span
+# time span
if i == 1
tspan = (0.0, 2000.0)
elseif i == 2
@@ -85,7 +94,9 @@ u₀[1] = uₒ
u₀map = Pair.(collect(X), u₀) # map species to its initial value
nothing #hide
```
+
Here we generate the reactions programmatically. We systematically create Catalyst `Reaction`s for each possible reaction shown in the figure on [Wikipedia](https://en.wikipedia.org/wiki/Smoluchowski_coagulation_equation). When `vᵢ[n] == vⱼ[n]`, we set the stoichiometric coefficient of the reactant multimer to two.
+
```@example smcoag1
# vector to store the Reactions in
rx = []
@@ -101,7 +112,9 @@ end
@named rs = ReactionSystem(rx, t, collect(X), [k])
rs = complete(rs)
```
+
We now convert the [`ReactionSystem`](@ref) into a `ModelingToolkit.JumpSystem`, and solve it using Gillespie's direct method. For details on other possible solvers (SSAs), see the [DifferentialEquations.jl](https://docs.sciml.ai/DiffEqDocs/stable/types/jump_types/) documentation
+
```@example smcoag1
# solving the system
jinputs = JumpInputs(rs, u₀map, tspan)
@@ -109,7 +122,9 @@ jprob = JumpProblem(jinputs, Direct(); save_positions = (false, false))
jsol = solve(jprob; saveat = tspan[2] / 30)
nothing #hide
```
+
Lets check the results for the first three polymers/cluster sizes. We compare to the analytical solution for this system:
+
```@example smcoag1
# Results for first three polymers...i.e. monomers, dimers and trimers
v_res = [1; 2; 3]
@@ -145,7 +160,9 @@ plot!(ϕ, sol[3,:] / Nₒ, line = (:dot, 4, :purple), label = "Analytical sol--X
```
---
+
## References
+
1. [https://en.wikipedia.org/wiki/Smoluchowski\_coagulation\_equation](https://en.wikipedia.org/wiki/Smoluchowski_coagulation_equation)
-2. Scott, W. T. (1968). Analytic Studies of Cloud Droplet Coalescence I, Journal of Atmospheric Sciences, 25(1), 54-65. Retrieved Feb 18, 2021, from https://journals.ametsoc.org/view/journals/atsc/25/1/1520-0469\_1968\_025\_0054\_asocdc\_2\_0\_co\_2.xml
-3. Ian J. Laurenzi, John D. Bartels, Scott L. Diamond, A General Algorithm for Exact Simulation of Multicomponent Aggregation Processes, Journal of Computational Physics, Volume 177, Issue 2, 2002, Pages 418-449, ISSN 0021-9991, https://doi.org/10.1006/jcph.2002.7017.
+2. Scott, W. T. (1968). Analytic Studies of Cloud Droplet Coalescence I, Journal of Atmospheric Sciences, 25(1), 54-65. Retrieved Feb 18, 2021, from
+3. Ian J. Laurenzi, John D. Bartels, Scott L. Diamond, A General Algorithm for Exact Simulation of Multicomponent Aggregation Processes, Journal of Computational Physics, Volume 177, Issue 2, 2002, Pages 418-449, ISSN 0021-9991, .
diff --git a/docs/src/model_creation/functional_parameters.md b/docs/src/model_creation/functional_parameters.md
index 6f47193da0..61637a0285 100644
--- a/docs/src/model_creation/functional_parameters.md
+++ b/docs/src/model_creation/functional_parameters.md
@@ -1,10 +1,13 @@
# [Inputs and time-dependent (or functional) parameters](@id time_dependent_parameters)
+
Catalyst supports the usage of "functional parameters". In practice, these are parameters that are given by (typically) time-dependent functions (they can also depend on e.g. species values, as discussed [here](@ref functional_parameters_sir)). They are a way to inject custom functions into models. Functional parameters can be used when rates depend on real data, or to represent complicated functions (which use e.g. `for` loops or random number generation). Here, the function's values are declared as a data interpolation (which interpolates discrete samples to a continuous function). This is then used as the functional parameter's value in the simulation. This tutorial first shows how to create time-dependent functional parameters, and then gives an example where the functional parameter depends on a species value.
An alternative approach for representing complicated functions is by [using `@register_symbolic`](@ref dsl_description_nonconstant_rates_function_registration).
## [Basic example](@id functional_parameters_basic_example)
+
Let us first consider an easy, quick-start example (the next section will discuss what is going on in more detail). We will consider a simple [birth-death model](@ref basic_CRN_library_bd), but where the birth rate is determined by an input parameter (for which the value depends on time). First, we [define the input parameter programmatically](@ref programmatic_CRN_construction), and its values across all time points using the [DataInterpolations.jl](https://github.com/SciML/DataInterpolations.jl) package. In this example we will use the input function $pIn(t) = (2 + t)/(1 + t)$. Finally, we plot the input function, demonstrating how while it is defined at discrete points, DataInterpolations.jl generalises this to a continuous function.
+
```@example functional_parameters_basic_example
using Catalyst, DataInterpolations, Plots
t = default_t()
@@ -14,14 +17,18 @@ spline = LinearInterpolation((2 .+ ts) ./ (1 .+ ts), ts)
@parameters (pIn::typeof(spline))(..)
plot(spline)
```
+
Next, we create our model, [interpolating](@ref dsl_advanced_options_symbolics_and_DSL_interpolation) the input parameter into the `@reaction_network` declaration.
+
```@example functional_parameters_basic_example
bd_model = @reaction_network begin
$pIn(t), 0 --> X
d, X --> 0
end
```
+
Finally, we can simulate our model as normal (but where we set the value of the `pIn` parameter to our interpolated data).
+
```@example functional_parameters_basic_example
using OrdinaryDiffEqDefault
u0 = [:X => 0.5]
@@ -30,12 +37,14 @@ oprob = ODEProblem(bd_model, u0, tend, ps)
sol = solve(oprob)
plot(sol)
```
+
!!! note
For this simple example, $(2 + t)/(1 + t)$ could have been used directly as a reaction rate (or written as a normal function), technically making the functional parameter approach unnecessary. However, here we used this function as a simple example of how discrete data can be made continuous using DataInterpolations, and then have its values inserted using a (functional) parameter.
-
## [Inserting a customised, time-dependent, input](@id functional_parameters_circ_rhythm)
+
Let us now go through everything again, but providing some more details. Let us first consider the input parameter. We have previously described how a [time-dependent rate can model a circadian rhythm](@ref dsl_description_nonconstant_rates_time). For real applications, due to e.g. clouds, sunlight is not a perfect sine wave. Here, a common solution is to take real sunlight data from some location and use in the model. Here, we will create synthetic (noisy) data as our light input:
+
```@example functional_parameters_circ_rhythm
using Plots
tend = 120.0
@@ -44,13 +53,17 @@ light = sin.(ts/6) .+ 1
light = [max(0.0, l - rand()) for l in light]
plot(ts, light; seriestype = :scatter, label = "Experienced light")
```
+
Now this input is only actually defined at the sample points, making it incompatible with a continuous ODE simulation. To enable this, we will use the DataInterpolations package to create an interpolated version of this data, which forms the actual input:
+
```@example functional_parameters_circ_rhythm
using DataInterpolations
interpolated_light = LinearInterpolation(light, ts)
plot(interpolated_light)
```
+
We are now ready to declare our model. We will consider a protein with an active and an inactive form ($Pₐ$ and $Pᵢ$) where the activation is driven by the presence of sunlight. In this example we we create our model using the [programmatic approach](@ref programmatic_CRN_construction). Do note the special syntax we use to declare our input parameter, where we both designate it as a generic function and its type as the type of our interpolated input. Also note that, within the model, we mark the input parameter (`light_in`) as a function of `t`.
+
```@example functional_parameters_circ_rhythm
using Catalyst
t = default_t()
@@ -64,7 +77,9 @@ rxs = [
@named rs = ReactionSystem(rxs, t)
rs = complete(rs)
```
+
Now we can simulate our model. Here, we use the interpolated data as the input parameter's value.
+
```@example functional_parameters_circ_rhythm
using OrdinaryDiffEqDefault
u0 = [Pᵢ => 1.0, Pₐ => 0.0]
@@ -75,33 +90,45 @@ plot(sol)
```
### [Interpolating the input into the DSL](@id functional_parameters_circ_rhythm_dsl)
+
It is possible to use time-dependent inputs when creating models [through the DSL](@ref dsl_description) as well. However, it can still be convenient to declare the input parameter programmatically as above. Next, we can [interpolate](@ref dsl_advanced_options_symbolics_and_DSL_interpolation) it into our DSL-declaration (ensuring to also make it a function of `t`):
+
```@example functional_parameters_circ_rhythm
rs_dsl = @reaction_network rs begin
(kA*$light_in(t), kD), Pᵢ <--> Pₐ
end
```
+
We can confirm that this model is identical to our programmatic one (and should we wish to, we can simulate it using identical syntax).
+
```@example functional_parameters_circ_rhythm
rs == rs_dsl
```
## [Non-time functional parameters](@id functional_parameters_sir)
+
Previously we have demonstrated functional parameters that are functions of time. However, functional parameters can be functions of any variable (however, currently, more than one argument is not supported). Here we will demonstrate this using a [SIR model](@ref basic_CRN_library_sir), but instead of having the infection rate scale linearly with the number of infected individuals, we instead assume we have measured data of the infection rate (as dependent on the number of infected individuals) and wish to use this instead. Normally we use the following infection reaction in the SIR model:
+
```julia
@reaction k1, S + I --> 2I
```
+
For ODE models, this would give the same equations as
+
```julia
@reaction k1*I, S --> I
```
+
Due to performance reasons (especially for jump simulations) the former approach is *strongly* encouraged. Here, however, we will assume that we have measured real data of how the number of infected individuals affects the infection rate, and wish to use this in our model, i.e. something like this:
+
```julia
@reaction k1*i_rate(I), S --> I
```
+
This is a case where we can use a functional parameter, whose value depends on the value of $I$.
We start by declaring the functional parameter that describes how the infection rate depends on the number of infected individuals. We also plot the measured infection rate, and compare it to the theoretical rate usually used in the SIR model.
+
```@example functional_parameters_sir
using DataInterpolations, Plots
I_grid = collect(0.0:5.0:100.0)
@@ -110,7 +137,9 @@ I_rate = LinearInterpolation(I_measured, I_grid)
plot(I_rate; label = "Measured infection rate")
plot!(I_grid, I_grid; label = "Normal SIR infection rate")
```
+
Next, we create our model (using the DSL approach).
+
```@example functional_parameters_sir
using Catalyst
@parameters (inf_rate::typeof(I_rate))(..)
@@ -120,7 +149,9 @@ sir = @reaction_network rs begin
end
nothing # hide
```
+
Finally, we can simulate our model.
+
```@example functional_parameters_sir
using OrdinaryDiffEqDefault
u0 = [:S => 99.0, :I => 1.0, :R => 0.0]
@@ -129,11 +160,14 @@ oprob = ODEProblem(sir, u0, 250.0, ps)
sol = solve(oprob)
plot(sol)
```
+
!!! note
- When declaring a functional parameter of time, it is easy to set its grid values (i.e. ensure they range from the first to the final time point). For Functional parameters that depend on species concentrations it is trickier, and one must make sure that any potential input-species values that can occur during the simulation are represented in the interpolation.
+ When declaring a functional parameter of time, it is easy to set its grid values (i.e. ensure they range from the first to the final time point). For Functional parameters that depend on species concentrations it is trickier, and one must make sure that any potential input-species values that can occur during the simulation are represented in the interpolation.
### [Using different data interpolation approaches](@id functional_parameters_interpolation_algs)
+
Up until now we have used [linear interpolation](https://en.wikipedia.org/wiki/Linear_interpolation) of our data. However, DataInterpolations.jl [supports other interpolation methods](https://docs.sciml.ai/DataInterpolations/stable/methods/). To demonstrate these we here generate a data set, and then show the linear, cubic, and constant interpolations:
+
```@example functional_parameters_interpolation_algs
using DataInterpolations, Plots
xs = collect(0.0:1.0:10.0)
@@ -145,4 +179,5 @@ plot(spline_linear)
plot!(spline_cubuc)
plot!(spline_const)
```
-Finally, DataInterpolations.jl also allows various [extrapolation methods](https://docs.sciml.ai/DataInterpolations/stable/extrapolation_methods/).
\ No newline at end of file
+
+Finally, DataInterpolations.jl also allows various [extrapolation methods](https://docs.sciml.ai/DataInterpolations/stable/extrapolation_methods/).
diff --git a/docs/src/model_creation/model_file_loading_and_export.md b/docs/src/model_creation/model_file_loading_and_export.md
index 223e839fb4..af0643bd1d 100644
--- a/docs/src/model_creation/model_file_loading_and_export.md
+++ b/docs/src/model_creation/model_file_loading_and_export.md
@@ -1,8 +1,11 @@
# [Loading Chemical Reaction Network Models from Files](@id model_file_import_export)
+
Catalyst stores chemical reaction network (CRN) models in `ReactionSystem` structures. This tutorial describes how to load such `ReactionSystem`s from, and save them to, files. This can be used to save models between Julia sessions, or transfer them from one session to another. Furthermore, to facilitate the computation modelling of CRNs, several standardised file formats have been created to represent CRN models (e.g. [SBML](https://sbml.org/)). This enables CRN models to be shared between different software and programming languages. While Catalyst itself does not have the functionality for loading such files, we will here (briefly) introduce a few packages that can load different file types to Catalyst `ReactionSystem`s.
## [Saving Catalyst models to, and loading them from, Julia files](@id model_file_import_export_crn_serialization)
+
Catalyst provides a `save_reactionsystem` function, enabling the user to save a `ReactionSystem` to a file. Here we demonstrate this by first creating a [simple cross-coupling model](@ref basic_CRN_library_cc):
+
```@example file_handling_1
using Catalyst
cc_system = @reaction_network begin
@@ -11,11 +14,15 @@ cc_system = @reaction_network begin
k₃, CP --> C + P
end
```
+
and next saving it to a file
+
```@example file_handling_1
save_reactionsystem("cross_coupling.jl", cc_system)
```
+
Here, `save_reactionsystem`'s first argument is the path to the file where we wish to save it. The second argument is the `ReactionSystem` we wish to save. To load the file, we use Julia's `include` function:
+
```@example file_handling_1
cc_loaded = include("cross_coupling.jl")
rm("cross_coupling.jl") # hide
@@ -26,7 +33,8 @@ cc_loaded # hide
The destination file can be in a folder. E.g. `save_reactionsystem("my\_folder/reaction_network.jl", rn)` saves the model to the file "reaction\_network.jl" in the folder "my_folder".
Here, `include` is used to execute the Julia code from any file. This means that `save_reactionsystem` actually saves the model as executable code which re-generates the exact model which was saved (this is the reason why we use the ".jl" extension for the saved file). Indeed, we can confirm this if we check what is printed in the file:
-```
+
+```julia
let
# Independent variable:
@@ -51,19 +59,24 @@ complete(rs)
end
```
+
!!! note
The code that `save_reactionsystem` prints uses [programmatic modelling](@ref programmatic_CRN_construction) to generate the written model.
In addition to transferring models between Julia sessions, the `save_reactionsystem` function can also be used or print a model to a text file where you can easily inspect its components.
## [Loading and saving arbitrary Julia variables using Serialization.jl](@id model_file_import_export_julia_serialisation)
+
Julia provides a general and lightweight interface for loading and saving Julia structures to and from files that it can be good to be aware of. It is called [Serialization.jl](https://docs.julialang.org/en/v1/stdlib/Serialization/) and provides two functions, `serialize` and `deserialize`. The first allows us to write a Julia structure to a file. E.g. if we wish to save a parameter set associated with our model, we can use
+
```@example file_handling_2
using Serialization
ps = [:k₁ => 1.0, :k₂ => 0.1, :k₃ => 2.0]
serialize("saved_parameters.jls", ps)
```
+
Here, we use the extension ".jls" (standing for **J**u**L**ia **S**erialization), however, any extension code can be used. To load a structure, we can then use
+
```@example file_handling_2
loaded_sol = deserialize("saved_parameters.jls")
rm("saved_parameters.jls") # hide
@@ -71,14 +84,18 @@ loaded_sol # hide
```
## [Loading .net files using ReactionNetworkImporters.jl](@id model_file_import_export_sbml_rni_net)
+
A general-purpose format for storing CRN models is so-called .net files. These can be generated by e.g. [BioNetGen](https://bionetgen.org/). The [ReactionNetworkImporters.jl](https://github.com/SciML/ReactionNetworkImporters.jl) package enables the loading of such files to Catalyst `ReactionSystem`. Here we load a [Repressilator](@ref basic_CRN_library_repressilator) model stored in the "repressilator.net" file:
+
```julia
using ReactionNetworkImporters
prn = loadrxnetwork(BNGNetwork(), "repressilator.net")
```
+
Here, .net files not only contain information regarding the reaction network itself, but also the numeric values (initial conditions and parameter values) required for simulating it. Hence, `loadrxnetwork` generates a `ParsedReactionNetwork` structure, containing all this information. You can access the model as `prn.rn`, the initial conditions as `prn.u0`, and the parameter values as `prn.p`. Furthermore, these initial conditions and parameter values are also made [*default* values](@ref dsl_advanced_options_default_vals) of the model.
A parsed reaction network's content can then be provided to various problem types for simulation. E.g. here we perform an ODE simulation of our repressilator model:
+
```julia
using Catalyst, OrdinaryDiffEqDefault, Plots
tspan = (0.0, 10000.0)
@@ -86,25 +103,29 @@ oprob = ODEProblem(prn.rn, Float64[], tspan, Float64[])
sol = solve(oprob)
plot(sol; idxs = [:mTetR, :mLacI, :mCI])
```
+

Note that, as all initial conditions and parameters have default values, we can provide empty vectors for these into our `ODEProblem`.
-
!!! note
It should be noted that .net files support a wide range of potential model features, not all of which are currently supported by ReactionNetworkImporters. Hence, there might be some .net files which `loadrxnetwork` will not be able to load.
A more detailed description of ReactionNetworkImporter's features can be found in its [documentation](https://docs.sciml.ai/ReactionNetworkImporters/stable/).
## [Loading SBML files using SBMLImporter.jl and SBMLToolkit.jl](@id model_file_import_export_sbml)
+
The Systems Biology Markup Language (SBML) is the most widespread format for representing CRN models. Currently, there exist two different Julia packages, [SBMLImporter.jl](https://github.com/sebapersson/SBMLImporter.jl) and [SBMLToolkit.jl](https://github.com/SciML/SBMLToolkit.jl), that are able to load SBML files to Catalyst `ReactionSystem` structures. SBML is able to represent a *very* wide range of model features, with both packages supporting most features. However, there exist SBML files (typically containing obscure model features such as events with time delays) that currently cannot be loaded into Catalyst models.
SBMLImporter's `load_SBML` function can be used to load SBML files. Here, we load a [Brusselator](@ref basic_CRN_library_brusselator) model stored in the "brusselator.xml" file:
+
```julia
using SBMLImporter
prn, cbs = load_SBML("brusselator.xml", massaction = true)
```
+
Here, while [ReactionNetworkImporters generates a `ParsedReactionSystem` only](@ref model_file_import_export_sbml_rni_net), SBMLImporter generates a `ParsedReactionSystem` (here stored in `prn`) and a [so-called `CallbackSet`](https://docs.sciml.ai/DiffEqDocs/stable/features/callback_functions/#CallbackSet) (here stored in `cbs`). While `prn` can be used to create various problems, when we simulate them, we must also supply `cbs`. E.g. to simulate our brusselator we use:
+
```julia
using Catalyst, OrdinaryDiffEqDefault, Plots
tspan = (0.0, 50.0)
@@ -112,6 +133,7 @@ oprob = ODEProblem(prn.rn, prn.u0, tspan, prn.p)
sol = solve(oprob; callback = cbs)
plot(sol)
```
+

Note that, while ReactionNetworkImporters adds initial condition and species values as default to the imported model, SBMLImporter does not do this. These must hence be provided to the `ODEProblem` directly.
@@ -122,22 +144,28 @@ A more detailed description of SBMLImporter's features can be found in its [docu
The `massaction = true` option informs the importer that the target model follows mass-action principles. When given, this enables SBMLImporter to make appropriate modifications to the model (which are important for e.g. jump simulation performance).
### [SBMLImporter and SBMLToolkit](@id model_file_import_export_package_alts)
+
Above, we described how to use SBMLImporter to import SBML files. Alternatively, SBMLToolkit can be used instead. It has a slightly different syntax, which is described in its [documentation](https://github.com/SciML/SBMLToolkit.jl), and does not support as wide a range of SBML features as SBMLImporter. A short comparison of the two packages can be found [here](https://github.com/sebapersson/SBMLImporter.jl?tab=readme-ov-file#differences-compared-to-sbmltoolkit). Generally, while they both perform well, we note that for *jump simulations* SBMLImporter is preferable (its way for internally representing reaction event enables more performant jump simulations).
## [Loading models from matrix representation using ReactionNetworkImporters.jl](@id model_file_import_export_matrix_representations)
+
While CRN models can be represented through various file formats, they can also be represented in various matrix forms. E.g. a CRN with $m$ species and $n$ reactions (and with constant rates) can be represented with either
+
- An $mxn$ substrate matrix (with each species's substrate stoichiometry in each reaction) and an $nxm$ product matrix (with each species's product stoichiometry in each reaction).
Or
+
- An $mxn$ complex stoichiometric matrix (...) and a $2mxn$ incidence matrix (...).
The advantage of these forms is that they offer a compact and very general way to represent a large class of CRNs. ReactionNetworkImporters have the functionality for converting matrices of these forms directly into Catalyst `ReactionSystem` models. Instructions on how to do this are available in [ReactionNetworkImporter's documentation](https://docs.sciml.ai/ReactionNetworkImporters/stable/#Loading-a-matrix-representation).
-
---
+
## [Citations](@id petab_citations)
+
If you use any of this functionality in your research, [in addition to Catalyst](@ref doc_index_citation), please cite the paper(s) corresponding to whichever package(s) you used:
-```
+
+```bibtex
@software{2022ReactionNetworkImporters,
author = {Isaacson, Samuel},
title = {{ReactionNetworkImporters.jl}},
@@ -145,7 +173,8 @@ If you use any of this functionality in your research, [in addition to Catalyst]
year = {2022}
}
```
-```
+
+```bibtex
@software{2024SBMLImporter,
author = {Persson, Sebastian},
title = {{SBMLImporter.jl}},
@@ -153,16 +182,17 @@ If you use any of this functionality in your research, [in addition to Catalyst]
year = {2024}
}
```
-```
+
+```bibtex
@article{LangJainRackauckas+2024,
- url = {https://doi.org/10.1515/jib-2024-0003},
- title = {SBMLToolkit.jl: a Julia package for importing SBML into the SciML ecosystem},
- title = {},
- author = {Paul F. Lang and Anand Jain and Christopher Rackauckas},
- pages = {20240003},
- journal = {Journal of Integrative Bioinformatics},
- doi = {doi:10.1515/jib-2024-0003},
- year = {2024},
- lastchecked = {2024-06-02}
+ url = {https://doi.org/10.1515/jib-2024-0003},
+ title = {SBMLToolkit.jl: a Julia package for importing SBML into the SciML ecosystem},
+ title = {},
+ author = {Paul F. Lang and Anand Jain and Christopher Rackauckas},
+ pages = {20240003},
+ journal = {Journal of Integrative Bioinformatics},
+ doi = {doi:10.1515/jib-2024-0003},
+ year = {2024},
+ lastchecked = {2024-06-02}
}
```
diff --git a/docs/src/model_creation/model_visualisation.md b/docs/src/model_creation/model_visualisation.md
index bd30a40f3c..96b12e07c6 100644
--- a/docs/src/model_creation/model_visualisation.md
+++ b/docs/src/model_creation/model_visualisation.md
@@ -1,10 +1,13 @@
# [Model Visualisation](@id visualisation)
+
Catalyst-created `ReactionSystem` models can be visualised either as LaTeX code (of either the model reactions or its equations) or as a network graph. This section describes both functionalities.
## [Displaying models using LaTeX](@id visualisation_latex)
+
Once a model has been created, the [Latexify.jl](https://github.com/korsbo/Latexify.jl) package can be used to generate LaTeX code of the model. This can either be used for easy model inspection (e.g. to check which equations are being simulated), or to generate code which can be directly pasted into a LaTeX document.
Let us consider a simple [Brusselator model](@ref basic_CRN_library_brusselator):
+
```@example visualisation_latex
using Catalyst
brusselator = @reaction_network begin
@@ -14,28 +17,34 @@ brusselator = @reaction_network begin
1, X --> ∅
end
```
+
To display its reaction (using LaTeX formatting) we run `latexify` with our model as input:
+
```@example visualisation_latex
using Latexify
latexify(brusselator)
brusselator # hide
```
+
Here, we note that the output of `latexify(brusselator)` is identical to how a model is displayed by default. Indeed, the reason is that Catalyst internally uses Latexify's `latexify` function to display its models. It is also possible to display the ODE equations a model would generate by adding the `form = :ode` argument:
+
```@example visualisation_latex
latexify(brusselator; form = :ode)
```
+
!!! note
Internally, `latexify(brusselator; form = :ode)` calls `latexify(convert(ODESystem, brusselator))`. Hence, if you have already generated the `ODESystem` corresponding to your model, it can be used directly as input to `latexify`.
-!!! note
+!!! note
It should be possible to also generate SDEs through the `form = :sde` input. This feature is, however, currently broken.
If you wish to copy the output to your [clipboard](https://en.wikipedia.org/wiki/Clipboard_(computing)) (e.g. so that you can paste it into a LaTeX document), run `copy_to_clipboard(true)` before you run `latexify`. A more throughout description of Latexify's features can be found in [its documentation](https://korsbo.github.io/Latexify.jl/stable/).
!!! note
- For a model to be nicely displayed you have to use an IDE that actually supports this (such as a [notebook](https://jupyter.org/)). Other environments (such as [the Julia REPL](https://docs.julialang.org/en/v1/stdlib/REPL/)) will simply return the full LaTeX code which would generate the desired expression.
+ For a model to be nicely displayed you have to use an IDE that actually supports this (such as a [notebook](https://jupyter.org/)). Other environments (such as [the Julia REPL](https://docs.julialang.org/en/v1/stdlib/REPL/)) will simply return the full LaTeX code which would generate the desired expression.
## [Displaying model networks](@id visualisation_graphs)
+
Catalyst uses [GraphMakie.jl](https://github.com/MakieOrg/GraphMakie.jl) to display representations of chemical reaction networks, including the complex graph and the species-reaction graph (which is similar to the [Petri net](https://en.wikipedia.org/wiki/Petri_net) representation). To get started, import Catalyst, GraphMakie, and NetworkLayout to load the `CatalystGraphMakieExtension` extension, and then load a Makie backend ([`CairoMakie`](https://github.com/MakieOrg/Makie.jl) is a good lightweight choice). Here, while Catalyst primarily uses [Plots.jl](https://github.com/JuliaPlots/Plots.jl) for plotting, [Makie](https://github.com/MakieOrg/Makie.jl) is used for displaying network graphs. Makie can also be used for plotting more generally (and is also a preferred option for some).
```@example visualisation_graphs
@@ -44,7 +53,8 @@ using CairoMakie
nothing # hide
```
-Let's declare a [Brusselator model](@ref basic_CRN_library_brusselator) to see this plotting functionality. The functions `plot_network` and `plot_complexes` are used to create the species-reaction and complex graphs, respectively. For a more thorough description of these two representations, please see the [network visualization](@ref network_visualization) section of the API, but the gist is that the species-reaction graph has species and reactions as nodes, and the complex graph has reaction complexes as nodes. Below we will plot the species-reaction graph using `plot_network`.
+Let's declare a [Brusselator model](@ref basic_CRN_library_brusselator) to see this plotting functionality. The functions `plot_network` and `plot_complexes` are used to create the species-reaction and complex graphs, respectively. For a more thorough description of these two representations, please see the [network visualization](@ref network_visualization) section of the API, but the gist is that the species-reaction graph has species and reactions as nodes, and the complex graph has reaction complexes as nodes. Below we will plot the species-reaction graph using `plot_network`.
+
```@example visualisation_graphs
brusselator = @reaction_network begin
A, ∅ --> X
@@ -56,6 +66,7 @@ plot_network(brusselator)
```
The species-reaction graph (or network graph) represents species as blue nodes and reactions as green dots. Black arrows from species to reactions indicate substrates, and are labelled with their respective stoichiometries. Similarly, black arrows from reactions to species indicate products (also labelled with their respective stoichiometries). If there are any reactions where a species affect the rate, but does not participate as a reactant, this is displayed with a dashed red arrow. This can be seen in the following [Repressilator model](@ref basic_CRN_library_repressilator):
+
```@example visualisation_graphs
repressilator = @reaction_network begin
hillr(Z,v,K,n), ∅ --> X
@@ -66,22 +77,27 @@ end
plot_network(repressilator)
```
-A generated graph can be saved using Makie's `save` function.
+A generated graph can be saved using Makie's `save` function.
+
```julia
repressilator_graph = plot_network(repressilator)
save("repressilator_graph.png", repressilator_graph)
```
Finally, a [network's reaction complexes](@ref network_analysis_reaction_complexes) (and the reactions in between these) can be displayed using the `plot_complexes(brusselator)` function:
+
```@example visualisation_graphs
plot_complexes(brusselator)
```
-Here, reaction complexes are displayed as blue nodes, and reactions between complexes are displayed as black arrows. Red arrows indicate that the rate constantof a reaction has a species-dependence. Edges can be optionally labeled with their rate expressions by calling with the option `show_rate_labels`.
+
+Here, reaction complexes are displayed as blue nodes, and reactions between complexes are displayed as black arrows. Red arrows indicate that the rate constant of a reaction has a species-dependence. Edges can be optionally labeled with their rate expressions by calling with the option `show_rate_labels`.
+
```@example visualisation_graphs
plot_complexes(brusselator, show_rate_labels = true)
```
## Customizing Plots
+
In this section we demonstrate some of the ways that plot objects can be manipulated to give nicer images. Let's start with our brusselator plot once again. Note that the `plot` function returns three objects: the `Figure`, the `Axis`, and the `Plot`, which can each be customized independently. See the general [Makie documentation](https://docs.makie.org/stable/) for more information.
```@example visualisation_graphs
@@ -89,13 +105,15 @@ f, ax, p = plot_complexes(brusselator, show_rate_labels = true)
```
It seems like a bit of the top node is cut off. Let's increase the top and bottom margins by increasing `yautolimitmargin`.
+
```@example visualisation_graphs
ax.yautolimitmargin = (0.3, 0.3) # defaults to (0.15, 0.15)
ax.aspect = DataAspect()
f
```
-There are many keyword arguments that can be passed to `plot_network` or `plot_complexes` to change the look of the graph (which get passed to the `graphplot` Makie recipe). Let's change the color of the nodes and make the inner labels a bit smaller. Let's also give the plot a title.
+There are many keyword arguments that can be passed to `plot_network` or `plot_complexes` to change the look of the graph (which get passed to the `graphplot` Makie recipe). Let's change the color of the nodes and make the inner labels a bit smaller. Let's also give the plot a title.
+
```@example visualisation_graphs
f, ax, p = plot_complexes(brusselator, show_rate_labels = true, node_color = :yellow, ilabels_fontsize = 10)
ax.title = "Brusselator"
@@ -105,12 +123,14 @@ f
Most of the kwargs that modify the nodes or edges will also accept a vector with the same length as the number of nodes or edges, respectively. See [here](https://graph.makie.org/stable/#The-graphplot-Recipe) for a full list of keyword arguments to `graph_plot`. Note that `plot_complexes` and `plot_network` default to `layout = Stress()` rather than `layout = Spring()`, since `Stress()` is better at generating plots with fewer edge crossings. More layout options and customizations (such as pinning nodes to certain positions) can be found in the [`NetworkLayout` documentation](https://juliagraphs.org/NetworkLayout.jl/stable/).
Once a graph is already created we can also change the keyword arguments by modifying the fields of the `Plot` object `p`.
+
```@example visualisation_graphs
p.node_color = :orange
f
```
Custom node positions can also be given, if the automatic layout is unsatisfactory.
+
```@example visualisation_graphs
fixedlayout = [(0,0), (1,0), (0,1), (1,1), (2,0)]
p.layout = fixedlayout
@@ -128,13 +148,15 @@ register_interaction!(ax, :ndrag, NodeDrag(p))
register_interaction!(ax, :edrag, EdgeDrag(p))
```
-The equivalent of `show` for Makie plots is the `display` function.
+The equivalent of `show` for Makie plots is the `display` function.
+
```julia
f = plot_network(brusselator)
display(f)
```
-Once you are happy with the graph plot, you can save it using the `save` function.
+Once you are happy with the graph plot, you can save it using the `save` function.
+
```julia
save("fig.png", f)
```
diff --git a/docs/src/model_creation/parametric_stoichiometry.md b/docs/src/model_creation/parametric_stoichiometry.md
index 6cb7394af3..8d17f2f41f 100644
--- a/docs/src/model_creation/parametric_stoichiometry.md
+++ b/docs/src/model_creation/parametric_stoichiometry.md
@@ -1,11 +1,14 @@
# [Symbolic Stoichiometries](@id parametric_stoichiometry)
+
Catalyst supports stoichiometric coefficients that involve parameters, species,
or even general expressions. In this tutorial we show several examples of how to
use symbolic stoichiometries, and discuss several caveats to be aware of.
## Using symbolic stoichiometry
+
Let's first consider a simple reversible reaction where the number of reactants
is a parameter, and the number of products is the product of two parameters.
+
```@example s1
using Catalyst, Latexify, OrdinaryDiffEqTsit5, ModelingToolkit, Plots
revsys = @reaction_network revsys begin
@@ -15,6 +18,7 @@ revsys = @reaction_network revsys begin
end
reactions(revsys)
```
+
Notice, as described in the [Reaction rate laws used in simulations](@ref introduction_to_catalyst_ratelaws)
section, the default rate laws involve factorials in the stoichiometric
coefficients. For this reason we explicitly specify `m` and `n` as integers (as
@@ -26,20 +30,25 @@ this example we have two species (`A` and `B`) and four parameters (`k₊`, `k
`m`, and `n`). In addition, the stoichiometry is applied to the rightmost symbol
in a given term, i.e. in the first equation the substrate `A` has stoichiometry
`m` and the product `B` has stoichiometry `m*n`. For example, in
+
```@example s1
rn = @reaction_network begin
k, A*C --> 2B
end
reactions(rn)
```
+
we see two species, `(B,C)`, with `A` treated as a parameter representing the
stoichiometric coefficient of `C`, i.e.
+
```@example s1
rx = reactions(rn)[1]
rx.substrates[1],rx.substoich[1]
```
+
We could have equivalently specified our systems directly via the Catalyst
API. For example, for `revsys` we would could use
+
```@example s1
t = default_t()
@parameters k₊ k₋ m::Int n::Int
@@ -49,33 +58,41 @@ rxs = [Reaction(k₊, [A], [B], [m], [m*n]),
revsys2 = ReactionSystem(rxs,t; name=:revsys)
revsys2 == revsys
```
+
or
+
```@example s1
rxs2 = [(@reaction k₊, $m*A --> ($m*$n)*B),
(@reaction k₋, B --> A)]
revsys3 = ReactionSystem(rxs2,t; name=:revsys)
revsys3 == revsys
```
+
Here we interpolate in the pre-declared `m` and `n` symbolic variables using `$m` and `$n` to ensure the parameter is known to be integer-valued. The `@reaction` macro again assumes all symbols are parameters except the
substrates or reactants (i.e. `A` and `B`). For example, in
`@reaction k, F*A + 2(H*G+B) --> D`, the substrates are `(A,G,B)` with
stoichiometries `(F,2*H,2)`.
Let's now convert `revsys` to ODEs and look at the resulting equations:
+
```@example s1
osys = convert(ODESystem, revsys)
osys = complete(osys)
equations(osys)
show(stdout, MIME"text/plain"(), equations(osys)) # hide
```
+
Specifying the parameter and initial condition values,
+
```@example s1
p = (revsys.k₊ => 1.0, revsys.k₋ => 1.0, revsys.m => 2, revsys.n => 2)
u₀ = [revsys.A => 1.0, revsys.B => 1.0]
oprob = ODEProblem(osys, u₀, (0.0, 1.0), p)
nothing # hide
```
+
we can now solve and plot the system
+
```@example s1
sol = solve(oprob, Tsit5())
plot(sol)
@@ -89,6 +106,7 @@ section. This requires passing the `combinatoric_ratelaws=false` keyword to
`ReactionSystem` instead of first converting to an `ODESystem`). For the
previous example this gives the following (different) system of ODEs where we
now let `m` and `n` be floating point valued parameters (the default):
+
```@example s1
revsys = @reaction_network revsys begin
k₊, m*A --> (m*n)*B
@@ -99,8 +117,10 @@ osys = complete(osys)
equations(osys)
show(stdout, MIME"text/plain"(), equations(osys)) # hide
```
+
Since we no longer have factorial functions appearing, our example will now run
with `m` and `n` treated as floating point parameters:
+
```@example s1
p = (revsys.k₊ => 1.0, revsys.k₋ => 1.0, revsys.m => 2.0, revsys.n => 2.0)
oprob = ODEProblem(osys, u₀, (0.0, 1.0), p)
@@ -109,6 +129,7 @@ plot(sol)
```
## Gene expression with randomly produced amounts of protein
+
As a second example, let's build the negative feedback model from
[MomentClosure.jl](https://augustinas1.github.io/MomentClosure.jl/dev/tutorials/geometric_reactions+conditional_closures/)
that involves a bursty reaction that produces a random amount of protein.
@@ -120,6 +141,7 @@ where `m` is a (shifted) geometric random variable with mean `b`. To define `m`
we must register the `Distributions.Geometric` distribution from
Distributions.jl with Symbolics.jl, after which we can use it in symbolic
expressions:
+
```@example s1
using Distributions: Geometric
@register_symbolic Geometric(b)
@@ -127,10 +149,12 @@ using Distributions: Geometric
m = rand(Geometric(1/b)) + 1
nothing # hide
```
+
Note, as we require the shifted geometric distribution, we add one to
Distributions.jl's `Geometric` random variable (which includes zero).
We can now define our model
+
```@example s1
burstyrn = @reaction_network burstyrn begin
k₊, G₋ --> G₊
@@ -141,11 +165,13 @@ end
reactions(burstyrn)
show(stdout, MIME"text/plain"(), reactions(burstyrn)) # hide
```
+
The parameter `b` does not need to be explicitly declared in the
`@reaction_network` macro as it is detected when the expression
`rand(Geometric(1/b)) + 1` is substituted for `m`.
We next convert our network to a jump process representation
+
```@example s1
using JumpProcesses
jsys = convert(JumpSystem, burstyrn; combinatoric_ratelaws = false)
@@ -153,19 +179,24 @@ jsys = complete(jsys)
equations(jsys)
show(stdout, MIME"text/plain"(), equations(jsys)) # hide
```
+
Notice, the `equations` of `jsys` have three `MassActionJump`s for the first
three reactions, and one `ConstantRateJump` for the last reaction. If we examine
the `ConstantRateJump` more closely we can see the generated `rate` and
`affect!` functions for the bursty reaction that makes protein
+
```@example s1
equations(jsys)[4].rate
show(stdout, MIME"text/plain"(), equations(jsys)[4].rate) # hide
```
+
```@example s1
equations(jsys)[4].affect!
show(stdout, MIME"text/plain"(), equations(jsys)[4].affect!) # hide
```
+
Finally, we can now simulate our `JumpSystem`
+
```@example s1
pmean = 200
bval = 70
@@ -181,10 +212,12 @@ jprob = JumpProblem(jsys, dprob, Direct())
sol = solve(jprob)
plot(sol.t, sol[jsys.P], legend = false, xlabel = "time", ylabel = "P(t)")
```
+
To double check our results are consistent with MomentClosure.jl, let's
calculate and plot the average amount of protein (which is also plotted in the
MomentClosure.jl
[tutorial](https://augustinas1.github.io/MomentClosure.jl/dev/tutorials/geometric_reactions+conditional_closures/)).
+
```@example s1
t = default_t()
function getmean(jprob, Nsims, tv)
@@ -201,4 +234,5 @@ psim_mean = getmean(jprob, 20000, tv)
plot(tv, psim_mean; ylabel = "average of P(t)", xlabel = "time",
xlim = (0.0,6.0), legend = false)
```
+
Comparing, we see similar averages for `P(t)`.
diff --git a/docs/src/model_creation/programmatic_CRN_construction.md b/docs/src/model_creation/programmatic_CRN_construction.md
index 6e621ed82b..d2f92af50b 100644
--- a/docs/src/model_creation/programmatic_CRN_construction.md
+++ b/docs/src/model_creation/programmatic_CRN_construction.md
@@ -1,4 +1,5 @@
# [Programmatic Construction of Symbolic Reaction Systems](@id programmatic_CRN_construction)
+
While the DSL provides a simple interface for creating `ReactionSystem`s, it can
often be convenient to build or augment a [`ReactionSystem`](@ref)
programmatically. In this tutorial we show how to build the repressilator model
@@ -7,19 +8,24 @@ then summarize the basic API functionality for accessing information stored
within `ReactionSystem`s.
## Directly building the repressilator with `ReactionSystem`s
+
We first load Catalyst
+
```@example ex
using Catalyst
```
+
and then define symbolic variables for each parameter and species in the system
(the latter corresponding to a `variable` or `unknown` in ModelingToolkit
terminology)
+
```@example ex
t = default_t()
@parameters α K n δ γ β μ
@species m₁(t) m₂(t) m₃(t) P₁(t) P₂(t) P₃(t)
nothing # hide
```
+
Note: each species is declared as a function of time. Here, we first import the *time independent variable*, and stores it in `t`, using `t = default_t()`, and then use it to declare out species.
!!! note
@@ -30,6 +36,7 @@ Note: each species is declared as a function of time. Here, we first import the
Next, we specify the chemical reactions that comprise the system using Catalyst
[`Reaction`](@ref)s
+
```@example ex
rxs = [Reaction(hillr(P₃,α,K,n), nothing, [m₁]),
Reaction(hillr(P₁,α,K,n), nothing, [m₂]),
@@ -48,13 +55,16 @@ rxs = [Reaction(hillr(P₃,α,K,n), nothing, [m₁]),
Reaction(μ, [P₃], nothing)]
nothing # hide
```
+
Here we use `nothing` where the DSL used ``\varnothing``. Finally, we are ready
to construct our [`ReactionSystem`](@ref) as
+
```@example ex
@named repressilator = ReactionSystem(rxs, t)
repressilator = complete(repressilator)
nothing # hide
```
+
Notice, the model is named `repressilator`. A name must always be specified when
directly constructing a `ReactionSystem` (the DSL will auto-generate one if left
out). Using `@named` when constructing a `ReactionSystem` causes the name of the
@@ -68,6 +78,7 @@ Alternatively, one can use the `name = :repressilator` keyword argument to the
We can check that this is the same model as the one we defined via the DSL as
follows (this requires that we use the same names for rates, species and the
system)
+
```@example ex
repressilator2 = @reaction_network repressilator begin
hillr(P₃,α,K,n), ∅ --> m₁
@@ -92,10 +103,12 @@ API docs. For a more extensive example of how to programmatically create a
smoluchowski_coagulation_equation).
## More general `Reaction`s
+
In the example above all the specified `Reaction`s were first or zero order. The
three-argument form of `Reaction` implicitly assumes all species have a
stoichiometric coefficient of one, i.e. for substrates `[S₁,...,Sₘ]` and
products `[P₁,...,Pₙ]` it has the possible forms
+
```julia
# rate, S₁ + ... + Sₘ --> P₁ + ... + Pₙ
Reaction(rate, [S₁,...,Sₘ], [P₁,...,Pₙ])
@@ -106,8 +119,10 @@ Reaction(rate, [S₁,...,Sₘ], nothing)
# rate, ∅ --> P₁ + ... + Pₙ
Reaction(rate, nothing, [P₁,...,Pₙ])
```
+
To allow for other stoichiometric coefficients we also provide a five argument
form
+
```julia
# rate, α₁*S₁ + ... + αₘ*Sₘ --> β₁*P₁ + ... + βₙ*Pₙ
Reaction(rate, [S₁,...,Sₘ], [P₁,...,Pₙ], [α₁,...,αₘ], [β₁,...,βₙ])
@@ -118,24 +133,29 @@ Reaction(rate, [S₁,...,Sₘ], nothing, [α₁,...,αₘ], nothing)
# rate, ∅ --> β₁*P₁ + ... + βₙ*Pₙ
Reaction(rate, nothing, [P₁,...,Pₙ], nothing, [β₁,...,βₙ])
```
+
Finally, we note that the rate constant, `rate` above, does not need to be a
constant or fixed function, but can be a general symbolic expression:
+
```julia
t = default_t()
@parameters α, β
@species A(t), B(t)
rx = Reaction(α + β*t*A, [A], [B])
```
+
[See the FAQs](@ref user_functions) for info on using general user-specified
functions for the rate constant.
## The `@reaction` macro for constructing `Reaction`s
+
In some cases one wants to build reactions incrementally, as in the
repressilator example, but it would be nice to still have a short hand as in the
[`@reaction_network`](@ref) DSL. In this case one can construct individual
reactions using the [`@reaction`](@ref) macro.
For example, the repressilator reactions could also have been constructed like
+
```julia
t = default_t()
@species P₁(t) P₂(t) P₃(t)
@@ -156,21 +176,26 @@ rxs = [(@reaction hillr($P₃,α,K,n), ∅ --> m₁),
(@reaction μ, P₃ --> ∅)]
@named repressilator = ReactionSystem(rxs, t)
```
+
Note, there are a few differences when using the `@reaction` macro to specify
one reaction versus using the full `@reaction_network` macro to create a
`ReactionSystem`. First, only one reaction (i.e. a single forward arrow type)
can be used, i.e. reversible arrows like `<-->` will not work (since they define
more than one reaction). Second, the `@reaction` macro does not have an option for designating what should be considered a species or parameter, and instead assumes that any symbol that appears as either a substrate or a product is a species, and everything else (including stoichiometric coefficients) are parameters. As such, the following are equivalent
+
```julia
rx = @reaction hillr(P,α,K,n), A --> B
```
+
is equivalent to
+
```julia
t = default_t()
@parameters P α K n
@variables A(t) B(t)
rx = Reaction(hillr(P,α,K,n), [A], [B])
```
+
Here `(P,α,K,n)` are parameters and `(A,B)` are species.
This behavior is the reason that in the repressilator example above we
@@ -183,67 +208,86 @@ This ensured they were properly treated as species and not parameters. See the
The [Catalyst.jl API](@ref api) provides a large variety of functionality for
querying properties of a reaction network. Here we go over a few of the most
-useful basic functions. Given the `repressillator` defined above we have that
+useful basic functions. Given the `repressilator` defined above we have that
+
```@example ex
species(repressilator)
```
+
```@example ex
parameters(repressilator)
```
+
```@example ex
reactions(repressilator)
```
We can test if a `Reaction` is mass action, i.e. the rate does not depend on `t`
or other species, as
+
```@example ex
# Catalyst.hillr(P₃(t), α, K, n), ∅ --> m₁
rx1 = reactions(repressilator)[1]
ismassaction(rx1,repressilator)
```
+
while
+
```@example ex
# δ, m₁ --> ∅
rx2 = reactions(repressilator)[4]
ismassaction(rx2,repressilator)
```
+
Similarly, we can determine which species a reaction's rate law will depend on
like
+
```@example ex
rn = @reaction_network begin
k*W, 2X + 3Y --> 5Z + W
end
dependents(reactions(rn)[1], rn)
```
+
Basic stoichiometry matrices can be obtained from a `ReactionSystem` as
+
```@example ex
substoichmat(repressilator)
```
+
```@example ex
prodstoichmat(repressilator)
```
+
```@example ex
netstoichmat(repressilator)
```
+
Here the ``(i,j)`` entry gives the corresponding stoichiometric coefficient
of species ``i`` for reaction ``j``.
Finally, we can directly access fields of individual reactions like
+
```@example ex
rx1.rate
```
+
```@example ex
rx1.substrates
```
+
```@example ex
rx1.products
```
+
```@example ex
rx1.substoich
```
+
```@example ex
rx1.prodstoich
```
+
```@example ex
rx1.netstoich
```
diff --git a/docs/src/model_creation/reactionsystem_content_accessing.md b/docs/src/model_creation/reactionsystem_content_accessing.md
index fd0bb3bec3..0842dd52f2 100644
--- a/docs/src/model_creation/reactionsystem_content_accessing.md
+++ b/docs/src/model_creation/reactionsystem_content_accessing.md
@@ -1,24 +1,31 @@
# [Accessing Model Properties](@id model_accessing)
+
Catalyst is based around the creation, analysis, and simulation of chemical reaction network models. Catalyst stores these models in [`ReactionSystem`](@ref) structures. This page describes some basic functions for accessing the content of these structures. This includes retrieving lists of species, parameters, or reactions that a model consists of. An extensive list of relevant functions for working with `ReactionSystem` models can be found in Catalyst's [API](@ref api).
!!! warning
Generally, a field of a Julia structure can be accessed through `struct.fieldname`. E.g. a simulation's time vector can be retrieved using `simulation.t`. While Catalyst `ReactionSystem`s are structures, one should *never* access their fields using this approach, but rather using the accessor functions described below and in the [API](@ref api_accessor_functions) (direct accessing of fields can yield unexpected behaviours). E.g. to retrieve the species of a `ReactionsSystem` called `rs`, use `Catalyst.get_species(rs)`, *not* `rs.species`. The reason is that, as shown [below](@ref model_accessing_symbolic_variables), Catalyst (and more generally any [ModelingToolkit](https://github.com/SciML/ModelingToolkit.jl) system types) reserves this type of accessing for accessing symbolic variables stored in the system. I.e. `rs.X` refers to the `X` symbolic variable, not a field in `rs` named "X".
## [Direct accessing of symbolic model parameter and species](@id model_accessing_symbolic_variables)
+
Previously we have described how the parameters and species that Catalyst models contain are represented using so-called [*symbolic variables*](@ref introduction_to_catalyst) (and how these enable the forming of [*symbolic expressions*](@ref introduction_to_catalyst)). We have described how, during [programmatic modelling](@ref programmatic_CRN_construction), the user has [direct access to these](@ref programmatic_CRN_construction) and how this can be [taken advantage of](@ref programmatic_CRN_construction). We have also described how, during [DSL-based modelling](@ref dsl_description), the need for symbolic representation can be circumvented by [using `@unpack`](@ref dsl_advanced_options_symbolics_and_DSL_unpack) or by [creating an observable](@ref dsl_advanced_options_observables). However, sometimes, it is easier to *directly access a symbolic variable through the model itself*, something which we will describe here.
Let us consider the following [two-state model](@ref basic_CRN_library_two_states)
+
```@example model_accessing_symbolic_variables
using Catalyst
rs = @reaction_network begin
(k1,k2), X1 <--> X2
end
```
-If we wish to access one of the symbolic variables stored in it (here `X1`, `X2`, `k1`, and `k2`), we simply write
+
+If we wish to access one of the symbolic variables stored in it (here `X1`, `X2`, `k1`, and `k2`), we simply write
+
```@example model_accessing_symbolic_variables
rs.X1
```
+
to access e.g. `X1`. This symbolic variable can be used just like those [declared using `@parameters` and `@species`](@ref programmatic_CRN_construction):
+
```@example model_accessing_symbolic_variables
using OrdinaryDiffEqDefault
u0 = [rs.X1 => 1.0, rs.X2 => 2.0]
@@ -27,17 +34,22 @@ oprob = ODEProblem(rs, u0, (0.0, 10.0), ps)
sol = solve(oprob)
nothing # hide
```
+
We can also use them to form symbolic expressions:
+
```@example model_accessing_symbolic_variables
Xtot = rs.X1 + rs.X2
```
+
which can be used when we e.g. [plot our simulation](@ref simulation_plotting_options):
+
```@example model_accessing_symbolic_variables
using Plots
plot(sol; idxs = [rs.X1, rs.X2, Xtot])
```
Next we create our two-state model [programmatically](@ref programmatic_CRN_construction):
+
```@example model_accessing_symbolic_variables
t = default_t()
@species X1(t) X2(t)
@@ -50,7 +62,9 @@ rxs = [
rs_prog = complete(rs_prog)
nothing # hide
```
+
Here, we can confirm that the symbolic variables we access through the model are identical to those we used to create it:
+
```@example model_accessing_symbolic_variables
isequal(rs.k1, k1)
```
@@ -61,7 +75,9 @@ isequal(rs.k1, k1)
## [Accessing basic model properties](@id model_accessing_basics)
### [Accessing model parameter and species](@id model_accessing_basics_parameters_n_species)
+
Previously we showed how to access individual parameters or species of a `ReactionSystem` model. Next, the `parameters` and [`species`](@ref) functions allow us to retrieve *all* parameters and species as vectors:
+
```@example model_accessing_basics
using Catalyst # hide
sir = @reaction_network begin
@@ -70,38 +86,51 @@ sir = @reaction_network begin
end
parameters(sir)
```
+
```@example model_accessing_basics
species(sir)
```
+
These vectors contain the exact same symbolic variables that we would access through the system:
+
```@example model_accessing_basics
issetequal([sir.S, sir.I, sir.R], species(sir))
```
If we wish to count the number of parameters or species in a system, we can do this directly through the [`numparams`](@ref) and [`numspecies`](@ref) functions:
+
```@example model_accessing_basics
numparams(sir)
```
+
```@example model_accessing_basics
numspecies(sir)
```
### [Accessing model reactions](@id model_accessing_basics_reactions)
+
A vector containing all a model's [reactions](@ref programmatic_CRN_construction) can be retrieved using the [`reactions`](@ref) function:
+
```@example model_accessing_basics
reactions(sir)
```
+
We can count the number of reactions in a model using the [`numreactions`](@ref) function:
+
```@example model_accessing_basics
numreactions(sir)
```
+
Finally, a vector with all the reactions' rates can be retrieved using [`reactionrates`](@ref):
+
```@example model_accessing_basics
reactionrates(sir)
```
### [Accessing content of models coupled to equations](@id model_accessing_basics_reactions)
+
Previously, we have shown how to [couple equations to a chemical reaction network model](@ref constraint_equations_coupling_constraints), creating models containing [non-species unknowns (variables)](@ref constraint_equations_coupling_constraints). Here we create a birth-death model where some nutrient supply (modelled through the variable $N$) is depleted in the presence of $X$:
+
```@example model_accessing_basics
using Catalyst # hide
coupled_crn = @reaction_network begin
@@ -109,44 +138,57 @@ coupled_crn = @reaction_network begin
(p/(1+N),d), 0 <--> X
end
```
+
Here, the `unknowns` function returns all unknowns (i.e. species and variables):
+
```@example model_accessing_basics
unknowns(coupled_crn)
```
+
Meanwhile, `species` returns the species only, while [`nonspecies`](@ref) returns the variables only:
+
```@example model_accessing_basics
species(coupled_crn)
```
+
```@example model_accessing_basics
nonspecies(coupled_crn)
```
Similarly, the `equations` function returns a vector with all reactions and equations of the model (ordered so that reactions occur first and equations thereafter):
+
```@example model_accessing_basics
equations(coupled_crn)
```
+
Meanwhile, [`reactions`](@ref) returns the reactions only, while [`nonreactions`](@ref) returns any algebraic or differential equations:
+
```@example model_accessing_basics
reactions(coupled_crn)
```
+
```@example model_accessing_basics
nonreactions(coupled_crn)
```
### [Accessing other model properties](@id model_accessing_basics_others)
-There exist several other functions for accessing model properties.
+
+There exist several other functions for accessing model properties.
The `observed`, `continuous_events`, `discrete_events` functions can be used to access a model's [observables](@ref dsl_advanced_options_observables), [continuous events](@ref constraint_equations_events), and [discrete events](@ref constraint_equations_events), respectively.
The `ModelingToolkit.get_iv` function can be used to retrieve a [model's independent variable](@ref programmatic_CRN_construction):
+
```@example model_accessing_basics
ModelingToolkit.get_iv(sir)
```
## [Accessing properties of hierarchical models](@id model_accessing_hierarchical)
+
Previously, we have described how [compositional modelling can be used to create hierarchical models](@ref compositional_modeling). There are some special considerations when accessing the content of hierarchical models, which we will describe here.
First, we will create a simple hierarchical model. It describes a protein ($X$) which is created in its inactive form ($Xᵢ$) in the nucleus, from which it is transported to the cytoplasm, where it is activated.
+
```@example model_accessing_hierarchical
using Catalyst # hide
# Declare submodels.
@@ -163,46 +205,60 @@ transport = @reaction kₜ, $(nucleus_sys.Xᵢ) --> $(cytoplasm_sys.Xᵢ)
@named rs = ReactionSystem([transport], default_t(); systems = [nucleus_sys, cytoplasm_sys])
rs = complete(rs)
```
+
This model consists of a top-level system, which contains the transportation reaction only, and two subsystems. We can retrieve all the subsystems of the top-level system through `Catalyst.get_systems`:
+
```@example model_accessing_hierarchical
Catalyst.get_systems(rs)
nothing # hide
```
+
!!! note
If either of the subsystems had had further subsystems, these would *not* be retrieved by `Catalyst.get_systems` (which only returns the direct subsystems of the input system).
### [Accessing parameter and species of hierarchical models](@id model_accessing_hierarchical_symbolic_variables)
+
Our hierarchical model consists of a top-level system (`rs`) with two subsystems (`nucleus_sys` and `cytoplasm_sys`). Note that we have given our subsystems [names](@ref dsl_advanced_options_naming) (`nucleus` and `cytoplasm`, respectively). Above, we retrieved the subsystems by calling `Catalyst.get_systems` on our top-level system. We can also retrieve a subsystem directly by calling:
+
```@example model_accessing_hierarchical
rs.nucleus
```
+
```@example model_accessing_hierarchical
rs.cytoplasm
```
+
!!! note
When accessing subsystems, we use the subsystems' [names](@ref dsl_advanced_options_naming), *not* the name of their variables (i.e. we call `rs.nucleus`, not `rs.nucleus_sys`).
Next, if we wish to access a species declared as a part of one of the subsystems, we do so through it. E.g. here we access `Xₐ` (which is part of the cytoplasm subsystem):
+
```@example model_accessing_hierarchical
rs.cytoplasm.Xₐ
```
+
Note that species contained in a subsystem have the subsystem's name prepended to their name when we access it.
Both subsystems contain a species `Xᵢ`. However, while they have the same name, *these are different species when accessed through their respective models*:
+
```@example model_accessing_hierarchical
isequal(rs.nucleus.Xᵢ, rs.cytoplasm.Xᵢ)
```
The same holds for the model parameters, i.e. while each subsystem contains a parameter `d`, these are considered different parameters:
+
```@example model_accessing_hierarchical
isequal(rs.nucleus.d, rs.cytoplasm.d)
```
+
The parameter `kₜ` is actually contained within the top-level model, and is accessed directly through it:
+
```@example model_accessing_hierarchical
rs.kₜ
```
Practically, when we simulate our hierarchical model, we use all of this to designate initial conditions and parameters. I.e. below we designate values for the two `Xᵢ` species and `d` parameters separately:
+
```@example model_accessing_hierarchical
using OrdinaryDiffEqDefault, Plots
u0 = [rs.nucleus.Xᵢ => 0.0, rs.cytoplasm.Xᵢ => 0.0, rs.cytoplasm.Xₐ => 0.0]
@@ -216,34 +272,45 @@ plot(sol)
When we access a symbolic variable through a subsystem (e.g. `rs.nucleus.Xᵢ`) that subsystem's name is prepended to the symbolic variable's name (we call this *namespacing*). This is also the case if we access it through the original model, i.e. `nucleus_sys.Xᵢ`. Namespacing is only performed when we access variables of [*incomplete systems*](@ref programmatic_CRN_construction). I.e. `isequal(nucleus_sys.d, cytoplasm_sys.d)` returns false (as the systems are incomplete and namespacing is performed). However, `isequal(complete(nucleus_sys).d, complete(cytoplasm_sys).d)` returns true (as the systems are complete and namespacing is not performed). This is the reason that the system top-level system's name is never prepended when we do e.g. `rs.kₜ` (because here, `rs` is complete).
### [Accessing the content of hierarchical models](@id model_accessing_hierarchical_symbolic_variables)
+
In the last section, we noted that our hierarchical model contained several instances of the `Xᵢ` species. The [`species`](@ref) function, which retrieves all of a model's species shows that our model has three species (two types of `Xᵢ`, and one type of `Xₐ`)
+
```@example model_accessing_hierarchical
species(rs)
```
+
Similarly, `parameters` retrieves five different parameters. Here, we note that `kₜ` (which has no model name prepended) belongs to the top-level system (and not a subsystem):
+
```@example model_accessing_hierarchical
parameters(rs)
```
If we wish to retrieve the species (or parameters) that are specifically contained in the top-level system (and not only indirectly through its subsystems), we can use the `Catalyst.get_species` (or `ModelingToolkit.getps`) functions:
+
```@example model_accessing_hierarchical
Catalyst.get_species(rs)
```
+
```@example model_accessing_hierarchical
ModelingToolkit.get_ps(rs)
```
+
Here, our top-level model contains a single parameter (`kₜ`), and two the two versions of the `Xᵢ` species. These are all the symbolic variables that occur in the transportation reaction (`@kₜ, $(nucleus_sys.Xᵢ) --> $(cytoplasm_sys.Xᵢ)`), which is the only reaction of the top-level system. We can apply these functions to the systems as well. However, when we do so, the systems' names are not prepended:
+
```@example model_accessing_hierarchical
Catalyst.get_ps(rs.nucleus)
```
Generally, functions starting with `get_` retrieve only the components stored in the input system (and do not consider its subsystems), while the corresponding function without `get_` also retrieves the components stored in subsystems. I.e. compare the `Catalyst.get_rxs` and [`reactions`](@ref) functions:
+
```@example model_accessing_hierarchical
reactions(rs)
```
+
```@example model_accessing_hierarchical
Catalyst.get_rxs(rs)
```
+
Other examples of similar pairs of functions are `get_unknowns` and `unknowns`, and `get_observed` and `observed`.
!!! note
diff --git a/docs/src/model_simulation/ensemble_simulations.md b/docs/src/model_simulation/ensemble_simulations.md
index c53c0c703c..343da130aa 100644
--- a/docs/src/model_simulation/ensemble_simulations.md
+++ b/docs/src/model_simulation/ensemble_simulations.md
@@ -1,12 +1,16 @@
# [Ensemble/Monte Carlo Simulations](@id ensemble_simulations)
+
In many contexts, a single model is re-simulated under similar conditions. Examples include:
+
- Performing Monte Carlo simulations of a stochastic model to gain insight in its behaviour.
- Scanning a model's behaviour for different parameter values and/or initial conditions.
While this can be handled using `for` loops, it is typically better to first create an `EnsembleProblem`, and then perform an ensemble simulation. Advantages include a more concise interface and the option for [automatic simulation parallelisation](@ref ode_simulation_performance_parallelisation). Here we provide a short tutorial on how to perform parallel ensemble simulations, with a more extensive documentation being available [here](https://docs.sciml.ai/DiffEqDocs/stable/features/ensemble/).
## [Monte Carlo simulations using unmodified conditions](@id ensemble_simulations_monte_carlo)
+
We will first consider Monte Carlo simulations where the simulation conditions are identical in-between simulations. First, we declare a [simple self-activation loop](@ref basic_CRN_library_self_activation) model
+
```@example ensemble
using Catalyst
sa_model = @reaction_network begin
@@ -18,60 +22,76 @@ tspan = (0.0, 1000.0)
ps = [:v0 => 0.1, :v => 2.5, :K => 40.0, :n => 4.0, :deg => 0.01]
nothing # hide
```
+
We wish to simulate it as an SDE. Rather than performing a single simulation, however, we want to perform multiple ones. Here, we first create a normal `SDEProblem`, and use it as the single input to a `EnsembleProblem` (`EnsembleProblem` are created similarly for ODE and jump simulations, but the `ODEProblem` or `JumpProblem` is used instead).
+
```@example ensemble
using StochasticDiffEq
sprob = SDEProblem(sa_model, u0, tspan, ps)
eprob = EnsembleProblem(sprob)
nothing # hide
```
+
Next, the `EnsembleProblem` can be used as input to the `solve` command. Here, we use exactly the same inputs that we use for single simulations, however, we add a `trajectories` argument to denote how many simulations we wish to carry out. Here we perform 10 simulations:
+
```@example ensemble
sols = solve(eprob, STrapezoid(); trajectories = 10)
nothing # hide
```
+
Finally, we can use our ensemble simulation solution as input to `plot` (just like normal simulations):
+
```@example ensemble
using Plots
plot(sols)
```
+
Here, each simulation is displayed as an individual trajectory.
!!! note
While not used here, the [`la` plotting option](@ref simulation_plotting_options) (which modifies line transparency) can help improve the plot visual when a large number of (overlapping) lines are plotted.
Various convenience functions are available for analysing and plotting ensemble simulations (a full list can be found [here](https://docs.sciml.ai/DiffEqDocs/dev/features/ensemble/#Analyzing-an-Ensemble-Experiment)). Here, we use these to first create an `EnsembleSummary` (retrieving each simulation's value at time points `0.0, 1.0, 2.0, ... 1000.0`). Next, we use this as an input to the `plot` command, which automatically plots the mean $X$ activity across the ensemble, while also displaying the 5% and 95% quantiles as the shaded area:
+
```@example ensemble
e_summary = EnsembleAnalysis.EnsembleSummary(sols, 0.0:1.0:1000.0)
plot(e_summary)
```
## [Ensemble simulations using varying simulation conditions](@id ensemble_simulations_varying_conditions)
+
Previously, we assumed that each simulation used the same initial conditions and parameter values. If this is not the case (when e.g. performing a parameter scan), a `prob_func` optional argument must be supplied to the `EnsembleProblem`, this describes how the problem should be modified for each individual simulation of the ensemble.
Here, we first create an `ODEProblem` of our previous self-activation loop:
+
```@example ensemble
using OrdinaryDiffEqTsit5
oprob = ODEProblem(sa_model, u0, tspan, ps)
nothing # hide
```
+
Next, we wish to simulate the model for a range of initial conditions of $X$`. To do this we create a problem function, which takes the following arguments:
+
- `prob`: The problem given to our `EnsembleProblem` (which is the problem that `prob_func` modifies in each iteration).
- `i`: The number of this specific Monte Carlo iteration in the interval `1:trajectories`.
- `repeat`: The iteration of the repeat of the simulation. Typically `1`, but potentially higher if [the simulation re-running option](https://docs.sciml.ai/DiffEqDocs/stable/features/ensemble/#Building-a-Problem) is used.
Here we will use the following problem function (utilising [remake](@ref simulation_structure_interfacing_problems_remake)), which will provide a uniform range of initial concentrations of $X$:
+
```@example ensemble
function prob_func(prob, i, repeat)
remake(prob; u0 = [:X => i * 5.0])
end
nothing # hide
```
+
Next, we create our `EnsembleProblem`, and simulate it 10 times:
+
```@example ensemble
eprob = EnsembleProblem(oprob; prob_func)
sols = solve(eprob, Tsit5(); trajectories = 10)
plot(sols)
```
+
Here we see that the deterministic model (unlike the stochastic one), only activates for some initial conditions (while other tends to an inactive state).
The `EnsembleProblem` constructor accepts a few additional optional options (`output_func`, `reduction`, `u_init`, and `safetycopy`), which are described in more detail [here](https://docs.sciml.ai/DiffEqDocs/stable/features/ensemble/#Building-a-Problem). These can be used to e.g. rerun an individual simulation which does not fulfil some condition, or extract and save only some selected information from a simulation (rather than the full simulation).
diff --git a/docs/src/model_simulation/examples/activation_time_distribution_measurement.md b/docs/src/model_simulation/examples/activation_time_distribution_measurement.md
index 842c4ece35..72ae45aeba 100644
--- a/docs/src/model_simulation/examples/activation_time_distribution_measurement.md
+++ b/docs/src/model_simulation/examples/activation_time_distribution_measurement.md
@@ -1,7 +1,9 @@
# [Measuring the Distribution of System Activation Times](@id activation_time_distribution_measurement)
+
In this example we will consider a model which, while initially inactive, activates in response to an input. The model is *stochastic*, causing the activation times to be *random*. By combining events, callbacks, and stochastic ensemble simulations, we will measure the probability distribution of the activation times (so-called [*first passage times*](https://en.wikipedia.org/wiki/First-hitting-time_model)).
Our model will be a version of the [simple self-activation loop](@ref basic_CRN_library_self_activation) (the ensemble simulations of which we have [considered previously](@ref ensemble_simulations_monte_carlo)). Here, we will consider the activation threshold parameter ($K$) to be activated by an input (at an input time $t = 0$). Before the input, $K$ is very large (essentially keeping the system inactive). After the input, it is reduced to a lower value (which permits the system to activate). We will model this using two additional parameters ($Kᵢ$ and $Kₐ$, describing the pre and post-activation values of $K$, respectively). Initially, $K$ will [default to](@ref dsl_advanced_options_default_vals) $Kᵢ$. Next, at the input time ($t = 0$), an event will change $K$'s value to $Kᵢ$.
+
```@example activation_time_distribution_measurement
using Catalyst
sa_model = @reaction_network begin
@@ -11,31 +13,39 @@ sa_model = @reaction_network begin
deg, X --> 0
end
```
+
Next, to perform stochastic simulations of the system we will create an `SDEProblem`. Here, we will need to assign parameter values to $Kᵢ$ and $Kₐ$, but not to $K$ (as its value is controlled by its default and the event). Also note that we start the simulation at a time $t = -200 < 0$. This ensures that by the input time ($t = 0$), the system has (more or less) reached its (inactive) steady state distribution. It also means that the activation time can be measured exactly as the simulation time at the time of activation (as this will be the time from the input at $t = 0$).
+
```@example activation_time_distribution_measurement
u0 = [:X => 10.0]
tspan = (-200.0, 2000.0)
ps = [:v0 => 0.1, :v => 2.5, :Kᵢ => 1000.0, :Kₐ => 40.0, :n => 3.0, :deg => 0.01]
sprob = SDEProblem(sa_model, u0, tspan, ps)
-nothing # hide
+nothing # hide
```
+
We can now create a simple `EnsembleProblem` and perform an ensemble simulation (as described [here](@ref ensemble_simulations)). Please note that the system has an event which modifies its parameters, hence we must add the `safetycopy = true` argument to `EnsembleProblem` (else, subsequent simulations would start with $K = Kₐ$).
+
```@example activation_time_distribution_measurement
using Plots, StochasticDiffEq
eprob = EnsembleProblem(sprob; safetycopy = true)
esol = solve(eprob, ImplicitEM(); trajectories = 10)
plot(esol)
```
+
Here we see how, after the input time, the system (randomly) switches from the inactive state to the active one (several examples of this, bistability-based, activation have been studied in the literature, both in models and experiments[^1][^2]).
Next, we wish to measure the distribution of these activation times. First we will create a [callback](https://docs.sciml.ai/DiffEqDocs/stable/features/callback_functions/) which terminates the simulation once it has reached a threshold. This both ensures that we do not have to expend unnecessary computer time on the simulation after its activation, and also enables us to measure the activation time as the final time point of the simulation. Here we will use a [discrete callback](https://docs.sciml.ai/DiffEqDocs/stable/features/callback_functions/#SciMLBase.DiscreteCallback). By looking at the previous simulations, we determine $X = 100$ as a suitable activation threshold. We use the `terminate!` function to terminate the simulation once this value has been reached.
+
```@example activation_time_distribution_measurement
condition(u, t, integrator) = integrator[:X] > 100.0
affect!(integrator) = terminate!(integrator)
callback = DiscreteCallback(condition, affect!)
nothing # hide
```
+
Next, we will perform our ensemble simulation. By default, for each simulation, these save the full trajectory. Here, however, we are only interested in the activation time. This enables us to utilise an [*output function*](https://docs.sciml.ai/DiffEqDocs/dev/features/ensemble/#Building-a-Problem). This will be called at the end of every single simulation and determine what to save (it can also be used to potentially rerun individual simulations, however, we will not use this feature here). Here we create an output function which saves only the simulation's final time point. We also make it throw a warning if the simulation reaches the end of the simulation time frame ($t = 2000$) (this indicates a simulation that never activated, the occurrences of such simulation would cause us to underestimate the activation times). Finally, just like previously, we must set `safetycopy = true`.
+
```@example activation_time_distribution_measurement
function output_func(sol, i)
(sol.t[end] == tspan[2]) && @warn "A simulation did not activate during the given time span."
@@ -45,13 +55,18 @@ eprob = EnsembleProblem(sprob; output_func, safetycopy = true)
esol = solve(eprob, ImplicitEM(); trajectories = 250, callback)
nothing # hide
```
+
Finally, we can plot the distribution of activation times. For this, we will use the [`histogram`](https://docs.juliaplots.org/latest/series_types/histogram/) function (with the `normalize = true` argument to create a probability density function). An alternative we also recommend is [StatsPlots.jl](https://docs.juliaplots.org/latest/generated/statsplots/)'s `density` function (which creates a smoothed histogram that is also easier to combine with other plots). The input to `density` is the activation times (which our output function has saved to `esol.u`).
+
```@example activation_time_distribution_measurement
histogram(esol.u; normalize = true, label = "Activation time distribution", xlabel = "Activation time")
```
+
Here we that the activation times take some form of long tail distribution (for non-trivial models like this one, it is generally not possible to identify the activation times as any known statistical distribution).
---
+
## References
+
[^1]: [David Frigola, Laura Casanellas, José M. Sancho, Marta Ibañes, *Asymmetric Stochastic Switching Driven by Intrinsic Molecular Noise*, PLoS One (2012).](https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0031407)
[^2]: [Christian P Schwall et al., *Tunable phenotypic variability through an autoregulatory alternative sigma factor circuit*, Molecular Systems Biology (2021).](https://www.embopress.org/doi/full/10.15252/msb.20209832)
diff --git a/docs/src/model_simulation/examples/interactive_brusselator_simulation.md b/docs/src/model_simulation/examples/interactive_brusselator_simulation.md
index 299e52e5bf..c5352236c2 100644
--- a/docs/src/model_simulation/examples/interactive_brusselator_simulation.md
+++ b/docs/src/model_simulation/examples/interactive_brusselator_simulation.md
@@ -2,17 +2,19 @@
Catalyst can utilize the [GLMakie.jl](https://github.com/JuliaPlots/GLMakie.jl) package for creating interactive visualizations of your reaction network dynamics. This tutorial provides a step-by-step guide to creating an interactive visualization of the Brusselator model, building upon the basic [Brusselator](@ref basic_CRN_library_brusselator) example.
-
## [Setting up the Brusselator model](@id setup_brusselator)
-Let's again use the oscillating Brusselator model, extending the basic simulation [plotting](@ref simulation_plotting) workflow we saw earlier.
+Let's again use the oscillating Brusselator model, extending the basic simulation [plotting](@ref simulation_plotting) workflow we saw earlier.
+
```@example interactive_brusselator
using Catalyst
using OrdinaryDiffEqTsit5
```
+
```julia
using GLMakie
```
+
```@example interactive_brusselator
using CairoMakie # hide
@@ -36,9 +38,10 @@ function solve_brusselator(A, B, X0, Y0, prob = oprob)
p = [:A => A, :B => B]
u0 = [:X => X0, :Y => Y0]
newprob = remake(prob, p=p, u0=u0)
- solve(newprob, Tsit5(), saveat = 0.1)
+ solve(newprob, Tsit5(), saveat = 0.1)
end
```
+
This code sets up our Brusselator model using Catalyst.jl's `@reaction_network` macro. We also define initial parameters, initial conditions, create an `ODEProblem`, and define a function to solve the ODE with given parameters. Setting `saveat = 0.1` in the call to `solve` ensures the solution is saved with the desired temporal frequency we want for our later plots.
!!! note
@@ -53,9 +56,9 @@ Let's start by creating a basic plot of our Brusselator model:
fig = Figure(size = (800, 600), fontsize = 18);
# Create an axis for the plot
-ax = Axis(fig[1, 1],
- title = "Brusselator Model",
- xlabel = "Time",
+ax = Axis(fig[1, 1],
+ title = "Brusselator Model",
+ xlabel = "Time",
ylabel = "Concentration")
# Solve the ODE
@@ -131,9 +134,9 @@ Now, let's create a plot that reacts to changes in our sliders:
```julia
# Create an axis for the plot
-ax = Axis(plot_layout[1, 1],
- title = "Brusselator Model",
- xlabel = "Time",
+ax = Axis(plot_layout[1, 1],
+ title = "Brusselator Model",
+ xlabel = "Time",
ylabel = "Concentration")
# Create an observable for the solution
@@ -151,6 +154,7 @@ axislegend(ax, position = :rt)
# Display the figure
fig
```
+

This plot will now update in real-time as you move the sliders, allowing for interactive exploration of the Brusselator's behavior under different conditions. (Note the figure above is not interactive, but for illustrative purposes to show what you should see locally.)
@@ -172,14 +176,14 @@ time_plot = plot_grid[1, 1] = GridLayout()
phase_plot = plot_grid[1, 2] = GridLayout()
# Create axes for the time series plot and phase plot
-ax_time = Axis(time_plot[1, 1],
- title = "Brusselator Model - Time Series",
- xlabel = "Time",
+ax_time = Axis(time_plot[1, 1],
+ title = "Brusselator Model - Time Series",
+ xlabel = "Time",
ylabel = "Concentration")
-ax_phase = Axis(phase_plot[1, 1],
- title = "Brusselator Model - Phase Plot",
- xlabel = "X",
+ax_phase = Axis(phase_plot[1, 1],
+ title = "Brusselator Model - Phase Plot",
+ xlabel = "X",
ylabel = "Y")
# Create sub-grids for sliders
@@ -211,15 +215,15 @@ connect!(B, slider_B.value)
connect!(X0, slider_X0.value)
connect!(Y0, slider_Y0.value)
-# Create an observable for the solution.
+# Create an observable for the solution.
solution = @lift(solve_brusselator($A, $B, $X0, $Y0))
# Plot the time series
-lines!(ax_time, lift(sol -> sol.t, solution), lift(sol -> sol[:X], solution), label = "X", color = :blue, linewidth = 3)
+lines!(ax_time, lift(sol -> sol.t, solution), lift(sol -> sol[:X], solution), label = "X", color = :blue, linewidth = 3)
lines!(ax_time, lift(sol -> sol.t, solution), lift(sol -> sol[:Y], solution), label = "Y", color = :red, linewidth = 3)
# Plot the phase plot
-phase_plot_obj = lines!(ax_phase, lift(sol -> sol[:X], solution), lift(sol -> sol[:Y], solution),
+phase_plot_obj = lines!(ax_phase, lift(sol -> sol[:X], solution), lift(sol -> sol[:Y], solution),
color = lift(sol -> sol.t, solution), colormap = :viridis)
# Add a colorbar for the phase plot
@@ -245,6 +249,7 @@ This will create a visualization with both time series and phase plots:
## [Common plotting options](@id common_makie_plotting_options)
Various plotting options can be provided as optional arguments to the `lines!` command. Common options include:
+
- `linewidth` or `lw`: Determine plot line widths.
- `linestyle`: Determines plot line style.
- `color`: Determines the line colors.
@@ -253,13 +258,14 @@ Various plotting options can be provided as optional arguments to the `lines!` c
For example:
```julia
-lines!(ax_time, lift(sol -> sol.t, solution), lift(sol -> sol[:X], solution),
+lines!(ax_time, lift(sol -> sol.t, solution), lift(sol -> sol[:X], solution),
label = "X", color = :green, linewidth = 2, linestyle = :dash)
```
## [Extending the interactive visualization](@id extending_interactive_visualization)
You can further extend this visualization by:
+
- Adding other interactive elements, such as [buttons](https://docs.makie.org/stable/reference/blocks/button) or [dropdown menus](https://docs.makie.org/stable/reference/blocks/menu) to control different aspects of the simulation or visualization.
- Adding additional axes to the plot, such as plotting the derivatives of the species.
- Color coding the slider and slider labels to match the plot colors.
diff --git a/docs/src/model_simulation/examples/periodic_events_simulation.md b/docs/src/model_simulation/examples/periodic_events_simulation.md
index bdfafcbd31..229e2108cd 100644
--- a/docs/src/model_simulation/examples/periodic_events_simulation.md
+++ b/docs/src/model_simulation/examples/periodic_events_simulation.md
@@ -1,8 +1,11 @@
# [Modelling a Periodic Event During ODE and Jump Simulations](@id periodic_event_simulation_example)
+
This tutorial will describe how to simulate systems with periodic events in ODEs and jump simulations (SDEs use identical syntax). We will consider a model with a [circadian rhythm](https://en.wikipedia.org/wiki/Circadian_rhythm), where a parameter represents the level of light. While outdoor light varies smoothly, in experimental settings a lamp is often simply turned on/off every 12 hours. Here we will model this toggling of the light using a periodic event that is triggered every 12 hours.
## [Modelling a circadian periodic event in an ODE simulation](@id periodic_event_simulation_example_ode)
+
We will consider a simple circadian model, consisting of a single protein ($X$), which is phosphorylated ($X \to Xᴾ$) in the presence of light ($l$). Here, the light parameter can either be $0$ (night) or $1$ (day). We can model this using a simple periodic event which switches the value of $l$ every 12 hours (here, `%` is the [remainder operator](https://docs.julialang.org/en/v1/manual/mathematical-operations/#Arithmetic-Operators)).
+
```@example periodic_event_example
using Catalyst
circadian_model = @reaction_network begin
@@ -12,7 +15,9 @@ circadian_model = @reaction_network begin
(kₚ*l,kᵢ), X <--> Xᴾ
end
```
+
We can now simulate this model, observing how a 24-hour cycle is reached
+
```@example periodic_event_example
using OrdinaryDiffEqDefault, Plots
u0 = [:X => 150.0, :Xᴾ => 50.0]
@@ -24,9 +29,11 @@ plot(sol)
```
## [Modelling a circadian periodic event in a jump simulation](@id periodic_event_simulation_example_ode)
+
We can define periodic events in an identical manner for jump simulations. Let's
reuse our previously defined network, but now simulate it as a stochastic
chemical kinetics jump process model
+
```@example periodic_event_example
using JumpProcesses
u0 = [:X => 150, :Xᴾ => 50] # define u0 as integers now
@@ -42,6 +49,7 @@ our event occurs at a fixed time or with a fixed frequency. For example, suppose
we want to skip the first event at time `t = 12` and then have the event be
periodic after that point every 12 units of time. We can do so using a more
general discrete callback as follows
+
```@example periodic_event_example
circadian_model = @reaction_network begin
@discrete_events begin
@@ -50,9 +58,11 @@ circadian_model = @reaction_network begin
(kₚ*l,kᵢ), X <--> Xᴾ
end
```
+
Here our condition `((t % 12 == 0) & (t > 12))` determines when the event
occurs, evaluating to `true` when `t` is a multiple of `12` that is also larger
than `12`. We now finish specifying our model
+
```@example periodic_event_example
using JumpProcesses
u0 = [:X => 150, :Xᴾ => 50]
@@ -62,18 +72,22 @@ jinput = JumpInputs(circadian_model, u0, tspan, ps)
jprob = JumpProblem(jinput)
nothing # hide
```
+
Next, if we simulate our model, we note that the events do not seem to be
triggered
+
```@example periodic_event_example
sol = solve(jprob)
plot(sol)
Catalyst.PNG(plot(sol; fmt = :png, dpi = 200)) # hide
```
+
The reason is that general discrete callbacks' conditions are only checked at
the end of each simulation time step, and the probability that these will
coincide with the callback's trigger times ($t = 12, 24, 36, ...$) is infinitely
small. Hence, we must directly instruct our simulation to stop at these time
points. We can do this using the `tstops` argument:
+
```@example periodic_event_example
tstops = range(12.0, tspan[2]; step = 12.0)
sol = solve(jprob; tstops)
@@ -82,7 +96,9 @@ Catalyst.PNG(plot(sol; fmt = :png, dpi = 200)) # hide
```
## [Plotting the light level](@id periodic_event_simulation_plotting_light)
+
Sometimes when simulating models with periodic parameters, one would like to plot the parameter's value across the simulation. For this, there are two potential strategies. One includes creating a [*saving callback*](https://docs.sciml.ai/DiffEqCallbacks/stable/output_saving/#DiffEqCallbacks.SavingCallback). The other one, which we will demonstrate here, includes turning the parameter $l$ to a *variable* (so that its value is recorded throughout the simulation):
+
```@example periodic_event_example
circadian_model = @reaction_network begin
@variables l(t)
@@ -93,7 +109,9 @@ circadian_model = @reaction_network begin
end
nothing # hide
```
+
Next, we simulate our model like before (but providing $l$'s value as an initial condition):
+
```@example periodic_event_example
u0 = [:X => 150.0, :Xᴾ => 50.0, :l => 1.0]
ps = [:kₚ => 0.1, :kᵢ => 0.1]
@@ -101,7 +119,9 @@ oprob = ODEProblem(circadian_model, u0, (0.0, 100.0), ps)
sol = solve(oprob)
nothing # hide
```
+
If we directly plot $l$'s value, it will be too small (compared to $X$ and $Xᴾ$ to be discernible). We instead [`@unpack` our variables](@ref dsl_advanced_options_symbolics_and_DSL_unpack), and then plot a re-scaled version:
+
```@example periodic_event_example
@unpack X, Xᴾ, l = circadian_model
plot(sol; idxs = [X, Xᴾ, 150*l], labels = ["X" "Xᴾ" "Light amplitude"])
diff --git a/docs/src/model_simulation/finite_state_projection_simulation.md b/docs/src/model_simulation/finite_state_projection_simulation.md
index db871e5b5b..6cddea0c61 100644
--- a/docs/src/model_simulation/finite_state_projection_simulation.md
+++ b/docs/src/model_simulation/finite_state_projection_simulation.md
@@ -1,8 +1,11 @@
# [Solving the chemical master equation using FiniteStateProjection.jl](@id finite-state_projection)
+
```@raw html
Environment setup and package installation
```
+
The following code sets up an environment for running the code on this page.
+
```julia
using Pkg
Pkg.activate(; temp = true) # Creates a temporary environment, which is deleted when the Julia session ends.
@@ -14,13 +17,17 @@ Pkg.add("OrdinaryDiffEqRosenbrock")
Pkg.add("Plots")
Pkg.add("SteadyStateDiffEq")
```
+
```@raw html
```
+
```@raw html
Quick-start example
```
+
The following code provides a brief example of how to simulate the chemical master equation using the [FiniteStateProjection.jl](https://github.com/SciML/FiniteStateProjection.jl) package.
+
```julia
# Create reaction network model (a bistable switch).
using Catalyst
@@ -48,16 +55,19 @@ using OrdinaryDiffEqRosenbrock, Plots
osol = solve(oprob, Rodas5P())
heatmap(0:19, 0:19, osol(50.0); xguide = "Y", yguide = "X")
```
+
```@raw html
```
+
\
-
+
As previously discussed, [*stochastic chemical kinetics*](@ref math_models_in_catalyst_sck_jumps) models are mathematically given by jump processes that capture the exact times at which individual reactions occur, and the exact (integer) amounts of each chemical species at a given time. They represent a more microscopic model than [chemical Langevin equation SDE](@ref math_models_in_catalyst_cle_sdes) and [reaction rate equation ODE](@ref math_models_in_catalyst_rre_odes) models, which can be interpreted as approximations to stochastic chemical kinetics models in the large population limit.
One can study the dynamics of stochastic chemical kinetics models by simulating the stochastic processes using Monte Carlo methods. For example, they can be [exactly sampled](@ref simulation_intro_jumps) using [Stochastic Simulation Algorithms](https://en.wikipedia.org/wiki/Gillespie_algorithm) (SSAs), which are also often referred to as Gillespie's method. To gain a good understanding of a system's dynamics, one typically has to carry out a large number of jump process simulations to minimize sampling error. To avoid such sampling error, an alternative approach is to solve ODEs for the *full probability distribution* that these processes have a given value at each time. Knowing this distribution, one can then calculate any statistic of interest that can be sampled via running many SSA simulations.
[*The chemical master equation*](https://en.wikipedia.org/wiki/Master_equation) (CME) describes the time development of this probability distribution[^1], and is given by a (possibly infinite) coupled system of ODEs (with one ODE for each possible chemical state, i.e. number configuration, of the system). For a simple [birth-death model](@ref basic_CRN_library_bd) ($\varnothing \xrightleftharpoons[d]{p} X$) the CME looks like
+
```math
\begin{aligned}
\frac{dP(X(t)=0)}{dt} &= d \cdot P(X(t)=1) - p \cdot P(X(t)=0) \\
@@ -67,17 +77,22 @@ One can study the dynamics of stochastic chemical kinetics models by simulating
&\vdots\\
\end{aligned}
```
+
A general form of the CME is provided [here](@ref math_models_in_catalyst_sck_jumps). For chemical reaction networks in which the total population is bounded, the CME corresponds to a finite set of ODEs. In contrast, for networks in which the system can (in theory) become unbounded, such as networks that include zero order reactions like $\varnothing \to X$, the CME will correspond to an infinite set of ODEs. Even in the finite case, the number of ODEs corresponds to the number of possible state vectors (i.e. vectors with components representing the integer populations of each species in the network), and can become exceptionally large. Therefore, for even simple reaction networks there can be many more ODEs than can be represented in typical levels of computer memory, and it becomes infeasible to numerically solve the full system of ODEs that correspond to the CME. However, in many cases the probability of the system attaining species values outside some small range can become negligibly small. Here, a truncated, approximating, version of the CME can be solved practically. An approach for this is the *finite state projection method*[^2]. Below we describe how to use the [FiniteStateProjection.jl](https://github.com/SciML/FiniteStateProjection.jl) package to solve the truncated CME (with the package's [documentation](https://docs.sciml.ai/FiniteStateProjection/dev/) providing a more extensive description). While the CME approach can be very powerful, we note that even for systems with a few species, the truncated CME typically has too many states for it to be feasible to solve the full set of ODEs.
## [Finite state projection simulation of single-species model](@id state_projection_one_species)
+
For this example, we will use a simple [birth-death model](@ref basic_CRN_library_bd), where a single species ($X$) is created and degraded at constant rates ($p$ and $d$, respectively).
+
```@example state_projection_one_species
using Catalyst
rs = @reaction_network begin
(p,d), 0 <--> X
end
```
+
Next, we perform jump simulations (using the [ensemble simulation interface](@ref ensemble_simulations_monte_carlo)) of the model. Here, we can see how it develops from an initial condition and reaches a steady state distribution.
+
```@example state_projection_one_species
using JumpProcesses, Plots
u0 = [:X => 5]
@@ -89,33 +104,41 @@ esol = solve(eprob, SSAStepper(); trajectories = 10)
plot(esol; ylimit = (0.0, Inf))
Catalyst.PNG(plot(esol; ylimit = (0.0, Inf), fmt = :png, dpi = 200)) # hide
```
-Using chemical master equation simulations, we want to simulate how the *full probability distribution* of these jump simulations develops across the simulation time frame.
+
+Using chemical master equation simulations, we want to simulate how the *full probability distribution* of these jump simulations develops across the simulation time frame.
As a first step, we import the FiniteStateProjection package. Next, we convert our [`ReactionSystem`](@ref) to a `FSPSystem` (from which we later will generate the ODEs that correspond to the truncated CME).
+
```@example state_projection_one_species
using FiniteStateProjection
fsp_sys = FSPSystem(rs)
nothing # hide
```
+
Next, we set our initial condition. For normal simulations, $X$'s initial condition would be a single value. Here, however, we will simulate $X$'s probability distribution. Hence, its initial condition will also be a probability distribution. In FiniteStateProjection's interface, the initial condition is an array, where the $i$'th index is the probability that $X$ have an initial value of $i-1$. The total sum of all probabilities across the vector should be normalised to $1.0$. Here we assume that $X$'s initial conditions is known to be $5$ (hence the corresponding probability is $1.0$, and the remaining ones are $0.0$):
+
```@example state_projection_one_species
u0 = zeros(75)
u0[6] = 1.0
bar(u0, label = "t = 0.0")
```
-We also plot the full distribution using the `bar` function. Finally, the initial condition vector defines the finite space onto which we project the CME. I.e. we will assume that, throughout the entire simulation, the probability of $X$ reaching values outside this initial vector is negligible.
+
+We also plot the full distribution using the `bar` function. Finally, the initial condition vector defines the finite space onto which we project the CME. I.e. we will assume that, throughout the entire simulation, the probability of $X$ reaching values outside this initial vector is negligible.
!!! warning
This last bit is important. Even if the probability seems to be very small on the boundary provided by the initial condition, there is still a risk that probability will "leak". Here, it can be good to make simulations using different projections, ensuring that the results are consistent (especially for longer simulations). It is also possible to (at any time point) sum up the total probability density to gain a measure of how much has "leaked" (ideally, this sum should be as close to 1 as possible). While solving the CME over a very large space will ensure correctness, a too large a space comes with an unnecessary performance penalty.
Now, we can finally create an `ODEProblem` using our `FSPSystem`, initial conditions, and the parameters declared previously. We can simulate this `ODEProblem` like any other ODE.
+
```@example state_projection_one_species
using OrdinaryDiffEqDefault
oprob = ODEProblem(fsp_sys, u0, tspan, ps)
osol = solve(oprob)
nothing # hide
```
+
Finally, we can plot $X$'s probability distribution at various simulation time points. Again, we will use the `bar` function to plot the distribution, and the interface described [here](@ref simulation_structure_interfacing_solutions) to access the simulation at specified time points.
+
```@example state_projection_one_species
bar(0:74, osol(1.0); bar_width = 1.0, linewidth = 0, alpha = 0.7, label = "t = 1.0")
bar!(0:74, osol(2.0); bar_width = 1.0, linewidth = 0, alpha = 0.7, label = "t = 2.0")
@@ -125,9 +148,11 @@ bar!(0:74, osol(10.0); bar_width = 1.0, linewidth = 0, alpha = 0.7, label = "t =
```
## [Finite state projection simulation of multi-species model](@id state_projection_multi_species)
+
Next, we will consider a system with more than one species. The workflow will be identical, however, we will have to make an additional consideration regarding the initial condition, simulation performance, and plotting approach.
For this example, we will consider a simple dimerisation model. In it, $X$ gets produced and degraded at constant rates, and can also dimerise to form $X₂$.
+
```@example state_projection_multi_species
using Catalyst # hide
rs = @reaction_network begin
@@ -135,20 +160,26 @@ rs = @reaction_network begin
(kB,kD), 2X <--> X₂
end
```
+
Next, we will declare our parameter values and initial condition. In this case, the initial condition is a matrix where element $(i,j)$ denotes the initial probability that $(X(0),X₂(0)) = (i-1,j-1)$. In this case, we will use an initial condition where we know that $(X(0),X₂(0)) = (5,0)$.
+
```@example state_projection_multi_species
ps = [:p => 1.0, :d => 0.2, :kB => 2.0, :kD => 5.0]
u0 = zeros(25,25)
u0[6,1] = 1.0
nothing # hide
```
+
In the next step, however, we have to make an additional consideration. Since we have more than one species, we have to define which dimension of the initial condition (and hence also the output solution) corresponds to which species. Here we provide a second argument to `FSPSystem`, which is a vector listing all species in the order they occur in the `u0` array.
+
```@example state_projection_multi_species
using FiniteStateProjection # hide
fsp_sys = FSPSystem(rs, [:X, :X₂])
nothing # hide
```
+
Finally, we can simulate the model just like in the 1-dimensional case. As we are simulating an ODE with $25⋅25 = 625$ states, we need to make some considerations regarding performance. In this case, we will simply specify the `Rodas5P()` ODE solver (more extensive advice on performance can be found [here](@ref ode_simulation_performance)). Here, we perform a simulation with a long time span ($t = 100.0$), aiming to find the system's steady state distribution. Next, we plot it using the `heatmap` function.
+
```@example state_projection_multi_species
using Plots # hide
using OrdinaryDiffEqRosenbrock
@@ -161,14 +192,18 @@ heatmap(0:24, 0:24, osol[end]; xguide = "X₂", yguide = "X")
The `heatmap` function "flips" the plot contrary to what many would consider intuitive. I.e. here the x-axis corresponds to the second species ($X₂$) and the y-axis to the first species ($X$).
## [Finite state projection steady state simulations](@id state_projection_steady_state_sim)
+
Previously, we have shown how the [SteadyStateDiffEq.jl](https://github.com/SciML/SteadyStateDiffEq.jl) package can be used to [find an ODE's steady state through forward simulation](@ref steady_state_stability). The same interface can be used for ODEs generated through FiniteStateProjection. Below, we use this to find the steady state of the dimerisation example studied in the last example.
+
```@example state_projection_multi_species
using SteadyStateDiffEq, OrdinaryDiffEqRosenbrock
ssprob = SteadyStateProblem(fsp_sys, u0, ps)
sssol = solve(ssprob, DynamicSS(Rodas5P()))
heatmap(0:24, 0:24, sssol; xguide = "X₂", yguide = "X")
```
+
Finally, we can also approximate this steady state through Monte Carlo jump simulations.
+
```@example state_projection_multi_species
using JumpProcesses # hide
jprob = JumpProblem(JumpInputs(rs, [:X => 0, :X₂ => 0], (0.0, 100.0), ps))
@@ -181,10 +216,12 @@ for endpoint in esol
end
heatmap(0:24, 0:24, ss_jump ./length(esol); xguide = "X₂", yguide = "X")
```
-Here we used an ensemble [output function](@ref activation_time_distribution_measurement) to only save each simulation's final state (and plot these using `heatmap`).
+Here we used an ensemble [output function](@ref activation_time_distribution_measurement) to only save each simulation's final state (and plot these using `heatmap`).
---
+
## References
+
[^1]: [Daniel T. Gillespie, *A rigorous derivation of the chemical master equation*, Physica A: Statistical Mechanics and its Applications (1992).](https://www.sciencedirect.com/science/article/abs/pii/037843719290283V)
-[^2]: [Brian Munsky, Mustafa Khammash, *The finite state projection algorithm for the solution of the chemical master equation*, Journal of Chemical Physics (2006).](https://pubs.aip.org/aip/jcp/article-abstract/124/4/044104/561868/The-finite-state-projection-algorithm-for-the?redirectedFrom=fulltext)
\ No newline at end of file
+[^2]: [Brian Munsky, Mustafa Khammash, *The finite state projection algorithm for the solution of the chemical master equation*, Journal of Chemical Physics (2006).](https://pubs.aip.org/aip/jcp/article-abstract/124/4/044104/561868/The-finite-state-projection-algorithm-for-the?redirectedFrom=fulltext)
diff --git a/docs/src/model_simulation/ode_simulation_performance.md b/docs/src/model_simulation/ode_simulation_performance.md
index b0f1a87ded..7b51936029 100644
--- a/docs/src/model_simulation/ode_simulation_performance.md
+++ b/docs/src/model_simulation/ode_simulation_performance.md
@@ -1,19 +1,23 @@
# [Advice for Performant ODE Simulations](@id ode_simulation_performance)
+
We have previously described how to perform ODE simulations of *chemical reaction network* (CRN) models. These simulations are typically fast and require little additional consideration. However, when a model is simulated many times (e.g. as a part of solving an inverse problem), or is very large, simulation run
-times may become noticeable. Here we will give some advice on how to improve performance for these cases [^1].
+times may become noticeable. Here we will give some advice on how to improve performance for these cases[^1].
Generally, there are few good ways to, before a simulation, determine the best options. Hence, while we below provide several options, if you face an application for which reducing run time is critical (e.g. if you need to simulate the same ODE many times), it might be required to manually trial these various options to see which yields the best performance ([BenchmarkTools.jl's](https://github.com/JuliaCI/BenchmarkTools.jl) `@btime` macro is useful for this purpose). It should be noted that the default options typically perform well, and it is primarily for large models where investigating alternative options is worthwhile. All ODE simulations of Catalyst models are performed using the OrdinaryDiffEq.jl package, [which documentation](https://docs.sciml.ai/DiffEqDocs/stable/) provides additional advice on performance.
Generally, this short checklist provides a quick guide for dealing with ODE performance:
+
1. If performance is not critical, use [the default solver choice](@ref ode_simulation_performance_solvers) and do not worry further about the issue.
2. If improved performance would be useful, read about solver selection (both in [this tutorial](@ref ode_simulation_performance_solvers) and [OrdinaryDiffEq's documentation](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/)) and then try a few different solvers to find one with good performance.
3. If you have a large ODE (approximately 100 variables or more), try the [various options for efficient Jacobian computation](@ref ode_simulation_performance_jacobian) (noting that some are non-trivial to use, and should only be investigated if truly required).
4. If you plan to simulate your ODE many times, try [parallelise it on CPUs or GPUs](@ref ode_simulation_performance_parallelisation) (with preference for the former, which is easier to use).
## [Regarding stiff and non-stiff problems and solvers](@id ode_simulation_performance_stiffness)
+
Generally, ODE problems can be categorised into [*stiff ODEs* and *non-stiff ODEs*](https://en.wikipedia.org/wiki/Stiff_equation). This categorisation is important due to stiff ODEs requiring specialised solvers. A common cause of failure to simulate an ODE is the use of a non-stiff solver for a stiff problem. There is no exact way to determine whether a given ODE is stiff or not, however, systems with several different time scales (e.g. a CRN with both slow and fast reactions) typically generate stiff ODEs.
Here we simulate the (stiff) [Brusselator](@ref basic_CRN_library_brusselator) model using the `Tsit5` solver (which is designed for non-stiff ODEs):
+
```@example ode_simulation_performance_1
using Catalyst, OrdinaryDiffEqTsit5, Plots
@@ -35,16 +39,21 @@ Catalyst.PNG(plot(sol1; fmt = :png, dpi = 200)) # hide
```
We get a warning, indicating that the simulation was terminated. Furthermore, the resulting plot ends at $t ≈ 12$, meaning that the simulation was not completed (as the simulation's endpoint is $t = 20$). Indeed, we can confirm this by checking the *return code* of the solution object:
+
```@example ode_simulation_performance_1
sol1.retcode
```
+
Next, we instead try the `Rodas5P` solver (which is designed for stiff problems):
+
```@example ode_simulation_performance_1
using OrdinaryDiffEqRosenbrock
sol2 = solve(oprob, Rodas5P())
plot(sol2)
```
+
This time the simulation was successfully completed, which can be confirmed by checking the return code:
+
```@example ode_simulation_performance_1
sol2.retcode
```
@@ -53,9 +62,10 @@ Generally, ODE solvers can be divided into [*explicit* and *implicit* solvers](h
Finally, we should note that stiffness is not tied to the model equations only. If we change the parameter values of our previous Brusselator model to `[:A => 1.0, :B => 4.0]`, the non-stiff `Tsit5` solver can successfully simulate it.
-
## [ODE solver selection](@id ode_simulation_performance_solvers)
+
OrdinaryDiffEq implements an unusually large number of ODE solvers, with the performance of the simulation heavily depending on which one is chosen. These are provided as the second argument to the `solve` command, e.g. here we use the `Tsit5` solver to simulate a simple [birth-death process](@ref basic_CRN_library_bd):
+
```@example ode_simulation_performance_2
using Catalyst, OrdinaryDiffEqTsit5
@@ -70,7 +80,9 @@ oprob = ODEProblem(bd_model, u0, tspan, ps)
solve(oprob, Tsit5())
nothing # hide
```
+
If no solver argument is provided to `solve`, and the `OrdinaryDiffEqDefault` sub-library or meta `OrdinaryDiffEq` library are loaded, then one is automatically selected:
+
```@example ode_simulation_performance_2
using OrdinaryDiffEqDefault
solve(oprob)
@@ -80,35 +92,42 @@ nothing # hide
While the default choice is typically enough for most single simulations, if performance is important, it can be worthwhile exploring the available solvers to find one that is especially suited for the given problem. A complete list of possible ODE solvers, with advice on optimal selection, can be found [here](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/). This section will give some general advice.
The most important part of solver selection is to select one appropriate for [the problem's stiffness](@ref ode_simulation_performance_stiffness). Generally, the `Tsit5` solver is good for non-stiff problems, and `Rodas5P` for stiff problems. For large stiff problems (with many species), `FBDF` can be a good choice. We can illustrate the impact of these choices by simulating our birth-death process using the `Tsit5`, `Vern7` (an explicit solver yielding [low error in the solution](@ref ode_simulation_performance_error)), `Rodas5P`, and `FBDF` solvers (benchmarking their respective performance using [BenchmarkTools.jl](https://github.com/JuliaCI/BenchmarkTools.jl)):
+
```julia
-using BenchmarkTools
+using BenchmarkTools
using OrdinaryDiffEqTsit5, OrdinaryDiffEqRosenbrock, OrdinaryDiffEqVerner, OrdinaryDiffEqBDF
@btime solve(oprob, Tsit5())
@btime solve(oprob, Vern7())
@btime solve(oprob, Rodas5P())
@btime solve(oprob, FBDF())
```
+
If you perform the above benchmarks on your machine, and check the results, you will note that the fastest solver is several times faster than the slowest one (`FBDF`, which is a poor choice for this ODE).
### [Simulation error, tolerance, and solver selection](@id ode_simulation_performance_error)
+
Numerical ODE simulations [approximate an ODEs' continuous solutions as discrete vectors](https://en.wikipedia.org/wiki/Discrete_time_and_continuous_time). This introduces errors in the computed solution. The magnitude of these errors can be controlled by setting solver *tolerances*. By reducing the tolerance, solution errors will be reduced, however, this will also increase simulation run times. The (absolute and relative) tolerance of a solver can be tuned through the `abstol` and `reltol` arguments. Here we see how run time increases with larger tolerances:
+
```julia
@btime solve(oprob, Tsit5(); abstol=1e-6, reltol=1e-6)
@btime solve(oprob, Tsit5(); abstol=1e-12, reltol=1e-12)
```
+
It should be noted, however, that the result of the second simulation is a lot more accurate. Thus, ODE solver performance cannot be determined solely from run time, but is a composite of run
time and error. Benchmarks comparing solver performance (by plotting the run time as a function of the error) for various CRN models can be found in the [SciMLBenchmarks repository](https://docs.sciml.ai/SciMLBenchmarksOutput/stable/Bio/BCR/).
Generally, whether solution error is a consideration depends on the application. If you want to compute the trajectory of an expensive space probe as it is sent from Earth, to slingshot Jupiter, and then reach Pluto a few years later, ensuring a minimal error will be essential. However, if you want to simulate a simple CRN to determine whether it oscillates for a given parameter set, a small error will not constitute a problem. An important aspect with regard to error is that it affects the selection of the optimal solver. E.g. if tolerance is low (generating larger errors) the `Rosenbrock23` method performs well for small, stiff, problems (again, more details can be found in [OrdinaryDiffEq's documentation](https://docs.sciml.ai/DiffEqDocs/stable/solvers/ode_solve/)).
-
## [Jacobian computation options for implicit solvers](@id ode_simulation_performance_jacobian)
+
As [previously mentioned](@ref ode_simulation_performance_stiffness), implicit ODE solvers require the computation of the system's [*Jacobian*](https://en.wikipedia.org/wiki/Jacobian_matrix_and_determinant). The reason is (roughly) that, in each time step, these solvers need to solve a non-linear equation to find the simulation's value at the next timestep (unlike explicit solvers, which compute the value at the next time step directly). Typically this is done using [the Newton-Raphson method](https://en.wikipedia.org/wiki/Newton%27s_method), which requires the Jacobian. Especially for large systems, this can be computationally expensive (and a potential strain on available memory), in which case one might consider various Jacobian-computation options (as described below). A throughout tutorial on simulating a large, stiff, ODE can be found [here](https://docs.sciml.ai/DiffEqDocs/stable/tutorials/advanced_ode_example/#stiff).
### [Building the Jacobian symbolically](@id ode_simulation_performance_symbolic_jacobian)
+
By default, OrdinaryDiffEq computes the Jacobian using [*automatic differentiation*](https://en.wikipedia.org/wiki/Automatic_differentiation) (however, using [*finite differences*](https://en.wikipedia.org/wiki/Finite_difference) is [also possible](https://docs.sciml.ai/DiffEqDocs/stable/features/performance_overloads/)). Since Catalyst builds its ODEs symbolically, it is able to *compute an analytic Jacobian symbolically*. Typically, this is only advantageous when you are also [using a sparse Jacobian](@ref ode_simulation_performance_sparse_jacobian).
To use this option, simply set `jac = true` when constructing an `ODEProblem`:
+
```@example ode_simulation_performance_3
using Catalyst, OrdinaryDiffEqDefault
@@ -127,32 +146,41 @@ nothing # hide
```
### [Using a sparse Jacobian](@id ode_simulation_performance_sparse_jacobian)
+
For a system with $n$ variables, the Jacobian will be an $n\times n$ matrix. This means that, as $n$ becomes large, the Jacobian can become *very* large, potentially causing a significant strain on memory. In these cases, most Jacobian entries are typically $0$. This means that a [*sparse*](https://en.wikipedia.org/wiki/Sparse_matrix) Jacobian (rather than a *dense* one, which is the default) can be advantageous. To designate sparse Jacobian usage, simply provide the `sparse = true` option when constructing an `ODEProblem`:
+
```@example ode_simulation_performance_3
oprob = ODEProblem(brusselator, u0, tspan, ps; sparse = true)
nothing # hide
```
### [Linear solver selection](@id ode_simulation_performance_symbolic_jacobian_linear_solver)
+
When implicit solvers use e.g. the Newton-Raphson method to (at each simulation time step) solve a (typically non-linear) equation, they actually solve a linearised version of this equation. For this, they use a linear solver, the choice of which can impact performance. To specify one, we use the `linsolve` option (given to the solver function, *not* the `solve` command). E.g. to use the `KLUFactorization` linear solver (which requires loading the [LinearSolve.jl](https://github.com/SciML/LinearSolve.jl) package) we run
+
```@example ode_simulation_performance_3
using LinearSolve, OrdinaryDiffEqRosenbrock
solve(oprob, Rodas5P(linsolve = KLUFactorization()))
nothing # hide
```
+
A full list of potential linear solvers can be found [here](https://docs.sciml.ai/LinearSolve/dev/solvers/solvers/#Full-List-of-Methods). Typically, the default choice performs well.
A unique approach to the linear solvers is to use a *matrix-free Newton-Krylov method*. These do not actually compute the Jacobian, but rather *the effect of multiplying it with a vector*. They are typically advantageous for large systems (with large Jacobians), and can be designated using the `KrylovJL_GMRES` linear solver:
+
```@example ode_simulation_performance_3
solve(oprob, Rodas5P(linsolve = KrylovJL_GMRES()))
nothing # hide
```
+
Since these methods do not depend on a Jacobian, certain Jacobian options (such as [computing it symbolically](@ref ode_simulation_performance_symbolic_jacobian)) are irrelevant to them.
### [Designating preconditioners for Jacobian-free linear solvers](@id ode_simulation_performance_preconditioners)
+
When an implicit method solves a linear equation through an (iterative) matrix-free Newton-Krylov method, the rate of convergence depends on the numerical properties of the matrix defining the linear system. To speed up convergence, a [*preconditioner*](https://en.wikipedia.org/wiki/Preconditioner) can be applied to both sides of the linear equation, attempting to create an equation that converges faster. Preconditioners are only relevant when using matrix-free Newton-Krylov methods.
In practice, preconditioners are implemented as functions with a specific set of arguments. How to implement these is non-trivial, and we recommend reading OrdinaryDiffEq's documentation pages [here](https://docs.sciml.ai/DiffEqDocs/stable/features/linear_nonlinear/#Preconditioners:-precs-Specification) and [here](https://docs.sciml.ai/DiffEqDocs/stable/tutorials/advanced_ode_example/#Adding-a-Preconditioner). In this example, we will define an [Incomplete LU](https://en.wikipedia.org/wiki/Incomplete_LU_factorization) preconditioner (which requires the [IncompleteLU.jl](https://github.com/haampie/IncompleteLU.jl) package):
+
```@example ode_simulation_performance_3
using IncompleteLU
function incompletelu(W, du, u, p, t, newW, Plprev, Prprev, solverdata)
@@ -165,18 +193,22 @@ function incompletelu(W, du, u, p, t, newW, Plprev, Prprev, solverdata)
end
nothing # hide
```
+
Next, `incompletelu` can be supplied to our solver using the `precs` argument:
+
```@example ode_simulation_performance_3
solve(oprob, Rodas5P(linsolve = KrylovJL_GMRES(), precs = incompletelu, concrete_jac = true))
nothing # hide
```
+
Finally, we note that when using preconditioners with a matrix-free method (like `KrylovJL_GMRES`, which is also the only case when these are relevant), the `concrete_jac = true` argument is required.
Generally, the use of preconditioners is only recommended for advanced users who are familiar with the concepts. However, for large systems, if performance is essential, they can be worth looking into.
-
## [Elimination of system conservation laws](@id ode_simulation_performance_conservation_laws)
+
Previously, we have described how Catalyst, when it generates ODEs, is able to [detect and eliminate conserved quantities](@ref conservation_laws). In certain cases, doing this can improve performance. E.g. in the following example we will eliminate the single conserved quantity in a [two-state model](@ref basic_CRN_library_two_states). This results in a differential algebraic equation with a single differential equation and a single algebraic equation (as opposed to two differential equations). However, as the algebraic equation is fully determined by the ODE solution, Catalyst moves it to be an observable and our new system therefore only contains one ODE that must be solved numerically. Conservation laws can be eliminated by providing the `remove_conserved = true` option to `ODEProblem`:
+
```@example ode_simulation_performance_conservation_laws
using Catalyst, OrdinaryDiffEqTsit5
@@ -192,16 +224,19 @@ oprob = ODEProblem(rs, u0, (0.0, 10.0), ps; remove_conserved = true)
sol = solve(oprob)
nothing # hide
```
-Conservation law elimination is not expected to ever impact performance negatively; it simply results in a (possibly) lower-dimensional system of ODEs to solve. However, eliminating conserved species may have minimal performance benefits; it is model-dependent whether elimination results in faster ODE solving times and/or increased solution accuracy.
+Conservation law elimination is not expected to ever impact performance negatively; it simply results in a (possibly) lower-dimensional system of ODEs to solve. However, eliminating conserved species may have minimal performance benefits; it is model-dependent whether elimination results in faster ODE solving times and/or increased solution accuracy.
## [Parallelisation on CPUs and GPUs](@id ode_simulation_performance_parallelisation)
+
Whenever an ODE is simulated a large number of times (e.g. when investigating its behaviour for different parameter values), the best way to improve performance is to [parallelise the simulation over multiple processing units](https://en.wikipedia.org/wiki/Parallel_computing). Indeed, an advantage of the Julia programming language is that it was designed after the advent of parallel computing, making it well-suited for this task. Roughly, parallelisation can be divided into parallelisation on [CPUs](https://en.wikipedia.org/wiki/Central_processing_unit) and on [GPUs](https://en.wikipedia.org/wiki/General-purpose_computing_on_graphics_processing_units). CPU parallelisation is most straightforward, while GPU parallelisation requires specialised ODE solvers (which Catalyst have access to).
Both CPU and GPU parallelisation require first building an `EnsembleProblem` (which defines the simulations you wish to perform) and then supplying this with the correct parallelisation options. `EnsembleProblem`s have [previously been introduced in Catalyst's documentation](@ref ensemble_simulations) (but in the context of convenient bundling of similar simulations, rather than to improve performance), with a more throughout description being found in [OrdinaryDiffEq's documentation](https://docs.sciml.ai/DiffEqDocs/stable/features/ensemble/#ensemble). Finally, a general documentation of parallel computing in Julia is available [here](https://docs.julialang.org/en/v1/manual/parallel-computing/).
### [CPU parallelisation](@id ode_simulation_performance_parallelisation_CPU)
+
For this example (and the one for GPUs), we will consider a modified [Michaelis-Menten enzyme kinetics model](@ref basic_CRN_library_mm), which describes an enzyme ($E$) that converts a substrate ($S$) to a product ($P$):
+
```@example ode_simulation_performance_4
using Catalyst
mm_model = @reaction_network begin
@@ -211,7 +246,9 @@ mm_model = @reaction_network begin
d, S --> ∅
end
```
+
The model can be simulated, showing how $P$ is produced from $S$:
+
```@example ode_simulation_performance_4
using OrdinaryDiffEqTsit5, Plots
u0 = [:S => 1.0, :E => 1.0, :SE => 0.0, :P => 0.0]
@@ -221,13 +258,16 @@ oprob = ODEProblem(mm_model, u0, tspan, ps)
sol = solve(oprob, Tsit5())
plot(sol)
```
+
Due to the degradation of $S$, if the production rate is not high enough, the total amount of $P$ produced is reduced. For this tutorial, we will investigate this effect for a range of values of $kP$. This will require a large number of simulations (for various $kP$ values), which we will parallelise on CPUs (this section) and GPUs ([next section](@ref ode_simulation_performance_parallelisation_GPU)).
To parallelise our simulations, we first need to create an `EnsembleProblem`. These describe which simulations we wish to perform. The input to this is:
+
- The `ODEProblem` corresponds to the model simulation (`SDEProblem` and `JumpProblem`s can also be supplied, enabling the parallelisation of these problem types).
- A function, `prob_func`, describing how to modify the problem for each simulation. If we wish to simulate the same, unmodified problem, in each simulation (primarily relevant for stochastic simulations), this argument is not required.
Here, `prob_func` takes 3 arguments:
+
- `prob`: The problem that it modifies at the start of each individual run (which will be the same as `EnsembleProblem`'s first argument).
- `i`: The index of the specific simulation (in the array of all simulations that are performed).
- `repeat`: The repeat of a specific simulation in the array. We will not use this option in this example, however, it is discussed in more detail [here](https://docs.sciml.ai/DiffEqDocs/stable/features/ensemble/#Building-a-Problem).
@@ -235,45 +275,60 @@ Here, `prob_func` takes 3 arguments:
and output the `ODEProblem` simulated in the i'th simulation.
Let us assume that we wish to simulate our model 100 times, for $kP = 0.01, 0.02, ..., 0.99, 1.0$. We define our `prob_func` using [`remake`](@ref simulation_structure_interfacing_problems_remake):
+
```@example ode_simulation_performance_4
function prob_func(prob, i, repeat)
return remake(prob; p = [:kP => 0.01*i])
end
nothing # hide
```
+
Next, we can create our `EnsembleProblem`:
+
```@example ode_simulation_performance_4
eprob = EnsembleProblem(oprob; prob_func)
nothing # hide
```
+
We can now solve our `ODEProblem` using the same syntax we would normally use to solve the original `ODEProblem`, with the exception that an additional argument, `trajectories`, is required (which denotes how many simulations should be performed).
+
```@example ode_simulation_performance_4
esol = solve(eprob, Tsit5(); trajectories = 100)
nothing # hide
```
+
To access the i'th solution we use `esol.u[i]`. To e.g. plot the 47'th solution we use:
+
```@example ode_simulation_performance_4
plot(esol.u[47])
```
+
To extract the amount of $P$ produced in each simulation, and plot this against the corresponding $kP$ value, we can use:
+
```@example ode_simulation_performance_4
plot(0.01:0.01:1.0, map(sol -> sol[:P][end], esol.u), xguide = "kP", yguide = "P produced", label="")
plot!(left_margin = 3Plots.Measures.mm) # hide
```
Above, we have simply used `EnsembleProblem` as a convenient interface to run a large number of similar simulations. However, these problems have the advantage that they allow the passing of an *ensemble algorithm* to the `solve` command, which describes a strategy for parallelising the simulations. By default, `EnsembleThreads` is used. This parallelises the simulations using [multithreading](https://en.wikipedia.org/wiki/Multithreading_(computer_architecture)) (parallelisation within a single process), which is typically advantageous for small problems on shared memory devices. An alternative is `EnsembleDistributed` which instead parallelises the simulations using [multiprocessing](https://en.wikipedia.org/wiki/Multiprocessing) (parallelisation across multiple processes). To do this, we simply supply this additional solver to the solve command:
+
```julia
esol = solve(eprob, Tsit5(), EnsembleDistributed(); trajectories = 100)
```
+
To utilise multiple processes, you must first give Julia access to these. You can check how many processes are available using the `nprocs` (which requires the [Distributed.jl](https://github.com/JuliaLang/Distributed.jl) package):
+
```julia
using Distributed
nprocs()
```
+
Next, more processes can be added using `addprocs`. E.g. here we add an additional 4 processes:
+
```julia
addprocs(4)
```
+
Powerful personal computers and HPC clusters typically have a large number of available additional processes that can be added to improve performance.
While `EnsembleThreads` and `EnsembleDistributed` cover the main cases, additional ensemble algorithms exist. A more throughout description of these can be found [here](https://docs.sciml.ai/DiffEqDocs/dev/features/ensemble/#EnsembleAlgorithms).
@@ -281,24 +336,30 @@ While `EnsembleThreads` and `EnsembleDistributed` cover the main cases, addition
Finally, it should be noted that OrdinaryDiffEq, if additional processes are available, automatically parallelises the [linear solve part of implicit simulations](@ref ode_simulation_performance_symbolic_jacobian_linear_solver). It is thus possible to see performance improvements from adding additional processes when running single simulations, not only multiple parallelised ones (this effect is primarily noticeable for large systems with many species).
### [GPU parallelisation](@id ode_simulation_performance_parallelisation_GPU)
+
GPUs are different from CPUs in that they are much more restricted in what computations they can carry out. However, unlike CPUs, they are typically available in far larger numbers. Their original purpose is for rendering graphics (which typically involves solving a large number of very simple computations, something CPUs with their few, but powerful, cores are unsuited for). Recently, they have also started to be applied to other problems, such as ODE simulations. Generally, GPU parallelisation is only worthwhile when you have a very large number of parallel simulations to run (and access to good GPU resources, either locally or on a cluster).
Generally, we can parallelise `EnsembleProblem`s across several GPUs in a very similar manner to how we parallelised them across several CPUs, but by using a different ensemble algorithm (such as `EnsembleGPUArray`). However, there are some additional requirements:
+
- GPU parallelisation requires the [DiffEqGPU.jl](https://github.com/SciML/DiffEqGPU.jl) package.
- Depending on which GPU hardware is used, a specific back-end package has to be installed and imported (e.g. CUDA for NVIDIA's GPUs or Metal for Apple's).
- For some cases, we must use a special ODE solver supporting simulations on GPUs.
Furthermore (while not required) to receive good performance, we should also make the following adaptations:
+
- By default, Julia's decimal numbers are implemented as `Float64`s, however, using `Float32`s is advantageous on GPUs. Ideally, all initial conditions and parameter values should be specified using these.
- We should designate all our vectors (i.e. initial conditions and parameter values) as [static vectors](https://github.com/JuliaArrays/StaticArrays.jl).
We will assume that we are using the CUDA GPU hardware, so we will first load the [CUDA.jl](https://github.com/JuliaGPU/CUDA.jl) backend package, as well as DiffEqGPU:
+
```julia
using CUDA, DiffEqGPU
```
+
Which backend package you should use depends on your available hardware, with the alternatives being listed [here](https://docs.sciml.ai/DiffEqGPU/stable/manual/backends/).
Next, we declare our model and `ODEProblem`. However, we make all values `Float64` (by appending `f0` to them) and all vectors static (by adding `@SVector` before their declaration, something which requires the [StaticArrays](https://github.com/JuliaArrays/StaticArrays.jl) package).
+
```@example ode_simulation_performance_5
using Catalyst, OrdinaryDiffEqDefault, StaticArrays
@@ -317,7 +378,9 @@ p = @SVector [kB => 1.0f0, kD => 0.1f0, kP => 0.5f0, d => 0.1f0]
oprob = ODEProblem(mm_model, u0, tspan, p)
nothing # hide
```
+
When we declare our `prob_func` and `EnsembleProblem` we need to ensure that the updated `ODEProblem` uses `Float32`:
+
```@example ode_simulation_performance_5
function prob_func(prob, i, repeat)
return remake(prob; p = [:kP => 0.0001f0*i])
@@ -325,30 +388,36 @@ end
eprob = EnsembleProblem(oprob; prob_func = prob_func)
nothing # hide
```
+
Here have we increased the number of simulations to 10,000, since this is a more appropriate number for GPU parallelisation (as compared to the 100 simulations we performed in our CPU example).
!!! note
Currently, declaration of static vectors requires symbolic, rather than symbol, form for species and parameters. Hence, we here first `@unpack` these before constructing `u0` and `ps` using `@SVector`.
We can now simulate our model using a GPU-based ensemble algorithm. Currently, two such algorithms are available, `EnsembleGPUArray` and `EnsembleGPUKernel`. Their differences are that
+
- Only `EnsembleGPUKernel` requires arrays to be static arrays (although it is still advantageous for `EnsembleGPUArray`).
- While `EnsembleGPUArray` can use standard ODE solvers, `EnsembleGPUKernel` requires specialised versions (such as `GPUTsit5`). A list of available such solvers can be found [here](https://docs.sciml.ai/DiffEqGPU/dev/manual/ensemblegpukernel/#specialsolvers).
Generally, it is recommended to use `EnsembleGPUArray` for large models (that have at least $100$ variables), and `EnsembleGPUKernel` for smaller ones. Here we simulate our model using both approaches (noting that `EnsembleGPUKernel` requires `GPUTsit5`):
+
```julia
esol1 = solve(eprob, Tsit5(), EnsembleGPUArray(CUDA.CUDABackend()); trajectories = 10000)
esol2 = solve(eprob, GPUTsit5(), EnsembleGPUKernel(CUDA.CUDABackend()); trajectories = 10000)
```
+
Note that we have to provide the `CUDA.CUDABackend()` argument to our ensemble algorithms (to designate our GPU backend, in this case, CUDA).
Just like OrdinaryDiffEq is able to utilise parallel CPU processes to speed up the linear solve part of ODE simulations, GPUs can also be used. More details on this can be found [here](https://docs.sciml.ai/DiffEqGPU/stable/tutorials/within_method_gpu/). This is only recommended when ODEs are very large (at least 1,000 species), which is typically not the case for CRNs.
For more information on differential equation simulations on GPUs in Julia, please read [DiffEqGPU's documentation](https://docs.sciml.ai/DiffEqGPU/stable/). Furthermore, if performance is critical, [this tutorial](https://docs.sciml.ai/DiffEqGPU/stable/tutorials/lower_level_api/) provides information on how to redesign your simulation code to make it more suitable for GPU simulations.
-
---
+
## Citation
+
If you use GPU simulations in your research, please cite the following paper to support the authors of the DiffEqGPU package:
-```
+
+```bibtex
@article{utkarsh2024automated,
title={Automated translation and accelerated solving of differential equations on multiple GPU platforms},
author={Utkarsh, Utkarsh and Churavy, Valentin and Ma, Yingbo and Besard, Tim and Srisuma, Prakitr and Gymnich, Tim and Gerlach, Adam R and Edelman, Alan and Barbastathis, George and Braatz, Richard D and others},
@@ -361,5 +430,7 @@ If you use GPU simulations in your research, please cite the following paper to
```
---
+
## References
+
[^1]: [E. Hairer, G. Wanner, *Solving Ordinary Differential Equations II*, Springer (1996).](https://link.springer.com/book/10.1007/978-3-642-05221-7)
diff --git a/docs/src/model_simulation/sde_simulation_performance.md b/docs/src/model_simulation/sde_simulation_performance.md
index 603ee53852..c5e765fcf6 100644
--- a/docs/src/model_simulation/sde_simulation_performance.md
+++ b/docs/src/model_simulation/sde_simulation_performance.md
@@ -1,29 +1,39 @@
# [Advice for Performant SDE Simulations](@id sde_simulation_performance)
+
While there exist relatively straightforward approaches to manage performance for [ODE](@ref ode_simulation_performance) and jump simulations, this is generally not the case for SDE simulations. Below, we briefly describe some options. However, as one starts to investigate these, one quickly reaches what is (or could be) active areas of research.
## [SDE solver selection](@id sde_simulation_performance_solvers)
+
We have previously described how [ODE solver selection](@ref ode_simulation_performance_solvers) can impact simulation performance. Again, it can be worthwhile to investigate solver selection's impact on performance for SDE simulations. Throughout this documentation, we generally use the `STrapezoid` solver as the default choice. However, if the `DifferentialEquations` package is loaded
+
```julia
using DifferentialEquations
```
-automatic SDE solver selection enabled (just like is the case for ODEs by default). Generally, the automatic SDE solver choice enabled by `DifferentialEquations` is better than just using `STrapezoid`. Next, if performance is critical, it can be worthwhile to check the [list of available SDE solvers](https://docs.sciml.ai/DiffEqDocs/stable/solvers/sde_solve/) to find one with advantageous performance for a given problem. When doing so, it is important to pick a solver compatible with *non-diagonal noise* and with [*Ito problems*](https://en.wikipedia.org/wiki/It%C3%B4_calculus).
+
+automatic SDE solver selection enabled (just like is the case for ODEs by default). Generally, the automatic SDE solver choice enabled by `DifferentialEquations` is better than just using `STrapezoid`. Next, if performance is critical, it can be worthwhile to check the [list of available SDE solvers](https://docs.sciml.ai/DiffEqDocs/stable/solvers/sde_solve/) to find one with advantageous performance for a given problem. When doing so, it is important to pick a solver compatible with *non-diagonal noise* and with [*Ito problems*](https://en.wikipedia.org/wiki/It%C3%B4_calculus).
## [Options for Jacobian computation](@id sde_simulation_performance_jacobian)
+
In the section on ODE simulation performance, we describe various [options for computing the system Jacobian](@ref ode_simulation_performance_jacobian), and how these could be used to improve performance for [implicit solvers](@ref ode_simulation_performance_stiffness). These can be used in tandem with implicit SDE solvers (such as `STrapezoid`). However, due to additional considerations during SDE simulations, it is much less certain whether these will actually have any impact on performance. So while these options might be worth reading about and trialling, there is no guarantee that they will be beneficial.
## [Parallelisation on CPUs and GPUs](@id sde_simulation_performance_parallelisation)
+
We have previously described how simulation parallelisation can be used to [improve performance when multiple ODE simulations are carried out](@ref ode_simulation_performance_parallelisation). The same approaches can be used for SDE simulations. Indeed, it is often more relevant for SDEs, as these are often re-simulated using identical simulation conditions (to investigate their typical behaviour across many samples). CPU parallelisation of SDE simulations uses the [same approach as ODEs](@ref ode_simulation_performance_parallelisation_CPU). GPU parallelisation requires some additional considerations, which are described below.
### [GPU parallelisation of SDE simulations](@id sde_simulation_performance_parallelisation_GPU)
+
GPU parallelisation of SDE simulations uses a similar approach as that for [ODE simulations](@ref ode_simulation_performance_parallelisation_GPU). The main differences are that SDE parallelisation requires a GPU SDE solver (like `GPUEM`) and fixed time stepping.
We will assume that we are using the CUDA GPU hardware, so we will first load the [CUDA.jl](https://github.com/JuliaGPU/CUDA.jl) backend package, as well as DiffEqGPU:
+
```julia
using CUDA, DiffEqGPU
```
+
Which backend package you should use depends on your available hardware, with the alternatives being listed [here](https://docs.sciml.ai/DiffEqGPU/stable/manual/backends/).
Next, we create the `SDEProblem` which we wish to simulate. Like for ODEs, we ensure that all vectors are [static vectors](https://github.com/JuliaArrays/StaticArrays.jl) and that all values are `Float32`s. Here we prepare the parallel simulations of a simple [birth-death process](@ref basic_CRN_library_bd).
+
```@example sde_simulation_performance_gpu
using Catalyst, StochasticDiffEq, StaticArrays
bd_model = @reaction_network begin
@@ -37,12 +47,16 @@ ps = @SVector [p => 10.0f0, d => 1.0f0]
sprob = SDEProblem(bd_model, u0, tspan, ps)
nothing # hide
```
+
The `SDEProblem` is then used to [create an `EnsembleProblem`](@ref ensemble_simulations_monte_carlo).
+
```@example sde_simulation_performance_gpu
eprob = EnsembleProblem(sprob)
nothing # hide
```
+
Finally, we can solve our `EnsembleProblem` while:
+
- Using a valid GPU SDE solver (either [`GPUEM`](https://docs.sciml.ai/DiffEqGPU/stable/manual/ensemblegpukernel/#DiffEqGPU.GPUEM) or [`GPUSIEA`](https://docs.sciml.ai/DiffEqGPU/stable/manual/ensemblegpukernel/#DiffEqGPU.GPUSIEA)).
- Designating the GPU ensemble method, `EnsembleGPUKernel` (with the correct GPU backend as input).
- Designating the number of trajectories we wish to simulate.
@@ -55,4 +69,5 @@ esol = solve(eprob, GPUEM(), EnsembleGPUKernel(CUDA.CUDABackend()); trajectories
Above we parallelise GPU simulations with identical initial conditions and parameter values. However, [varying these](@ref ensemble_simulations_varying_conditions) is also possible.
### [Multilevel Monte Carlo](@id sde_simulation_performance_parallelisation_mlmc)
-An approach for speeding up parallel stochastic simulations is so-called [*multilevel Monte Carlo approaches*](https://en.wikipedia.org/wiki/Multilevel_Monte_Carlo_method) (MLMC). These are used when a stochastic process is simulated repeatedly using identical simulation conditions. Here, instead of performing all simulations using identical [tolerance](@ref ode_simulation_performance_error), the ensemble is simulated using a range of tolerances (primarily lower ones, which yields faster simulations). Currently, [StochasticDiffEq.jl](https://github.com/SciML/StochasticDiffEq.jl) do not have a native implementation for performing MLMC simulations (this will hopefully be added in the future). However, if high performance of parallel SDE simulations is required, these approaches may be worth investigating.
\ No newline at end of file
+
+An approach for speeding up parallel stochastic simulations is so-called [*multilevel Monte Carlo approaches*](https://en.wikipedia.org/wiki/Multilevel_Monte_Carlo_method) (MLMC). These are used when a stochastic process is simulated repeatedly using identical simulation conditions. Here, instead of performing all simulations using identical [tolerance](@ref ode_simulation_performance_error), the ensemble is simulated using a range of tolerances (primarily lower ones, which yields faster simulations). Currently, [StochasticDiffEq.jl](https://github.com/SciML/StochasticDiffEq.jl) do not have a native implementation for performing MLMC simulations (this will hopefully be added in the future). However, if high performance of parallel SDE simulations is required, these approaches may be worth investigating.
diff --git a/docs/src/model_simulation/simulation_introduction.md b/docs/src/model_simulation/simulation_introduction.md
index 986d717844..3d440b4821 100644
--- a/docs/src/model_simulation/simulation_introduction.md
+++ b/docs/src/model_simulation/simulation_introduction.md
@@ -1,9 +1,11 @@
# [Model Simulation Introduction](@id simulation_intro)
+
Catalyst's core functionality is the creation of *chemical reaction network* (CRN) models that can be simulated using ODE, SDE, and jump simulations. How such simulations are carried out has already been described in [Catalyst's introduction](@ref introduction_to_catalyst). This page provides a deeper introduction, giving some additional background and introducing various simulation-related options.
Here we will focus on the basics, with other sections of the simulation documentation describing various specialised features, or giving advice on performance. Anyone who plans on using Catalyst's simulation functionality extensively is recommended to also read the documentation on [solution plotting](@ref simulation_plotting), and on how to [interact with simulation problems, integrators, and solutions](@ref simulation_structure_interfacing). Anyone with an application for which performance is critical should consider reading the corresponding page on performance advice for [ODEs](@ref ode_simulation_performance) or [SDEs](@ref sde_simulation_performance).
### [Background to CRN simulations](@id simulation_intro_theory)
+
This section provides some brief theory on CRN simulations. For details on how to carry out these simulations in actual code, please skip to the following sections.
CRNs are defined by a set of *species* (with the amounts of these determining the system's state during simulations) and a set of *reaction events* (rules for how the state of the system changes). In real systems, the species amounts are *discrete copy-numbers*, describing the exact numbers of each species type present in the system (in systems biology this can e.g. be the number of a specific molecule present in a cell). Given rates for these reaction events, *stochastic chemical kinetics* provides a formula for simulating the system that recreates its real reaction process. During stochastic chemical kinetics simulations, the system's state is defined by discrete copy-numbers (denoting the number of each species present in the system). Next, at the occurrence of individual *reaction events*, the system's state is updated according to the occurred reaction. The result is a stochastic process. The most well-known approach for simulating stochastic chemical kinetics is [Gillespie's algorithm](https://en.wikipedia.org/wiki/Gillespie_algorithm).
@@ -13,6 +15,7 @@ In practice, these jump simulations are computationally expensive. In many cases
Here, the RRE enables fast, approximate, and deterministic simulations of CRNs, while stochastic chemical kinetics enables exact, stochastic, simulations of the true process. An intermediary approach is to use the [*chemical Langevin equation*](https://pubs.aip.org/aip/jcp/article/113/1/297/184125/The-chemical-Langevin-equation) (CLE) to formulate a stochastic differential equation (SDE). This approximates the system's state as continuous concentrations, but *does not* assume that its time development is deterministic. Generally, the CLE is used when copy-numbers are large enough that the continuous approximation holds, but not so large that the system's behaviour is deterministic. Generally, the advantage of SDE simulations (compared to jump ones) is that they are faster. Also, since the system state is continuous, interpretation of e.g. stability and steady state results from the deterministic (also continuous) domain is easier for SDEs (however one *should be careful* when making such interpretations).
These three different approaches are summed up in the following table:
+
```@raw html