From a9717b694c614d636574d03c52d5298484f0d1d1 Mon Sep 17 00:00:00 2001 From: Stefan Pinnow Date: Tue, 14 Oct 2025 20:32:58 +0200 Subject: [PATCH 01/26] docs: MD022/blanks-around-headings --- docs/old_files/advanced.md | 3 + .../unused_tutorials/advanced_examples.md | 4 +- docs/old_files/unused_tutorials/models.md | 4 ++ docs/src/api/core_api.md | 20 ++++++- docs/src/api/network_analysis_api.md | 1 + docs/src/devdocs/dev_guide.md | 8 ++- docs/src/faqs.md | 37 +++++++----- docs/src/index.md | 25 +++++--- .../catalyst_for_new_julia_users.md | 13 +++++ .../introduction_to_catalyst.md | 19 ++++++- .../math_models_intro.md | 11 +++- .../behaviour_optimisation.md | 7 +++ .../examples/ode_fitting_oscillation.md | 4 +- .../global_sensitivity_analysis.md | 13 ++++- .../optimization_ode_param_fitting.md | 41 ++++++++----- .../petab_ode_param_fitting.md | 38 +++++++++++-- .../structural_identifiability.md | 25 ++++++-- .../chemistry_related_functionality.md | 14 +++-- .../model_creation/compositional_modeling.md | 5 ++ docs/src/model_creation/conservation_laws.md | 9 ++- .../model_creation/constraint_equations.md | 9 ++- docs/src/model_creation/dsl_advanced.md | 45 ++++++++++----- docs/src/model_creation/dsl_basics.md | 20 +++++++ .../examples/basic_CRN_library.md | 13 ++++- .../examples/hodgkin_huxley_equation.md | 29 +++++----- .../examples/noise_modelling_approaches.md | 16 ++++-- .../programmatic_generative_linear_pathway.md | 12 +++- .../smoluchowski_coagulation_equation.md | 3 + .../model_creation/functional_parameters.md | 10 +++- .../model_file_loading_and_export.md | 9 +++ .../src/model_creation/model_visualisation.md | 18 +++--- .../parametric_stoichiometry.md | 3 + .../programmatic_CRN_construction.md | 4 ++ .../reactionsystem_content_accessing.md | 13 ++++- .../model_simulation/ensemble_simulations.md | 3 + ...ctivation_time_distribution_measurement.md | 5 +- .../examples/periodic_events_simulation.md | 4 ++ .../finite_state_projection_simulation.md | 14 +++-- .../ode_simulation_performance.md | 19 ++++++- .../sde_simulation_performance.md | 10 +++- .../simulation_introduction.md | 13 +++++ .../model_simulation/simulation_plotting.md | 5 ++ .../simulation_structure_interfacing.md | 10 +++- docs/src/network_analysis/crn_theory.md | 57 +++++++++++-------- .../network_analysis/network_properties.md | 1 + docs/src/network_analysis/odes.md | 41 +++++++------ .../lattice_reaction_systems.md | 29 +++++++--- .../lattice_simulation_plotting.md | 9 ++- ...ttice_simulation_structure_ interaction.md | 11 +++- .../spatial_jump_simulations.md | 3 +- .../spatial_ode_simulations.md | 11 ++-- .../bifurcation_diagrams.md | 18 ++++-- .../dynamical_systems.md | 16 ++++-- .../examples/bifurcationkit_codim2.md | 15 +++-- .../bifurcationkit_periodic_orbits.md | 7 ++- .../examples/nullcline_plotting.md | 5 +- .../homotopy_continuation.md | 10 +++- .../nonlinear_solve.md | 9 ++- .../steady_state_stability_computation.md | 11 ++-- docs/src/v14_migration_guide.md | 9 +++ docs/unpublished/pdes.md | 1 + 61 files changed, 645 insertions(+), 206 deletions(-) diff --git a/docs/old_files/advanced.md b/docs/old_files/advanced.md index 80388d12bb..e6f81f00d5 100644 --- a/docs/old_files/advanced.md +++ b/docs/old_files/advanced.md @@ -1,8 +1,10 @@ # 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 @@ -16,6 +18,7 @@ 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 diff --git a/docs/old_files/unused_tutorials/advanced_examples.md b/docs/old_files/unused_tutorials/advanced_examples.md index cce12dd1f7..9b1e77da70 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 @@ -20,10 +21,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..12b1362cb4 100644 --- a/docs/old_files/unused_tutorials/models.md +++ b/docs/old_files/unused_tutorials/models.md @@ -1,10 +1,12 @@ # 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 @@ -53,6 +55,7 @@ sol = solve(prob, DynamicSS(Tsit5())) ``` #### Stochastic simulations using SDEs + In a similar way an SDE can be created using ```julia using StochasticDiffEq @@ -62,6 +65,7 @@ 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: diff --git a/docs/src/api/core_api.md b/docs/src/api/core_api.md index 849c6ac805..ec4140b2e1 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 @@ -86,6 +88,7 @@ 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. @@ -102,6 +105,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 @@ -166,6 +170,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,6 +192,7 @@ 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 @@ -197,6 +203,7 @@ ModelingToolkit.diff_equations ``` ## Basic species properties + The following functions permits the querying of species properties. ```@docs isspecies @@ -206,6 +213,7 @@ Catalyst.isvalidreactant ``` ## Basic reaction properties + ```@docs ismassaction dependents @@ -217,6 +225,7 @@ 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 @@ -228,6 +237,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 +249,7 @@ Catalyst.flatten ``` ## Network comparison + ```@docs ==(rn1::Reaction, rn2::Reaction) isequivalent @@ -246,6 +257,7 @@ 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 @@ -266,13 +278,14 @@ 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. @@ -287,6 +300,7 @@ hillar ``` ## Transformations + ```@docs Base.convert JumpInputs @@ -295,6 +309,7 @@ set_default_noise_scaling ``` ## Chemistry-related functionalities + Various functionalities primarily relevant to modelling of chemical systems (but potentially also in biology). ```@docs @compound @@ -306,17 +321,20 @@ 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 diff --git a/docs/src/api/network_analysis_api.md b/docs/src/api/network_analysis_api.md index a4e3b21121..4894fa06f5 100644 --- a/docs/src/api/network_analysis_api.md +++ b/docs/src/api/network_analysis_api.md @@ -1,4 +1,5 @@ # [Network analysis and representations](@id api_network_analysis) + ```@meta CurrentModule = Catalyst ``` diff --git a/docs/src/devdocs/dev_guide.md b/docs/src/devdocs/dev_guide.md index e57937f8ba..b3d22b4fa7 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,7 +12,7 @@ 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 @@ -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: ![Failed builddocs link](../assets/devdocs/failed_builddocs_link.png) @@ -39,10 +41,11 @@ 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. @@ -52,4 +55,5 @@ To build the Catalyst documentation locally: 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..4027f6460d 100644 --- a/docs/src/faqs.md +++ b/docs/src/faqs.md @@ -1,6 +1,7 @@ # 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 @@ -59,6 +60,7 @@ 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 @@ -71,7 +73,7 @@ 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 @@ -82,6 +84,7 @@ nothing # hide ``` ## How to use non-integer stoichiometric coefficients? + ```@example faq2 using Catalyst rn = @reaction_network begin @@ -114,6 +117,7 @@ coefficients, please see the [Symbolic Stochiometries](@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: @@ -170,6 +174,7 @@ 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 @@ -195,6 +200,7 @@ 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 @@ -219,6 +225,7 @@ 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 @@ -239,6 +246,7 @@ 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 @@ -270,6 +278,7 @@ 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 @@ -284,6 +293,7 @@ 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 @@ -305,7 +315,7 @@ Inference of species, variables, and parameters follows the following steps: 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 @@ -326,7 +336,7 @@ Symbols occurring within other expressions will not be inferred as anything. The ```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 ``` @@ -335,7 +345,7 @@ is not permitted. E.g. here `Xadd` must be explicitly declared as a parameter us 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 +353,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 @@ -362,7 +373,7 @@ When constructing `NonlinearSystem`s or `NonlinearProblem`s with `remove_conserv # 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 @@ -398,9 +409,9 @@ 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 @@ -416,7 +427,7 @@ 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 @@ -425,7 +436,7 @@ 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 @@ -436,7 +447,7 @@ 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 @@ -463,7 +474,7 @@ We can figure out its index in `u0` via 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]]) ``` @@ -474,7 +485,7 @@ 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 @@ -487,4 +498,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..af89d3e3f9 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. @@ -84,6 +88,7 @@ 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 @@ -114,15 +119,15 @@ 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 +135,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,6 +162,7 @@ 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). @@ -170,13 +177,14 @@ 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 @@ -216,11 +224,13 @@ 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 @@ -242,6 +252,7 @@ could cite our work: ``` ## [Reproducibility](@id doc_index_reproducibility) + ```@raw html
The documentation of this SciML package was built using these direct dependencies, ``` 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..b903617725 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,6 +7,7 @@ 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. @@ -48,6 +50,7 @@ nothing # hide 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: @@ -71,6 +74,7 @@ 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. @@ -128,6 +132,7 @@ 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) @@ -183,9 +188,11 @@ 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: @@ -201,6 +208,7 @@ 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 @@ -216,6 +224,7 @@ 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. @@ -247,10 +256,14 @@ Some additional useful Pkg commands are: --- + ## 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..1dfbade273 100644 --- a/docs/src/introduction_to_catalyst/introduction_to_catalyst.md +++ b/docs/src/introduction_to_catalyst/introduction_to_catalyst.md @@ -1,4 +1,5 @@ # [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 @@ -83,8 +84,8 @@ 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 @@ -113,6 +114,7 @@ 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 @@ -169,7 +171,7 @@ 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) @@ -185,6 +187,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 @@ -228,7 +231,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,7 +281,9 @@ 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 @@ -310,7 +317,9 @@ 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 @@ -361,7 +370,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 +392,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) diff --git a/docs/src/introduction_to_catalyst/math_models_intro.md b/docs/src/introduction_to_catalyst/math_models_intro.md index 569d2cee72..f5f256c8d0 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,6 +15,7 @@ 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} @@ -37,6 +39,7 @@ a_k(\mathbf{X}(t)) = k \prod_{m=1}^M \frac{X_m(t) (X_m(t)-1) \dots (X_m(t)-\alph for stochastic chemical kinetics jump process models. ### 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)) @@ -84,6 +87,7 @@ 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. @@ -95,6 +99,7 @@ These models can be generated by creating `ODEProblem`s from Catalyst `ReactionS 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 @@ -111,6 +116,7 @@ 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, @@ -118,6 +124,7 @@ d X_m = \sum_{k=1}^K \nu_m^k a_k(\mathbf{X}(t),t) dt + \sum_{k=1}^K \nu_m^k \sqr 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 @@ -138,6 +145,7 @@ 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. @@ -151,6 +159,7 @@ Let $P(\mathbf{x},t) = \operatorname{Prob}[\mathbf{X}(t) = \mathbf{x}]$ represen 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 @@ -175,4 +184,4 @@ 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..7ff38ac9ec 100644 --- a/docs/src/inverse_problems/behaviour_optimisation.md +++ b/docs/src/inverse_problems/behaviour_optimisation.md @@ -1,7 +1,9 @@ # [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$. @@ -71,10 +73,13 @@ 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: ``` @software{vaibhav_kumar_dixit_2023_7738525, @@ -90,6 +95,8 @@ If you use this functionality in your research, please cite the following paper ``` --- + ## 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..4962cc8d01 100644 --- a/docs/src/inverse_problems/examples/ode_fitting_oscillation.md +++ b/docs/src/inverse_problems/examples/ode_fitting_oscillation.md @@ -1,4 +1,5 @@ # [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. @@ -49,7 +50,7 @@ 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]) @@ -116,6 +117,7 @@ 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 diff --git a/docs/src/inverse_problems/global_sensitivity_analysis.md b/docs/src/inverse_problems/global_sensitivity_analysis.md index 037ce9fd74..826921af01 100644 --- a/docs/src/inverse_problems/global_sensitivity_analysis.md +++ b/docs/src/inverse_problems/global_sensitivity_analysis.md @@ -1,4 +1,5 @@ # [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. @@ -6,11 +7,13 @@ 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 @@ -59,6 +62,7 @@ on the domain $10^β ∈ (-3.0,-1.0)$, $10^a ∈ (-2.0,0.0)$, $10^γ ∈ (-2.0,0 ## [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) @@ -89,6 +93,7 @@ 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)]) @@ -96,7 +101,7 @@ 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: @@ -114,9 +119,11 @@ Generally, Morris's method is computationally less intensive, and has easier to ## [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) @@ -141,7 +148,9 @@ 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: ``` @article{dixit2022globalsensitivity, @@ -156,5 +165,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..f7ee562589 100644 --- a/docs/src/inverse_problems/optimization_ode_param_fitting.md +++ b/docs/src/inverse_problems/optimization_ode_param_fitting.md @@ -1,7 +1,8 @@ # [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 +11,7 @@ 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 @@ -19,7 +20,7 @@ rn = @reaction_network begin 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 +43,7 @@ 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, _) @@ -76,14 +77,14 @@ nothing # hide 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) @@ -95,7 +96,7 @@ Catalyst.PNG(plot(plt; fmt = :png, dpi = 200)) # hide 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 @@ -103,6 +104,7 @@ 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: @@ -118,8 +120,9 @@ 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 +136,7 @@ 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) @@ -145,7 +148,7 @@ 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 +160,9 @@ 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 +170,9 @@ 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) @@ -177,21 +182,23 @@ function objective_function_known_kD(p, _) 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 @@ -207,7 +214,7 @@ 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,7 +225,9 @@ 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: ``` @software{vaibhav_kumar_dixit_2023_7738525, @@ -234,5 +243,7 @@ If you use this functionality in your research, please cite the following paper ``` --- + ## 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..683253a66c 100644 --- a/docs/src/inverse_problems/petab_ode_param_fitting.md +++ b/docs/src/inverse_problems/petab_ode_param_fitting.md @@ -1,9 +1,11 @@ # [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 @@ -43,6 +45,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,6 +58,7 @@ 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)). @@ -71,9 +75,11 @@ 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. @@ -94,6 +100,7 @@ Since, in our example, all measurements are of the same observable, we can set ` ### 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) @@ -101,6 +108,7 @@ 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) @@ -126,11 +134,13 @@ 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 @@ -144,6 +154,7 @@ 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 σ @@ -163,6 +174,7 @@ 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) @@ -177,6 +189,7 @@ 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) @@ -192,6 +205,7 @@ 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 @@ -205,6 +219,7 @@ par_kB = PEtabParameter(:kB; prior = Normal(1.0,0.2), prior_on_linear_scale = fa 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`): @@ -264,7 +279,7 @@ 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 ``` @@ -273,6 +288,7 @@ Note that the `u0` we pass into `PEtabModel` through the `speciemap` argument no ## [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] @@ -297,11 +313,13 @@ 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 @@ -331,11 +349,12 @@ 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 @@ -381,9 +400,9 @@ While in our basic example, we do not provide any additional information to our 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 ``` @@ -392,6 +411,7 @@ where we simulate our ODE model using the `Rodas5P` method (with absolute and re ## [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)) @@ -410,6 +430,7 @@ 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) @@ -424,6 +445,7 @@ We can find the: 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. @@ -461,6 +483,7 @@ 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 @@ -491,6 +514,7 @@ 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: @@ -517,7 +541,9 @@ 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: ``` @misc{2023Petabljl, @@ -544,6 +570,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..557d3625a5 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,14 @@ 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). @@ -30,7 +30,7 @@ Global identifiability can be assessed using the `assess_identifiability` functi - 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 @@ -50,6 +50,7 @@ assess_identifiability(gwo; measured_quantities = [:M, :P, :E], loglevel = Loggi 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) @@ -59,6 +60,7 @@ Not only does this turn the previously non-identifiable `pₑ` (globally) identi 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 @@ -74,6 +76,7 @@ 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 @@ -82,6 +85,7 @@ 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) @@ -90,6 +94,7 @@ 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) @@ -97,6 +102,7 @@ assess_local_identifiability(gwo; measured_quantities = [:M], loglevel = Logging 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) + 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) @@ -106,6 +112,7 @@ Again, these results are consistent with those produced by `assess_identifiabili `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]) @@ -118,6 +125,7 @@ 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 @@ -128,6 +136,7 @@ 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 @@ -135,10 +144,12 @@ rn_si_impossible = @reaction_network begin 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: ``` @article{structidjl, @@ -154,7 +165,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..1e33301fa0 100644 --- a/docs/src/model_creation/chemistry_related_functionality.md +++ b/docs/src/model_creation/chemistry_related_functionality.md @@ -8,6 +8,7 @@ While Catalyst has primarily been designed around the modelling of biological sy ## [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 @@ -56,6 +57,7 @@ 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 @@ -69,7 +71,7 @@ rn = @reaction_network begin 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 @@ -82,6 +84,7 @@ 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 @@ -100,16 +103,18 @@ 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 @@ -124,7 +129,7 @@ Let us consider a more elaborate example, the reaction between ammonia (NH₃) a ```@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 @@ -144,6 +149,7 @@ 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 @@ -160,4 +166,4 @@ 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..e5377f884f 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,6 +7,7 @@ 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`: @@ -35,6 +37,7 @@ 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` @@ -123,6 +126,7 @@ 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 @@ -166,6 +170,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 diff --git a/docs/src/model_creation/conservation_laws.md b/docs/src/model_creation/conservation_laws.md index 5bb77eb649..0117d5f3a5 100644 --- a/docs/src/model_creation/conservation_laws.md +++ b/docs/src/model_creation/conservation_laws.md @@ -1,4 +1,5 @@ # [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: @@ -91,6 +92,7 @@ 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)) @@ -103,7 +105,7 @@ 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 @@ -130,13 +132,14 @@ 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..f518f8c986 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,6 +18,7 @@ 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)$: @@ -52,6 +54,7 @@ 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 @@ -135,6 +138,7 @@ 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,7 +174,7 @@ 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 @@ -189,7 +193,7 @@ 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 @@ -221,6 +225,7 @@ flexible DifferentialEquations.jl event/callback interface, see the 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 diff --git a/docs/src/model_creation/dsl_advanced.md b/docs/src/model_creation/dsl_advanced.md index 3aeb468132..30ab82b3d0 100644 --- a/docs/src/model_creation/dsl_advanced.md +++ b/docs/src/model_creation/dsl_advanced.md @@ -1,7 +1,8 @@ # [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 @@ -9,6 +10,7 @@ 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 @@ -44,7 +46,7 @@ While designating something which would default to a parameter as a species is s 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) @@ -74,7 +76,7 @@ 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. @@ -89,6 +91,7 @@ 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 @@ -132,7 +135,8 @@ 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₀ @@ -160,7 +164,8 @@ 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 @@ -204,6 +209,7 @@ 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 @@ -226,6 +232,7 @@ 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 @@ -251,6 +258,7 @@ 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 @@ -272,7 +280,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 +290,13 @@ 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: ``` 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 @@ -307,6 +316,7 @@ 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 @@ -352,6 +362,7 @@ If you wish to check for identity, and wish that models that have different name 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`: @@ -376,7 +387,7 @@ 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 @@ -413,7 +424,7 @@ Observables are by default considered [variables](@ref constraint_equations) (no ```@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 @@ -466,6 +477,7 @@ species(rn) ## [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 @@ -502,9 +514,11 @@ 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 @@ -537,7 +551,8 @@ 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 @@ -569,13 +584,14 @@ rxs = [ @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 @@ -636,7 +652,7 @@ rn = @reaction_network begin 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 @@ -646,4 +662,3 @@ end latexify(rn; form = :ode) ``` 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..f37feee2c1 100644 --- a/docs/src/model_creation/dsl_basics.md +++ b/docs/src/model_creation/dsl_basics.md @@ -1,4 +1,5 @@ # [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. @@ -9,6 +10,7 @@ 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 @@ -19,6 +21,7 @@ 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 @@ -37,6 +40,7 @@ Each reaction line declares, in order, the rate, the substrate(s), and the produ 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 @@ -57,6 +61,7 @@ Generally, anything that is a [permitted Julia variable name](https://docs.julia ## [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 @@ -72,6 +77,7 @@ 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 @@ -81,6 +87,7 @@ 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 @@ -101,6 +108,7 @@ 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 @@ -131,6 +139,7 @@ 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 @@ -198,6 +207,7 @@ is not permitted (due to this notation's similarity to a bidirectional reaction) ## [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: @@ -241,6 +251,7 @@ 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 @@ -258,6 +269,7 @@ 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 @@ -274,6 +286,7 @@ 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 @@ -285,6 +298,7 @@ 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 @@ -311,6 +325,7 @@ 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 @@ -321,6 +336,7 @@ 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 @@ -336,9 +352,11 @@ 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 @@ -348,6 +366,7 @@ 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. @@ -362,6 +381,7 @@ 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ᴾ`). diff --git a/docs/src/model_creation/examples/basic_CRN_library.md b/docs/src/model_creation/examples/basic_CRN_library.md index 3cb5fe9c54..5893bd327d 100644 --- a/docs/src/model_creation/examples/basic_CRN_library.md +++ b/docs/src/model_creation/examples/basic_CRN_library.md @@ -1,4 +1,5 @@ # [Library of Basic Chemical Reaction Network Models](@id basic_CRN_library) + ```@raw html
Environment setup and package installation ``` @@ -16,10 +17,11 @@ Pkg.add("Plots")
``` \ - + 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 @@ -68,6 +70,7 @@ 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 @@ -99,6 +102,7 @@ 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 @@ -140,6 +144,7 @@ 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 @@ -182,6 +187,7 @@ 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 @@ -212,6 +218,7 @@ 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 @@ -240,6 +247,7 @@ 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 @@ -273,6 +281,7 @@ 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 @@ -302,6 +311,7 @@ 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 @@ -340,6 +350,7 @@ 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 diff --git a/docs/src/model_creation/examples/hodgkin_huxley_equation.md b/docs/src/model_creation/examples/hodgkin_huxley_equation.md index 5d433e80e2..697c1fa302 100644 --- a/docs/src/model_creation/examples/hodgkin_huxley_equation.md +++ b/docs/src/model_creation/examples/hodgkin_huxley_equation.md @@ -22,6 +22,7 @@ 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 +54,7 @@ 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 +67,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,7 +82,7 @@ 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 @@ -133,7 +134,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. @@ -143,22 +144,22 @@ We start by defining systems to model each ionic current. Note we now use 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 diff --git a/docs/src/model_creation/examples/noise_modelling_approaches.md b/docs/src/model_creation/examples/noise_modelling_approaches.md index 39f6d94088..323dde60eb 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,6 +8,7 @@ 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 @@ -19,7 +21,8 @@ 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 @@ -39,6 +42,7 @@ 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). @@ -64,9 +68,10 @@ 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) @@ -106,6 +111,7 @@ Like in the previous two cases, this generates heterogeneous trajectories across ## [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) @@ -127,6 +133,8 @@ This is a well-known phenomenon (especially in circadian biology[^2]). Here, as 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..36dda8c9da 100644 --- a/docs/src/model_creation/examples/programmatic_generative_linear_pathway.md +++ b/docs/src/model_creation/examples/programmatic_generative_linear_pathway.md @@ -1,13 +1,15 @@ # [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 @@ -24,11 +26,12 @@ g(\tau; \alpha, \beta) = \frac{\beta^{\alpha}\tau^{\alpha-1}}{\Gamma(\alpha)}e^{ 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 @@ -75,6 +78,7 @@ 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. @@ -94,7 +98,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). @@ -131,7 +135,9 @@ 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..6532ad71f4 100644 --- a/docs/src/model_creation/examples/smoluchowski_coagulation_equation.md +++ b/docs/src/model_creation/examples/smoluchowski_coagulation_equation.md @@ -1,4 +1,5 @@ # [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. @@ -145,7 +146,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. diff --git a/docs/src/model_creation/functional_parameters.md b/docs/src/model_creation/functional_parameters.md index 6f47193da0..8b79d17d3d 100644 --- a/docs/src/model_creation/functional_parameters.md +++ b/docs/src/model_creation/functional_parameters.md @@ -1,9 +1,11 @@ # [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 @@ -35,6 +37,7 @@ plot(sol) ## [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 @@ -75,6 +78,7 @@ 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 @@ -87,6 +91,7 @@ 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 @@ -130,9 +135,10 @@ 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 @@ -145,4 +151,4 @@ 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..de2d53a114 100644 --- a/docs/src/model_creation/model_file_loading_and_export.md +++ b/docs/src/model_creation/model_file_loading_and_export.md @@ -1,7 +1,9 @@ # [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 @@ -57,6 +59,7 @@ end 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 @@ -71,6 +74,7 @@ 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 @@ -97,6 +101,7 @@ Note that, as all initial conditions and parameters have default values, we can 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: @@ -122,9 +127,11 @@ 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). @@ -135,7 +142,9 @@ The advantage of these forms is that they offer a compact and very general way t --- + ## [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: ``` @software{2022ReactionNetworkImporters, diff --git a/docs/src/model_creation/model_visualisation.md b/docs/src/model_creation/model_visualisation.md index bd30a40f3c..1dad8b6f91 100644 --- a/docs/src/model_creation/model_visualisation.md +++ b/docs/src/model_creation/model_visualisation.md @@ -1,7 +1,9 @@ # [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): @@ -27,15 +29,16 @@ 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 +47,7 @@ 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 @@ -66,7 +69,7 @@ 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) @@ -82,6 +85,7 @@ 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 @@ -95,7 +99,7 @@ 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" @@ -128,13 +132,13 @@ 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..0bbbaa8ef1 100644 --- a/docs/src/model_creation/parametric_stoichiometry.md +++ b/docs/src/model_creation/parametric_stoichiometry.md @@ -1,9 +1,11 @@ # [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 @@ -109,6 +111,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. diff --git a/docs/src/model_creation/programmatic_CRN_construction.md b/docs/src/model_creation/programmatic_CRN_construction.md index 6e621ed82b..e1a97aa643 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,6 +8,7 @@ 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 @@ -92,6 +94,7 @@ 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 @@ -130,6 +133,7 @@ rx = Reaction(α + β*t*A, [A], [B]) 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 diff --git a/docs/src/model_creation/reactionsystem_content_accessing.md b/docs/src/model_creation/reactionsystem_content_accessing.md index fd0bb3bec3..944595975c 100644 --- a/docs/src/model_creation/reactionsystem_content_accessing.md +++ b/docs/src/model_creation/reactionsystem_content_accessing.md @@ -1,10 +1,12 @@ # [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) @@ -14,7 +16,7 @@ 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 ``` @@ -61,6 +63,7 @@ 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 @@ -87,6 +90,7 @@ 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) @@ -101,6 +105,7 @@ 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 @@ -134,7 +139,8 @@ 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. @@ -144,6 +150,7 @@ 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. @@ -172,6 +179,7 @@ nothing # hide 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 @@ -216,6 +224,7 @@ 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) diff --git a/docs/src/model_simulation/ensemble_simulations.md b/docs/src/model_simulation/ensemble_simulations.md index c53c0c703c..f08da161cb 100644 --- a/docs/src/model_simulation/ensemble_simulations.md +++ b/docs/src/model_simulation/ensemble_simulations.md @@ -1,4 +1,5 @@ # [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. @@ -6,6 +7,7 @@ In many contexts, a single model is re-simulated under similar conditions. Examp 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 @@ -46,6 +48,7 @@ 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: 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..f047974fdf 100644 --- a/docs/src/model_simulation/examples/activation_time_distribution_measurement.md +++ b/docs/src/model_simulation/examples/activation_time_distribution_measurement.md @@ -1,4 +1,5 @@ # [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ᵢ$. @@ -17,7 +18,7 @@ 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 @@ -52,6 +53,8 @@ histogram(esol.u; normalize = true, label = "Activation time distribution", xlab 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/periodic_events_simulation.md b/docs/src/model_simulation/examples/periodic_events_simulation.md index bdfafcbd31..13589ec759 100644 --- a/docs/src/model_simulation/examples/periodic_events_simulation.md +++ b/docs/src/model_simulation/examples/periodic_events_simulation.md @@ -1,7 +1,9 @@ # [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 @@ -24,6 +26,7 @@ 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 @@ -82,6 +85,7 @@ 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 diff --git a/docs/src/model_simulation/finite_state_projection_simulation.md b/docs/src/model_simulation/finite_state_projection_simulation.md index db871e5b5b..6a0f388112 100644 --- a/docs/src/model_simulation/finite_state_projection_simulation.md +++ b/docs/src/model_simulation/finite_state_projection_simulation.md @@ -1,4 +1,5 @@ # [Solving the chemical master equation using FiniteStateProjection.jl](@id finite-state_projection) + ```@raw html
Environment setup and package installation ``` @@ -52,7 +53,7 @@ heatmap(0:19, 0:19, osol(50.0); xguide = "Y", yguide = "X")
``` \ - + 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. @@ -70,6 +71,7 @@ One can study the dynamics of stochastic chemical kinetics models by simulating 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 @@ -89,7 +91,7 @@ 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 @@ -103,7 +105,7 @@ 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. @@ -125,6 +127,7 @@ 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₂$. @@ -161,6 +164,7 @@ 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 @@ -185,6 +189,8 @@ Here we used an ensemble [output function](@ref activation_time_distribution_mea --- + ## 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..a40be11d8a 100644 --- a/docs/src/model_simulation/ode_simulation_performance.md +++ b/docs/src/model_simulation/ode_simulation_performance.md @@ -1,4 +1,5 @@ # [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]. @@ -11,6 +12,7 @@ Generally, this short checklist provides a quick guide for dealing with ODE perf 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): @@ -55,6 +57,7 @@ Finally, we should note that stiffness is not tied to the model equations only. ## [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 @@ -81,7 +84,7 @@ While the default choice is typically enough for most single simulations, if per 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()) @@ -91,6 +94,7 @@ using OrdinaryDiffEqTsit5, OrdinaryDiffEqRosenbrock, OrdinaryDiffEqVerner, Ordin 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) @@ -103,9 +107,11 @@ Generally, whether solution error is a consideration depends on the application. ## [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`: @@ -127,6 +133,7 @@ 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) @@ -134,6 +141,7 @@ 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 @@ -150,6 +158,7 @@ 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): @@ -176,6 +185,7 @@ Generally, the use of preconditioners is only recommended for advanced users who ## [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 @@ -196,11 +206,13 @@ Conservation law elimination is not expected to ever impact performance negative ## [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 @@ -281,6 +293,7 @@ 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: @@ -346,7 +359,9 @@ For more information on differential equation simulations on GPUs in Julia, plea --- + ## Citation + If you use GPU simulations in your research, please cite the following paper to support the authors of the DiffEqGPU package: ``` @article{utkarsh2024automated, @@ -361,5 +376,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..3705b99b80 100644 --- a/docs/src/model_simulation/sde_simulation_performance.md +++ b/docs/src/model_simulation/sde_simulation_performance.md @@ -1,20 +1,25 @@ # [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: @@ -55,4 +60,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..3bec00ddc3 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). @@ -79,6 +81,7 @@ These three different approaches are summed up in the following table: ``` ## [Performing (ODE) simulations](@id simulation_intro_ODEs) + The following section gives a (more complete introduction of how to simulate Catalyst models than our [previous introduction](@ref introduction_to_catalyst_massaction_ode)). This is illustrated using ODE simulations (some ODE-specific options will also be discussed). Later on, we will describe things specific to [SDE](@ref simulation_intro_SDEs) and [jump](@ref simulation_intro_jumps) simulations. All ODE simulations are performed using the [OrdinaryDiffEq.jl](https://github.com/SciML/OrdinaryDiffEq.jl) package, which full documentation can be found [here](https://docs.sciml.ai/OrdinaryDiffEq/stable/). A dedicated section giving advice on how to optimise ODE simulation performance can be found [here](@ref ode_simulation_performance) To perform any simulation, we must first define our model, as well as the simulation's initial conditions, time span, and parameter values. Here we will use a simple [two-state model](@ref basic_CRN_library_two_states): @@ -116,6 +119,7 @@ Some additional considerations: ### [Designating solvers and solver options](@id simulation_intro_solver_options) + While good defaults are generally selected, OrdinaryDiffEq enables the user to customise simulations through a long range of options that can be provided to the `solve` function. This includes specifying a [solver algorithm](https://en.wikipedia.org/wiki/Numerical_methods_for_ordinary_differential_equations), which can be provided as a second argument to `solve` (if none is provided, a suitable choice is automatically made). E.g. here we specify that the `Rodas5P` method should be used: ```@example simulation_intro_ode using OrdinaryDiffEqRosenbrock @@ -144,6 +148,7 @@ Here follows a list of solver options which might be of interest to the user. A full list of solver options can be found [here](https://docs.sciml.ai/DiffEqDocs/stable/basics/common_solver_opts/). ### [Alternative problem input forms](@id simulation_intro_ODEs_input_forms) + Throughout Catalyst's documentation, we typically provide initial condition and parameter values as vectors. However, these can also be provided as tuples: ```@example simulation_intro_ode u0 = (:X1 => 100.0, :X2 => 200.0) @@ -173,6 +178,7 @@ nothing # hide ``` ## [Performing SDE simulations](@id simulation_intro_SDEs) + Catalyst uses the [StochasticDiffEq.jl](https://github.com/SciML/StochasticDiffEq.jl) package to perform SDE simulations. This section provides a brief introduction, with [StochasticDiffEq's documentation](https://docs.sciml.ai/StochasticDiffEq/stable/) providing a more extensive description. By default, Catalyst generates SDEs from CRN models using the chemical Langevin equation. A dedicated section giving advice on how to optimise SDE simulation performance can be found [here](@ref sde_simulation_performance). SDE simulations are performed in a similar manner to ODE simulations. The only exception is that an `SDEProblem` is created (rather than an `ODEProblem`). Furthermore, the [StochasticDiffEq.jl](https://github.com/SciML/StochasticDiffEq.jl) package (rather than the OrdinaryDiffEq package) is required for performing simulations. Here we simulate the two-state model for the same parameter set as previously used: @@ -196,6 +202,7 @@ we can see that while this simulation (unlike the ODE ones) exhibits some fluctu Unlike for ODE and jump simulations, there are no good heuristics for automatically selecting suitable SDE solvers. Hence, for SDE simulations a solver must be provided. `STrapezoid` will work for a large number of cases. When this is not the case, however, please check the list of [available SDE solvers](https://docs.sciml.ai/DiffEqDocs/stable/solvers/sde_solve/) for a suitable alternative (making sure to select one compatible with non-diagonal noise and the [Ito interpretation](https://en.wikipedia.org/wiki/It%C3%B4_calculus). ### [Common SDE simulation pitfalls](@id simulation_intro_SDEs_pitfalls) + Next, let us reduce species amounts (using [`remake`](@ref simulation_structure_interfacing_problems_remake)), thereby also increasing the relative amount of noise, we encounter a problem when the model is simulated: ```@example simulation_intro_sde sprob = remake(sprob; u0 = [:X1 => 0.33, :X2 => 0.66]) @@ -227,6 +234,7 @@ plot(sol) ``` ### [SDE simulations with fixed time stepping](@id simulation_intro_SDEs_fixed_dt) + StochasticDiffEq implements SDE solvers with adaptive time stepping. However, when using a non-adaptive solver (or using the `adaptive = false` argument to turn adaptive time stepping off for an adaptive solver) a fixed time step `dt` must be designated. Here we simulate the same `SDEProblem` which we struggled with previously, but using the non-adaptive [`EM`](https://en.wikipedia.org/wiki/Euler%E2%80%93Maruyama_method) solver and a fixed `dt`: ```@example simulation_intro_sde sol = solve(sprob, EM(); dt = 0.001) @@ -238,6 +246,7 @@ We note that this approach also enables us to successfully simulate the SDE we p Generally, using a smaller fixed `dt` provides a more exact simulation, but also increases simulation runtime. ### [Scaling the noise in the chemical Langevin equation](@id simulation_intro_SDEs_noise_saling) + When using the CLE to generate SDEs from a CRN, it can sometimes be desirable to scale the magnitude of the noise. This can be done by introducing a *noise scaling term*, with each noise term generated by the CLE being multiplied with this term. A noise scaling term can be set using the `@default_noise_scaling` option: ```@example simulation_intro_sde two_state_model = @reaction_network begin @@ -332,6 +341,7 @@ plot(sol) ``` ### [Designating aggregators and simulation methods for jump simulations](@id simulation_intro_jumps_solver_designation) + Jump simulations (just like ODEs and SDEs) are performed using stochastic simulation algorithms (SSAs) to generate exact samples of the underlying jump process. In JumpProcesses.jl and Catalyst, we call SSAs *aggregators*. These methods determine the time until, and type of, the next reaction in a system. A separate time-stepping method is then used to actually step from one reaction instance to the next. Several different aggregators are available (a full list is provided [here](https://docs.sciml.ai/JumpProcesses/stable/jump_types/#Jump-Aggregators-for-Exact-Simulation)). To designate a specific one, provide it as the second argument to the `JumpProblem`. E.g. to designate that the sorting direct method (`SortingDirect`) should be used, use: @@ -343,6 +353,7 @@ Especially for large systems, the choice of aggregator can dramatically impact simulation performance. ### [Jump simulations where some rate depends on time](@id simulation_intro_jumps_variableratejumps) + For some models, the rate terms of reactions may explicitly depend on time. E.g. consider the following [circadian clock (inspired) model](https://en.wikipedia.org/wiki/Circadian_rhythm), where the production rate of some protein ($P$) depends on a sinusoid function: ```@example simulation_intro_jumps circadian_model = @reaction_network begin @@ -363,7 +374,9 @@ plot(sol; idxs = :P, lw = 2) ``` --- + ## [Citation](@id simulation_intro_citation) + When you simulate Catalyst models in your research, please cite the corresponding paper(s) to support the simulation package authors. For ODE simulations: ``` @article{DifferentialEquations.jl-2017, diff --git a/docs/src/model_simulation/simulation_plotting.md b/docs/src/model_simulation/simulation_plotting.md index 89fe89a484..df5882392f 100644 --- a/docs/src/model_simulation/simulation_plotting.md +++ b/docs/src/model_simulation/simulation_plotting.md @@ -1,10 +1,12 @@ # [Simulation Plotting](@id simulation_plotting) + Catalyst uses the [Plots.jl](https://github.com/JuliaPlots/Plots.jl) package for performing all plots. This section provides a brief summary of some useful plotting options, while [Plots.jl's documentation](https://docs.juliaplots.org/stable/) provides a more throughout description of how to tune your plots. !!! note [Makie.jl](https://github.com/MakieOrg/Makie.jl) is a popular alternative to the Plots.jl package. While it is not used within Catalyst's documentation, it is worth considering (especially for users interested in interactivity, or increased control over their plots). ## [Common plotting options](@id simulation_plotting_options) + Let us consider the oscillating [Brusselator](@ref basic_CRN_library_brusselator) model. We have previously shown how model simulation solutions can be plotted using the `plot` function. Here we plot an ODE simulation from the Brusselator: ```@example simulation_plotting using Catalyst, OrdinaryDiffEqDefault, Plots @@ -59,6 +61,7 @@ plot(sol; idxs = brusselator.X + brusselator.Y) ``` ## [Multi-plot plots](@id simulation_plotting_options_subplots) + It is possible to save plots in variables. These can then be used as input to the `plot` command. Here, the plot command can be used to create plots containing multiple plots (by providing multiple inputs). E.g. here we plot the concentration of `X` and `Y` in separate subplots: ```@example simulation_plotting plt_X = plot(sol; idxs = [:X]) @@ -72,6 +75,7 @@ plot(plt_X, plt_Y; layout = (2,1), size = (700,500)) ``` ## [Saving plots](@id simulation_plotting_options_saving) + Once a plot has been saved to a variable, the `savefig` function can be used to save it to a file. Here we save our Brusselator plot simulation (the first argument) to a file called "saved_plot.png" (the second argument): ```@example simulation_plotting plt = plot(sol) @@ -81,6 +85,7 @@ rm("saved_plot.png") # hide The plot file type is automatically determined from the extension (if none is given, a .png file is created). ## [Phase-space plots](@id simulation_plotting_options_phasespace) + By default, simulations are plotted as species concentrations over time. However, [phase space](https://en.wikipedia.org/wiki/Phase_space#:~:text=In%20dynamical%20systems%20theory%20and,point%20in%20the%20phase%20space.) plots are also possible. This is done by designating the axis arguments using the `idxs` option, but providing them as a tuple. E.g. here we plot our simulation in $X-Y$ space: ```@example simulation_plotting plot(sol; idxs = (:X, :Y)) diff --git a/docs/src/model_simulation/simulation_structure_interfacing.md b/docs/src/model_simulation/simulation_structure_interfacing.md index dd8f2b034c..a5d64abdac 100644 --- a/docs/src/model_simulation/simulation_structure_interfacing.md +++ b/docs/src/model_simulation/simulation_structure_interfacing.md @@ -1,4 +1,5 @@ # [Interfacing Problems, Integrators, and Solutions](@id simulation_structure_interfacing) + When simulating a model, one begins with creating a [problem](https://docs.sciml.ai/DiffEqDocs/stable/basics/problem/). Next, a simulation is performed on the problem, during which the simulation's state is recorded through an [integrator](https://docs.sciml.ai/DiffEqDocs/stable/basics/integrator/). Finally, the simulation output is returned as a [solution](https://docs.sciml.ai/DiffEqDocs/stable/basics/solution/). This tutorial describes how to access (or modify) the state (or parameter) values of problem, integrator, and solution structures. Generally, when we have a structure `simulation_struct` and want to interface with the unknown (or parameter) `x`, we use `simulation_struct[:x]` to access the value. For situations where a value is accessed (or changed) a large number of times, it can *improve performance* to first create a [specialised getter/setter function](@ref simulation_structure_interfacing_functions). @@ -16,7 +17,7 @@ end u0 = [:S₁ => 1.0, :C => 0.05, :S₂ => 1.2, :S₁C => 0.0, :CP => 0.0, :P => 0.0] tspan = (0., 10.0) -ps = [:k₁ => 5.0, :k₂ => 5.0, :k₃ => 100.0] +ps = [:k₁ => 5.0, :k₂ => 5.0, :k₃ => 100.0] oprob = ODEProblem(cc_system, u0, tspan, ps) nothing # hide ``` @@ -44,6 +45,7 @@ oprob.tspan Here we have used an `ODEProblem`to demonstrate all interfacing functionality. However, identical workflows work for the other problem types. ### [Remaking problems using the `remake` function](@id simulation_structure_interfacing_problems_remake) + To modify a problem, the `remake` function should be used. It takes an already created problem, and returns a new, updated, one (the input problem is unchanged). The `remake` function takes the following inputs: - The problem that it remakes. - (optionally) `u0`: A vector with initial conditions that should be updated. The vector takes the same form as normal initial condition vectors, but does not need to be complete (in which case only a subset of the initial conditions are updated). @@ -104,7 +106,7 @@ Parameter values can also be accessed (however, here we only get a single value) ```@example structure_indexing sol.ps[:k₃] ``` -Unlike for problems and integrators, species or parameter values of solutions cannot be changed. +Unlike for problems and integrators, species or parameter values of solutions cannot be changed. A vector with the time values for all simulation time steps can be retrieved using ```@example structure_indexing @@ -118,9 +120,10 @@ sol(1.0) This works whenever the simulations actually stopped at time $t = 1.0$ (if not, an interpolated value is returned). To get the simulation's values for a specific subset of species, we can use the `idxs` optional argument. I.e. here we get the value of $C$ at time $t = 1.0$ ```@example structure_indexing sol(1.0; idxs = [:C]) -``` +``` ## [Interfacing using specialised getter/setter functions](@id simulation_structure_interfacing_functions) + Internally, species and parameter values are stored in vectors. Whenever e.g. `oprob[:C]` is called, Julia must first find which index in the storage vector $C$ is stored in. Next, its value can be retrieved. If `oprob[:C]` is called a large number of times, this index must be found in each call. If a large number of such accesses are carried out, and performance is essential, it can be worthwhile to pre-compute a function to carry this out. There exist four different functions, each returning a function for performing a specific type of interfacing: @@ -148,6 +151,7 @@ get_S(oprob) ``` ## [Interfacing using symbolic representations](@id simulation_structure_interfacing_symbolic_representation) + When e.g. [programmatic modelling is used](@ref programmatic_CRN_construction), species and parameters can be represented as *symbolic variables*. These can be used to index a problem, just like symbol-based representations can. Here we create a simple [two-state model](@ref basic_CRN_library_two_states) programmatically, and use its symbolic variables to check, and update, an initial condition: ```@example structure_indexing_symbolic_variables using Catalyst, OrdinaryDiffEqDefault diff --git a/docs/src/network_analysis/crn_theory.md b/docs/src/network_analysis/crn_theory.md index 9e6262cac6..d9c117582c 100644 --- a/docs/src/network_analysis/crn_theory.md +++ b/docs/src/network_analysis/crn_theory.md @@ -5,7 +5,7 @@ reaction networks can often have their steady-state properties (number of steady simply by analyzing the graph structure of the network. The subfield of chemistry and math studying this relationship is called [Chemical Reaction Network Theory](https://en.wikipedia.org/wiki/Chemical_reaction_network_theory). -In this tutorial we give a broad overview of chemical reaction network theory, building on the discussion of the structure of +In this tutorial we give a broad overview of chemical reaction network theory, building on the discussion of the structure of the reaction network ODEs in the previous section. We also introduce several of the Catalyst API functions for network analysis. A complete summary of the exported functions is given in the API section @@ -15,7 +15,7 @@ Broadly, results from chemical reaction network theory relate a purely graph-structural property (e.g. [deficiency](@ref network_analysis_structural_aspects_deficiency)) to dynamical properties of the reaction system (e.g. [complex balance](@ref network_analysis_complex_and_detailed_balance)). The relevant graph here is the one corresponding to the [reaction complex representation](@ref network_analysis_reaction_complexes) of the network, where the nodes represent the reaction complexes and the edges represent reactions. -Let us illustrate some of the types of network properties that +Let us illustrate some of the types of network properties that Catalyst can determine. To begin, consider the following reaction network: @@ -37,6 +37,7 @@ plot_complexes(rn) ``` ## [Linkage classes and sub-networks of the reaction network](@id network_analysis_structural_aspects_linkage) + The preceding reaction complex graph shows that `rn` is composed of two disconnected sub-graphs, one containing the complexes ``A+B``, ``C``, ``D+E``, and ``F``, the other containing the complexes ``2A``, ``B + G``, and ``H``. These sets, @@ -72,6 +73,7 @@ and, ``` ## [Deficiency of the network](@id network_analysis_structural_aspects_deficiency) + A famous theorem in Chemical Reaction Network Theory, the Deficiency Zero Theorem[^1], allows us to use knowledge of the net stoichiometry matrix and the linkage classes of a *mass action* [RRE ODE system](@ref network_analysis_matrix_vector_representation) to draw conclusions about the @@ -127,6 +129,7 @@ Quoting Feinberg [^1] ## [Reversibility of the network](@id network_analysis_structural_aspects_reversibility) + A reaction network is *reversible* if the "arrows" of the reactions are symmetric so that every reaction is accompanied by its reverse reaction. Catalyst's API provides the [`isreversible`](@ref) function to determine whether @@ -174,6 +177,7 @@ Every reversible network is also weakly reversible, but not every weakly reversible network is reversible. ## [Deficiency Zero Theorem](@id network_analysis_structural_aspects_deficiency_zero_theorem) + Knowing the deficiency and weak reversibility of a mass action chemical reaction network ODE model allows us to make inferences about the corresponding steady state behavior. Before illustrating how this works for one example, we @@ -271,19 +275,20 @@ satisfiesdeficiencyzero(def0_rn) ``` ## Deficiency One Theorem -Very analogous to the deficiency zero theorem is the deficiency one theorem. The deficiency one theorem applies to a network with the following properties: -1. The deficiency of each *linkage class* of the network is at most 1, + +Very analogous to the deficiency zero theorem is the deficiency one theorem. The deficiency one theorem applies to a network with the following properties: +1. The deficiency of each *linkage class* of the network is at most 1, 2. The sum of the linkage class deficiencies is the total deficiency of the network, and 3. Each linkage class has at most one terminal linkage class, which is a linkage class that is 1) strongly connected, and 2) has no outgoing reactions. For the set of reactions $A \to B, B \to A, B \to C$, $\{A, B, C\}$ is a linkage class, and $\{A, B\}$ is a strong linkage class (since A is reachable from B and vice versa). However, $\{A, B\}$ is not a terminal linkage class, because the reaction $B \to C$ goes -to a complex outside the linkage class. +to a complex outside the linkage class. -If these conditions are met, then the network will have at most one steady state in each -stoichiometric compatibility class for any choice of rate constants and parameters. Unlike -the deficiency zero theorem, networks obeying the deficiency one theorem are not guaranteed +If these conditions are met, then the network will have at most one steady state in each +stoichiometric compatibility class for any choice of rate constants and parameters. Unlike +the deficiency zero theorem, networks obeying the deficiency one theorem are not guaranteed to have stable solutions. Let's look at an example network. @@ -314,20 +319,21 @@ satisfiesdeficiencyone(def1_network) ``` ## [Complex and Detailed Balance](@id network_analysis_complex_and_detailed_balance) -A reaction network's steady state is **complex-balanced** if the total production of each -*complex* is zero at the steady state. A reaction network's steady state is **detailed balanced** -if every reaction is balanced by its reverse reaction at the steady-state (this corresponds to -the usual notion of chemical equilibrium; note that this requires every reaction be reversible). + +A reaction network's steady state is **complex-balanced** if the total production of each +*complex* is zero at the steady state. A reaction network's steady state is **detailed balanced** +if every reaction is balanced by its reverse reaction at the steady-state (this corresponds to +the usual notion of chemical equilibrium; note that this requires every reaction be reversible). Note that detailed balance at a given steady state implies complex balance for that steady state, i.e. detailed balance is a stronger property than complex balance. -Remarkably, having just one positive steady state that is complex (detailed) balance implies that -complex (detailed) balance holds for *every* positive steady state, so we say that a network -is complex (detailed) balanced if any one of its steady states are complex (detailed) balanced. -Additionally, there will be exactly one steady state in every positive stoichiometric +Remarkably, having just one positive steady state that is complex (detailed) balance implies that +complex (detailed) balance holds for *every* positive steady state, so we say that a network +is complex (detailed) balanced if any one of its steady states are complex (detailed) balanced. +Additionally, there will be exactly one steady state in every positive stoichiometric compatibility class, and this steady state is asymptotically stable. (For proofs of these results, -please consult Martin Feinberg's *Foundations of Chemical Reaction Network Theory*[^1]). So +please consult Martin Feinberg's *Foundations of Chemical Reaction Network Theory*[^1]). So knowing that a network is complex balanced is really quite powerful. Let's check whether the deficiency 0 reaction network that we defined above is complex balanced by providing a set of rates: @@ -341,11 +347,11 @@ We can do a similar check for detailed balance. isdetailedbalanced(rn, rates) ``` -The reason that the deficiency zero theorem puts such strong restrictions on the steady state -properties of the reaction network is because it implies that the reaction network will be -complex balanced for any set of rate constants and parameters. The fact that this holds -from a purely structural property of the graph, regardless of kinetics, is what makes it so -useful. But in some cases it might be desirable to check complex balance on its own, as for +The reason that the deficiency zero theorem puts such strong restrictions on the steady state +properties of the reaction network is because it implies that the reaction network will be +complex balanced for any set of rate constants and parameters. The fact that this holds +from a purely structural property of the graph, regardless of kinetics, is what makes it so +useful. But in some cases it might be desirable to check complex balance on its own, as for higher deficiency networks. ```@docs; canonical=false @@ -354,16 +360,17 @@ isdetailedbalanced ``` ## [Concentration Robustness](@id network_analysis_concentration_robustness) + Certain reaction networks have species that do not change their concentration, regardless of whether the system is perturbed to a different stoichiometric compatibility class. This is a very useful property to have in biological contexts, where it might be important to keep the concentration of a critical species relatively stable in -the face of changes in its environment. +the face of changes in its environment. Determining every species with concentration-robustness in a network is in general very difficult. However, there are certain cases where there are sufficient conditions that can be checked relatively easily. One example is for deficiency one networks. -**Theorem (a sufficient condition for concentration robustness for deficiency one networks)**: If there are two *non-terminal* reaction complexes that differ only in species ``s``, then the system is absolutely concentration robust with respect to ``s``. +**Theorem (a sufficient condition for concentration robustness for deficiency one networks)**: If there are two *non-terminal* reaction complexes that differ only in species ``s``, then the system is absolutely concentration robust with respect to ``s``. This is the check provided by the API function `robustspecies(rn)`. More general concentration robustness analysis can be done using the forthcoming CatalystNetworkAnalysis package. @@ -372,5 +379,7 @@ robustspecies ``` --- + ## References + [^1]: [Feinberg, M. *Foundations of Chemical Reaction Network Theory*, Applied Mathematical Sciences 202, Springer (2019).](https://link.springer.com/book/10.1007/978-3-030-03858-8?noAccess=true) diff --git a/docs/src/network_analysis/network_properties.md b/docs/src/network_analysis/network_properties.md index b3c9cc8f2f..694749628d 100644 --- a/docs/src/network_analysis/network_properties.md +++ b/docs/src/network_analysis/network_properties.md @@ -1,4 +1,5 @@ # [Caching of Network Properties in `ReactionSystems`](@id network_analysis_caching_properties) + When calling many of the network API functions, Catalyst calculates and caches in `rn` a variety of information. For example the first call to ```julia diff --git a/docs/src/network_analysis/odes.md b/docs/src/network_analysis/odes.md index adf8fa7e4d..1e81d72812 100644 --- a/docs/src/network_analysis/odes.md +++ b/docs/src/network_analysis/odes.md @@ -1,9 +1,9 @@ # [Decomposing the Reaction Network ODEs](@id network_analysis_odes) -In this tutorial we will discuss the specific mathematical +In this tutorial we will discuss the specific mathematical structure of the [ODEs that arise from the mass-action dynamics](@ref math_models_in_catalyst_rre_odes) of chemical reaction networks, and decompose them as a product -of matrices that describe the network. A complete summary of +of matrices that describe the network. A complete summary of the exported functions is given in the API section [Network Analysis and Representations](@ref api_network_analysis). Please consult Feinberg's *Foundations of Chemical Reaction Network Theory*[^1] for more discussion about the concepts on this page. @@ -12,6 +12,7 @@ do not work with constant species (which are generated by SBML, and can be [decl in Catalyst as well](@ref dsl_advanced_options_constant_species). ## [Network representation of the Repressilator `ReactionSystem`](@id network_analysis_repressilator_representation) + We first load Catalyst and construct our model of the repressilator ```@example s1 using Catalyst, CairoMakie, GraphMakie, NetworkLayout @@ -53,6 +54,7 @@ the reaction rate equation (RRE) ODE model for the repressilator is ``` ## [Matrix-vector reaction rate equation representation](@id network_analysis_matrix_vector_representation) + In general, reaction rate equation (RRE) ODE models for chemical reaction networks can be represented as a first-order system of ODEs in a compact matrix-vector notation. Suppose we have a reaction network with ``K`` reactions and ``M`` species, labelled by the state vector @@ -105,6 +107,7 @@ isequal(odes, odes2) ``` ## [Reaction complex representation](@id network_analysis_reaction_complexes) + We now introduce a further decomposition of the RRE ODEs, which has been used to facilitate analysis of a variety of reaction network properties. Consider a simple reaction system like @@ -207,7 +210,8 @@ parameter, and red arrows indicate the conversion of substrate complexes into product complexes where the rate is an expression involving chemical species. # Full decomposition of the reaction network ODEs (flux matrix and mass-action vector) -So far we have covered two equivalent descriptions of the chemical reaction network ODEs: + +So far we have covered two equivalent descriptions of the chemical reaction network ODEs: ```math \begin{align} \frac{d\mathbf{x}}{dt} &= N \mathbf{v}(\mathbf{x}(t),t) \\ @@ -215,11 +219,11 @@ So far we have covered two equivalent descriptions of the chemical reaction netw \end{align} ``` -In this section we discuss a further decomposition of the ODEs. Recall the reaction rate vector $\mathbf{v}$, which is a vector of length $R$ whose elements are the rate expressions for each reaction. Its elements can be written as +In this section we discuss a further decomposition of the ODEs. Recall the reaction rate vector $\mathbf{v}$, which is a vector of length $R$ whose elements are the rate expressions for each reaction. Its elements can be written as ```math \mathbf{v}_{y \rightarrow y'} = k_{y \rightarrow y'} \mathbf{x}^y, ``` -where $\mathbf{x}^y = \prod_s x_s^{y_s}$ denotes the mass-action product of the substrate complex $y$ from the $y \rightarrow y'$ reaction. We can define a new vector called the mass action vector $\Phi(\mathbf{x}(t))$, a vector of length $C$ whose elements are the mass action products of each complex: +where $\mathbf{x}^y = \prod_s x_s^{y_s}$ denotes the mass-action product of the substrate complex $y$ from the $y \rightarrow y'$ reaction. We can define a new vector called the mass action vector $\Phi(\mathbf{x}(t))$, a vector of length $C$ whose elements are the mass action products of each complex: ```@example s1 Φ = massactionvector(rn) ``` @@ -230,29 +234,29 @@ An important thing to note is this function assumes [combinatoric ratelaws](@ref Φ_2 = massactionvector(rn; combinatoric_ratelaws = false) ``` -Then the reaction rate vector $\mathbf{v}$ can be written as +Then the reaction rate vector $\mathbf{v}$ can be written as ```math \mathbf{v}(\mathbf{x}(t)) = K \Phi(\mathbf{x}(t)) ``` -where $K$ is an $R$-by-$C$ matrix called the flux matrix, where $K_{rc}$ is the rate constant of reaction $r$ if $c$ is the index of the substrate complex of reaction $r$, and 0 otherwise. In Catalyst, the API function for $K$ is `fluxmat`: +where $K$ is an $R$-by-$C$ matrix called the flux matrix, where $K_{rc}$ is the rate constant of reaction $r$ if $c$ is the index of the substrate complex of reaction $r$, and 0 otherwise. In Catalyst, the API function for $K$ is `fluxmat`: ```@example s1 K = fluxmat(rn) ``` -Since we have that $\mathbf{v} = K\Phi$, we can rewrite the above decompositions as follows: +Since we have that $\mathbf{v} = K\Phi$, we can rewrite the above decompositions as follows: ```math \begin{align} \frac{d\mathbf{x}}{dt} &= N \mathbf{v}(\mathbf{x}(t),t) \\ &= N K \Phi(\mathbf{x}(t),t) \\ -&= Z B K \Phi(\mathbf{x}(t),t). +&= Z B K \Phi(\mathbf{x}(t),t). \end{align} ``` -The final matrix to discuss is the product of $A_k = BK$, which is a $C$-by-$C$ matrix that turns out to be exactly the negative of the [graph Laplacian](https://en.wikipedia.org/wiki/Laplacian_matrix) of the weighted graph whose nodes are reaction complexes and whose edges represent reactions, weighted by the rate constants. The API function for $A_k$ is the `laplacianmat`: +The final matrix to discuss is the product of $A_k = BK$, which is a $C$-by-$C$ matrix that turns out to be exactly the negative of the [graph Laplacian](https://en.wikipedia.org/wiki/Laplacian_matrix) of the weighted graph whose nodes are reaction complexes and whose edges represent reactions, weighted by the rate constants. The API function for $A_k$ is the `laplacianmat`: ```@example s1 A_k = laplacianmat(rn) ``` -We can check that +We can check that ```@example s1 isequal(A_k, B * K) ``` @@ -267,12 +271,12 @@ In summary, we have that \begin{align} \frac{d\mathbf{x}}{dt} &= N \mathbf{v}(\mathbf{x}(t),t) \\ &= N K \Phi(\mathbf{x}(t),t) \\ -&= Z B K \Phi(\mathbf{x}(t),t) \\ -&= Z A_k \Phi(\mathbf{x}(t),t). +&= Z B K \Phi(\mathbf{x}(t),t) \\ +&= Z A_k \Phi(\mathbf{x}(t),t). \end{align} ``` -All three of the objects introduced in this section (the flux matrix, mass-action vector, and Laplacian matrix) will return symbolic outputs by default, but can be made to return numerical outputs if values are specified. +All three of the objects introduced in this section (the flux matrix, mass-action vector, and Laplacian matrix) will return symbolic outputs by default, but can be made to return numerical outputs if values are specified. For example, `massactionvector` will return a numerical output if a set of species concentrations is supplied using a dictionary, tuple, or vector of Symbol-value pairs. ```@example s1 concmap = Dict([:A => 3., :B => 5., :C => 2.4, :D => 1.5]) @@ -290,6 +294,7 @@ laplacianmat(rn, parammap) ``` ## Symbolic ODE functions + In some cases it might be useful to generate the function defining the system of ODEs as a symbolic Julia function that can be used for further analysis. This can be done using Symbolics' [`build_function`](https://docs.sciml.ai/Symbolics/stable/getting_started/#Building-Functions), which takes a symbolic expression and a set of desired arguments, and converts it into a Julia function taking those arguments. Let's build the full symbolic function corresponding to our ODE system. `build_function` will return two expressions, one for a function that outputs a new vector for the result, and one for a function that modifies the input in-place. Either expression can then be evaluated to return a Julia function. @@ -307,7 +312,7 @@ The generated `f` now corresponds to the $f(\mathbf{x}(t))$ on the right-hand si Above we have generated a numeric rate matrix to substitute the rate constants into the symbolic expressions. We could have used a symbolic rate matrix, but then we would need to define the parameters `k, b`, so that the function `f` knows what `k` and `b` in its output refer to. ```@example s1 -@parameters k b +@parameters k b K = fluxmat(rn) odes = N * K * Φ f_oop_expr, f_iip_expr = Symbolics.build_function(odes, species(rn)) @@ -331,6 +336,7 @@ f(c, ks) Note also that `f` can take any vector with the right dimension (i.e. the number of species), not just a vector of `Number`, so it can be used to build, e.g. a vector of polynomials in Nemo for commutative algebraic methods. ## Properties of matrix null spaces + The null spaces of the matrices discussed in this section often have special meaning. Below we will discuss some of these properties. Recall that we may write the net stoichiometry matrix ``N = ZB``, where `Z` is the complex stoichiometry matrix and `B` is the incidence matrix of the graph. @@ -340,7 +346,8 @@ Recall that we may write the net stoichiometry matrix ``N = ZB``, where `Z` is t [Complex balance](@ref network_analysis_complex_and_detailed_balance) can be compactly formulated as the following: a set of steady state reaction fluxes is complex-balanced if it is in the nullspace of the incidence matrix ``B``. ## API Section for matrices and vectors -We have that: + +We have that: - ``N`` is the `netstoichmat` - ``Z`` is the `complexstoichmat` - ``B`` is the `incidencemat` @@ -358,5 +365,7 @@ massactionvector ``` --- + ## References + [^1]: [Feinberg, M. *Foundations of Chemical Reaction Network Theory*, Applied Mathematical Sciences 202, Springer (2019).](https://link.springer.com/book/10.1007/978-3-030-03858-8?noAccess=true) diff --git a/docs/src/spatial_modelling/lattice_reaction_systems.md b/docs/src/spatial_modelling/lattice_reaction_systems.md index bc41d816ad..6218721ed7 100644 --- a/docs/src/spatial_modelling/lattice_reaction_systems.md +++ b/docs/src/spatial_modelling/lattice_reaction_systems.md @@ -1,8 +1,9 @@ # [Introduction to Spatial Modelling with Catalyst](@id spatial_lattice_modelling_intro) + Catalyst supports the expansion of non-spatial [`ReactionSystem`](@ref)s (created using e.g. the `@reaction_network` DSL) to spatial domains. Spatial simulation of Catalyst models is a work in progress. Currently, the following is supported: - Spatial ODE and Jump simulations. - Discrete spatial domains. -- Constant-rate transportation reactions (species moving spatially at constant rates). +- Constant-rate transportation reactions (species moving spatially at constant rates). Features for which support is planned in future updates include: - Models on continuous domains with automatic discretisation (these models can already be simulated if the user provides a discretisation). @@ -13,6 +14,7 @@ This tutorial introduces spatial modelling on discrete domains, here called latt ## [Basic example of a spatial simulation on a discrete domain](@id spatial_lattice_modelling_intro_example) + To perform discrete-space spatial simulations, the user must first define a [`LatticeReactionSystem`](@ref). These combine: - A (non-spatial) `ReactionSystem`(@ref) model (created using standard Catalyst syntax). - A vector of spatial reactions, describing how species can move spatially across the domain. @@ -43,7 +45,7 @@ ps = [:k1 => 1.0, :k2 => 2.0, :D => 0.2] oprob = ODEProblem(lrs, u0, tspan, ps) nothing # hide ``` -In this example we used non-uniform values for $X1$'s initial condition, but uniform values for the remaining initial condition and parameter values. More details of uniform and non-uniform initial conditions and parameter values are provided [here](@ref spatial_lattice_modelling_intro_simulation_inputs). We also note that the diffusion reaction introduces a new parameter, $D$ (determining $X1$'s diffusion rate), whose value must be designated in the parameter vector. +In this example we used non-uniform values for $X1$'s initial condition, but uniform values for the remaining initial condition and parameter values. More details of uniform and non-uniform initial conditions and parameter values are provided [here](@ref spatial_lattice_modelling_intro_simulation_inputs). We also note that the diffusion reaction introduces a new parameter, $D$ (determining $X1$'s diffusion rate), whose value must be designated in the parameter vector. We can now simulate our model: ```@example spatial_intro_basics @@ -51,7 +53,7 @@ using OrdinaryDiffEqDefault sol = solve(oprob) nothing # hide ``` -We note that simulations of spatial models are often computationally expensive. Advice on the performance of spatial ODE simulations is provided [here](@ref spatial_lattice_ode_simulations_solvers). +We note that simulations of spatial models are often computationally expensive. Advice on the performance of spatial ODE simulations is provided [here](@ref spatial_lattice_ode_simulations_solvers). Finally, we can access "$X1$'s value across the simulation using ```@example spatial_intro_basics @@ -66,8 +68,9 @@ lattice_animation(sol, :X1, lrs, "lattice_simulation.mp4") More information on how to retrieve values from spatial simulations can be found [here](@ref lattice_simulation_structure_interaction_simulation_species), and for plotting them, [here](@ref lattice_simulation_plotting). Finally, a list of functions for querying `LatticeReactionSystems` for various properties can be found [here](@ref api_lattice_simulations). ## [Spatial reactions](@id spatial_lattice_modelling_intro_spatial_reactions) + Spatial reactions describe reaction events which involve species across two connected compartments. Currently, only so-called *transportation reactions* are supported. These consist of: -- A rate at which the reaction occurs. As for non-spatial reactions, this can be any expression. However, currently, it may only consist of parameters and other constants. +- A rate at which the reaction occurs. As for non-spatial reactions, this can be any expression. However, currently, it may only consist of parameters and other constants. - A single species which is transported from one compartment to an adjacent one. At the occurrence of a transport reaction, the specific species moves to the adjacent compartment. Many common spatial models can be represented using transport reactions only. These can model phenomena such as diffusion or constant flux. A transportation reaction can be created using the `@transportation_reaction` macro. E.g. above we used @@ -87,6 +90,7 @@ nothing # hide Any species which occurs is occurs in a transport reaction that is used to construct a [`LatticeReactionSystem`](@ref) must also occur in the corresponding non-spatial [`ReactionSystem`](@ref). ### [Creating transport reactions programmatically](@id spatial_lattice_modelling_intro_spatial_reactions_programmatic) + If models are created [programmatically](@ref programmatic_CRN_construction) it is also possible to create transportation reactions programmatically. To do so, use the `TransportReaction` constructor, providing first the rate and then the transported species: ```@example spatial_intro_spat_rxs @variables t @@ -98,6 +102,7 @@ nothing # hide Note that in this example, we specifically designate $D$ as an [edge parameter](@ref spatial_lattice_modelling_intro_simulation_edge_parameters). ## [Defining discrete spatial domains (lattices)](@id spatial_lattice_modelling_intro_lattices) + Discrete spatial domains can represent: 1. Systems which are composed of a (finite number of) compartments, where each compartment can be considered well-mixed (e.g. can be modelled non-spatially) and where (potentially) species can move between adjacent compartments. Tissues, where each compartment corresponds to a biological cell, are examples of such systems. 2. Systems that are continuous in nature, but have been approximated as a discrete domain. Future Catalyst updates will include the ability for the definition, and automatic discretisation, of continuous domains. Currently, however, the user has to perform this discretisation themselves. @@ -110,6 +115,7 @@ Catalyst supports three distinct types of lattices: Here, Cartesian lattices are a subset of the masked lattices, which are a subset of the unstructured lattices. If possible, it is advantageous to use as narrow a lattice definition as possible (this may both improve simulation performance and simplify syntax). Cartesian and masked lattices can be defined as one, two, and three-dimensional. By default, these lattices assume that diagonally neighbouring compartments are non-adjacent (do not permit direct movement of species in between themselves). To change this, provide the `diagonally_adjacent = true` argument to your [`LatticeReactionSystem`](@ref) when it is created. ### [Defining Cartesian lattices](@id spatial_lattice_modelling_intro_lattices_cartesian) + A Cartesian lattice is defined using the `CartesianGrid` function, which takes a single argument. For a 1d grid, simply provide the length of the grid as a single argument: ```@example spatial_intro_lattices using Catalyst # hide @@ -124,6 +130,7 @@ nothing # hide ``` ### [Defining masked lattices](@id spatial_lattice_modelling_intro_lattices_masked) + Masked lattices are defined through 1d, 2d, or 3d Boolean arrays. Each position in the array is `true` if it corresponds to a compartment, and `false` if it does not. E.g. to define a 1d grid corresponding to two disjoint sets, each with 3 compartments, use: ```@example spatial_intro_lattices rgrid_1d = [true, true, true, false, true, true, true] @@ -145,6 +152,7 @@ nothing # hide ``` ### [Defining unstructured lattices](@id spatial_lattice_modelling_intro_lattices_graph) + To define unstructured lattices, we must first import the [Graphs.jl](https://github.com/JuliaGraphs/Graphs.jl) package. Next, we can either use some [pre-defined formula for building graphs](https://juliagraphs.org/Graphs.jl/stable/core_functions/simplegraphs_generators/#Generators-for-common-graphs), or [build a graph from scratch](https://juliagraphs.org/Graphs.jl/stable/first_steps/construction/). Here we create a cyclic graph (where each compartment is connected to exactly two other compartments): ```@example spatial_intro_lattices using Graphs @@ -159,6 +167,7 @@ nothing # hide ``` ## [Non-uniform initial conditions and parameter values](@id spatial_lattice_modelling_intro_simulation_inputs) + For spatial models, initial conditions and parameter values are provided similarly as for non-spatial models. Wherever a single value is provided, it is used *uniformly* across the lattice. E.g. if we, for our [previous two-state model](@ref spatial_lattice_modelling_intro_example), set ```@example spatial_intro_nonuniform_vals u0 = [:X1 => 1.0, :X2 => 2.0] @@ -170,6 +179,7 @@ The initial condition will be $1.0$ for $X1$ across compartments, and $2.0$ for Below we describe how to set non-uniform values in the various cases. ### [Non-uniform compartment values for Cartesian lattices](@id spatial_lattice_modelling_intro_simulation_inputs_cartesian) + To provide non-uniform values across a Cartesian lattice, simply provide the values in an array of the same dimension and size as the Cartesian lattice. E.g. for a 5x10 Cartesian lattice: ```@example spatial_intro_nonuniform_vals using Catalyst # hide @@ -184,6 +194,7 @@ nothing # hide Non-uniform values for parameters (which values are tied to compartments) are provided similarly. ### [Non-uniform compartment values for masked lattices](@id spatial_lattice_modelling_intro_simulation_inputs_masked) + Non-uniform values for masked lattices are provided in the same manner as for Cartesian lattices (however, values at coordinates that do not hold compartments are ignored). E.g. To provide random values for a masked lattice contained within a 5x10 Cartesian lattices we can again set: ```@example spatial_intro_nonuniform_vals [:X1 => rand(5, 10), :X2 => 10.0] @@ -192,6 +203,7 @@ nothing # hide If we want, it is also possible to provide the values as a [*sparse array*](https://github.com/JuliaSparse/SparseArrays.jl) with values only in the coordinates that corresponds to compartments. ### [Non-uniform compartment values for unstructured lattices](@id spatial_lattice_modelling_intro_simulation_inputs_graphs) + In graphs (which are used to represent unstructured lattices) each vertex (i.e. compartment) has a specific index. To set non-uniform values for unstructured lattices, provide a vector where the $i$'th value corresponds to the value in the compartment with index $i$ in the graph. E.g. for a graph with 5 vertices, where we want $X$ to be zero in all compartments bar one (where it is $1.0$) we use: ```@example spatial_intro_nonuniform_vals [:X1 => [0.0, 0.0, 0.0, 0.0, 1.0], :X2 => 10.0] @@ -199,11 +211,12 @@ nothing # hide ``` ### [Non-uniform values for edge-parameters](@id spatial_lattice_modelling_intro_simulation_inputs_edge_parameters) -Adjacent compartments are connected by edges (with which compartments are connected by edges being defined by the lattice). For unstructured lattices, it is possible (if a directed graph was used) to have edges from one compartment to another, but not in the opposite direction. For a lattice with $N$ compartments, edge values are set by a $NxN$ matrix, where value $(i,j)$ corresponds to the parameter's values in the edge going *from* compartment $i$ *to* compartment $j$. This matrix can be either [sparse or non-sparse](https://docs.julialang.org/en/v1/stdlib/SparseArrays/). In the latter cases, values corresponding to non-existing edges are ignored. + +Adjacent compartments are connected by edges (with which compartments are connected by edges being defined by the lattice). For unstructured lattices, it is possible (if a directed graph was used) to have edges from one compartment to another, but not in the opposite direction. For a lattice with $N$ compartments, edge values are set by a $NxN$ matrix, where value $(i,j)$ corresponds to the parameter's values in the edge going *from* compartment $i$ *to* compartment $j$. This matrix can be either [sparse or non-sparse](https://docs.julialang.org/en/v1/stdlib/SparseArrays/). In the latter cases, values corresponding to non-existing edges are ignored. Let's consider a 1d Cartesian lattice with 4 compartments. Here, an edge parameter's values are provided in a 4x4 matrix. For [the Brusselator model described previously](@ref spatial_lattice_modelling_intro_example), $D$'s value was tied to edges. If we wish to set the value of $D$ to various values between $0.1$ and $0.4$ we can do: ```@example spatial_intro_nonuniform_vals -ps = [:k1 => 1.0, :k2 => 2.0, +ps = [:k1 => 1.0, :k2 => 2.0, :D => [ 0.0 0.1 0.0 0.0; 0.1 0.0 0.2 0.0; @@ -215,6 +228,7 @@ nothing # hide Here, the value at index $i,j$ corresponds to $D$'s value in the edge from compartment $i$ to compartment $j$. `0.0` is used for elements that do not correspond to an edge. The [`make_edge_p_values`](@ref) and [`make_directed_edge_values`](@ref) provide convenient interfaces for generating non-uniform edge parameter values. ## [Edge parameters and compartment parameters](@id spatial_lattice_modelling_intro_simulation_edge_parameters) + Parameters can be divided into *edge parameters* and *compartment parameters* (initial condition values are always tied to compartments). Here, edge parameters have their values tied to edges, while compartment parameters have their values tied to compartments. All parameters that are part of the rates (or stoichiometries) of non-spatial reactions must be compartment parameters. Parameters that are part of spatial reactions can be either compartment parameters or edge parameters. When a spatial reaction's rate is computed, edge parameters fetch their values for from the edge of the transition, and compartment parameters from the compartment *from which the edge originates*. When a [`LatticeReactionSystem`](@ref) is created, its parameters is the union of all parameters occurring in the (non-spatial) [`ReactionSystem`](@ref) and in all spatial reactions. By default, parameters occurring only in spatial reactions are considered edge parameters (and if they occur in the non-spatial [`ReactionSystem`](@ref) they are considered compartment parameters). It is, however, possible to designate a parameter specifically as an edge parameter (or not), by using the `edgeparameter` [metadata](@ref dsl_advanced_options_species_and_parameters_metadata). E.g. to designate that `D` (when declared in a non-spatial [`ReactionSystem`](@ref) using the DSL) is an edge parameter, not a compartment parameter, we use: @@ -239,6 +253,7 @@ edge_parameters(lrs) ``` ## [Spatial modelling limitations](@id spatial_lattice_modelling_intro_limitations) + Many features which are supported for non-spatial `ReactionSystem`s are currently unsupported for [`LatticeReactionSystem`](@ref)s. This includes [observables](@ref dsl_advanced_options_observables), [algebraic and differential equations](@ref constraint_equations), [hierarchical models](@ref compositional_modeling), and [events](@ref constraint_equations_events). It is possible that these features will be supported in the future. Furthermore, [removal of conserved quantities](@ref conservation_laws) is not supported when creating spatial `ODEProblem`s. -If you are using Catalyst's features for spatial modelling, please give us feedback on how we can improve these features. Additionally, just letting us know that you use these features is useful, as it helps inform us whether continued development of spatial modelling features is worthwhile. +If you are using Catalyst's features for spatial modelling, please give us feedback on how we can improve these features. Additionally, just letting us know that you use these features is useful, as it helps inform us whether continued development of spatial modelling features is worthwhile. diff --git a/docs/src/spatial_modelling/lattice_simulation_plotting.md b/docs/src/spatial_modelling/lattice_simulation_plotting.md index e135a637fb..93381b44ee 100644 --- a/docs/src/spatial_modelling/lattice_simulation_plotting.md +++ b/docs/src/spatial_modelling/lattice_simulation_plotting.md @@ -8,15 +8,16 @@ To aid the investigation of spatial simulations we have implemented several help The first two functions can be applied to [graph](@ref spatial_lattice_modelling_intro_lattices_graph) and 1d and 2d [Cartesian](@ref spatial_lattice_modelling_intro_lattices_cartesian) and [masked](@ref spatial_lattice_modelling_intro_lattices_masked) lattice based simulations, while `lattice_kymograph` can only be applied to 1d Cartesian and masked lattice based simulations. Currently, there is no functionality for plotting simulations based on 3d Cartesian and masked lattice. Here we will demonstrate all plotting functions using ODE simulations, but they work equally well for jump simulations. !!! note - The plotting interfaces used for non-spatial Catalyst simulations have seen lots of work to ensure high quality plots. However, the corresponding functions for spatial simulations are primarily intended to aid the user to investigate their simulation results. Thus, they might not be fully suitable for e.g. creating publication-quality graphics. If you are using these functions, please let us now. This helps inform us whether continued development of spatial modelling features is worthwhile. + The plotting interfaces used for non-spatial Catalyst simulations have seen lots of work to ensure high quality plots. However, the corresponding functions for spatial simulations are primarily intended to aid the user to investigate their simulation results. Thus, they might not be fully suitable for e.g. creating publication-quality graphics. If you are using these functions, please let us now. This helps inform us whether continued development of spatial modelling features is worthwhile. !!! note - To create animations we use [Makie.jl](https://docs.makie.org/stable/), which is an alternative plotting package to [Plots.jl](https://github.com/JuliaPlots/Plots.jl) (which is typically the preferred plotting package within the context of Catalyst). Generally, Makie is good at creating animations, hence we use it here (however, it is also a [popular competitor to Plots.jl for general-purpose plotting](https://juliapackagecomparisons.github.io/pages/plotting/)). + To create animations we use [Makie.jl](https://docs.makie.org/stable/), which is an alternative plotting package to [Plots.jl](https://github.com/JuliaPlots/Plots.jl) (which is typically the preferred plotting package within the context of Catalyst). Generally, Makie is good at creating animations, hence we use it here (however, it is also a [popular competitor to Plots.jl for general-purpose plotting](https://juliapackagecomparisons.github.io/pages/plotting/)). !!! warning These plotting interfaces are a work in progress. Hence, they and their interfaces may see more change that Catalyst features typically do. This include *the possibility of breaking changes without breaking releases to Catalyst*. ## [Animation and plotting of 1d Cartesian or masked lattice simulations](@id lattice_simulation_plotting_1d_grids) + Let us consider a spatial simulation on a 1d Cartesian grid lattice: ```@example lattice_plotting_1d using Catalyst, OrdinaryDiffEqDefault @@ -60,6 +61,7 @@ Here, we require neither a filename nor a `t` to be provided. However, the `nfra For more information of either function, and additional optional arguments, please read their corresponding api sections ([`lattice_plot`](@ref), [`lattice_animation`](@ref), and [`lattice_kymograph`](@ref)). ## [Animation and plotting of 2d Cartesian or masked lattice simulations](@id lattice_simulation_plotting_2d_grids) + Two-dimensional lattice simulations can be plotted in the same manner as one-dimensional ones. However, instead of displaying a species's value as a line plot, it is displayed as a heatmap. E.g. here we simulate a spatial [Brusselator](@ref basic_CRN_library_brusselator) model and display the value of $X$ at a designated time point. ```@example lattice_plotting_2d using Catalyst, OrdinaryDiffEqBDF @@ -93,6 +95,7 @@ lattice_animation(sol, :X, lrs, "lattice_simulation_2d.mp4") Again, please check the API pages for the [`lattice_plot`](@ref) and [`lattice_animation`](@ref) functions to see more details of their various options. ## [Animation and plotting of graph lattice simulations](@id lattice_simulation_plotting_graphs) + Finally, we consider lattice simulations on graph lattices. We first simulate a simple [birth-death process](@ref basic_CRN_library_bd) on a (6-node cyclic) graph lattice. ```@example lattice_plotting_graphs using Catalyst, Graphs, OrdinaryDiffEqDefault @@ -129,7 +132,7 @@ lattice_plot(osol, :X, lrs; layout) Finally, animations of graph lattice simulation work similarly to 2d ones, but accept the additional arguments relevant to plotting graphs. ## [Final notes](@id lattice_simulation_plotting_notes) + If you are using these interfaces, but there is some feature that is missing, you might wish to consider modifying the original code. This can be found [here](https://github.com/SciML/Catalyst.jl/blob/master/ext/CatalystCairoMakieExtension/cairo_makie_extension_spatial_modelling.jl), from which you can copy any code you need to make your own plotting interfaces. If you do so, please provide any feedback by raising [an issue](https://github.com/SciML/Catalyst.jl/issues) on the Catalyst GitHub page. As mentioned, these plotting interfaces are a work in progress, and input from users is valuable to improve them further. Many of Makie's plotting arguments, even those not described here, are handled by these functions, and someone familiar with the package should be able to use these to customise the plots further. It should also be noted that these interfaces has note been optimised for performance, and the generation of an animation can often surpass 1 second for larger models. Again, this can likely be improved, and if performance is an problem do raise an issue, in which case an additional effort can be made to improve performance. - diff --git a/docs/src/spatial_modelling/lattice_simulation_structure_ interaction.md b/docs/src/spatial_modelling/lattice_simulation_structure_ interaction.md index 58f6d784d1..1e76ab7bd5 100644 --- a/docs/src/spatial_modelling/lattice_simulation_structure_ interaction.md +++ b/docs/src/spatial_modelling/lattice_simulation_structure_ interaction.md @@ -1,4 +1,5 @@ # [Interfacing with Lattice Problems, Integrators, and Solutions](@id lattice_simulation_structure_interaction) + We have [previously described](@ref simulation_structure_interfacing) how to retrieve species and parameter values stored in non-spatial problems, integrators, and solutions. This section describes similar workflows for simulations based on [`LatticeReactionSystem`](@ref)s. Generally, while for non-spatial systems these operations can typically be done by indexing a structure directly, e.g. through @@ -15,6 +16,7 @@ Furthermore, there are some cases of interfacing which are currently not support Below we will describe various features using ODE simulations as examples. However, all interfaces (unless where else is stated) work identically for jump simulations. ## [Retrieving values from lattice simulations](@id lattice_simulation_structure_interaction_simulation_species) + Let us consider a simulation of a [`LatticeReactionSystem`](@ref): ```@example lattice_struct_interaction_sims using Catalyst, OrdinaryDiffEqDefault @@ -48,6 +50,7 @@ Here, the output is a vector with $X1$'s value at each simulation time step. How Unlike for non-spatial simulations, `lat_getu` does not take vector (e.g. `lat_getu(sol, [:X1, :X2], lrs)`) or symbolic expression (e.g. `lat_getu(sol, [X1 + X2], lrs)`) inputs. However, it is possible to use symbolic variables as input (e.g. `lat_getu(sol, two_state_model.X1, lrs)`). ### [Retrieving lattice simulations values at specific time points](@id lattice_simulation_structure_interaction_simulation_species_ts) + Just like for non-spatial solutions, it is possible to access the simulation's values at designated time points. This is possible even if the simulation did not stop at those specific time points (in which case an interpolated value is returned). To do this, the desired time points to sample are provided as a vector to `lat_getu` using the optional argument `t`. E.g. here we retrieve the simulation's (interpolated) values at time points `0.5` and `0.75`: ```@example lattice_struct_interaction_sims @@ -55,6 +58,7 @@ lat_getu(sol, :X1, lrs; t = [0.5, 0.75]) ``` ## [Retrieving and updating species values in problems and integrators](@id lattice_simulation_structure_interaction_prob_int_species) + Let us consider a spatial `ODEProblem` ```@example lattice_struct_interaction_prob_ints using Catalyst, OrdinaryDiffEqDefault @@ -95,11 +99,12 @@ lat_setu!(oprob, :X1, lrs, 1.0) Species values in [integrators](@ref simulation_structure_interfacing_integrators) can be interfaced with using identical syntax as for problems. ## [Retrieving and updating parameter values in problems and integrators](@id lattice_simulation_structure_interaction_prob_int_parameters) + Retrieval and updating of parameter values for problems and integrators works similarly as for species, but with the following differences: - The `lat_getp` and `lat_setp!` functions are used. - It is currently not possible to interface with parameter values of `JumpProblem`s and their integrators. - After parameter values are modified, the `rebuild_lat_internals!` function must be applied before the problem/integrator can be used for further analysis. -- Updating of [edge parameters](@ref spatial_lattice_modelling_intro_simulation_edge_parameters) is limited and uses a different interface. +- Updating of [edge parameters](@ref spatial_lattice_modelling_intro_simulation_edge_parameters) is limited and uses a different interface. Let us consider the spatial `ODEProblem` we previously declared. We can check the value of $k1$ by using `lat_getp` ```@example lattice_struct_interaction_prob_ints @@ -123,7 +128,8 @@ Parameter values of integrators can be interfaced with just like for problems (t ### [Retrieving and updatingedge parameter values in problems and integrators](@id lattice_simulation_structure_interaction_prob_int_parameters_edge_ps) -The `lat_getp` and `lat_setp!` functions cannot currently be applied to [edge parameters](@ref spatial_lattice_modelling_intro_simulation_edge_parameters). Instead, to access the value of an edge parameter, use + +The `lat_getp` and `lat_setp!` functions cannot currently be applied to [edge parameters](@ref spatial_lattice_modelling_intro_simulation_edge_parameters). Instead, to access the value of an edge parameter, use ```@example lattice_struct_interaction_prob_ints oprob.ps[:D] ``` @@ -136,4 +142,3 @@ This interface is somewhat limited, and the following aspects should be noted: - Edge parameter values can only be interfaced with if the edge parameter's value is spatially uniform. - When accessing an (spatially uniform) edge parameter's value, its single value will be encapsulated in a vector. - When setting an (spatially uniform) edge parameter's value, you must encapsulate the new value in a vector. - diff --git a/docs/src/spatial_modelling/spatial_jump_simulations.md b/docs/src/spatial_modelling/spatial_jump_simulations.md index a7879003ce..96a698d2ad 100644 --- a/docs/src/spatial_modelling/spatial_jump_simulations.md +++ b/docs/src/spatial_modelling/spatial_jump_simulations.md @@ -1,4 +1,5 @@ # [Spatial Jump Simulations](@id spatial_lattice_jump_simulations) + Our [introduction to spatial lattice simulations](@ref spatial_lattice_modelling_intro) has already described how to simulate [`LatticeReactionSystem`](@ref)s using ODEs. Jump simulations of [`LatticeReactionSystem`](@ref) are carried out using an almost identical approach. However, just like for non-spatial models, we must first create a `DiscreteProblem`, which is then used as input to our `JumpProblem`. Furthermore, a spatial [jump aggregator](@ref simulation_intro_jumps_solver_designation) (like `NSM`) can be used. Spatial jump simulations in Catalyst are built on top of JumpProcesses.jl's spatial jump simulators, more details on which can be found [here](https://docs.sciml.ai/JumpProcesses/stable/tutorials/spatial/). Below we perform a spatial jump simulation of a simple [birth-death process](@ref basic_CRN_library_bd). Note that we use our [`LatticeReactionSystem`](@ref) as input to both our `DiscreteProblem` and `JumpProblem`, and that we [provide](@ref simulation_intro_jumps_solver_designation) the spatial `NSM` jump aggregator to `JumpProblem`. @@ -24,4 +25,4 @@ We can now access the values of `sol` using the interfaces described [here](@ref Currently, the only available spatial jump aggregators are `NSM` and `DirectCRDirect`, with `DirectCRDirect` expected to perform better for large networks. !!! note - Currently, spatial jump simulations are only supported when all reaction of the non-spatial `ReactionSystem` are `MassActionJump`s, i.e. have constant rates. This means that reactions with e.g. Michaelis-Menten rates are currently not supported. \ No newline at end of file + Currently, spatial jump simulations are only supported when all reaction of the non-spatial `ReactionSystem` are `MassActionJump`s, i.e. have constant rates. This means that reactions with e.g. Michaelis-Menten rates are currently not supported. diff --git a/docs/src/spatial_modelling/spatial_ode_simulations.md b/docs/src/spatial_modelling/spatial_ode_simulations.md index a052f7ed08..ea7c223148 100644 --- a/docs/src/spatial_modelling/spatial_ode_simulations.md +++ b/docs/src/spatial_modelling/spatial_ode_simulations.md @@ -1,10 +1,13 @@ # [Spatial ODE simulations](@id spatial_lattice_ode_simulations) + Our [introduction to spatial lattice simulations](@ref spatial_lattice_modelling_intro) has already provided an extensive description of how to simulate [`LatticeReactionSystem`](@ref)s using ODEs. Further tutorials have also shown how to [retrieve values from simulations](@ref lattice_simulation_structure_interaction_simulation_species) and or how to [plot them](@ref lattice_simulation_plotting). Here we will build on this, primarily discussing strategies for increasing ODE simulation performance. This is especially important for spatial simulations, as these typically are more computationally demanding as compared to non-spatial ones. While focusing on non-spatial simulations, this [ODE performance tutorial](@ref ode_simulation_performance) is also be useful to read. ## [Solver selection for spatial ODE simulations](@id spatial_lattice_ode_simulations_solvers) + Previously we have described [how to select ODE solvers, and how this can impact simulation performance](@ref ode_simulation_performance_solvers). This is especially relevant for spatial simulations. For stiff problems, `FBDF` is a good first solver to try. For non-stiff problems, `ROCK2` is instead a good first alternative. However, it is still worthwhile to explore a range of alternative solvers. ## [Jacobian options for spatial ODE simulations](@id spatial_lattice_ode_simulations_jacobians) + We have previously described how, when [implicit solvers are used to solve stiff ODEs](@ref ode_simulation_performance_stiffness), the [strategy for computing the system Jacobian](@ref ode_simulation_performance_jacobian) is important. This is especially the case for spatial simulations, where the Jacobian often is large and highly sparse. Catalyst implements special methods for spatial Jacobians. To utilise these, provide the `jac = true` argument to your `ODEProblem` when it is created (if `jac = false`, which is the default, [*automatic differentiation*](https://en.wikipedia.org/wiki/Automatic_differentiation) will be used for Jacobian computation). Here we simulate a [Brusselator](@ref basic_CRN_library_brusselator) while designating to use Catalyst's computed Jacobian: ```@example spatial_ode using Catalyst, OrdinaryDiffEqBDF @@ -24,12 +27,12 @@ ps = [:A => 1.0, :B => 4.0, :D => 0.2] oprob = ODEProblem(lrs, u0, tspan, ps; jac = true) sol = solve(oprob, FBDF()) nothing # hide -``` -For large systems, building a dense Jacobian can be problematic, in which case a [*sparse*](@ref ode_simulation_performance_sparse_jacobian) Jacobian can be designated using `sparse = true`: +``` +For large systems, building a dense Jacobian can be problematic, in which case a [*sparse*](@ref ode_simulation_performance_sparse_jacobian) Jacobian can be designated using `sparse = true`: ```@example spatial_ode oprob = ODEProblem(lrs, u0, tspan, ps; jac = true, sparse = true) sol = solve(oprob, FBDF()) nothing # hide -``` +``` -It is possible to use `sparse = true` while `jac = false`, in which case a sparse Jacobian is computed using automatic differentiation. +It is possible to use `sparse = true` while `jac = false`, in which case a sparse Jacobian is computed using automatic differentiation. diff --git a/docs/src/steady_state_functionality/bifurcation_diagrams.md b/docs/src/steady_state_functionality/bifurcation_diagrams.md index 76d6a05097..0417a60351 100644 --- a/docs/src/steady_state_functionality/bifurcation_diagrams.md +++ b/docs/src/steady_state_functionality/bifurcation_diagrams.md @@ -5,6 +5,7 @@ Bifurcation diagrams describe how, for a dynamical system, the quantity and type This tutorial briefly introduces how to use Catalyst with BifurcationKit through basic examples, with BifurcationKit.jl providing [a more extensive documentation](https://bifurcationkit.github.io/BifurcationKitDocs.jl/stable/). Especially for more complicated systems, where careful tuning of algorithm options might be required, reading the BifurcationKit documentation is recommended. Finally, BifurcationKit provides many additional features not described here, including [computation of periodic orbits](@ref bifurcationkit_periodic_orbits), [tracking of bifurcation points along secondary parameters](@ref bifurcationkit_codim2), and [bifurcation computations for PDEs](https://bifurcationkit.github.io/BifurcationKitDocs.jl/dev/tutorials/tutorials/#PDEs:-bifurcations-of-equilibria). ## [Basic example](@id bifurcation_diagrams_basic_example) + For this example, we will use a modified version of the [Wilhelm model](@ref basic_CRN_library_wilhelm) (which demonstrates a bistable switch as the parameter $k1$ is varied). We declare the model using Catalyst: ```@example ex1 using Catalyst @@ -45,7 +46,7 @@ Finally, we compute our bifurcation diagram using: bif_dia = bifurcationdiagram(bprob, PALC(), 2, opts_br; bothside = true) nothing # hide ``` -Where `PALC()` designates that we wish to use the pseudo-arclength continuation method to track our solution. The third argument (`2`) designates the maximum number of recursions when branches of branches are computed (branches appear as continuation encounters certain bifurcation points). For diagrams with highly branched structures (rare for many common small chemical reaction networks) this input is important. Finally, `bothside = true` designates that we wish to perform continuation on both sides of the initial point (which is typically the case). +Where `PALC()` designates that we wish to use the pseudo-arclength continuation method to track our solution. The third argument (`2`) designates the maximum number of recursions when branches of branches are computed (branches appear as continuation encounters certain bifurcation points). For diagrams with highly branched structures (rare for many common small chemical reaction networks) this input is important. Finally, `bothside = true` designates that we wish to perform continuation on both sides of the initial point (which is typically the case). We can plot our bifurcation diagram using the Plots.jl package: ```@example ex1 @@ -55,6 +56,7 @@ plot(bif_dia; xguide = bif_par, yguide = plot_var, branchlabel = "Steady state c Here, the steady state concentration of $X$ is shown as a function of $k1$'s value. Stable steady states are shown with thick lines, unstable ones with thin lines. The two [fold bifurcation points](https://en.wikipedia.org/wiki/Saddle-node_bifurcation) are marked with "bp". ## [Additional `ContinuationPar` options](@id bifurcation_diagrams_continuationpar) + Most of the options required by the `bifurcationdiagram` function are provided through the `ContinuationPar` structure. For full details, please read the [BifurcationKit documentation](https://bifurcationkit.github.io/BifurcationKitDocs.jl/dev/library/#BifurcationKit.ContinuationPar). However, a few common options, and how they affect the continuation computation, are described here: - `p_min` and `p_max`: Set the interval over which the bifurcation diagram is computed (with the continuation stopping if it reaches these bounds). - `dsmin` and `dsmax`: The minimum and maximum length of the continuation steps (in the bifurcation parameter's value). @@ -66,7 +68,7 @@ The previous bifurcation diagram can be computed, with these various options spe ```@example ex1 p_span = (2.0, 20.0) newton_options = NewtonPar(tol = 1e-9, max_iterations = 20) -opts_br = ContinuationPar(; p_min = p_span[1], p_max = p_span[2], ds = 0.005, +opts_br = ContinuationPar(; p_min = p_span[1], p_max = p_span[2], ds = 0.005, dsmin = 0.001, dsmax = 0.01, max_steps = 1000, newton_options) bif_dia = bifurcationdiagram(bprob, PALC(), 2, opts_br; bothside = true) nothing # hide @@ -74,6 +76,7 @@ nothing # hide (In this case, however, these additional settings have no significant effect on the result) ## [Bifurcation diagrams with disjoint branches](@id bifurcation_diagrams_disjoint_branches) + Let's consider the previous case, but instead compute the bifurcation diagram over the interval $(8.0,15.0)$: ```@example ex1 p_span = (8.0, 15.0) @@ -90,7 +93,7 @@ perturb_solution(x, _, _) = (x .+ 0.1 .* rand(length(x))) defalg = DefCont(; deflation_operator, perturb_solution) nothing # hide ``` -Next we compute our bifurcation diagram using `defalg` (instead of `PALC()` as previous) as our method. We also use a range of additional continuation parameter options to ensure a smooth tracking of the solution. Here we have +Next we compute our bifurcation diagram using `defalg` (instead of `PALC()` as previous) as our method. We also use a range of additional continuation parameter options to ensure a smooth tracking of the solution. Here we have ```@example ex1 newton_options = NewtonPar(max_iterations = 10) cont_par = ContinuationPar(; p_min = p_span[1], p_max = p_span[2], ds = 0.001, max_steps = 10000, newton_options) @@ -103,6 +106,7 @@ plot(bif_dia; xguide = bif_par, yguide = plot_var, ylimit = (0.0, 17.0), color = ``` ## [Systems with conservation laws](@id bifurcation_diagrams_cons_laws) + Some systems are under-determined at steady state, so that for a given parameter set they have an infinite number of possible steady state solutions, preventing bifurcation diagrams from being computed. Similar to when we [compute steady states for fixed parameter values](@ref homotopy_continuation_conservation_laws), we can utilise Catalyst's ability to [detect and eliminate conservation laws](@ref conservation_laws) to resolve this issue. This requires us to provide information of the species concentrations at which we wish to compute the bifurcation diagram (to determine the values of conserved quantities). These are provided to the `BifurcationProblem` using the `u0` argument. To illustrate this, we will create a simple model of a kinase that is produced and degraded (at rates $p$ and $d$). The kinase facilitates the phosphorylation of a protein ($X$), which is dephosphorylated at a constant rate. For this system, we will compute a bifurcation diagram, showing how the concentration of the phosphorylated protein ($Xp$) depends on the degradation rate of the kinase ($d$). We will set the total amount of protein ($X + Xp$) to $1.0$. @@ -123,7 +127,7 @@ opts_br = ContinuationPar(p_min = p_span[1], p_max = p_span[2]) bif_dia = bifurcationdiagram(bprob, PALC(), 2, opts_br; bothside = true) plot(bif_dia; xguide = "d", yguide = "Xp") ``` -This bifurcation diagram does not contain any interesting features (such as bifurcation points), and only shows how the steady state concentration of $Xp$ is reduced as $d$ increases. +This bifurcation diagram does not contain any interesting features (such as bifurcation points), and only shows how the steady state concentration of $Xp$ is reduced as $d$ increases. Finally, for additional clarity, we reiterate the purpose of the two `u` arguments used: - `u_guess`: A guess of the initial steady states (which BifurcationKit uses to find its starting point). Typically, most trivial guesses work (e.g. setting all species concentrations to `1.0`). `u_guess` *does not* have to fulfil the conserved concentrations provided in `u0`. @@ -131,9 +135,13 @@ Finally, for additional clarity, we reiterate the purpose of the two `u` argumen --- + ## [Citation](@id bifurcationkit_periodic_orbits_citation) + If you use BifurcationKit.jl for your work, we ask that you **cite** the following paper!! Open source development strongly depends on this. It is referenced on [HAL-Inria](https://hal.archives-ouvertes.fr/hal-02902346) with *bibtex* entry [CITATION.bib](https://github.com/bifurcationkit/BifurcationKit.jl/blob/master/CITATION.bib). --- + ## References -[^1]: [Yuri A. Kuznetsov, *Elements of Applied Bifurcation Theory*, Springer (2023).](https://link.springer.com/book/10.1007/978-3-031-22007-4) \ No newline at end of file + +[^1]: [Yuri A. Kuznetsov, *Elements of Applied Bifurcation Theory*, Springer (2023).](https://link.springer.com/book/10.1007/978-3-031-22007-4) diff --git a/docs/src/steady_state_functionality/dynamical_systems.md b/docs/src/steady_state_functionality/dynamical_systems.md index 11c48d3734..8f982129bb 100644 --- a/docs/src/steady_state_functionality/dynamical_systems.md +++ b/docs/src/steady_state_functionality/dynamical_systems.md @@ -1,10 +1,12 @@ # [Analysing Model Steady State Properties with DynamicalSystems.jl](@id dynamical_systems) + The [DynamicalSystems.jl package](https://github.com/JuliaDynamics/DynamicalSystems.jl) implements a wide range of methods for analysing dynamical systems[^1][^2]. This includes both continuous-time systems (i.e. ODEs) and discrete-times ones (difference equations, however, these are not relevant to chemical reaction network modelling). Here we give two examples of how DynamicalSystems.jl can be used, with the package's [documentation describing many more features](https://juliadynamics.github.io/DynamicalSystemsDocs.jl/dynamicalsystems/dev/tutorial/). Finally, it should also be noted that DynamicalSystems.jl contain several tools for [analysing data measured from dynamical systems](https://juliadynamics.github.io/DynamicalSystemsDocs.jl/dynamicalsystems/dev/contents/#Exported-submodules). ## [Finding basins of attraction](@id dynamical_systems_basins_of_attraction) -Given enough time, an ODE will eventually reach a so-called [*attractor*](https://en.wikipedia.org/wiki/Attractor). For chemical reaction networks (CRNs), this will typically be either a *steady state* or a *limit cycle*. Since ODEs are deterministic, which attractor a simulation will reach is uniquely determined by the initial condition (assuming parameter values are fixed). Conversely, each attractor is associated with a set of initial conditions such that model simulations originating in these will tend to that attractor. These sets are called *basins of attraction*. Here, phase space (the space of all possible states of the system) can be divided into a number of basins of attraction equal to the number of attractors. -DynamicalSystems.jl provides a simple interface for finding an ODE's basins of attraction across any given subspace of phase space. In this example we will use the bistable [Wilhelm model](https://bmcsystbiol.biomedcentral.com/articles/10.1186/1752-0509-3-90) (which steady states we have previous [computed using homotopy continuation](@ref homotopy_continuation)). As a first step, we create an `ODEProblem` corresponding to the model which basins of attraction we wish to compute. For this application, `u0` and `tspan` is unused, and their values are of little importance (the only exception is than `tspan`, for implementation reason, must provide a not too small interval, we recommend minimum `(0.0, 1.0)`). +Given enough time, an ODE will eventually reach a so-called [*attractor*](https://en.wikipedia.org/wiki/Attractor). For chemical reaction networks (CRNs), this will typically be either a *steady state* or a *limit cycle*. Since ODEs are deterministic, which attractor a simulation will reach is uniquely determined by the initial condition (assuming parameter values are fixed). Conversely, each attractor is associated with a set of initial conditions such that model simulations originating in these will tend to that attractor. These sets are called *basins of attraction*. Here, phase space (the space of all possible states of the system) can be divided into a number of basins of attraction equal to the number of attractors. + +DynamicalSystems.jl provides a simple interface for finding an ODE's basins of attraction across any given subspace of phase space. In this example we will use the bistable [Wilhelm model](https://bmcsystbiol.biomedcentral.com/articles/10.1186/1752-0509-3-90) (which steady states we have previous [computed using homotopy continuation](@ref homotopy_continuation)). As a first step, we create an `ODEProblem` corresponding to the model which basins of attraction we wish to compute. For this application, `u0` and `tspan` is unused, and their values are of little importance (the only exception is than `tspan`, for implementation reason, must provide a not too small interval, we recommend minimum `(0.0, 1.0)`). ```@example dynamical_systems_basins using Catalyst wilhelm_model = @reaction_network begin @@ -37,7 +39,7 @@ attractors ``` Here, `attractors` is a dictionary that maps attractor labels (integers) to attractors. In this case we have two fixed points, one at $(0.0,0.0)$ and one at $(4.5,6.0)$. Next, `basins` is a matrix of equal size to `grid`, where each value is an integer describing to which attractor's basin that state belongs. -DynamicalSystems.jl also provides a simple interface for plotting the resulting basins. This uses [Makie.jl](https://docs.makie.org/stable/), an alternative plotting package to [Plots.jl](https://github.com/JuliaPlots/Plots.jl) (which is typically the preferred plotting package within the context of Catalyst). Generally, Makie is good at creating animations or interactive graphics (however, it is also a [popular competitor to Plots.jl for general-purpose plotting](https://juliapackagecomparisons.github.io/pages/plotting/)). +DynamicalSystems.jl also provides a simple interface for plotting the resulting basins. This uses [Makie.jl](https://docs.makie.org/stable/), an alternative plotting package to [Plots.jl](https://github.com/JuliaPlots/Plots.jl) (which is typically the preferred plotting package within the context of Catalyst). Generally, Makie is good at creating animations or interactive graphics (however, it is also a [popular competitor to Plots.jl for general-purpose plotting](https://juliapackagecomparisons.github.io/pages/plotting/)). ```@example dynamical_systems_basins using CairoMakie heatmap_basins_attractors(grid, basins, attractors) @@ -50,7 +52,8 @@ Here, in addition to the basins of attraction, the system's three steady states More information on how to compute basins of attractions for ODEs using DynamicalSystems.jl can be found [here](https://juliadynamics.github.io/DynamicalSystemsDocs.jl/attractors/stable/basins/). ## [Computing Lyapunov exponents](@id dynamical_systems_lyapunov_exponents) -[*Lyapunov exponents*](https://en.wikipedia.org/wiki/Lyapunov_exponent) are scalar values that can be computed for any attractor of an ODE. For an ODE with $n$ variables, for each state, a total of $n$ Lyapunov exponents can be computed (and these are collectively called the *Lyapunov spectrum*). Positive Lyapunov exponents indicate that trajectories initially infinitesimally close diverge from each other. Conversely, negative Lyapunov exponents suggests that trajectories converge to each others. + +[*Lyapunov exponents*](https://en.wikipedia.org/wiki/Lyapunov_exponent) are scalar values that can be computed for any attractor of an ODE. For an ODE with $n$ variables, for each state, a total of $n$ Lyapunov exponents can be computed (and these are collectively called the *Lyapunov spectrum*). Positive Lyapunov exponents indicate that trajectories initially infinitesimally close diverge from each other. Conversely, negative Lyapunov exponents suggests that trajectories converge to each others. While Lyapunov exponents can be used for other purposes, they are primarily used to characterise [*chaotic behaviours*](https://en.wikipedia.org/wiki/Chaos_theory) (where small changes in initial conditions has large effect on the resulting trajectories). Generally, an ODE exhibit chaotic behaviour if its attractor(s) have *at least one* positive Lyapunov exponent. Practically, Lyapunov exponents can be computed using DynamicalSystems.jl's `lyapunovspectrum` function. Here we will use it to investigate two models, one which exhibits chaos and one which do not. @@ -124,7 +127,9 @@ More details on how to compute Lyapunov exponents using DynamicalSystems.jl can --- + ## [Citations](@id dynamical_systems_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 author of the DynamicalSystems.jl package: ``` @article{DynamicalSystems.jl-2018, @@ -143,13 +148,16 @@ If you use this functionality in your research, [in addition to Catalyst](@ref d --- + ## Learning more If you want to learn more about analysing dynamical systems, including chaotic behaviour, see the textbook [Nonlinear Dynamics](https://link.springer.com/book/10.1007/978-3-030-91032-7). It utilizes DynamicalSystems.jl and provides a concise, hands-on approach to learning nonlinear dynamics and analysing dynamical systems [^3]. --- + ## References + [^1]: [S. H. Strogatz, *Nonlinear Dynamics and Chaos*, Westview Press (1994).](http://users.uoa.gr/~pjioannou/nonlin/Strogatz,%20S.%20H.%20-%20Nonlinear%20Dynamics%20And%20Chaos.pdf) [^2]: [A. M. Lyapunov, *The general problem of the stability of motion*, International Journal of Control (1992).](https://www.tandfonline.com/doi/abs/10.1080/00207179208934253) [^3]: [G. Datseris, U. Parlitz, *Nonlinear dynamics: A concise introduction interlaced with code*, Springer (2022).](https://link.springer.com/book/10.1007/978-3-030-91032-7) diff --git a/docs/src/steady_state_functionality/examples/bifurcationkit_codim2.md b/docs/src/steady_state_functionality/examples/bifurcationkit_codim2.md index 2d71798948..76028bc9ba 100644 --- a/docs/src/steady_state_functionality/examples/bifurcationkit_codim2.md +++ b/docs/src/steady_state_functionality/examples/bifurcationkit_codim2.md @@ -1,7 +1,9 @@ # [Tracking Bifurcation Point w.r.t. Secondary Parameters using BifurcationKit.jl](@id bifurcationkit_codim2) + Previously, we have shown how to [compute bifurcation diagrams](@ref bifurcation_diagrams) using [BifurcationKit.jl](https://github.com/bifurcationkit/BifurcationKit.jl). In this example, we will show how, after computing the initial diagram, we can track how the position of a bifurcation point moves as a secondary parameter is changed (so-called codimensional 2 bifurcation analysis). More information on how to track bifurcation points along secondary parameters can be found in the [BifurcationKit documentation](https://bifurcationkit.github.io/BifurcationKitDocs.jl/stable/tutorials/ode/tutorialCO/#CO-oxidation-(codim-2)). ## [Computing the bifurcation diagram for the Repressilator](@id bifurcationkit_codim2_bifdia) + We will first compute the bifurcation diagram, using the same approach as in the [corresponding tutorial](@ref bifurcation_diagrams). For this example, we will use the oscillating [Repressilator](@ref basic_CRN_library_repressilator) model. ```@example bifurcationkit_codim2 using Catalyst @@ -48,6 +50,7 @@ plot(plot(sol_nosc; title = "v = 5"), plot(sol_osc; title = "v = 15"), size = (1 ``` ## [Tracking the bifurcation point w.r.t. a second parameter](@id bifurcationkit_codim2_2ndpar_cont) + Next, we will investigate how the Hopf bifurcation point moves (in $v$-$X$ space) as a second parameter ($K$) is changed. To do this we will use BifurcationKit.jl's [`continuation` function](https://bifurcationkit.github.io/BifurcationKitDocs.jl/dev/library/#BifurcationKit.continuation) (the [`bifurcationdiagram` function](https://bifurcationkit.github.io/BifurcationKitDocs.jl/dev/library/#BifurcationKit.bifurcationdiagram), which we previously have used, works by calling `continuation` recursively). We will call it on the Hopf bifurcation point. First we need to retrieve some indexes that are required to make Catalyst (which primarily indexes through symbols) work with BifurcationKit (which primarily indexes through numbers). A smoother interface for this will hopefully be added in the future. ```@example bifurcationkit_codim2 K_idx = findfirst(isequal(repressilator.K), parameters(complete(convert(NonlinearSystem, repressilator)))) @@ -70,12 +73,12 @@ nothing # hide ``` We can now plot how the position of the bifurcation point changes with $K$. Here, we use `vars = (v_sym, :x)` to designate that we wish to plot (across the continuation branch) the plotting variable ($X$, which we designated when we created our `BifurcationProblem`) against the first parameter ($v$). ```@example bifurcationkit_codim2 -plot(bifdia; branchlabel = "Continuation of steady state w.r.t. v, (K = 15)", linewidthstable = 6, +plot(bifdia; branchlabel = "Continuation of steady state w.r.t. v, (K = 15)", linewidthstable = 6, linewidthunstable = 3, markersize = 5) -plot!(cont_hopf; vars = (v_sym, :x), xlimit = v_span, xguide = bif_par, yguide = plot_var, +plot!(cont_hopf; vars = (v_sym, :x), xlimit = v_span, xguide = bif_par, yguide = plot_var, branchlabel = "Continuation of Hopf bifurcation w.r.t. K") ``` -In this case we cannot see directly which part of the $K$ continuation branch corresponds to low values, however, for low $K$ the Hopf bifurcation occurs for much lower values of $v$ (and corresponds to lower steady state values of $X$). We can check this by e.g. re-computing the Hopf branch for `K_span = (0.01, 20.0)` and see that the rightmost part of the branch is shortened. +In this case we cannot see directly which part of the $K$ continuation branch corresponds to low values, however, for low $K$ the Hopf bifurcation occurs for much lower values of $v$ (and corresponds to lower steady state values of $X$). We can check this by e.g. re-computing the Hopf branch for `K_span = (0.01, 20.0)` and see that the rightmost part of the branch is shortened. We can confirm that the new line corresponds to the Hopf Bifurcation point by recomputing the initial bifurcation diagram, but for a lower $K$ value. ```@example bifurcationkit_codim2 @@ -91,7 +94,7 @@ Finally, we have already noted that the Hopf bifurcation splits parameter space ```@example bifurcationkit_codim2 xlimit = extrema(getfield.(cont_hopf.branch, v_sym)) ylimit = extrema(getfield.(cont_hopf.branch, K_sym)) -plot(cont_hopf; vars = (v_sym, K_sym), xlimit, ylimit, branchlabel = "Hopf bifurcation", +plot(cont_hopf; vars = (v_sym, K_sym), xlimit, ylimit, branchlabel = "Hopf bifurcation", xguide = "v", yguide = "K", lw = 6) ``` Next, we colour parameter space according to whether the steady state is stable (blue) or unstable (red). We also mark two sample values (one in each region). @@ -116,5 +119,7 @@ plot(plot(sol_nosc; title = "No oscillation"), plot(sol_osc; title = "Oscillatio --- + ## [Citation](@id bifurcationkit_periodic_orbits_citation) -If you use BifurcationKit.jl for your work, we ask that you **cite** the following paper!! Open source development strongly depends on this. It is referenced on [HAL-Inria](https://hal.archives-ouvertes.fr/hal-02902346) with *bibtex* entry [CITATION.bib](https://github.com/bifurcationkit/BifurcationKit.jl/blob/master/CITATION.bib). \ No newline at end of file + +If you use BifurcationKit.jl for your work, we ask that you **cite** the following paper!! Open source development strongly depends on this. It is referenced on [HAL-Inria](https://hal.archives-ouvertes.fr/hal-02902346) with *bibtex* entry [CITATION.bib](https://github.com/bifurcationkit/BifurcationKit.jl/blob/master/CITATION.bib). diff --git a/docs/src/steady_state_functionality/examples/bifurcationkit_periodic_orbits.md b/docs/src/steady_state_functionality/examples/bifurcationkit_periodic_orbits.md index f656e3a491..b0dea9ce63 100644 --- a/docs/src/steady_state_functionality/examples/bifurcationkit_periodic_orbits.md +++ b/docs/src/steady_state_functionality/examples/bifurcationkit_periodic_orbits.md @@ -1,7 +1,9 @@ # [Computing Periodic Orbits (Oscillations) Using BifurcationKit.jl](@id bifurcationkit_periodic_orbits) + Previously, we have shown how to [compute bifurcation diagrams](@ref bifurcation_diagrams) using [BifurcationKit.jl](https://github.com/bifurcationkit/BifurcationKit.jl). In this example we will consider a system which exhibits an oscillation and show how to use BifurcationKit to track not just the system's (potentially unstable) steady state, but also the periodic orbit itself. More information on how to track periodic orbits can be found in the [BifurcationKit documentation](https://bifurcationkit.github.io/BifurcationKitDocs.jl/stable/tutorials/tutorials/#Periodic-orbits). ## [Computing the bifurcation diagram for the Repressilator](@id bifurcationkit_periodic_orbits_bifdia) + We will first compute the bifurcation diagram, using the same approach as in the [corresponding tutorial](@ref bifurcation_diagrams). For this example, we will use the oscillating [Repressilator](@ref basic_CRN_library_repressilator) model. ```@example bifurcationkit_periodic_orbits using Catalyst @@ -48,6 +50,7 @@ plot(plot(sol_nosc; title = "v = 5"), plot(sol_osc; title = "v = 15"), size = (1 ``` ## [Tracking the periodic orbits](@id bifurcationkit_periodic_orbits_pos) + Next, we will use BifurcationKit.jl's [`continuation` function](https://bifurcationkit.github.io/BifurcationKitDocs.jl/dev/library/#BifurcationKit.continuation) (the [`bifurcationdiagram` function](https://bifurcationkit.github.io/BifurcationKitDocs.jl/dev/library/#BifurcationKit.bifurcationdiagram), which we previously have used, works by calling `continuation` recursively) to track the periodic orbit which appears with the Hopf bifurcation point. First, we set the options for the continuation. Just like for bifurcation diagrams we must set our [continuation parameters](@ref bifurcation_diagrams_continuationpar). Here we will use the same one as for the initial diagram (however, additional ones can be supplied). @@ -95,5 +98,7 @@ In the plot we see that the period starts at around $18$ time units, and slowly --- + ## [Citation](@id bifurcationkit_periodic_orbits_citation) -If you use BifurcationKit.jl for your work, we ask that you **cite** the following paper!! Open source development strongly depends on this. It is referenced on [HAL-Inria](https://hal.archives-ouvertes.fr/hal-02902346) with *bibtex* entry [CITATION.bib](https://github.com/bifurcationkit/BifurcationKit.jl/blob/master/CITATION.bib). \ No newline at end of file + +If you use BifurcationKit.jl for your work, we ask that you **cite** the following paper!! Open source development strongly depends on this. It is referenced on [HAL-Inria](https://hal.archives-ouvertes.fr/hal-02902346) with *bibtex* entry [CITATION.bib](https://github.com/bifurcationkit/BifurcationKit.jl/blob/master/CITATION.bib). diff --git a/docs/src/steady_state_functionality/examples/nullcline_plotting.md b/docs/src/steady_state_functionality/examples/nullcline_plotting.md index 6258b0b1f0..1764043234 100644 --- a/docs/src/steady_state_functionality/examples/nullcline_plotting.md +++ b/docs/src/steady_state_functionality/examples/nullcline_plotting.md @@ -1,4 +1,5 @@ # [Plotting Nullclines and Steady States in Phase Space](@id nullcline_plotting) + In this tutorial we will show how to extract a system's steady states and [nullclines](https://en.wikipedia.org/wiki/Nullcline), and how to plot these in [phase space](https://en.wikipedia.org/wiki/Phase_space). Generally, while nullclines are not directly needed for most types analysis, plotting these can give some understanding of a system's steady state and stability properties. For an ordinary differential equation @@ -13,7 +14,8 @@ For an ordinary differential equation the $i$'th nullcline is the surface along which $\frac{dx_i}{dt} = 0$, i.e. the implicit surface given by $f_i(x_1,\dots,x_n) = 0$. Nullclines are frequently used when visualizing the phase-planes of two-dimensional models (as these can be easily plotted). ## [Computing nullclines and steady states for a bistable switch](@id nullcline_plotting_computation) -For our example we will use a simple bistable switch model, consisting of two species ($X$ and $Y$) which mutually inhibit each other through repressive Hill functions. + +For our example we will use a simple bistable switch model, consisting of two species ($X$ and $Y$) which mutually inhibit each other through repressive Hill functions. ```@example nullcline_plotting using Catalyst bs_switch = @reaction_network begin @@ -70,6 +72,7 @@ Here we can see how the steady states occur at the nullclines intersections. Here we use an inherent Plots function to plot the nullclines. However, there are also specialised packages for these kinds of plots, such as [ImplicitPlots.jl](https://github.com/saschatimme/ImplicitPlots.jl). ## [Plotting system directions in phase space](@id nullcline_plotting_directions) + One useful property of nullclines is that the sign of $dX/dt$ will only switch whenever the solution crosses the $dX/dt=0$ nullcline. This means that, within each region defined by the nullclines, the direction of the solution remains constant. Below we use this to, for each such region, plot arrows showing the solution's direction. ```@example nullcline_plotting # Creates a function for plotting the ODE's direction at a point in phase space. diff --git a/docs/src/steady_state_functionality/homotopy_continuation.md b/docs/src/steady_state_functionality/homotopy_continuation.md index 85b7274cea..b1d682bae1 100644 --- a/docs/src/steady_state_functionality/homotopy_continuation.md +++ b/docs/src/steady_state_functionality/homotopy_continuation.md @@ -39,6 +39,7 @@ The order of the species in the output vectors are the same as in `species(wilhe It should be noted that the steady state multivariate polynomials produced by reaction systems may have both imaginary and negative roots, which are filtered away by `hc_steady_states`. If you want the negative roots, you can use the `hc_steady_states(wilhelm_2009_model, ps; filter_negative=false)` argument. ## [Systems with conservation laws](@id homotopy_continuation_conservation_laws) + Some systems are under-determined, and have an infinite number of possible steady states. These are typically systems containing a conservation law, e.g. ```@example hc_claws @@ -56,13 +57,16 @@ hc_steady_states(two_state_model, ps; u0) ``` ## Final notes + - `hc_steady_states` support any systems where all rates are systems of rational polynomials (such as Hill functions with integer Hill coefficients). -- When providing initial conditions to compute conservation laws, values are only required for those species that are part of conserved quantities. If this set of species is unknown, it is recommended to provide initial conditions for all species. +- When providing initial conditions to compute conservation laws, values are only required for those species that are part of conserved quantities. If this set of species is unknown, it is recommended to provide initial conditions for all species. - Additional arguments provided to `hc_steady_states` are automatically passed to HomotopyContinuation's `solve` command. Use e.g. `show_progress = false` to disable the progress bar. --- + ## [Citation](@id homotopy_continuation_citation) + If you use this functionality in your research, please cite the following paper to support the authors of the HomotopyContinuation package: ``` @inproceedings{HomotopyContinuation.jl, @@ -77,7 +81,9 @@ If you use this functionality in your research, please cite the following paper --- + ## References + [^1]: [Andrew J Sommese, Charles W Wampler *The Numerical Solution of Systems of Polynomials Arising in Engineering and Science*, World Scientific (2005).](https://www.worldscientific.com/worldscibooks/10.1142/5763#t=aboutBook) [^2]: [Daniel J. Bates, Paul Breiding, Tianran Chen, Jonathan D. Hauenstein, Anton Leykin, Frank Sottile, *Numerical Nonlinear Algebra*, arXiv (2023).](https://arxiv.org/abs/2302.08585) -[^3]: [Paul Breiding, Sascha Timme, *HomotopyContinuation.jl: A Package for Homotopy Continuation in Julia*, International Congress on Mathematical Software (2018).](https://link.springer.com/chapter/10.1007/978-3-319-96418-8_54) \ No newline at end of file +[^3]: [Paul Breiding, Sascha Timme, *HomotopyContinuation.jl: A Package for Homotopy Continuation in Julia*, International Congress on Mathematical Software (2018).](https://link.springer.com/chapter/10.1007/978-3-319-96418-8_54) diff --git a/docs/src/steady_state_functionality/nonlinear_solve.md b/docs/src/steady_state_functionality/nonlinear_solve.md index 4a84198ee9..e658cd7e99 100644 --- a/docs/src/steady_state_functionality/nonlinear_solve.md +++ b/docs/src/steady_state_functionality/nonlinear_solve.md @@ -11,6 +11,7 @@ While these approaches only find a single steady state, they offer two advantage In practice, model steady states are found through [nonlinear system solving](@ref steady_state_solving_nonlinear) by creating a `NonlinearProblem`, and through forward ODE simulation by creating a `SteadyStateProblem`. These are then solved through solvers implemented in the [NonlinearSolve.jl](https://github.com/SciML/NonlinearSolve.jl), package (with the latter approach also requiring the [SteadyStateDiffEq.jl](https://github.com/SciML/SteadyStateDiffEq.jl) package). This tutorial describes how to find steady states through these two approaches. More extensive descriptions of available solvers and options can be found in [NonlinearSolve's documentation](https://docs.sciml.ai/NonlinearSolve/stable/). ## [Steady state finding through nonlinear solving](@id steady_state_solving_nonlinear) + Let us consider a simple dimerisation system, where a protein ($P$) can exist in a monomer and a dimer form. The protein is produced at a constant rate from its mRNA, which is also produced at a constant rate. ```@example steady_state_solving_nonlinear using Catalyst @@ -56,8 +57,9 @@ Typically, a good default method is automatically selected for any problem. Howe sol_ntr = solve(nlprob, TrustRegion()) sol ≈ sol_ntr ``` - + ### [Systems with conservation laws](@id steady_state_solving_nonlinear_conservation_laws) + As described in the section on homotopy continuation, when finding the steady states of systems with conservation laws, [additional considerations have to be taken](@ref homotopy_continuation_conservation_laws). E.g. consider the following [two-state system](@ref basic_CRN_library_two_states): ```@example steady_state_solving_claws using Catalyst, NonlinearSolve # hide @@ -84,6 +86,7 @@ sol[[:X1, :X2]] ``` ## [Finding steady states through ODE simulations](@id steady_state_solving_simulation) + The `NonlinearProblem`s generated by Catalyst corresponds to ODEs. A common method of solving these is to simulate the ODE from an initial condition until a steady state is reached. Here we do so for the dimerisation system considered in the previous section. First, we declare our model, initial condition, and parameter values. ```@example steady_state_solving_simulation using Catalyst # hide @@ -132,7 +135,9 @@ However, especially when the forward ODE simulation approach is used, it is reco --- + ## [Citations](@id nonlinear_solve_citation) + 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 NonlinearSolve.jl package: ``` @article{pal2024nonlinearsolve, @@ -145,5 +150,7 @@ If you use this functionality in your research, [in addition to Catalyst](@ref d --- + ## References + [^1]: [J. Nocedal, S. J. Wright, *Numerical Optimization*, Springer (2006).](https://www.math.uci.edu/~qnie/Publications/NumericalOptimization.pdf) diff --git a/docs/src/steady_state_functionality/steady_state_stability_computation.md b/docs/src/steady_state_functionality/steady_state_stability_computation.md index 8404498231..430808bd50 100644 --- a/docs/src/steady_state_functionality/steady_state_stability_computation.md +++ b/docs/src/steady_state_functionality/steady_state_stability_computation.md @@ -1,14 +1,16 @@ # [Steady state stability computation](@id steady_state_stability) + After system steady states have been found using [HomotopyContinuation.jl](@ref homotopy_continuation), [NonlinearSolve.jl](@ref steady_state_solving), or other means, their stability can be computed using Catalyst's `steady_state_stability` function. Systems with conservation laws will automatically have these removed, permitting stability computation on systems with singular Jacobian. -!!! warning +!!! warning Catalyst currently computes steady state stabilities using the naive approach of checking whether a system's largest eigenvalue real part is negative. Furthermore, Catalyst uses a tolerance `tol = 10*sqrt(eps())` to determine whether a computed eigenvalue is far away enough from 0 to be reliably considered non-zero. This threshold can be changed through the `tol` keyword argument. ## [Basic examples](@id steady_state_stability_basics) + Let us consider the following basic example: ```@example stability_1 using Catalyst -rn = @reaction_network begin +rn = @reaction_network begin (p,d), 0 <--> X end ``` @@ -21,7 +23,7 @@ steady_state_stability(steady_state, rn, ps) Next, let us consider the following [self-activation loop](@ref basic_CRN_library_self_activation): ```@example stability_1 -sa_loop = @reaction_network begin +sa_loop = @reaction_network begin (hill(X,v,K,n),d), 0 <--> X end ``` @@ -43,7 +45,8 @@ nothing# hide ``` ## [Pre-computing the Jacobian to increase performance when computing stability for many steady states](@id steady_state_stability_jacobian) -Catalyst uses the system Jacobian to compute steady state stability, and the Jacobian is computed once for each call to `steady_state_stability`. If you repeatedly compute stability for steady states of the same system, pre-computing the Jacobian and supplying it to the `steady_state_stability` function can improve performance. + +Catalyst uses the system Jacobian to compute steady state stability, and the Jacobian is computed once for each call to `steady_state_stability`. If you repeatedly compute stability for steady states of the same system, pre-computing the Jacobian and supplying it to the `steady_state_stability` function can improve performance. In this example we use the self-activation loop from previously, pre-computes its Jacobian, and uses it to multiple `steady_state_stability` calls: ```@example stability_1 diff --git a/docs/src/v14_migration_guide.md b/docs/src/v14_migration_guide.md index 2603472fb0..ec15aab5e3 100644 --- a/docs/src/v14_migration_guide.md +++ b/docs/src/v14_migration_guide.md @@ -6,6 +6,7 @@ Catalyst is built on the [ModelingToolkit.jl](https://github.com/SciML/ModelingT Catalyst version 14 also introduces several new features. These will not be discussed here, however, they are described in Catalyst's [history file](https://github.com/SciML/Catalyst.jl/blob/master/HISTORY.md). ## System completeness + In ModelingToolkit v9 (and thus also Catalyst v14) all systems (e.g. `ReactionSystem`s and `ODESystem`s) are either *complete* or *incomplete*. Complete and incomplete systems differ in that - Only complete systems can be used as inputs to simulations or most tools for model analysis. - Only incomplete systems can be [composed with other systems to form hierarchical models](@ref compositional_modeling). @@ -87,6 +88,7 @@ Catalyst.iscomplete(rs3) ``` ## Unknowns instead of states + Previously, "states" was used as a term for system variables (both species and non-species variables). MTKv9 has switched to using the term "unknowns" instead. This means that there have been a number of changes to function names (e.g. `states` => `unknowns` and `get_states` => `get_unknowns`). E.g. here we declare a `ReactionSystem` model containing both species and non-species unknowns: @@ -118,6 +120,7 @@ nonspecies(rs) ``` ## Lost support for most units + As part of its v9 update, ModelingToolkit changed how units were handled. This includes using the package [DynamicQuantities.jl](https://github.com/SymbolicML/DynamicQuantities.jl) to manage units (instead of [Unitful.jl](https://github.com/PainterQubits/Unitful.jl), like previously). While this should lead to long-term improvements, unfortunately, as part of the process support for most units was removed. Currently, only the main SI units are supported (`s`, `m`, `kg`, `A`, `K`, `mol`, and `cd`). Composite units (e.g. `N = kg/(m^2)`) are no longer supported. Furthermore, prefix units (e.g. `mm = m/1000`) are not supported either. This means that most units relevant to Catalyst (such as `µM`) cannot be used directly. While composite units can still be written out in full and used (e.g. `kg/(m^2)`) this is hardly user-friendly. @@ -125,11 +128,13 @@ While this should lead to long-term improvements, unfortunately, as part of the The maintainers of ModelingToolkit have been notified of this issue. We are unsure when this will be fixed, however, we do not think it will be a permanent change. ## Removed support for system-mutating functions + According to the ModelingToolkit system API, systems should not be mutable. In accordance with this, the following functions have been deprecated and removed: `addparam!`, `addreaction!`, `addspecies!`, `@add_reactions`, and `merge!`. Please use `ModelingToolkit.extend` and `ModelingToolkit.compose` to generate new merged and/or composed `ReactionSystems` from multiple component systems. It is still possible to add default values to a created `ReactionSystem`, i.e. the `setdefaults!` function is still supported. ## New interface for creating time variable (`t`) and its differential (`D`) + Previously, the time-independent variable (typically called `t`) was declared using ```@example v14_migration_3 using Catalyst @@ -158,6 +163,7 @@ nothing # hide If you look at ModelingToolkit documentation, these defaults are instead retrieved using `using ModelingToolkit: t_nounits as t, D_nounits as D`. This will also work, however, in Catalyst we have opted to instead use the functions `default_t()` and `default_time_deriv()` as our main approach. ## New interface for accessing problem/integrator/solution parameter (and species) values + Previously, it was possible to directly index problems to query them for their parameter values. e.g. ```@example v14_migration_4 using Catalyst @@ -184,6 +190,7 @@ For more details on how to query various structures for parameter and species va ## Other changes #### Modification of problems with conservation laws broken + While it is possible to update e.g. `ODEProblem`s using the [`remake`](@ref simulation_structure_interfacing_problems_remake) function, this is currently not possible if the `remove_conserved = true` option was used. E.g. while ```@example v14_migration_5 using Catalyst, OrdinaryDiffEqDefault @@ -207,9 +214,11 @@ This might generate a silent error, where the remade problem is different from t This bug was likely present on earlier versions as well, but was only recently discovered. While we hope it will be fixed soon, the issue is in ModelingToolkit, and will not be fixed until its maintainers find the time to do so. #### Depending on parameter order is even more dangerous than before + In early versions of Catalyst, parameters and species were provided as vectors (e.g. `[1.0, 2.0]`) rather than maps (e.g. `[p => 1.0, d => 2.0]`). While we previously *strongly* recommended users to use the map form (or they might produce unintended results), the vector form was still supported (technically). Due to recent internal ModelingToolkit updates, the purely numeric form is no longer supported and should never be used -- it will potentially lead to incorrect values for parameters and/or initial conditions. Note that if `rn` is a complete `ReactionSystem` you can now specify such mappings via `[rn.p => 1.0, rn.d => 2.0]`. *Users should never use vector-forms to represent parameter and species values* #### Additional deprecated functions + The `reactionparams`, `numreactionparams`, and `reactionparamsmap` functions have been deprecated. diff --git a/docs/unpublished/pdes.md b/docs/unpublished/pdes.md index 46e758bd6e..3f1bd2fb9e 100644 --- a/docs/unpublished/pdes.md +++ b/docs/unpublished/pdes.md @@ -1,4 +1,5 @@ # Partial Differential Equation Models + **Note** this functionality is work in progress in both Catalyst, ModelingToolkit, and MethodOfLines (generally across SciML). As such, the recommended workflows, API features, and observed simulation performance may From 275e9230b038c8fa742160cf3cc0fa1574d21a4e Mon Sep 17 00:00:00 2001 From: Stefan Pinnow Date: Tue, 14 Oct 2025 20:50:32 +0200 Subject: [PATCH 02/26] docs: MD031/blanks-around-fences --- docs/old_files/advanced.md | 4 + .../unused_tutorials/advanced_examples.md | 1 + docs/old_files/unused_tutorials/models.md | 15 +++ docs/src/api/core_api.md | 19 ++++ docs/src/faqs.md | 90 +++++++++++++++ docs/src/index.md | 29 +++++ .../catalyst_for_new_julia_users.md | 44 ++++++++ .../introduction_to_catalyst.md | 42 +++++++ .../math_models_intro.md | 42 +++++++ .../behaviour_optimisation.md | 13 +++ .../examples/ode_fitting_oscillation.md | 12 ++ .../global_sensitivity_analysis.md | 22 ++++ .../optimization_ode_param_fitting.md | 30 +++++ .../petab_ode_param_fitting.md | 70 ++++++++++++ .../structural_identifiability.md | 24 ++++ .../chemistry_related_functionality.md | 35 ++++++ .../model_creation/compositional_modeling.md | 37 +++++++ docs/src/model_creation/conservation_laws.md | 37 +++++++ .../model_creation/constraint_equations.md | 25 +++++ docs/src/model_creation/dsl_advanced.md | 104 ++++++++++++++++++ docs/src/model_creation/dsl_basics.md | 61 ++++++++++ .../examples/basic_CRN_library.md | 44 ++++++++ .../examples/hodgkin_huxley_equation.md | 8 ++ .../examples/noise_modelling_approaches.md | 19 ++++ .../programmatic_generative_linear_pathway.md | 22 ++++ .../smoluchowski_coagulation_equation.md | 14 +++ .../model_creation/functional_parameters.md | 30 +++++ .../model_file_loading_and_export.md | 21 ++++ .../src/model_creation/model_visualisation.md | 18 +++ .../parametric_stoichiometry.md | 31 ++++++ .../programmatic_CRN_construction.md | 40 +++++++ .../reactionsystem_content_accessing.md | 58 ++++++++++ .../model_simulation/ensemble_simulations.md | 15 +++ ...ctivation_time_distribution_measurement.md | 12 ++ .../interactive_brusselator_simulation.md | 41 ++++--- .../examples/periodic_events_simulation.md | 16 +++ .../finite_state_projection_simulation.md | 32 ++++++ .../ode_simulation_performance.md | 53 +++++++++ .../sde_simulation_performance.md | 8 ++ .../simulation_introduction.md | 56 ++++++++++ .../model_simulation/simulation_plotting.md | 15 +++ .../simulation_structure_interfacing.md | 36 ++++++ docs/src/network_analysis/crn_theory.md | 41 +++++++ .../network_analysis/network_properties.md | 8 ++ docs/src/network_analysis/odes.md | 55 +++++++++ .../lattice_reaction_systems.md | 38 +++++++ .../lattice_simulation_plotting.md | 16 +++ ...ttice_simulation_structure_ interaction.md | 24 ++++ .../spatial_jump_simulations.md | 2 + .../spatial_ode_simulations.md | 3 + .../bifurcation_diagrams.md | 19 ++++ .../dynamical_systems.md | 21 ++++ .../examples/bifurcationkit_codim2.md | 24 ++++ .../bifurcationkit_periodic_orbits.md | 21 ++++ .../examples/nullcline_plotting.md | 10 ++ .../homotopy_continuation.md | 8 ++ .../nonlinear_solve.md | 27 +++++ .../steady_state_stability_computation.md | 10 ++ docs/src/v14_migration_guide.md | 36 ++++++ docs/unpublished/pdes.md | 6 + 60 files changed, 1696 insertions(+), 18 deletions(-) diff --git a/docs/old_files/advanced.md b/docs/old_files/advanced.md index e6f81f00d5..d46af2afa7 100644 --- a/docs/old_files/advanced.md +++ b/docs/old_files/advanced.md @@ -7,12 +7,14 @@ chemical reaction network models (still not very complicated!). 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/). @@ -21,11 +23,13 @@ Symbolics.jl before their use in Catalyst, see the discussion 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 9b1e77da70..c1452c0bdd 100644 --- a/docs/old_files/unused_tutorials/advanced_examples.md +++ b/docs/old_files/unused_tutorials/advanced_examples.md @@ -12,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 diff --git a/docs/old_files/unused_tutorials/models.md b/docs/old_files/unused_tutorials/models.md index 12b1362cb4..98d6cff567 100644 --- a/docs/old_files/unused_tutorials/models.md +++ b/docs/old_files/unused_tutorials/models.md @@ -9,10 +9,13 @@ and more broadly used within [SciML](https://sciml.ai) packages. 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 @@ -25,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 @@ -36,19 +40,24 @@ 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) ``` + ![models1](../assets/models1.svg) 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())) @@ -57,10 +66,12 @@ 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. @@ -69,6 +80,7 @@ Phys. 2000) will be used to generate stochastic differential equations. 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 @@ -82,11 +94,14 @@ 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) ``` + ![models2](../assets/models2.svg) ### [`Reaction`](@ref) fields diff --git a/docs/src/api/core_api.md b/docs/src/api/core_api.md index ec4140b2e1..c05909c3ff 100644 --- a/docs/src/api/core_api.md +++ b/docs/src/api/core_api.md @@ -18,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. @@ -195,6 +197,7 @@ isautonomous 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 @@ -205,6 +208,7 @@ ModelingToolkit.diff_equations ## Basic species properties The following functions permits the querying of species properties. + ```@docs isspecies Catalyst.isconstant @@ -227,6 +231,7 @@ 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 @@ -260,25 +265,31 @@ isequivalent [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. + ```@docs plot_network(::ReactionSystem) plot_complexes(::ReactionSystem) @@ -289,6 +300,7 @@ plot_complexes(::ReactionSystem) 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 @@ -311,6 +323,7 @@ set_default_noise_scaling ## Chemistry-related functionalities Various functionalities primarily relevant to modelling of chemical systems (but potentially also in biology). + ```@docs @compound @compounds @@ -336,11 +349,13 @@ 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 @@ -360,15 +375,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! @@ -378,6 +396,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/faqs.md b/docs/src/faqs.md index 4027f6460d..f0f0d79f69 100644 --- a/docs/src/faqs.md +++ b/docs/src/faqs.md @@ -5,6 +5,7 @@ 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 @@ -12,49 +13,65 @@ 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]) ``` @@ -67,14 +84,17 @@ 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 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 @@ -91,7 +111,9 @@ rn = @reaction_network begin k, 2.5*A --> 3*B end ``` + or directly via + ```@example faq2 t = default_t() @parameters k b @@ -104,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 @@ -121,6 +144,7 @@ parametric_stoichiometry) section. 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 @@ -135,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() @@ -152,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) @@ -164,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 @@ -179,6 +206,7 @@ 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 @@ -190,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] @@ -206,6 +236,7 @@ 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() @@ -217,8 +248,10 @@ 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) @@ -231,6 +264,7 @@ 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 @@ -249,6 +283,7 @@ dAdteq = Equation(dAdteq.lhs, dAdteq.rhs + 1 + sin(t)) 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 @@ -256,31 +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) @@ -288,6 +329,7 @@ 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/). @@ -295,12 +337,14 @@ Symbolics.jl before their use in Catalyst, see the discussion ## [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). @@ -316,6 +360,7 @@ Inference of species, variables, and parameters follows the following steps: 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 + ```@example faq_dsl_inference using Catalyst rn = @reaction_network begin @@ -324,6 +369,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. @@ -333,6 +379,7 @@ 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 @@ -340,7 +387,9 @@ rn_error = @reaction_network begin 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 @@ -369,6 +418,7 @@ 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) @@ -376,8 +426,10 @@ nsys = convert(NonlinearSystem, rn; remove_conserved = true) # 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 @@ -390,66 +442,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 + ```@example faq_remake 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₃)]) ``` + 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] ``` + 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₃)]) ``` + and + ```@example faq_remake println("Correct Γ is: ", 4.0, "\n", "remade value is: ", nlprob4.ps[:Γ][1]) ``` @@ -458,39 +536,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 + ```@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₃)]) ``` + 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. diff --git a/docs/src/index.md b/docs/src/index.md index af89d3e3f9..f29d49f317 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -64,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 @@ -90,29 +97,35 @@ 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") @@ -120,9 +133,11 @@ Pkg.add("Plots") ``` 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 installed into an existing environment, such as the default Julia global environment, the presence @@ -164,6 +179,7 @@ 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. @@ -186,6 +202,7 @@ 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 declare our model: + ```@example home_elaborate_example using Catalyst cell_model = @reaction_network begin @@ -200,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) \\ @@ -215,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) @@ -235,6 +258,7 @@ The software in this ecosystem was developed as part of academic research. If yo 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: + ``` @article{CatalystPLOSCompBio2023, doi = {10.1371/journal.pcbi.1011530}, @@ -256,20 +280,25 @@ could cite our work: ```@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 b903617725..f5923f2a64 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 @@ -11,6 +11,7 @@ Julia can be downloaded [here](https://julialang.org/downloads/). Generally, it 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 @@ -18,33 +19,44 @@ 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). @@ -54,21 +66,27 @@ This is useful to know when you e.g. declare, simulate, or plot, a Catalyst mode 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/). + ```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/). @@ -82,12 +100,14 @@ 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 @@ -96,16 +116,19 @@ Next, we wish to simulate our model. To do this, we need to provide some additio * 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 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. + ```@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] ``` @@ -113,16 +136,19 @@ 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). 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) ``` @@ -136,6 +162,7 @@ For more information about the numerical simulation package, please see the [Dif 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) + ```julia Pkg.add("JumpProcesses") using JumpProcesses @@ -155,15 +182,18 @@ Each reaction is also associated with a specific rate (corresponding to a parame * *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) @@ -172,13 +202,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 @@ -196,10 +229,12 @@ We have previously introduced how to install and activate Julia packages. While 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. @@ -210,17 +245,23 @@ This will: ### [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) @@ -230,11 +271,14 @@ We have previously described how to set up new Julia environments, how to instal 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() ``` diff --git a/docs/src/introduction_to_catalyst/introduction_to_catalyst.md b/docs/src/introduction_to_catalyst/introduction_to_catalyst.md index 1dfbade273..e5f6d1c31e 100644 --- a/docs/src/introduction_to_catalyst/introduction_to_catalyst.md +++ b/docs/src/introduction_to_catalyst/introduction_to_catalyst.md @@ -13,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 @@ -29,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 ``` @@ -40,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₁ @@ -57,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 @@ -64,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: + ```@example tut1 using Catalyst import CairoMakie, GraphMakie, NetworkLayout @@ -99,16 +112,19 @@ 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) ``` @@ -118,9 +134,11 @@ save("repressilator_graph.png", g) 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.) @@ -130,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) @@ -145,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: @@ -156,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. @@ -177,6 +202,7 @@ plot the solutions: 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/). @@ -220,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) @@ -289,6 +316,7 @@ 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 @@ -300,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) @@ -325,19 +355,26 @@ 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!}, & @@ -345,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, & diff --git a/docs/src/introduction_to_catalyst/math_models_intro.md b/docs/src/introduction_to_catalyst/math_models_intro.md index f5f256c8d0..bd34d2959b 100644 --- a/docs/src/introduction_to_catalyst/math_models_intro.md +++ b/docs/src/introduction_to_catalyst/math_models_intro.md @@ -17,40 +17,53 @@ documentation for more details on how Catalyst supports such functionality. ## 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: 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. ``` @@ -59,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) @@ -67,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 @@ -89,18 +109,23 @@ 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 @@ -110,7 +135,9 @@ 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) ``` @@ -118,14 +145,17 @@ 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 @@ -133,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 @@ -147,20 +179,25 @@ 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 @@ -168,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) \\ @@ -176,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] \\ @@ -184,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$. diff --git a/docs/src/inverse_problems/behaviour_optimisation.md b/docs/src/inverse_problems/behaviour_optimisation.md index 7ff38ac9ec..1abbf8bbd4 100644 --- a/docs/src/inverse_problems/behaviour_optimisation.md +++ b/docs/src/inverse_problems/behaviour_optimisation.md @@ -7,6 +7,7 @@ In previous tutorials we have described how to use [PEtab.jl](https://github.com 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 @@ -17,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] @@ -28,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]]) @@ -42,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] @@ -70,6 +81,7 @@ 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) @@ -81,6 +93,7 @@ How to use Optimization.jl is discussed in more detail in [this tutorial](@ref o ## [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: + ``` @software{vaibhav_kumar_dixit_2023_7738525, author = {Vaibhav Kumar Dixit and Christopher Rackauckas}, diff --git a/docs/src/inverse_problems/examples/ode_fitting_oscillation.md b/docs/src/inverse_problems/examples/ode_fitting_oscillation.md index 4962cc8d01..6a3c40158a 100644 --- a/docs/src/inverse_problems/examples/ode_fitting_oscillation.md +++ b/docs/src/inverse_problems/examples/ode_fitting_oscillation.md @@ -3,6 +3,7 @@ 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 @@ -12,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 @@ -25,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 @@ -38,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)) @@ -52,6 +56,7 @@ 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`, `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, @@ -77,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) @@ -98,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) @@ -105,15 +113,18 @@ 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 @@ -123,6 +134,7 @@ 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 826921af01..162a6cabf2 100644 --- a/docs/src/inverse_problems/global_sensitivity_analysis.md +++ b/docs/src/inverse_problems/global_sensitivity_analysis.md @@ -15,6 +15,7 @@ While local sensitivities are primarily used as a subroutine of other methodolog ## [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 @@ -23,7 +24,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 @@ -40,17 +43,20 @@ 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 @@ -64,10 +70,12 @@ on the domain $10^β ∈ (-3.0,-1.0)$, $10^a ∈ (-2.0,0.0)$, $10^γ ∈ (-2.0,0 ## [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: @@ -76,25 +84,31 @@ Sobol's method computes so-called *Sobol indices*, each measuring some combinati - `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 @@ -105,11 +119,13 @@ Morris's method computes, for parameter samples across parameter space, their *e - `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 @@ -125,6 +141,7 @@ GlobalSensitivity also implements additional methods for GSA, more details on th ## [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]] @@ -137,14 +154,18 @@ 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. --- @@ -152,6 +173,7 @@ Here, the function's sensitivity is evaluated with respect to each output indepe ## [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: + ``` @article{dixit2022globalsensitivity, title={GlobalSensitivity. jl: Performant and Parallel Global Sensitivity Analysis with Julia}, diff --git a/docs/src/inverse_problems/optimization_ode_param_fitting.md b/docs/src/inverse_problems/optimization_ode_param_fitting.md index f7ee562589..cf066b98c0 100644 --- a/docs/src/inverse_problems/optimization_ode_param_fitting.md +++ b/docs/src/inverse_problems/optimization_ode_param_fitting.md @@ -11,6 +11,7 @@ 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 using Catalyst rn = @reaction_network begin @@ -19,7 +20,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 optimization_paramfit_1 # Define initial conditions and parameters. u0 = [:S => 1.0, :E => 1.0, :SE => 0.0, :P => 0.0] @@ -43,6 +46,7 @@ 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 ps_init = [:kB => 1.0, :kD => 1.0, :kP => 1.0] oprob_base = ODEProblem(rn, u0, (0.0, 10.0), ps_init) @@ -54,6 +58,7 @@ 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. @@ -62,12 +67,14 @@ 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. @@ -77,6 +84,7 @@ nothing # hide 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 using OptimizationNLopt optsol = solve(optprob, NLopt.LN_NELDERMEAD()) @@ -84,6 +92,7 @@ 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 oprob_fitted = remake(oprob_base; p = Pair.([:kB, :kD, :kP], optsol.u)) fitted_sol = solve(oprob_fitted) @@ -96,11 +105,13 @@ Catalyst.PNG(plot(plt; fmt = :png, dpi = 200)) # hide 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 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) @@ -108,12 +119,15 @@ to solve `optprob` for this combination of solve and implementation. 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()) @@ -122,6 +136,7 @@ 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 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] @@ -136,6 +151,7 @@ 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 function objective_function_S_P(p, _) p = Pair.([:kB, :kD, :kP], p) @@ -145,9 +161,11 @@ 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 optprob_S_P = OptimizationProblem(objective_function_S_P, p_guess) optsol_S_P = solve(optprob_S_P, NLopt.LN_NELDERMEAD()) @@ -162,6 +180,7 @@ 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 optprob = OptimizationProblem(objective_function, [1.0, 1.0, 1.0]; lb = [1e-1, 1e-1, 1e-1], ub = [1e1, 1e1, 1e1]) nothing # hide @@ -172,6 +191,7 @@ In addition to boundaries, Optimization.jl also supports setting [linear and non ## [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 function objective_function_known_kD(p, _) p = Pair.([:kB, :kD, :kP], [p[1], 0.1, p[2]]) @@ -181,7 +201,9 @@ 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 optprob_known_kD = OptimizationProblem(objective_function_known_kD, [1.0, 1.0]) optsol_known_kD = solve(optprob_known_kD, NLopt.LN_NELDERMEAD()) @@ -191,15 +213,18 @@ 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 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 @@ -208,12 +233,16 @@ 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 function objective_function_logtransformed(p, _) p = Pair.([:kB, :kD, :kP], 10.0 .^ p) @@ -229,6 +258,7 @@ 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: + ``` @software{vaibhav_kumar_dixit_2023_7738525, author = {Vaibhav Kumar Dixit and Christopher Rackauckas}, diff --git a/docs/src/inverse_problems/petab_ode_param_fitting.md b/docs/src/inverse_problems/petab_ode_param_fitting.md index 683253a66c..25f9d958d0 100644 --- a/docs/src/inverse_problems/petab_ode_param_fitting.md +++ b/docs/src/inverse_problems/petab_ode_param_fitting.md @@ -7,6 +7,7 @@ While PEtab's interface generally is very flexible, there might be specific use- ## 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 @@ -16,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] @@ -65,6 +68,7 @@ Each parameter of the system can either be 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) @@ -72,6 +76,7 @@ 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 @@ -92,16 +97,19 @@ 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). ### 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 @@ -110,12 +118,15 @@ 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) @@ -125,6 +136,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) @@ -142,6 +154,7 @@ PEtab.jl also supports [multistart optimisation](@ref petab_multistart_optimisat ### [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 @@ -156,17 +169,22 @@ A common application for this is to define an [*offset* and a *scale* for each o ### [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) ``` @@ -176,13 +194,16 @@ obs_P = PEtabObservable(P, σ; transformation = :log) ### [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 @@ -191,9 +212,11 @@ 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) @@ -212,10 +235,13 @@ If we have prior knowledge about the distribution of a parameter, it is possible 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) @@ -223,19 +249,24 @@ In this example, setting `prior_on_linear_scale=false` makes sense as a (linear) 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 @@ -283,6 +314,7 @@ 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) @@ -290,13 +322,16 @@ Note that the `u0` we pass into `PEtabModel` through the `speciemap` argument no ### [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) @@ -304,12 +339,15 @@ 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 @@ -321,6 +359,7 @@ Sometimes, the parameters that are used vary between the different conditions. C ### [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 @@ -332,12 +371,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) @@ -346,6 +389,7 @@ 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) @@ -353,6 +397,7 @@ and we can use our updated `rn`, `u0`, and `params` as input to our `PEtabModel` 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 @@ -391,6 +436,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) @@ -398,6 +444,7 @@ 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, @@ -406,6 +453,7 @@ PEtabODEProblem(petab_model, 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) @@ -413,6 +461,7 @@ where we simulate our ODE model using the `Rodas5P` method (with absolute and re ### [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 @@ -423,22 +472,27 @@ 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`. @@ -458,6 +512,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 @@ -467,19 +522,24 @@ 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) @@ -490,17 +550,22 @@ So far, we have assumed that all experiments, after initiation, run without inte 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] @@ -518,12 +583,14 @@ More details on how to use events, including how to create events with multiple 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) @@ -534,6 +601,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) ``` @@ -545,6 +613,7 @@ There exist several types of plots for both types of calibration results. More d ## [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: + ``` @misc{2023Petabljl, author = {Ognissanti, Damiano AND Arutjunjan, Rafael AND Persson, Sebastian AND Hasselgren, Viktor}, @@ -553,6 +622,7 @@ If you use this functionality in your research, [in addition to Catalyst](@ref d year = {2023} } ``` + ``` @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}, diff --git a/docs/src/inverse_problems/structural_identifiability.md b/docs/src/inverse_problems/structural_identifiability.md index 557d3625a5..a650137f24 100644 --- a/docs/src/inverse_problems/structural_identifiability.md +++ b/docs/src/inverse_problems/structural_identifiability.md @@ -32,6 +32,7 @@ Global identifiability can be assessed using the `assess_identifiability` functi - 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,20 +42,25 @@ 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. @@ -62,13 +68,16 @@ To, in a similar manner, indicate that certain initial conditions are known is a ### [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) @@ -78,6 +87,7 @@ 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) @@ -87,26 +97,32 @@ 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) 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). @@ -114,11 +130,14 @@ Again, these results are consistent with those produced by `assess_identifiabili ## [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 @@ -127,23 +146,27 @@ 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). --- @@ -151,6 +174,7 @@ is currently not possible. Hopefully this will be a supported feature in the fut ## [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: + ``` @article{structidjl, author = {Dong, R. and Goodbrake, C. and Harrington, H. and Pogudin G.}, diff --git a/docs/src/model_creation/chemistry_related_functionality.md b/docs/src/model_creation/chemistry_related_functionality.md index 1e33301fa0..a407e9c28a 100644 --- a/docs/src/model_creation/chemistry_related_functionality.md +++ b/docs/src/model_creation/chemistry_related_functionality.md @@ -10,37 +10,50 @@ While Catalyst has primarily been designed around the modelling of biological sy ### [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 @@ -48,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 @@ -59,6 +73,7 @@ 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) @@ -70,7 +85,9 @@ 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 rn = @reaction_network begin @compounds begin @@ -81,51 +98,65 @@ 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) @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() @@ -138,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] ``` @@ -151,6 +184,7 @@ Reactions declared as a part of a `ReactionSystem` (e.g. using the DSL) can be r ### 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) @@ -166,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. diff --git a/docs/src/model_creation/compositional_modeling.md b/docs/src/model_creation/compositional_modeling.md index e5377f884f..ad9fb9d735 100644 --- a/docs/src/model_creation/compositional_modeling.md +++ b/docs/src/model_creation/compositional_modeling.md @@ -11,13 +11,16 @@ compartments. 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() @@ -25,12 +28,16 @@ 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) @@ -42,6 +49,7 @@ 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 @@ -52,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`. @@ -59,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) ``` + ![rn network with subsystems](../assets/rn_treeplot.svg) We could also have directly constructed `rn` using the same reaction as in `basern` as + ```@example ex1 t = default_t() @parameters k @@ -94,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) ``` @@ -130,6 +157,7 @@ in the [ModelingToolkit docs](http://docs.sciml.ai/ModelingToolkit/stable/). 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 @@ -141,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) @@ -154,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) ``` + ![repressilator tree plot](../assets/repressilator_treeplot.svg) In building the repressilator we needed to use two new features. First, we @@ -188,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 @@ -211,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) ``` + ![graph of gene regulation model](../assets/compartment_gene_regulation.svg) diff --git a/docs/src/model_creation/conservation_laws.md b/docs/src/model_creation/conservation_laws.md index 0117d5f3a5..b4659aa98a 100644 --- a/docs/src/model_creation/conservation_laws.md +++ b/docs/src/model_creation/conservation_laws.md @@ -3,13 +3,16 @@ 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] @@ -18,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] @@ -54,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`. @@ -78,26 +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]] @@ -105,6 +136,7 @@ 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). !!! 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]`). @@ -112,17 +144,21 @@ Generally, for each conservation law, one can omit specifying either the conserv 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₂] @@ -140,6 +176,7 @@ If the value of the conservation law parameter $Γ$'s value *has never been spec ### [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 ``` diff --git a/docs/src/model_creation/constraint_equations.md b/docs/src/model_creation/constraint_equations.md index f518f8c986..fd69118301 100644 --- a/docs/src/model_creation/constraint_equations.md +++ b/docs/src/model_creation/constraint_equations.md @@ -22,6 +22,7 @@ produced at a rate proportional $V$ and can be degraded. 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 @@ -45,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()) @@ -57,6 +60,7 @@ plot(sol) 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 @@ -113,6 +117,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 @@ -123,6 +128,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) @@ -130,6 +136,7 @@ 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()) @@ -179,15 +186,19 @@ rn = @reaction_network growing_cell begin 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 @@ -206,6 +217,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 @@ -213,12 +225,14 @@ 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) @@ -228,6 +242,7 @@ on event handling using callbacks. 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() @@ -240,12 +255,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) @@ -255,9 +274,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 @@ -265,7 +286,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] @@ -273,7 +296,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 30ab82b3d0..2897220e7e 100644 --- a/docs/src/model_creation/dsl_advanced.md +++ b/docs/src/model_creation/dsl_advanced.md @@ -5,6 +5,7 @@ Within the Catalyst DSL, each line can represent either *a reaction* or *an opti 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 ``` @@ -12,16 +13,21 @@ 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) @@ -29,21 +35,25 @@ 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 @@ -59,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 @@ -66,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 @@ -75,6 +88,7 @@ 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. @@ -93,6 +107,7 @@ Generally, there are four main reasons for specifying species/parameters using t ### [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 @@ -101,7 +116,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 = [] @@ -111,7 +128,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] @@ -119,7 +138,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 @@ -137,6 +158,7 @@ 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₀`. + ```@example dsl_advanced_defaults rn = @reaction_network begin @species X(t)=X₀ @@ -144,7 +166,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 = [] @@ -153,7 +177,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] @@ -161,6 +187,7 @@ 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) @@ -168,6 +195,7 @@ Please note that `X₀` is still a parameter of the system, and as such its valu 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 @@ -176,7 +204,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)] @@ -185,6 +215,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)] @@ -193,6 +224,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 @@ -204,6 +236,7 @@ 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) ``` @@ -211,6 +244,7 @@ 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 @@ -218,11 +252,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ᴾ @@ -234,6 +272,7 @@ A common use-case for constant species is when modelling systems where some spec ### [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 @@ -242,13 +281,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."] @@ -260,6 +302,7 @@ 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 @@ -268,7 +311,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]] @@ -290,11 +335,15 @@ rn = @reaction_network begin (k1, k2), A <--> B end ``` + Running the code above will yield the following error: + ``` 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. using Catalyst @@ -318,6 +367,7 @@ The following cases in which the DSL would normally infer variables will all thr ## [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 @@ -325,7 +375,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 @@ -334,6 +386,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 @@ -343,11 +396,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 @@ -357,6 +414,7 @@ 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. @@ -366,6 +424,7 @@ Setting model names is primarily useful for [hierarchical modelling](@ref compos 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 @@ -376,7 +435,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] @@ -388,19 +449,24 @@ 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 + ```@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 @@ -408,10 +474,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 @@ -421,6 +489,7 @@ 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) @@ -439,6 +508,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 @@ -447,12 +517,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 @@ -462,7 +535,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 @@ -479,6 +554,7 @@ species(rn) ## [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 @@ -489,6 +565,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"]) @@ -497,6 +574,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] @@ -506,6 +584,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) @@ -520,6 +599,7 @@ We have previously described how Catalyst represents its models symbolically (en ### [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 @@ -527,16 +607,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] @@ -555,6 +641,7 @@ plot(sol) 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() @@ -564,7 +651,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 @@ -575,9 +664,11 @@ 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 @@ -593,16 +684,19 @@ nothing # hide ## [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 @@ -613,13 +707,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) @@ -633,32 +730,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. 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$. diff --git a/docs/src/model_creation/dsl_basics.md b/docs/src/model_creation/dsl_basics.md index f37feee2c1..d01f984d5b 100644 --- a/docs/src/model_creation/dsl_basics.md +++ b/docs/src/model_creation/dsl_basics.md @@ -5,6 +5,7 @@ In the [introduction to Catalyst](@ref introduction_to_catalyst) we described ho 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 ``` @@ -12,17 +13,20 @@ 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 @@ -30,6 +34,7 @@ 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*. @@ -42,6 +47,7 @@ Finally, `rn = ` is used to store the model in the variable `rn` (a normal Julia ## [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 @@ -50,12 +56,14 @@ 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) @@ -63,13 +71,16 @@ Generally, anything that is a [permitted Julia variable name](https://docs.julia ### [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 @@ -79,6 +90,7 @@ 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 @@ -89,15 +101,18 @@ 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) @@ -110,57 +125,72 @@ end ### [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 @@ -168,6 +198,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) @@ -175,6 +206,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 @@ -183,6 +215,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) @@ -190,19 +223,23 @@ 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. @@ -211,29 +248,36 @@ is not permitted (due to this notation's similarity to a bidirectional reaction) 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. @@ -242,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 @@ -253,13 +298,16 @@ 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 @@ -271,6 +319,7 @@ 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 @@ -288,6 +337,7 @@ Catalyst comes with the following predefined functions: ### [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) @@ -300,6 +350,7 @@ 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 @@ -308,6 +359,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 @@ -327,23 +379,28 @@ end ### [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] @@ -358,6 +415,7 @@ Julia permits any Unicode characters to be used in variable names, thus Catalyst ### [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 @@ -373,6 +431,7 @@ Catalyst uses `-->`, `<-->`, and `<--` to denote forward, bi-directional, and ba - `<`, `←`, `↢`, `↤`, `⇽`, `⟵`, `⟻`, `⥚`, `⥞`, `↼`, , 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 @@ -389,6 +448,7 @@ A range of possible characters are available which can be incorporated into spec - 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 @@ -400,6 +460,7 @@ nothing # hide ``` This functionality can also be used to create less serious models: + ```@example dsl_basics rn = @reaction_network begin 🍦, 😢 --> 😃 diff --git a/docs/src/model_creation/examples/basic_CRN_library.md b/docs/src/model_creation/examples/basic_CRN_library.md index 5893bd327d..cda1a186c8 100644 --- a/docs/src/model_creation/examples/basic_CRN_library.md +++ b/docs/src/model_creation/examples/basic_CRN_library.md @@ -3,7 +3,9 @@ ```@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(".") @@ -13,9 +15,11 @@ 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. @@ -23,27 +27,34 @@ Below we will present various simple and established chemical reaction network ( ## [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) @@ -51,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) @@ -60,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)") @@ -72,6 +87,7 @@ 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 @@ -93,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)") @@ -104,6 +122,7 @@ 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 @@ -112,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.) @@ -146,6 +167,7 @@ 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 @@ -153,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] @@ -165,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) @@ -189,6 +215,7 @@ 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 @@ -197,11 +224,13 @@ 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] @@ -220,6 +249,7 @@ 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 @@ -229,7 +259,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] @@ -249,6 +281,7 @@ 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 @@ -256,9 +289,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] @@ -283,6 +318,7 @@ 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 @@ -292,7 +328,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] @@ -313,6 +351,7 @@ 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 @@ -322,7 +361,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] @@ -352,6 +393,7 @@ 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 @@ -364,7 +406,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 697c1fa302..29bc223ea5 100644 --- a/docs/src/model_creation/examples/hodgkin_huxley_equation.md +++ b/docs/src/model_creation/examples/hodgkin_huxley_equation.md @@ -17,6 +17,7 @@ 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 ``` @@ -55,6 +56,7 @@ 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. + ```@example hh1 Iapp(t,I₀) = I₀ * sin(2*pi*t/30)^2 ``` @@ -88,6 +90,7 @@ hhmodel = @reaction_network hhmodel begin 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 @@ -142,6 +145,7 @@ 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 @@ -167,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 @@ -175,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 323dde60eb..eb2837ae0d 100644 --- a/docs/src/model_creation/examples/noise_modelling_approaches.md +++ b/docs/src/model_creation/examples/noise_modelling_approaches.md @@ -10,6 +10,7 @@ We note that these approaches can all be combined. E.g. an intrinsic noise model ## [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 @@ -25,6 +26,7 @@ end 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 @@ -32,13 +34,16 @@ 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) @@ -48,6 +53,7 @@ Next, we consider extrinsic noise. This is randomness caused by stochasticity ex 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)]) @@ -57,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) @@ -65,6 +73,7 @@ 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) @@ -72,6 +81,7 @@ We note that a similar approach can be used to also randomise the initial condit 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. + ```@example noise_modelling_approaches using DataInterpolations function make_K_series(; K_mean = 20.0, n = 500, θ = 0.01) @@ -84,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()) @@ -96,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()]     @@ -107,24 +121,29 @@ 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. ## [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: 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 36dda8c9da..ea5c20b005 100644 --- a/docs/src/model_creation/examples/programmatic_generative_linear_pathway.md +++ b/docs/src/model_creation/examples/programmatic_generative_linear_pathway.md @@ -5,33 +5,44 @@ This example will show how to use programmatic, generative, modelling to model a ## [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$. 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$. + ```@example programmatic_generative_linear_pathway_dsl using Catalyst @@ -57,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] @@ -69,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) @@ -82,6 +97,7 @@ plot!(sol_n10; idxs = :X10, label = "n = 10") 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() @@ -107,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) @@ -119,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)) @@ -127,7 +147,9 @@ 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") diff --git a/docs/src/model_creation/examples/smoluchowski_coagulation_equation.md b/docs/src/model_creation/examples/smoluchowski_coagulation_equation.md index 6532ad71f4..e94a44f662 100644 --- a/docs/src/model_creation/examples/smoluchowski_coagulation_equation.md +++ b/docs/src/model_creation/examples/smoluchowski_coagulation_equation.md @@ -5,12 +5,15 @@ This tutorial shows how to programmatically construct a [`ReactionSystem`](@ref) 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 @@ -32,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 @@ -49,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 @@ -64,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 @@ -86,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 = [] @@ -102,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) @@ -110,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] diff --git a/docs/src/model_creation/functional_parameters.md b/docs/src/model_creation/functional_parameters.md index 8b79d17d3d..4a29287b2d 100644 --- a/docs/src/model_creation/functional_parameters.md +++ b/docs/src/model_creation/functional_parameters.md @@ -7,6 +7,7 @@ An alternative approach for representing complicated functions is by [using `@re ## [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() @@ -16,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] @@ -32,6 +37,7 @@ 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. @@ -39,6 +45,7 @@ plot(sol) ## [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 @@ -47,13 +54,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() @@ -67,7 +78,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] @@ -80,12 +93,15 @@ 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 ``` @@ -93,20 +109,27 @@ 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) @@ -115,7 +138,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))(..) @@ -125,7 +150,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] @@ -134,12 +161,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. ### [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) @@ -151,4 +180,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/). 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 de2d53a114..1ccf698967 100644 --- a/docs/src/model_creation/model_file_loading_and_export.md +++ b/docs/src/model_creation/model_file_loading_and_export.md @@ -5,6 +5,7 @@ Catalyst stores chemical reaction network (CRN) models in `ReactionSystem` struc ## [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 @@ -13,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 @@ -28,6 +33,7 @@ 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: + ``` let @@ -53,6 +59,7 @@ complete(rs) end ``` + !!! note The code that `save_reactionsystem` prints uses [programmatic modelling](@ref programmatic_CRN_construction) to generate the written model. @@ -61,12 +68,15 @@ In addition to transferring models between Julia sessions, the `save_reactionsys ## [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 @@ -76,13 +86,16 @@ 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) @@ -90,6 +103,7 @@ oprob = ODEProblem(prn.rn, Float64[], tspan, Float64[]) sol = solve(oprob) plot(sol; idxs = [:mTetR, :mLacI, :mCI]) ``` + ![Repressilator Simulation](../assets/repressilator_sim_ReactionNetworkImporters.svg) Note that, as all initial conditions and parameters have default values, we can provide empty vectors for these into our `ODEProblem`. @@ -105,11 +119,14 @@ A more detailed description of ReactionNetworkImporter's features can be found i 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) @@ -117,6 +134,7 @@ oprob = ODEProblem(prn.rn, prn.u0, tspan, prn.p) sol = solve(oprob; callback = cbs) plot(sol) ``` + ![Brusselator Simulation](../assets/brusselator_sim_SBMLImporter.svg) 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. @@ -146,6 +164,7 @@ The advantage of these forms is that they offer a compact and very general way t ## [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: + ``` @software{2022ReactionNetworkImporters, author = {Isaacson, Samuel}, @@ -154,6 +173,7 @@ If you use any of this functionality in your research, [in addition to Catalyst] year = {2022} } ``` + ``` @software{2024SBMLImporter, author = {Persson, Sebastian}, @@ -162,6 +182,7 @@ If you use any of this functionality in your research, [in addition to Catalyst] year = {2024} } ``` + ``` @article{LangJainRackauckas+2024, url = {https://doi.org/10.1515/jib-2024-0003}, diff --git a/docs/src/model_creation/model_visualisation.md b/docs/src/model_creation/model_visualisation.md index 1dad8b6f91..3af4c44815 100644 --- a/docs/src/model_creation/model_visualisation.md +++ b/docs/src/model_creation/model_visualisation.md @@ -7,6 +7,7 @@ Catalyst-created `ReactionSystem` models can be visualised either as LaTeX code 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 @@ -16,16 +17,21 @@ 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`. @@ -48,6 +54,7 @@ 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`. + ```@example visualisation_graphs brusselator = @reaction_network begin A, ∅ --> X @@ -59,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 @@ -70,16 +78,20 @@ plot_network(repressilator) ``` 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`. + ```@example visualisation_graphs plot_complexes(brusselator, show_rate_labels = true) ``` @@ -93,6 +105,7 @@ 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() @@ -100,6 +113,7 @@ 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. + ```@example visualisation_graphs f, ax, p = plot_complexes(brusselator, show_rate_labels = true, node_color = :yellow, ilabels_fontsize = 10) ax.title = "Brusselator" @@ -109,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 @@ -133,12 +149,14 @@ register_interaction!(ax, :edrag, EdgeDrag(p)) ``` 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. + ```julia save("fig.png", f) ``` diff --git a/docs/src/model_creation/parametric_stoichiometry.md b/docs/src/model_creation/parametric_stoichiometry.md index 0bbbaa8ef1..8d17f2f41f 100644 --- a/docs/src/model_creation/parametric_stoichiometry.md +++ b/docs/src/model_creation/parametric_stoichiometry.md @@ -8,6 +8,7 @@ use symbolic stoichiometries, and discuss several caveats to be aware of. 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 @@ -17,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 @@ -28,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 @@ -51,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) @@ -91,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 @@ -101,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) @@ -123,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) @@ -130,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₊ @@ -144,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) @@ -156,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 @@ -184,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) @@ -204,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 e1a97aa643..4343ab2fd8 100644 --- a/docs/src/model_creation/programmatic_CRN_construction.md +++ b/docs/src/model_creation/programmatic_CRN_construction.md @@ -10,18 +10,22 @@ 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 @@ -32,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₂]), @@ -50,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 @@ -70,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₁ @@ -99,6 +108,7 @@ 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ₙ]) @@ -109,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ₙ], [α₁,...,αₘ], [β₁,...,βₙ]) @@ -121,14 +133,17 @@ 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. @@ -140,6 +155,7 @@ repressilator example, but it would be nice to still have a short hand as in the 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) @@ -160,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 @@ -188,66 +209,85 @@ 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 + ```@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 944595975c..0842dd52f2 100644 --- a/docs/src/model_creation/reactionsystem_content_accessing.md +++ b/docs/src/model_creation/reactionsystem_content_accessing.md @@ -10,17 +10,22 @@ Catalyst is based around the creation, analysis, and simulation of chemical reac 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 + ```@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] @@ -29,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) @@ -52,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) ``` @@ -65,6 +77,7 @@ isequal(rs.k1, k1) ### [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 @@ -73,18 +86,23 @@ 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) ``` @@ -92,14 +110,19 @@ 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) ``` @@ -107,6 +130,7 @@ 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 @@ -114,26 +138,35 @@ 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) ``` @@ -145,6 +178,7 @@ 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) ``` @@ -154,6 +188,7 @@ ModelingToolkit.get_iv(sir) 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. @@ -170,47 +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] @@ -226,33 +274,43 @@ plot(sol) ### [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 f08da161cb..a895b3de45 100644 --- a/docs/src/model_simulation/ensemble_simulations.md +++ b/docs/src/model_simulation/ensemble_simulations.md @@ -9,6 +9,7 @@ While this can be handled using `for` loops, it is typically better to first cre ## [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 @@ -20,28 +21,36 @@ 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) @@ -52,29 +61,35 @@ plot(e_summary) 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 f047974fdf..72ae45aeba 100644 --- a/docs/src/model_simulation/examples/activation_time_distribution_measurement.md +++ b/docs/src/model_simulation/examples/activation_time_distribution_measurement.md @@ -3,6 +3,7 @@ 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 @@ -12,7 +13,9 @@ 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) @@ -20,23 +23,29 @@ ps = [:v0 => 0.1, :v => 2.5, :Kᵢ => 1000.0, :Kₐ => 40.0, :n => 3.0, :deg => sprob = SDEProblem(sa_model, u0, tspan, ps) 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." @@ -46,10 +55,13 @@ 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). --- diff --git a/docs/src/model_simulation/examples/interactive_brusselator_simulation.md b/docs/src/model_simulation/examples/interactive_brusselator_simulation.md index 299e52e5bf..2ca9851cee 100644 --- a/docs/src/model_simulation/examples/interactive_brusselator_simulation.md +++ b/docs/src/model_simulation/examples/interactive_brusselator_simulation.md @@ -5,14 +5,17 @@ Catalyst can utilize the [GLMakie.jl](https://github.com/JuliaPlots/GLMakie.jl) ## [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 +39,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 +57,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 +135,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 +155,7 @@ axislegend(ax, position = :rt) # Display the figure fig ``` + ![Interactive Brusselator Plot](../../assets/interactive_brusselator.png) 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 +177,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 +216,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 @@ -253,7 +258,7 @@ 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) ``` diff --git a/docs/src/model_simulation/examples/periodic_events_simulation.md b/docs/src/model_simulation/examples/periodic_events_simulation.md index 13589ec759..229e2108cd 100644 --- a/docs/src/model_simulation/examples/periodic_events_simulation.md +++ b/docs/src/model_simulation/examples/periodic_events_simulation.md @@ -5,6 +5,7 @@ This tutorial will describe how to simulate systems with periodic events in ODEs ## [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 @@ -14,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] @@ -30,6 +33,7 @@ plot(sol) 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 @@ -45,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 @@ -53,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] @@ -65,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) @@ -87,6 +98,7 @@ 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) @@ -97,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] @@ -105,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 6a0f388112..501795fa85 100644 --- a/docs/src/model_simulation/finite_state_projection_simulation.md +++ b/docs/src/model_simulation/finite_state_projection_simulation.md @@ -3,7 +3,9 @@ ```@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. @@ -15,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 @@ -49,9 +55,11 @@ 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. @@ -59,6 +67,7 @@ As previously discussed, [*stochastic chemical kinetics*](@ref math_models_in_ca 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) \\ @@ -68,18 +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] @@ -91,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. 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. !!! 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") @@ -131,6 +152,7 @@ bar!(0:74, osol(10.0); bar_width = 1.0, linewidth = 0, alpha = 0.7, label = "t = 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 @@ -138,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 @@ -166,13 +194,16 @@ heatmap(0:24, 0:24, osol[end]; xguide = "X₂", yguide = "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)) @@ -185,6 +216,7 @@ 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`). diff --git a/docs/src/model_simulation/ode_simulation_performance.md b/docs/src/model_simulation/ode_simulation_performance.md index a40be11d8a..34b75655a7 100644 --- a/docs/src/model_simulation/ode_simulation_performance.md +++ b/docs/src/model_simulation/ode_simulation_performance.md @@ -16,6 +16,7 @@ Generally, this short checklist provides a quick guide for dealing with ODE perf 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 @@ -37,16 +38,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 ``` @@ -59,6 +65,7 @@ Finally, we should note that stiffness is not tied to the model equations only. ## [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 @@ -73,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) @@ -83,6 +92,7 @@ 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 OrdinaryDiffEqTsit5, OrdinaryDiffEqRosenbrock, OrdinaryDiffEqVerner, OrdinaryDiffEqBDF @@ -91,15 +101,18 @@ using OrdinaryDiffEqTsit5, OrdinaryDiffEqRosenbrock, OrdinaryDiffEqVerner, Ordin @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/). @@ -115,6 +128,7 @@ As [previously mentioned](@ref ode_simulation_performance_stiffness), implicit O 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 @@ -135,6 +149,7 @@ 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 @@ -143,18 +158,22 @@ 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) @@ -162,6 +181,7 @@ Since these methods do not depend on a Jacobian, certain Jacobian options (such 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) @@ -174,11 +194,14 @@ 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. @@ -187,6 +210,7 @@ Generally, the use of preconditioners is only recommended for advanced users who ## [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 @@ -202,6 +226,7 @@ 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. @@ -214,6 +239,7 @@ Both CPU and GPU parallelisation require first building an `EnsembleProblem` (wh ### [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 @@ -223,7 +249,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] @@ -233,6 +261,7 @@ 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: @@ -247,45 +276,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). @@ -306,12 +350,15 @@ Furthermore (while not required) to receive good performance, we should also mak - 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 @@ -330,7 +377,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]) @@ -338,6 +387,7 @@ 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`. @@ -347,10 +397,12 @@ We can now simulate our model using a GPU-based ensemble algorithm. Currently, t - 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. @@ -363,6 +415,7 @@ For more information on differential equation simulations on GPUs in Julia, plea ## Citation If you use GPU simulations in your research, please cite the following paper to support the authors of the DiffEqGPU package: + ``` @article{utkarsh2024automated, title={Automated translation and accelerated solving of differential equations on multiple GPU platforms}, diff --git a/docs/src/model_simulation/sde_simulation_performance.md b/docs/src/model_simulation/sde_simulation_performance.md index 3705b99b80..6ee0811512 100644 --- a/docs/src/model_simulation/sde_simulation_performance.md +++ b/docs/src/model_simulation/sde_simulation_performance.md @@ -5,9 +5,11 @@ While there exist relatively straightforward approaches to manage performance fo ## [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). ## [Options for Jacobian computation](@id sde_simulation_performance_jacobian) @@ -23,12 +25,15 @@ We have previously described how simulation parallelisation can be used to [impr 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 @@ -42,11 +47,14 @@ 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). diff --git a/docs/src/model_simulation/simulation_introduction.md b/docs/src/model_simulation/simulation_introduction.md index 3bec00ddc3..7ea3af15de 100644 --- a/docs/src/model_simulation/simulation_introduction.md +++ b/docs/src/model_simulation/simulation_introduction.md @@ -15,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