diff --git a/HISTORY.md b/HISTORY.md
index 270d64b177..ff92e9b5f5 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -3,103 +3,190 @@
## Catalyst unreleased (master branch)
## Catalyst 14.0
-- The `reactionparams`, `numreactionparams`, and `reactionparamsmap` functions have been removed.
-- To be more consistent with ModelingToolkit's immutability requirement for systems, we have removed API functions that mutate `ReactionSystem`s such as `addparam!`, `addreaction!`, `addspecies`, `@add_reactions`, and `merge!`. Please use `ModelingToolkit.extend` and `ModelingToolkit.compose` to generate new merged and/or composed `ReactionSystem`s from multiple component systems.
-- Added CatalystStructuralIdentifiabilityExtension, which permits StructuralIdentifiability.jl function to be applied directly to Catalyst systems. E.g. use
-```julia
-using Catalyst, StructuralIdentifiability
-goodwind_oscillator = @reaction_network begin
- (mmr(P,pₘ,1), dₘ), 0 <--> M
- (pₑ*M,dₑ), 0 <--> E
- (pₚ*E,dₚ), 0 <--> P
-end
-assess_identifiability(goodwind_oscillator; measured_quantities=[:M])
-```
-to assess (global) structural identifiability for all parameters and variables of the `goodwind_oscillator` model (under the presumption that we can measure `M` only).
-- Automatically handles conservation laws for structural identifiability problems (eliminates these internally to speed up computations).
-- Adds a tutorial to illustrate the use of the extension.
-- Enable adding metadata to individual reactions, e.g:
-```julia
-rn = @reaction_network begin
- @parameters η
- k, 2X --> X2, [noise_scaling=η]
-end
-getnoisescaling(rn)
-```
-- `SDEProblem` no longer takes the `noise_scaling` argument (see above for new approach to handle noise scaling).
-- Changed fields of internal `Reaction` structure. `ReactionSystems`s saved using `serialize` on previous Catalyst versions cannot be loaded using this (or later) versions.
-- Simulation of spatial ODEs now supported. For full details, please see https://github.com/SciML/Catalyst.jl/pull/644 and upcoming documentation. Note that these methods are currently considered alpha, with the interface and approach changing even in non-breaking Catalyst releases.
-- LatticeReactionSystem structure represents a spatial reaction network:
+
+#### Breaking changes
+Catalyst v14 was prompted by the (breaking) release of ModelingToolkit v9, which
+introduced several breaking changes to Catalyst. A summary of these (and how to
+handle them) can be found
+[here](https://docs.sciml.ai/Catalyst/stable/v14_migration_guide/). These are
+briefly summarised in the following bullet points:
+- `ReactionSystem`s must now be marked *complete* before they are exposed to
+ most forms of simulation and analysis. With the exception of `ReactionSystem`s
+ created through the `@reaction_network` macro, all `ReactionSystem`s are *not*
+ marked complete upon construction. The `complete` function can be used to mark
+ `ReactionSystem`s as complete. To construct a `ReactionSystem` that is not
+ marked complete via the DSL the new `@network_component` macro can be used.
+- The `states` function has been replaced with `unknowns`. The `get_states`
+ function has been replaced with `get_unknowns`.
+- Support for most units (with the exception of `s`, `m`, `kg`, `A`, `K`, `mol`,
+ and `cd`) has currently been dropped by ModelingToolkit, and hence they are
+ unavailable via Catalyst too. Its is expected that eventually support for
+ relevant chemical units such as molar will return to ModelingToolkit (and
+ should then immediately work in Catalyst too).
+- Problem parameter values are now accessed through `prob.ps[p]` (rather than
+ `prob[p]`).
+- ModelingToolkit currently does not support the safe application of the
+ `remake` function, or safe direct mutation, for problems for which
+ `remove_conserved = true` was used when updating the values of initial
+ conditions. Instead, the values of each conserved constant must be directly
+ specified.
+- The `reactionparams`, `numreactionparams`, and `reactionparamsmap` functions
+ have been deprecated and removed.
+- To be more consistent with ModelingToolkit's immutability requirement for
+ systems, we have removed API functions that mutate `ReactionSystem`s such as
+ `addparam!`, `addreaction!`, `addspecies`, `@add_reactions`, and `merge!`.
+ Please use `ModelingToolkit.extend` and `ModelingToolkit.compose` to generate
+ new merged and/or composed `ReactionSystem`s from multiple component systems.
+
+#### General changes
+- The `default_t()` and `default_time_deriv()` functions are now the preferred
+ approaches for creating the default time independent variable and its
+ differential. i.e.
+ ```julia
+ # do
+ t = default_t()
+ @species A(t)
+
+ # avoid
+ @variables t
+ @species A(t)
+- It is now possible to add metadata to individual reactions, e.g. using:
+ ```julia
+ rn = @reaction_network begin
+ @parameters η
+ k, 2X --> X2, [description="Dimerisation"]
+ end
+ getdescription(rn)
+ ```
+ a more detailed description can be found [here](https://docs.sciml.ai/Catalyst/dev/model_creation/dsl_advanced/#dsl_advanced_options_reaction_metadata).
+- `SDEProblem` no longer takes the `noise_scaling` argument. Noise scaling is
+ now handled through the `noise_scaling` metadata (described in more detail
+ [here](https://docs.sciml.ai/Catalyst/stable/model_simulation/simulation_introduction/#simulation_intro_SDEs_noise_saling))
+- Fields of the internal `Reaction` structure have been changed.
+ `ReactionSystems`s saved using `serialize` on previous Catalyst versions
+ cannot be loaded using this (or later) versions.
+- A new function, `save_reactionsystem`, which permits the writing of
+ `ReactionSystem` models to files, has been created. A thorough description of
+ this function can be found
+ [here](https://docs.sciml.ai/Catalyst/stable/model_creation/model_file_loading_and_export/#Saving-Catalyst-models-to,-and-loading-them-from,-Julia-files)
+- Updated how compounds are created. E.g. use
+ ```julia
+ @variables t C(t) O(t)
+ @compound CO2 ~ C + 2O
+ ```
+ to create a compound species `CO2` that consists of `C` and two `O`.
+- Added documentation for chemistry-related functionality (compound creation and
+ reaction balancing).
+- Added function `isautonomous` to check if a `ReactionSystem` is autonomous.
+- Added function `steady_state_stability` to compute stability for steady
+ states. Example:
```julia
+ # Creates model.
rn = @reaction_network begin
(p,d), 0 <--> X
end
- tr = @transport_reaction D X
- lattice = Graphs.grid([5, 5])
- lrs = LatticeReactionSystem(rn, [tr], lattice)
-```
-- Here, if a `u0` or `p` vector is given with scalar values:
+ p = [:p => 1.0, :d => 0.5]
+
+ # Finds (the trivial) steady state, and computes stability.
+ steady_state = [2.0]
+ steady_state_stability(steady_state, rn, p)
+ ```
+ Here, `steady_state_stability` takes an optional keyword argument `tol =
+ 10*sqrt(eps())`, which is used to check that the real part of all eigenvalues
+ are at least `tol` away from zero. Eigenvalues within `tol` of zero indicate
+ that stability may not be reliably calculated.
+- Added a DSL option, `@combinatoric_ratelaws`, which can be used to toggle
+ whether to use combinatorial rate laws within the DSL (this feature was
+ already supported for programmatic modelling). Example:
+ ```julia
+ # Creates model.
+ rn = @reaction_network begin
+ @combinatoric_ratelaws false
+ (kB,kD), 2X <--> X2
+ end
+ ```
+- Added a DSL option, `@observables` for [creating
+ observables](https://docs.sciml.ai/Catalyst/stable/model_creation/dsl_advanced/#dsl_advanced_options_observables)
+ (this feature was already supported for programmatic modelling).
+- Added DSL options `@continuous_events` and `@discrete_events` to add events to
+ a model as part of its creation (this feature was already supported for
+ programmatic modelling). Example:
+ ```julia
+ rn = @reaction_network begin
+ @continuous_events begin
+ [X ~ 1.0] => [X ~ X + 1.0]
+ end
+ d, X --> 0
+ end
+ ```
+- Added DSL option `@equations` to add (algebraic or differential) equations to
+ a model as part of its creation (this feature was already supported for
+ programmatic modelling). Example:
```julia
- u0 = [:X => 1.0]
- p = [:p => 1.0, :d => 0.5, :D => 0.1]
+ rn = @reaction_network begin
+ @equations begin
+ D(V) ~ 1 - V
+ end
+ (p/V,d/V), 0 <--> X
+ end
```
- this value will be used across the entire system. If their values are instead vectors, different values are used across the spatial system. Here
+ couples the ODE $dV/dt = 1 - V$ to the reaction system.
+- Coupled reaction networks and differential equation (or algebraic differential
+ equation) systems can now be converted to `SDESystem`s and `NonlinearSystem`s.
+
+#### Structural identifiability extension
+- Added CatalystStructuralIdentifiabilityExtension, which permits
+ StructuralIdentifiability.jl to be applied directly to Catalyst systems. E.g.
+ use
```julia
- X0 = zeros(25)
- X0[1] = 1.0
- u0 = [:X => X0]
+ using Catalyst, StructuralIdentifiability
+ goodwind_oscillator = @reaction_network begin
+ (mmr(P,pₘ,1), dₘ), 0 <--> M
+ (pₑ*M,dₑ), 0 <--> E
+ (pₚ*E,dₚ), 0 <--> P
+ end
+ assess_identifiability(goodwind_oscillator; measured_quantities=[:M])
```
- X's value will be `1.0` in the first vertex, but `0.0` in the remaining one (the system have 25 vertexes in total). SInce th parameters `p` and `d` are part of the non-spatial reaction network, their values are tied to vertexes. However, if the `D` parameter (which governs diffusion between vertexes) is given several values, these will instead correspond to the specific edges (and transportation along those edges.)
+ to assess (global) structural identifiability for all parameters and variables
+ of the `goodwind_oscillator` model (under the presumption that we can measure
+ `M` only).
+- Automatically handles conservation laws for structural identifiability
+ problems (eliminates these internally to speed up computations).
+- A more detailed of how this extension works can be found
+ [here](https://docs.sciml.ai/Catalyst/stable/inverse_problems/structural_identifiability/).
-- Update how compounds are created. E.g. use
-```julia
-@variables t C(t) O(t)
-@compound CO2 ~ C + 2O
-```
-to create a compound species `CO2` that consists of `C` and 2 `O`.
-- Added documentation for chemistry related functionality (compound creation and reaction balancing).
-- Add a CatalystBifurcationKitExtension, permitting BifurcationKit's `BifurcationProblem`s to be created from Catalyst reaction networks. Example usage:
-```julia
-using Catalyst
-wilhelm_2009_model = @reaction_network begin
- k1, Y --> 2X
- k2, 2X --> X + Y
- k3, X + Y --> Y
- k4, X --> 0
- k5, 0 --> X
-end
+#### Bifurcation analysis extension
+- Add a CatalystBifurcationKitExtension, permitting BifurcationKit's
+ `BifurcationProblem`s to be created from Catalyst reaction networks. Example
+ usage:
+ ```julia
+ using Catalyst
+ wilhelm_2009_model = @reaction_network begin
+ k1, Y --> 2X
+ k2, 2X --> X + Y
+ k3, X + Y --> Y
+ k4, X --> 0
+ k5, 0 --> X
+ end
-using BifurcationKit
-bif_par = :k1
-u_guess = [:X => 5.0, :Y => 2.0]
-p_start = [:k1 => 4.0, :k2 => 1.0, :k3 => 1.0, :k4 => 1.5, :k5 => 1.25]
-plot_var = :X
-bprob = BifurcationProblem(wilhelm_2009_model, u_guess, p_start, bif_par; plot_var=plot_var)
+ using BifurcationKit
+ bif_par = :k1
+ u_guess = [:X => 5.0, :Y => 2.0]
+ p_start = [:k1 => 4.0, :k2 => 1.0, :k3 => 1.0, :k4 => 1.5, :k5 => 1.25]
+ plot_var = :X
+ bprob = BifurcationProblem(wilhelm_2009_model, u_guess, p_start, bif_par; plot_var = plot_var)
-p_span = (2.0, 20.0)
-opts_br = ContinuationPar(p_min = p_span[1], p_max = p_span[2], max_steps=1000)
+ p_span = (2.0, 20.0)
+ opts_br = ContinuationPar(p_min = p_span[1], p_max = p_span[2], max_steps = 1000)
-bif_dia = bifurcationdiagram(bprob, PALC(), 2, (args...) -> opts_br; bothside=true)
+ bif_dia = bifurcationdiagram(bprob, PALC(), 2, (args...) -> opts_br; bothside = true)
-using Plots
-plot(bif_dia; xguide="k1", yguide="X")
-```
-- Automatically handles elimination of conservation laws for computing bifurcation diagrams.
+ using Plots
+ plot(bif_dia; xguide = "k1", guide = "X")
+ ```
+- Automatically handles elimination of conservation laws for computing
+ bifurcation diagrams.
- Updated Bifurcation documentation with respect to this new feature.
-- Added function `isautonomous` to check if a `ReactionSystem` is autonomous.
-- Added function `steady_state_stability` to compute stability for steady states. Example:
-```julia
-# Creates model.
-rn = @reaction_network begin
- (p,d), 0 <--> X
-end
-p = [:p => 1.0, :d => 0.5]
-
-# Finds (the trivial) steady state, and computes stability.
-steady_state = [2.0]
-steady_state_stability(steady_state, rn, p)
-```
-Here, `steady_state_stability` take an optional argument `tol = 10*sqrt(eps())`, which is used to determine whether a eigenvalue real part is reliably less that 0.
## Catalyst 13.5
- Added a CatalystHomotopyContinuationExtension extension, which exports the `hc_steady_state` function if HomotopyContinuation is exported. `hc_steady_state` finds the steady states of a reaction system using the homotopy continuation method. This feature is only available for julia versions 1.9+. Example:
@@ -658,7 +745,7 @@ hc_steady_states(wilhelm_2009_model, ps)
field has been changed (only when created through the `@reaction_network`
macro). Previously they were ordered according to the order with which they
appeared in the macro. Now they are ordered according the to order with which
- they appeard after the `end` part. E.g. in
+ they appeared after the `end` part. E.g. in
```julia
rn = @reaction_network begin
(p,d), 0 <--> X
@@ -763,7 +850,7 @@ which gives
![rn_complexes](https://user-images.githubusercontent.com/9385167/130252763-4418ba5a-164f-47f7-b512-a768e4f73834.png)
*2.* Support for units via ModelingToolkit and
-[Uniftul.jl](https://github.com/PainterQubits/Unitful.jl) in directly constructed
+[Unitful.jl](https://github.com/PainterQubits/Unitful.jl) in directly constructed
`ReactionSystem`s:
```julia
# ]add Unitful
diff --git a/Project.toml b/Project.toml
index b9371b3656..5d2088785a 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,6 +1,6 @@
name = "Catalyst"
uuid = "479239e8-5488-4da2-87a7-35f2df7eef83"
-version = "13.5.1"
+version = "14.0.0"
[deps]
Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa"
@@ -76,6 +76,7 @@ SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462"
SciMLNLSolve = "e9a6253c-8580-4d32-9898-8661bb511710"
StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
+StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
SteadyStateDiffEq = "9672c7b4-1e72-59bd-8a11-6ac3964bc41f"
StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0"
@@ -84,4 +85,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
[targets]
-test = ["BifurcationKit", "DiffEqCallbacks", "DomainSets", "Graphviz_jll", "HomotopyContinuation", "Logging", "NonlinearSolve", "OrdinaryDiffEq", "Plots", "Random", "SafeTestsets", "SciMLBase", "SciMLNLSolve", "StableRNGs", "Statistics", "SteadyStateDiffEq", "StochasticDiffEq", "StructuralIdentifiability", "Test", "Unitful"]
+test = ["BifurcationKit", "DiffEqCallbacks", "DomainSets", "Graphviz_jll", "HomotopyContinuation", "Logging", "NonlinearSolve", "OrdinaryDiffEq", "Plots", "Random", "SafeTestsets", "SciMLBase", "SciMLNLSolve", "StableRNGs", "StaticArrays", "Statistics", "SteadyStateDiffEq", "StochasticDiffEq", "StructuralIdentifiability", "Test", "Unitful"]
diff --git a/README.md b/README.md
index 0f8d03e474..761b2a26b3 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
# Catalyst.jl
-[![Join the chat at https://julialang.zulipchat.com #sciml-bridged](https://img.shields.io/static/v1?label=Zulip&message=chat&color=9558b2&labelColor=389826)](https://julialang.zulipchat.com/#narrow/stream/279055-sciml-bridged)
-[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://docs.sciml.ai/Catalyst/stable/)
-[![API Stable](https://img.shields.io/badge/API-stable-blue.svg)](https://docs.sciml.ai/Catalyst/stable/api/catalyst_api/)
-
+[![Latest Release (for users)](https://img.shields.io/badge/docs-latest_release_(for_users)-blue.svg)](https://docs.sciml.ai/Catalyst/stable/)
+[![API Latest Release (for users)](https://img.shields.io/badge/API-latest_release_(for_users)-blue.svg)](https://docs.sciml.ai/Catalyst/stable/api/catalyst_api/)
+[![Master (for developers)](https://img.shields.io/badge/docs-master_branch_(for_devs)-blue.svg)](https://docs.sciml.ai/Catalyst/dev/)
+[![API Master (for developers](https://img.shields.io/badge/API-master_branch_(for_devs)-blue.svg)](https://docs.sciml.ai/Catalyst/dev/api/catalyst_api/)
+
[![Build Status](https://github.com/SciML/Catalyst.jl/workflows/CI/badge.svg)](https://github.com/SciML/Catalyst.jl/actions?query=workflow%3ACI)
[![codecov.io](https://codecov.io/gh/SciML/Catalyst.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/SciML/Catalyst.jl)
@@ -12,182 +12,197 @@
[![ColPrac: Contributor's Guide on Collaborative Practices for Community Packages](https://img.shields.io/badge/ColPrac-Contributor's%20Guide-blueviolet)](https://github.com/SciML/ColPrac)
[![SciML Code Style](https://img.shields.io/static/v1?label=code%20style&message=SciML&color=9558b2&labelColor=389826)](https://github.com/SciML/SciMLStyle)
+[![Citation](https://img.shields.io/badge/Publication-389826)](https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1011530)
-Catalyst.jl is a symbolic modeling package for analysis and high performance
+Catalyst.jl is a symbolic modeling package for analysis and high-performance
simulation of chemical reaction networks. Catalyst defines symbolic
[`ReactionSystem`](https://docs.sciml.ai/Catalyst/stable/catalyst_functionality/programmatic_CRN_construction/)s,
which can be created programmatically or easily
-specified using Catalyst's domain specific language (DSL). Leveraging
-[ModelingToolkit](https://github.com/SciML/ModelingToolkit.jl) and
+specified using Catalyst's domain-specific language (DSL). Leveraging
+[ModelingToolkit.jl](https://github.com/SciML/ModelingToolkit.jl) and
[Symbolics.jl](https://github.com/JuliaSymbolics/Symbolics.jl), Catalyst enables
large-scale simulations through auto-vectorization and parallelism. Symbolic
`ReactionSystem`s can be used to generate ModelingToolkit-based models, allowing
the easy simulation and parameter estimation of mass action ODE models, Chemical
Langevin SDE models, stochastic chemical kinetics jump process models, and more.
-Generated models can be used with solvers throughout the broader
-[SciML](https://sciml.ai) ecosystem, including higher level SciML packages (e.g.
+Generated models can be used with solvers throughout the broader Julia and
+[SciML](https://sciml.ai) ecosystems, including higher-level SciML packages (e.g.
for sensitivity analysis, parameter estimation, machine learning applications,
etc).
## Breaking changes and new features
-**NOTE:** version 14 is a breaking release, prompted by the release of ModelingToolkit.jl version 9. This caused several breaking changes in how Catalyst models are represented and interfaced with.
+**NOTE:** Version 14 is a breaking release, prompted by the release of ModelingToolkit.jl version 9. This caused several breaking changes in how Catalyst models are represented and interfaced with.
-Breaking changes and new functionality are summarized in the
-[HISTORY.md](HISTORY.md) file.
+Breaking changes and new functionality are summarized in the [HISTORY.md](HISTORY.md) file. Furthermore, a migration guide on how to adapt your workflows to the new v14 update can be found [here](https://docs.sciml.ai/Catalyst/stable/v14_migration_guide/).
## Tutorials and documentation
-The latest tutorials and information on using the package are available in the [stable
+The latest tutorials and information on using Catalyst are available in the [stable
documentation](https://docs.sciml.ai/Catalyst/stable/). The [in-development
documentation](https://docs.sciml.ai/Catalyst/dev/) describes unreleased features in
the current master branch.
-Several Youtube video tutorials and overviews are also available, but note these use older
-Catalyst versions with slightly different notation (for example, in building reaction networks):
-- From JuliaCon 2023: A short 15 minute overview of Catalyst as of version 13 is
-available in the talk [Catalyst.jl, Modeling Chemical Reaction Networks](https://www.youtube.com/watch?v=yreW94n98eM&ab_channel=TheJuliaProgrammingLanguage).
-- From JuliaCon 2022: A three hour tutorial workshop overviewing how to use
- Catalyst and its more advanced features as of version 12.1. [Workshop
- video](https://youtu.be/tVfxT09AtWQ), [Workshop Pluto.jl
- Notebooks](https://github.com/SciML/JuliaCon2022_Catalyst_Workshop).
-- From SIAM CSE 2021: A short 15 minute overview of Catalyst as of version 6 is
-available in the talk [Modeling Biochemical Systems with
-Catalyst.jl](https://www.youtube.com/watch?v=5p1PJE5A5Jw).
-- From JuliaCon 2018: A short 13 minute overview of Catalyst when it was known
- as DiffEqBiological in older versions is available in the talk [Efficient
- Modelling of Biochemical Reaction
- Networks](https://www.youtube.com/watch?v=s1e72k5XD6s)
-
-Finally, an overview of the package and its features (as of version 13) can also be found in its corresponding research paper, [Catalyst: Fast and flexible modeling of reaction networks](https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1011530).
+An overview of the package, its features, and comparative benchmarking (as of version 13) can also
+be found in its corresponding research paper, [Catalyst: Fast and flexible modeling of reaction networks](https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1011530).
## Features
-- A DSL provides a simple and readable format for manually specifying chemical
- reactions.
-- Catalyst `ReactionSystem`s provide 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/).
-- Non-integer (e.g. `Float64`) stoichiometric coefficients are supported for generating
- ODE models, and symbolic expressions for stoichiometric coefficients are supported for
- all system types.
-- The [Catalyst.jl API](http://docs.sciml.ai/Catalyst/stable/api/catalyst_api) provides functionality for extending networks,
- building networks programmatically, network analysis, and for composing multiple
- networks together.
-- `ReactionSystem`s generated by the DSL can be converted to a variety of
- `ModelingToolkit.AbstractSystem`s, including symbolic ODE, SDE and jump process
- representations.
-- Coupled differential and algebraic constraint equations can be included in
- Catalyst models, and are incorporated during conversion to ODEs or steady
- state equations.
-- Conservation laws can be detected and applied to reduce system sizes, and generate
- non-singular Jacobians, during conversion to ODEs, SDEs, and steady state equations.
-- By leveraging ModelingToolkit, users have a variety of options for generating
- optimized system representations to use in solvers. These include construction
- of dense or sparse Jacobians, multithreading or parallelization of generated
- derivative functions, automatic classification of reactions into optimized
- jump types for Gillespie type simulations, automatic construction of
- dependency graphs for jump systems, and more.
-- Generated systems can be solved using any
- [DifferentialEquations.jl](https://docs.sciml.ai/DiffEqDocs/stable/)
- ODE/SDE/jump solver, and can be used within `EnsembleProblem`s for carrying
- out parallelized parameter sweeps and statistical sampling. Plot recipes
- are available for visualizing the solutions.
-- [Symbolics.jl](https://github.com/JuliaSymbolics/Symbolics.jl) symbolic
- expressions and Julia `Expr`s can be obtained for all rate laws and functions
- determining the deterministic and stochastic terms within resulting ODE, SDE
- or jump models.
-- [Latexify](https://korsbo.github.io/Latexify.jl/stable/) can be used to generate
- LaTeX expressions corresponding to generated mathematical models or the
- underlying set of reactions.
-- [Graphviz](https://graphviz.org/) can be used to generate and visualize
- reaction network graphs. (Reusing the Graphviz interface created in
- [Catlab.jl](https://algebraicjulia.github.io/Catlab.jl/stable/).)
-
-## Packages supporting Catalyst
-- Catalyst [`ReactionSystem`](@ref)s can be imported from SBML files via
- [SBMLToolkit.jl](https://github.com/SciML/SBMLToolkit.jl), and from BioNetGen .net
- files 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, representing 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.
-- [BondGraphs.jl](https://github.com/jedforrest/BondGraphs.jl) a package for
- constructing and analyzing bond graphs models, which can take Catalyst models as input.
-- [PEtab.jl](https://github.com/sebapersson/PEtab.jl) a package that implements the PEtab format for fitting reaction network ODEs to data. Input can be provided either as SBML files or as Catalyst `ReactionSystem`s.
-
-
-## Illustrative examples
-#### Gillespie simulations of Michaelis-Menten enzyme kinetics
+#### Features of Catalyst
+- [The Catalyst DSL](https://docs.sciml.ai/Catalyst/stable/model_creation/dsl_basics/) 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](http://docs.sciml.ai/Catalyst/stable/api/catalyst_api) provides functionality for building networks programmatically and for composing multiple networks together.
+- Leveraging ModelingToolkit, generated models can be converted to symbolic reaction rate equation ODE models, symbolic Chemical Langevin Equation models, and symbolic stochastic chemical kinetics (jump process) models. These can be simulated using any [DifferentialEquations.jl](https://docs.sciml.ai/DiffEqDocs/stable/) [ODE/SDE/jump solver](https://docs.sciml.ai/Catalyst/stable/model_simulation/simulation_introduction/), and can be used within `EnsembleProblem`s for carrying out [parallelized parameter sweeps and statistical sampling](https://docs.sciml.ai/Catalyst/stable/model_simulation/ensemble_simulations/). Plot recipes are available for [visualization of all solutions](https://docs.sciml.ai/Catalyst/stable/model_simulation/simulation_plotting/).
+- Non-integer (e.g. `Float64`) stoichiometric coefficients [are supported](https://docs.sciml.ai/Catalyst/stable/model_creation/dsl_basics/#dsl_description_stoichiometries_decimal) for generating ODE models, and symbolic expressions for stoichiometric coefficients [are supported](https://docs.sciml.ai/Catalyst/stable/model_creation/parametric_stoichiometry/) for all system types.
+- A [network analysis suite](https://docs.sciml.ai/Catalyst/stable/model_creation/network_analysis/) permits the computation of linkage classes, deficiencies, reversibility, and other network properties.
+- [Conservation laws can be detected and utilized](https://docs.sciml.ai/Catalyst/stable/model_creation/network_analysis/#network_analysis_deficiency) to reduce system sizes, and to generate non-singular Jacobians (e.g. during conversion to ODEs, SDEs, and steady state equations).
+- Catalyst reaction network models can be [coupled with differential and algebraic equations](https://docs.sciml.ai/Catalyst/stable/model_creation/constraint_equations/) (which are then incorporated during conversion to ODEs, SDEs, and steady state equations).
+- Models can be [coupled with events](https://docs.sciml.ai/Catalyst/stable/model_creation/constraint_equations/#constraint_equations_events) that affect the system and its state during simulations.
+- By leveraging ModelingToolkit, users have a variety of options for generating optimized system representations to use in solvers. These include construction of [dense or sparse Jacobians](https://docs.sciml.ai/Catalyst/stable/model_simulation/ode_simulation_performance/#ode_simulation_performance_sparse_jacobian), [multithreading or parallelization of generated derivative functions](https://docs.sciml.ai/Catalyst/stable/model_simulation/ode_simulation_performance/#ode_simulation_performance_parallelisation), [automatic classification of reactions into optimized jump types for Gillespie type simulations](https://docs.sciml.ai/JumpProcesses/stable/jump_types/#jump_types), [automatic construction of dependency graphs for jump systems](https://docs.sciml.ai/JumpProcesses/stable/jump_types/#Jump-Aggregators-Requiring-Dependency-Graphs), and more.
+- [Symbolics.jl](https://github.com/JuliaSymbolics/Symbolics.jl) symbolic expressions and Julia `Expr`s can be obtained for all rate laws and functions determining the deterministic and stochastic terms within resulting ODE, SDE, or jump models.
+- [Steady states](https://docs.sciml.ai/Catalyst/stable/steady_state_functionality/homotopy_continuation/) (and their [stabilities](https://docs.sciml.ai/Catalyst/stable/steady_state_functionality/steady_state_stability_computation/)) can be computed for model ODE representations.
+
+#### Features of Catalyst composing with other packages
+- [OrdinaryDiffEq.jl](https://github.com/SciML/OrdinaryDiffEq.jl) Can be used to numerically solver 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](https://docs.sciml.ai/Catalyst/stable/model_simulation/ode_simulation_performance/#ode_simulation_performance_parallelisation), including parallelization of [ODE simulations on GPUs](https://docs.sciml.ai/Catalyst/stable/model_simulation/ode_simulation_performance/#ode_simulation_performance_parallelisation_GPU) using [DiffEqGPU.jl](https://github.com/SciML/DiffEqGPU.jl).
+- [Latexify](https://korsbo.github.io/Latexify.jl/stable/) can be used to [generate LaTeX expressions](https://docs.sciml.ai/Catalyst/stable/model_creation/model_visualisation/#visualisation_latex) corresponding to generated mathematical models or the underlying set of reactions.
+- [Graphviz](https://graphviz.org/) can be used to generate and [visualize reaction network graphs](https://docs.sciml.ai/Catalyst/stable/model_creation/model_visualisation/#visualisation_graphs) (reusing the Graphviz interface created in [Catlab.jl](https://algebraicjulia.github.io/Catlab.jl/stable/)).
+- Model steady states can be [computed through homotopy continuation](https://docs.sciml.ai/Catalyst/stable/steady_state_functionality/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](https://docs.sciml.ai/Catalyst/stable/steady_state_functionality/nonlinear_solve/#steady_state_solving_simulation) using [SteadyStateDiffEq.jl](https://github.com/SciML/SteadyStateDiffEq.jl), or by [numerically solving steady-state nonlinear equations](https://docs.sciml.ai/Catalyst/stable/steady_state_functionality/nonlinear_solve/#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](https://docs.sciml.ai/Catalyst/stable/steady_state_functionality/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](https://docs.sciml.ai/Catalyst/stable/steady_state_functionality/dynamical_systems/#dynamical_systems_basins_of_attraction), [Lyapunov spectrums](https://docs.sciml.ai/Catalyst/stable/steady_state_functionality/dynamical_systems/#dynamical_systems_lyapunov_exponents), and other dynamical system properties.
+- [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) can be used to [perform structural identifiability analysis](https://docs.sciml.ai/Catalyst/stable/inverse_problems/structural_identifiability/).
+- [Optimization.jl](https://github.com/SciML/Optimization.jl), [DiffEqParamEstim.jl](https://github.com/SciML/DiffEqParamEstim.jl), and [PEtab.jl](https://github.com/sebapersson/PEtab.jl) can all be used to [fit model parameters to data](https://sebapersson.github.io/PEtab.jl/stable/Define_in_julia/).
+- [GlobalSensitivity.jl](https://github.com/SciML/GlobalSensitivity.jl) can be used to perform [global sensitivity analysis](https://docs.sciml.ai/Catalyst/stable/inverse_problems/global_sensitivity_analysis/) of model behaviors.
+- [SciMLSensitivity.jl](https://github.com/SciML/SciMLSensitivity.jl) can be used to compute local sensitivities of functions containing forward model simulations.
+
+#### Features of packages built upon Catalyst
+- Catalyst [`ReactionSystem`](@ref)s can be [imported from SBML files](https://docs.sciml.ai/Catalyst/stable/model_creation/model_file_loading_and_export/#Loading-SBML-files-using-SBMLImporter.jl-and-SBMLToolkit.jl) via [SBMLImporter.jl](https://github.com/SciML/SBMLImporter.jl) and [SBMLToolkit.jl](https://github.com/SciML/SBMLToolkit.jl), and [from BioNetGen .net files](https://docs.sciml.ai/Catalyst/stable/model_creation/model_file_loading_and_export/#file_loading_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.
+- [BondGraphs.jl](https://github.com/jedforrest/BondGraphs.jl), a package for constructing and analyzing bond graphs models, which can take Catalyst models as input.
+
+
+## Illustrative example
+
+#### Deterministic ODE simulation of Michaelis-Menten enzyme kinetics
+Here we show a simple example where a model is created using the Catalyst DSL, and then simulated as
+an ordinary differential equation.
```julia
-using Catalyst, Plots, JumpProcesses
-rs = @reaction_network begin
- c1, S + E --> SE
- c2, SE --> S + E
- c3, SE --> P + E
+# Fetch required packages.
+using Catalyst, OrdinaryDiffEq, Plots
+
+# Create model.
+model = @reaction_network begin
+ kB, S + E --> SE
+ kD, SE --> S + E
+ kP, SE --> P + E
end
-p = (:c1 => 0.00166, :c2 => 0.0001, :c3 => 0.1)
-tspan = (0., 100.)
-u0 = [:S => 301, :E => 100, :SE => 0, :P => 0]
-
-# solve JumpProblem
-dprob = DiscreteProblem(rs, u0, tspan, p)
-jprob = JumpProblem(rs, dprob, Direct())
-jsol = solve(jprob, SSAStepper())
-plot(jsol; lw = 2, title = "Gillespie: Michaelis-Menten Enzyme Kinetics")
-```
-![](https://user-images.githubusercontent.com/1814174/87864114-3bf9dd00-c932-11ea-83a0-58f38aee8bfb.png)
+# Create an ODE that can be simulated.
+u0 = [:S => 50.0, :E => 10.0, :SE => 0.0, :P => 0.0]
+tspan = (0., 200.)
+ps = [:kB => 0.01, :kD => 0.1, :kP => 0.1]
+ode = ODEProblem(model, u0, tspan, ps)
-#### Adaptive time stepping SDEs for a birth-death process
+# Simulate ODE and plot results.
+sol = solve(ode)
+plot(sol; lw = 5)
+```
+![ODE simulation](docs/src/assets/readme_ode_plot.svg)
+#### Stochastic jump simulations
+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.
```julia
-using Catalyst, Plots, StochasticDiffEq
-rs = @reaction_network begin
- c1, X --> 2X
- c2, X --> 0
- c3, 0 --> X
+# 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.
+using JumpProcesses
+u0_integers = [:S => 50, :E => 10, :SE => 0, :P => 0]
+dprob = DiscreteProblem(model, u0_integers, tspan, ps)
+jprob = JumpProblem(model, dprob, Direct())
+jump_sol = solve(jprob, SSAStepper())
+plot(jump_sol; lw = 2)
+```
+![Jump simulation](docs/src/assets/readme_jump_plot.svg)
+
+
+## More 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 (modeled as the cyclic sinusoid $k_a (\sin(t) + 1)$),
+which phosphorylates the growth factor (producing $G^P$). When the cell reaches a
+critical volume ($V_m$) it undergoes cell division. First, we declare our model:
+```julia
+using Catalyst
+cell_model = @reaction_network begin
+ @parameters Vₘ g
+ @equations begin
+ D(V) ~ g*Gᴾ
+ end
+ @continuous_events begin
+ [V ~ Vₘ] => [V ~ V/2]
+ end
+ kₚ*(sin(t)+1)/V, G --> Gᴾ
+ kᵢ/V, Gᴾ --> G
end
-p = (:c1 => 1.0, :c2 => 2.0, :c3 => 50.)
-tspan = (0.,10.)
-u0 = [:X => 5.]
-sprob = SDEProblem(rs, u0, tspan, p)
-ssol = solve(sprob, LambaEM(), reltol=1e-3)
-plot(ssol; lw = 2, title = "Adaptive SDE: Birth-Death Process")
```
+We now study the system as a Chemical Langevin Dynamics SDE model, which can be generated as follows
+```julia
+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) \\
+dG^P(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) \\
+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.
+```julia
+using StochasticDiffEq, Plots
+sol = solve(sprob, EM(); dt = 0.05)
+plot(sol; xguide = "Time (au)", lw = 2)
+```
+![Elaborate SDE simulation](docs/src/assets/readme_elaborate_sde_plot.svg)
-![](https://user-images.githubusercontent.com/1814174/87864113-3bf9dd00-c932-11ea-8275-f903eef90b91.png)
-
-## Getting help
-Catalyst developers are active on the [Julia
-Discourse](https://discourse.julialang.org/), the [Julia Slack](https://julialang.slack.com) channels \#sciml-bridged and \#sciml-sysbio, and the [Julia Zulip sciml-bridged channel](https://julialang.zulipchat.com/#narrow/stream/279055-sciml-bridged).
-For bugs or feature requests [open an issue](https://github.com/SciML/Catalyst.jl/issues).
+Some features we used here:
+- The cell volume was [modeled as a differential equation, which was coupled to the reaction network model](https://docs.sciml.ai/Catalyst/stable/model_creation/constraint_equations/#constraint_equations_coupling_constraints).
+- The cell divisions were created by [incorporating events into the model](https://docs.sciml.ai/Catalyst/stable/model_creation/constraint_equations/#constraint_equations_events).
+- We designated a specific numeric [solver and corresponding solver options](https://docs.sciml.ai/Catalyst/stable/model_simulation/simulation_introduction/#simulation_intro_solver_options).
+- The model simulation was [plotted using Plots.jl](https://docs.sciml.ai/Catalyst/stable/model_simulation/simulation_plotting/).
+## Getting help or getting involved
+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
-The software in this ecosystem was developed as part of academic research. If you would like to help support it,
-please star the repository as such metrics may help us secure funding in the future. If you use Catalyst as part
-of your research, teaching, or other activities, we would be grateful if you could cite our work:
+The software in this ecosystem was developed as part of academic research. If you would like to help
+support it, please star the repository as such metrics may help us secure funding in the future. If
+you use Catalyst as part of your research, teaching, or other activities, we would be grateful if you
+could cite our work:
```
@article{CatalystPLOSCompBio2023,
- doi = {10.1371/journal.pcbi.1011530},
- author = {Loman, Torkel E. AND Ma, Yingbo AND Ilin, Vasily AND Gowda, Shashi AND Korsbo, Niklas AND Yewale, Nikhil AND Rackauckas, Chris AND Isaacson, Samuel A.},
- journal = {PLOS Computational Biology},
- publisher = {Public Library of Science},
- title = {Catalyst: Fast and flexible modeling of reaction networks},
- year = {2023},
- month = {10},
- volume = {19},
- url = {https://doi.org/10.1371/journal.pcbi.1011530},
- pages = {1-19},
- number = {10},
+ doi = {10.1371/journal.pcbi.1011530},
+ author = {Loman, Torkel E. AND Ma, Yingbo AND Ilin, Vasily AND Gowda, Shashi AND Korsbo, Niklas AND Yewale, Nikhil AND Rackauckas, Chris AND Isaacson, Samuel A.},
+ journal = {PLOS Computational Biology},
+ publisher = {Public Library of Science},
+ title = {Catalyst: Fast and flexible modeling of reaction networks},
+ year = {2023},
+ month = {10},
+ volume = {19},
+ url = {https://doi.org/10.1371/journal.pcbi.1011530},
+ pages = {1-19},
+ number = {10},
}
```
diff --git a/docs/Project.toml b/docs/Project.toml
index b92da6ef5d..0fe8c869b8 100644
--- a/docs/Project.toml
+++ b/docs/Project.toml
@@ -39,7 +39,7 @@ Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
BenchmarkTools = "1.5"
BifurcationKit = "0.3.4"
CairoMakie = "0.12"
-Catalyst = "13"
+Catalyst = "14"
DataFrames = "1.6"
DiffEqParamEstim = "2.2"
Distributions = "0.25"
@@ -55,7 +55,7 @@ ModelingToolkit = "9.16.0"
NonlinearSolve = "3.12"
Optim = "1.9"
Optimization = "3.25"
-OptimizationBBO = "0.2.1"
+OptimizationBBO = "0.3"
OptimizationNLopt = "0.2.1"
OptimizationOptimJL = "0.3.1"
OptimizationOptimisers = "0.2.1"
diff --git a/docs/make.jl b/docs/make.jl
index 8595cd6ffa..0d354c641a 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -40,7 +40,7 @@ makedocs(sitename = "Catalyst.jl",
doctest = false,
clean = true,
pages = pages,
- pagesonly = true,
+ pagesonly = false,
warnonly = [:missing_docs])
deploydocs(repo = "github.com/SciML/Catalyst.jl.git";
diff --git a/docs/old_files/advanced.md b/docs/old_files/advanced.md
index 2888749744..80388d12bb 100644
--- a/docs/old_files/advanced.md
+++ b/docs/old_files/advanced.md
@@ -25,7 +25,7 @@ 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 langauge technical reasons). This means that the reaction
+`⇒`, `⟾`, `=>`, `⇔`, `⟺` (`<=>` currently not possible due to Julia language technical reasons). This means that the reaction
```julia
rn = @reaction_network begin
diff --git a/docs/pages.jl b/docs/pages.jl
index a8772bd62b..dbb52df4e6 100644
--- a/docs/pages.jl
+++ b/docs/pages.jl
@@ -27,7 +27,8 @@ pages = Any[
"model_simulation/simulation_plotting.md",
"model_simulation/simulation_structure_interfacing.md",
"model_simulation/ensemble_simulations.md",
- "model_simulation/ode_simulation_performance.md"
+ "model_simulation/ode_simulation_performance.md",
+ "model_simulation/sde_simulation_performance.md"
],
"Steady state analysis" => Any[
"steady_state_functionality/homotopy_continuation.md",
diff --git a/docs/src/api.md b/docs/src/api.md
index f34cbfa8e3..459dbd1c1b 100644
--- a/docs/src/api.md
+++ b/docs/src/api.md
@@ -1,4 +1,4 @@
-# Catalyst.jl API
+# [Catalyst.jl API](@id api)
```@meta
CurrentModule = Catalyst
```
@@ -77,6 +77,7 @@ plot(p1, p2, p3; layout = (3,1))
```@docs
@reaction_network
+@network_component
make_empty_network
@reaction
Reaction
@@ -127,7 +128,7 @@ can call:
* `ModelingToolkit.unknowns(rn)` returns all species *and variables* across the
system, *all sub-systems*, and all constraint systems. Species are ordered
before non-species variables in `unknowns(rn)`, with the first `numspecies(rn)`
- entires in `unknowns(rn)` being the same as `species(rn)`.
+ entries in `unknowns(rn)` being the same as `species(rn)`.
* [`species(rn)`](@ref) is a vector collecting all the chemical species within
the system and any sub-systems that are also `ReactionSystems`.
* `ModelingToolkit.parameters(rn)` returns all parameters across the
diff --git a/docs/src/assets/Project.toml b/docs/src/assets/Project.toml
deleted file mode 100644
index b92da6ef5d..0000000000
--- a/docs/src/assets/Project.toml
+++ /dev/null
@@ -1,72 +0,0 @@
-[deps]
-BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
-BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665"
-CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
-Catalyst = "479239e8-5488-4da2-87a7-35f2df7eef83"
-DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
-DiffEqParamEstim = "1130ab10-4a5a-5621-a13d-e4788d82bd4c"
-Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
-Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
-DynamicalSystems = "61744808-ddfa-5f27-97ff-6e42cc95d634"
-GlobalSensitivity = "af5da776-676b-467e-8baf-acd8249e4f0f"
-HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327"
-IncompleteLU = "40713840-3770-5561-ab4c-a76e7d0d7895"
-JumpProcesses = "ccbc3e58-028d-4f4c-8cd5-9ae44345cda5"
-Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316"
-LinearSolve = "7ed4a6bd-45f5-4d41-b270-4a48e9bafcae"
-Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
-ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
-NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec"
-Optim = "429524aa-4258-5aef-a3af-852621145aeb"
-Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba"
-OptimizationBBO = "3e6eede4-6085-4f62-9a71-46d9bc1eb92b"
-OptimizationNLopt = "4e6fcdb7-1186-4e1f-a706-475e75c168bb"
-OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e"
-OptimizationOptimisers = "42dfb2eb-d2b4-4451-abcd-913932933ac1"
-OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
-Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
-QuasiMonteCarlo = "8a4e6c94-4038-4cdc-81c3-7e6ffdb2a71b"
-SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462"
-SciMLSensitivity = "1ed8b502-d754-442c-8d5d-10ac956f44a1"
-SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b"
-StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
-SteadyStateDiffEq = "9672c7b4-1e72-59bd-8a11-6ac3964bc41f"
-StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0"
-StructuralIdentifiability = "220ca800-aa68-49bb-acd8-6037fa93a544"
-Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
-
-[compat]
-BenchmarkTools = "1.5"
-BifurcationKit = "0.3.4"
-CairoMakie = "0.12"
-Catalyst = "13"
-DataFrames = "1.6"
-DiffEqParamEstim = "2.2"
-Distributions = "0.25"
-Documenter = "1.4.1"
-DynamicalSystems = "3.3"
-GlobalSensitivity = "2.6"
-HomotopyContinuation = "2.9"
-IncompleteLU = "0.2"
-JumpProcesses = "9.11"
-Latexify = "0.16"
-LinearSolve = "2.30"
-ModelingToolkit = "9.16.0"
-NonlinearSolve = "3.12"
-Optim = "1.9"
-Optimization = "3.25"
-OptimizationBBO = "0.2.1"
-OptimizationNLopt = "0.2.1"
-OptimizationOptimJL = "0.3.1"
-OptimizationOptimisers = "0.2.1"
-OrdinaryDiffEq = "6.80.1"
-Plots = "1.40"
-QuasiMonteCarlo = "0.3"
-SciMLBase = "2.39"
-SciMLSensitivity = "7.60"
-SpecialFunctions = "2.4"
-StaticArrays = "1.9"
-SteadyStateDiffEq = "2.2"
-StochasticDiffEq = "6.65"
-StructuralIdentifiability = "0.5.8"
-Symbolics = "5.30.1"
diff --git a/docs/src/assets/long_ploting_times/model_creation/mm_kinetics.svg b/docs/src/assets/long_ploting_times/model_creation/mm_kinetics.svg
deleted file mode 100644
index 824a5fd376..0000000000
--- a/docs/src/assets/long_ploting_times/model_creation/mm_kinetics.svg
+++ /dev/null
@@ -1,128 +0,0 @@
-
-
diff --git a/docs/src/assets/long_ploting_times/model_creation/sir_outbreaks.svg b/docs/src/assets/long_ploting_times/model_creation/sir_outbreaks.svg
deleted file mode 100644
index 3e213ebbdd..0000000000
--- a/docs/src/assets/long_ploting_times/model_creation/sir_outbreaks.svg
+++ /dev/null
@@ -1,128 +0,0 @@
-
-
diff --git a/docs/src/assets/long_ploting_times/model_simulation/incomplete_brusselator_simulation.svg b/docs/src/assets/long_ploting_times/model_simulation/incomplete_brusselator_simulation.svg
deleted file mode 100644
index 4f9f01fedf..0000000000
--- a/docs/src/assets/long_ploting_times/model_simulation/incomplete_brusselator_simulation.svg
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
diff --git a/docs/src/assets/readme_elaborate_sde_plot.svg b/docs/src/assets/readme_elaborate_sde_plot.svg
new file mode 100644
index 0000000000..503e76d2ee
--- /dev/null
+++ b/docs/src/assets/readme_elaborate_sde_plot.svg
@@ -0,0 +1,50 @@
+
+
diff --git a/docs/src/assets/readme_jump_plot.svg b/docs/src/assets/readme_jump_plot.svg
new file mode 100644
index 0000000000..5c45563c97
--- /dev/null
+++ b/docs/src/assets/readme_jump_plot.svg
@@ -0,0 +1,54 @@
+
+
diff --git a/docs/src/assets/readme_ode_plot.svg b/docs/src/assets/readme_ode_plot.svg
new file mode 100644
index 0000000000..df9c2eb095
--- /dev/null
+++ b/docs/src/assets/readme_ode_plot.svg
@@ -0,0 +1,54 @@
+
+
diff --git a/docs/src/index.md b/docs/src/index.md
index 8ad0ea19fe..7ba02ee07f 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -1,160 +1,220 @@
-# Catalyst.jl for Reaction Network Modeling
+# [Catalyst.jl for Reaction Network Modeling](@id doc_index)
-Catalyst.jl is a symbolic modeling package for analysis and high performance
+Catalyst.jl is a symbolic modeling package for analysis and high-performance
simulation of chemical reaction networks. Catalyst defines symbolic
[`ReactionSystem`](@ref)s, which can be created programmatically or easily
-specified using Catalyst's domain specific language (DSL). Leveraging
-[ModelingToolkit.jl](https://docs.sciml.ai/ModelingToolkit/stable/) and
-[Symbolics.jl](https://docs.sciml.ai/Symbolics/stable/), Catalyst enables
+specified using Catalyst's domain-specific language (DSL). Leveraging
+[ModelingToolkit.jl](https://github.com/SciML/ModelingToolkit.jl) and
+[Symbolics.jl](https://github.com/JuliaSymbolics/Symbolics.jl), Catalyst enables
large-scale simulations through auto-vectorization and parallelism. Symbolic
`ReactionSystem`s can be used to generate ModelingToolkit-based models, allowing
the easy simulation and parameter estimation of mass action ODE models, Chemical
Langevin SDE models, stochastic chemical kinetics jump process models, and more.
-Generated models can be used with solvers throughout the broader
-[SciML](https://sciml.ai) ecosystem, including higher level SciML packages (e.g.
+Generated models can be used with solvers throughout the broader Julia and
+[SciML](https://sciml.ai) ecosystems, including higher-level SciML packages (e.g.
for sensitivity analysis, parameter estimation, machine learning applications,
etc).
-## Features
-- A DSL provides a simple and readable format for manually specifying chemical
- reactions.
-- Catalyst `ReactionSystem`s provide 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/).
-- Non-integer (e.g. `Float64`) stoichiometric coefficients are supported for generating
- ODE models, and symbolic expressions for stoichiometric coefficients are supported for
- all system types.
-- The [Catalyst.jl API](@ref) provides functionality for extending networks,
- building networks programmatically, network analysis, and for composing multiple
- networks together.
-- `ReactionSystem`s generated by the DSL can be converted to a variety of
- `ModelingToolkit.AbstractSystem`s, including symbolic ODE, SDE and jump process
- representations.
-- Coupled differential and algebraic constraint equations can be included in
- Catalyst models, and are incorporated during conversion to ODEs or steady
- state equations.
-- Conservation laws can be detected and applied to reduce system sizes, and
- generate non-singular Jacobians, during conversion to ODEs, SDEs, and steady
- state equations.
-- By leveraging ModelingToolkit, users have a variety of options for generating
- optimized system representations to use in solvers. These include construction
- of dense or sparse Jacobians, multithreading or parallelization of generated
- derivative functions, automatic classification of reactions into optimized
- jump types for Gillespie type simulations, automatic construction of
- dependency graphs for jump systems, and more.
-- Generated systems can be solved using any
- [DifferentialEquations.jl](https://docs.sciml.ai/DiffEqDocs/stable/)
- ODE/SDE/jump solver, and can be used within `EnsembleProblem`s for carrying
- out parallelized parameter sweeps and statistical sampling. Plot recipes
- are available for visualizing the solutions.
-- [Symbolics.jl](https://github.com/JuliaSymbolics/Symbolics.jl) symbolic
- expressions and Julia `Expr`s can be obtained for all rate laws and functions
- determining the deterministic and stochastic terms within resulting ODE, SDE
- or jump models.
-- [Latexify](https://korsbo.github.io/Latexify.jl/stable/) can be used to generate
- LaTeX expressions corresponding to generated mathematical models or the
- underlying set of reactions.
-- [Graphviz](https://graphviz.org/) can be used to generate and visualize
- reaction network graphs. (Reusing the Graphviz interface created in
- [Catlab.jl](https://algebraicjulia.github.io/Catlab.jl/stable/).)
-
-## Packages Supporting Catalyst
-- Catalyst [`ReactionSystem`](@ref)s can be imported from SBML files via
- [SBMLToolkit.jl](https://docs.sciml.ai/SBMLToolkit/stable/), and from BioNetGen .net
- files and various stoichiometric matrix network representations using
- [ReactionNetworkImporters.jl](https://docs.sciml.ai/ReactionNetworkImporters/stable/).
-- [MomentClosure.jl](https://augustinas1.github.io/MomentClosure.jl/dev) allows
- generation of symbolic ModelingToolkit `ODESystem`s, representing moment
- closure approximations to moments of the Chemical Master Equation, from
- reaction networks defined in Catalyst.
-- [FiniteStateProjection.jl](https://kaandocal.github.io/FiniteStateProjection.jl/dev/)
- allows the construction and numerical solution of Chemical Master Equation
- models from reaction networks defined in Catalyst.
-- [DelaySSAToolkit.jl](https://palmtree2013.github.io/DelaySSAToolkit.jl/dev/) can
- augment Catalyst reaction network models with delays, and can simulate the
- resulting stochastic chemical kinetics with delays models.
-- [BondGraphs.jl](https://github.com/jedforrest/BondGraphs.jl) a package for
- constructing and analyzing bond graphs models, which can take Catalyst models as input.
-- [PEtab.jl](https://github.com/sebapersson/PEtab.jl) a package that implements the PEtab format for fitting reaction network ODEs to data. Input can be provided either as SBML files or as Catalyst `ReactionSystem`s.
-
-
-## Installation
-Catalyst can be installed through the Julia package manager:
+## [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.
+- Leveraging ModelingToolkit, generated models can be converted to symbolic reaction rate equation ODE models, symbolic Chemical Langevin Equation models, and symbolic stochastic chemical kinetics (jump process) models. These can be simulated using any [DifferentialEquations.jl](https://docs.sciml.ai/DiffEqDocs/stable/) [ODE/SDE/jump solver](@ref simulation_intro), and can be used within `EnsembleProblem`s for carrying out [parallelized parameter sweeps and statistical sampling](@ref ensemble_simulations). Plot recipes are available for [visualization of all solutions](@ref simulation_plotting).
+- Non-integer (e.g. `Float64`) stoichiometric coefficients [are supported](@ref dsl_description_stoichiometries_decimal) for generating ODE models, and symbolic expressions for stoichiometric coefficients [are supported](@ref parametric_stoichiometry) for all system types.
+- A [network analysis suite](@ref network_analysis) permits the computation of linkage classes, deficiencies, reversibility, and other network properties.
+- [Conservation laws can be detected and utilized](@ref network_analysis_deficiency) to reduce system sizes, and to generate non-singular Jacobians (e.g. during conversion to ODEs, SDEs, and steady state equations).
+- Catalyst reaction network models can be [coupled with differential and algebraic equations](@ref constraint_equations_coupling_constraints) (which are then incorporated during conversion to ODEs, SDEs, and steady state equations).
+- Models can be [coupled with events](@ref constraint_equations_events) that affect the system and its state during simulations.
+- By leveraging ModelingToolkit, users have a variety of options for generating optimized system representations to use in solvers. These include construction of [dense or sparse Jacobians](@ref ode_simulation_performance_sparse_jacobian), [multithreading or parallelization of generated derivative functions](@ref ode_simulation_performance_parallelisation), [automatic classification of reactions into optimized jump types for Gillespie type simulations](https://docs.sciml.ai/JumpProcesses/stable/jump_types/#jump_types), [automatic construction of dependency graphs for jump systems](https://docs.sciml.ai/JumpProcesses/stable/jump_types/#Jump-Aggregators-Requiring-Dependency-Graphs), and more.
+- [Symbolics.jl](https://github.com/JuliaSymbolics/Symbolics.jl) symbolic expressions and Julia `Expr`s can be obtained for all rate laws and functions determining the deterministic and stochastic terms within resulting ODE, SDE, or jump models.
+- [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 solver 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 simulations on GPUs](@ref ode_simulation_performance_parallelisation_GPU) 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.
+- [Graphviz](https://graphviz.org/) can be used to generate and [visualize reaction network graphs](@ref visualisation_graphs) (reusing the Graphviz interface created in [Catlab.jl](https://algebraicjulia.github.io/Catlab.jl/stable/)).
+- 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](@ref 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.
+- [StructuralIdentifiability.jl](https://github.com/SciML/StructuralIdentifiability.jl) can be used to [perform structural identifiability analysis](@ref structural_identifiability).
+- [Optimization.jl](https://github.com/SciML/Optimization.jl), [DiffEqParamEstim.jl](https://github.com/SciML/DiffEqParamEstim.jl), and [PEtab.jl](https://github.com/sebapersson/PEtab.jl) can all be used to [fit model parameters to data](https://sebapersson.github.io/PEtab.jl/stable/Define_in_julia/).
+- [GlobalSensitivity.jl](https://github.com/SciML/GlobalSensitivity.jl) can be used to perform [global sensitivity analysis](@ref global_sensitivity_analysis) of model behaviors.
+- [SciMLSensitivity.jl](https://github.com/SciML/SciMLSensitivity.jl) can be used to compute local sensitivities of functions containing forward model simulations.
+
+#### [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/SciML/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.
+- [BondGraphs.jl](https://github.com/jedforrest/BondGraphs.jl), a package for constructing and analyzing bond graphs models, which can take Catalyst models as input.
+
+## [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.
+
+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
+end
+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")
```
-To solve Catalyst models and visualize solutions, it is also recommended to
-install DifferentialEquations.jl and Plots.jl
+Many Catalyst features require the installation of additional packages. E.g. for ODE-solving and simulation plotting
```julia
Pkg.add("OrdinaryDiffEq")
Pkg.add("Plots")
```
+is also needed.
-## Illustrative Example
-Here is a simple example of generating, visualizing and solving an SIR ODE
-model. We first define the SIR reaction model using Catalyst
-```@example ind1
-using Catalyst
-rn = @reaction_network begin
- α, S + I --> 2I
- β, I --> R
+A more thorough guide for setting up Catalyst and installing Julia packages can be found [here](@ref catalyst_for_new_julia_users_packages).
+
+## [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.
+
+```@example home_simple_example
+# Fetch required packages.
+using Catalyst, OrdinaryDiffEq, Plots
+
+# Create model.
+model = @reaction_network begin
+ kB, S + E --> SE
+ kD, SE --> S + E
+ kP, SE --> P + E
end
+
+# Create an ODE that can be simulated.
+u0 = [:S => 50.0, :E => 10.0, :SE => 0.0, :P => 0.0]
+tspan = (0., 200.)
+ps = [:kB => 0.01, :kD => 0.1, :kP => 0.1]
+ode = ODEProblem(model, u0, tspan, ps)
+
+# Simulate ODE and plot results.
+sol = solve(ode)
+plot(sol; lw = 5)
```
-Assuming [Graphviz](https://graphviz.org/) and is installed and *command line
-accessible*, the network can be visualized using the [`Graph`](@ref) command
-```julia
-Graph(rn)
-```
-which in Jupyter notebooks will give the figure
-![SIR Network Graph](assets/SIR_rn.svg)
+#### [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.
+using JumpProcesses
+u0_integers = [:S => 50, :E => 10, :SE => 0, :P => 0]
+dprob = DiscreteProblem(model, u0_integers, tspan, ps)
+jprob = JumpProblem(model, dprob, Direct())
+jump_sol = solve(jprob, SSAStepper())
+jump_sol = solve(jprob, SSAStepper(); seed = 1234) # hide
+plot(jump_sol; lw = 2)
+```
-To generate and solve a mass action ODE version of the model we use
-```@example ind1
-using OrdinaryDiffEq
-p = [:α => .1/1000, :β => .01]
-tspan = (0.0,250.0)
-u0 = [:S => 999.0, :I => 1.0, :R => 0.0]
-op = ODEProblem(rn, u0, tspan, p)
-sol = solve(op, Tsit5()) # use Tsit5 ODE solver
+## [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 (modeled as the cyclic sinusoid $k_a (\sin(t) + 1)$),
+which phosphorylates the growth factor (producing $G^P$). 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
+ @parameters Vₘ g
+ @equations begin
+ D(V) ~ g*Gᴾ
+ end
+ @continuous_events begin
+ [V ~ Vₘ] => [V ~ V/2]
+ end
+ kₚ*(sin(t)+1)/V, G --> Gᴾ
+ 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) \\
+dG^P(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) \\
+dV(t) &= \left(g \, G^P(t)\right) dt
+\end{align*}
```
-which we can plot as
-```@example ind1
-using Plots
-plot(sol, lw=2)
+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)
+sol = solve(sprob, EM(); dt = 0.05, seed = 1234) # hide
+plot(sol; xguide = "Time (au)", lw = 2)
```
-## Getting Help
-Catalyst developers are active on the [Julia
-Discourse](https://discourse.julialang.org/), and the [Julia
-Slack's](https://julialang.slack.com) \#sciml-bridged and \#sciml-sysbio channels.
-For bugs or feature requests [open an
-issue](https://github.com/SciML/Catalyst.jl/issues).
+## [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 catalyst_citation)
-The software in this ecosystem was developed as part of academic research. If you would like to help support it,
-please star the repository as such metrics may help us secure funding in the future. If you use Catalyst as part
-of your research, teaching, or other activities, we would be grateful if you could cite our work:
+## [Supporting and Citing Catalyst.jl](@id doc_index_citation)
+The software in this ecosystem was developed as part of academic research. If you would like to help
+support it, please star the repository as such metrics may help us secure funding in the future. If
+you use Catalyst as part of your research, teaching, or other activities, we would be grateful if you
+could cite our work:
```
@article{CatalystPLOSCompBio2023,
- doi = {10.1371/journal.pcbi.1011530},
- author = {Loman, Torkel E. AND Ma, Yingbo AND Ilin, Vasily AND Gowda, Shashi AND Korsbo, Niklas AND Yewale, Nikhil AND Rackauckas, Chris AND Isaacson, Samuel A.},
- journal = {PLOS Computational Biology},
- publisher = {Public Library of Science},
- title = {Catalyst: Fast and flexible modeling of reaction networks},
- year = {2023},
- month = {10},
- volume = {19},
- url = {https://doi.org/10.1371/journal.pcbi.1011530},
- pages = {1-19},
- number = {10},
+ doi = {10.1371/journal.pcbi.1011530},
+ author = {Loman, Torkel E. AND Ma, Yingbo AND Ilin, Vasily AND Gowda, Shashi AND Korsbo, Niklas AND Yewale, Nikhil AND Rackauckas, Chris AND Isaacson, Samuel A.},
+ journal = {PLOS Computational Biology},
+ publisher = {Public Library of Science},
+ title = {Catalyst: Fast and flexible modeling of reaction networks},
+ year = {2023},
+ month = {10},
+ volume = {19},
+ url = {https://doi.org/10.1371/journal.pcbi.1011530},
+ pages = {1-19},
+ number = {10},
}
```
-## Reproducibility
+## [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/introduction_to_catalyst.md b/docs/src/introduction_to_catalyst/introduction_to_catalyst.md
index bf63cd4e60..d46d5a9397 100644
--- a/docs/src/introduction_to_catalyst/introduction_to_catalyst.md
+++ b/docs/src/introduction_to_catalyst/introduction_to_catalyst.md
@@ -1,29 +1,42 @@
# [Introduction to Catalyst](@id introduction_to_catalyst)
In this tutorial we provide an introduction to using Catalyst to specify
chemical reaction networks, and then to solve ODE, jump, and SDE models
-generated from them[1]. At the end we show what mathematical rate laws and
+generated from them [1]. At the end we show what mathematical rate laws and
transition rate functions (i.e. intensities or propensities) are generated by
Catalyst for ODE, SDE and jump process models.
-Let's start by using the Catalyst [`@reaction_network`](@ref) macro
-to specify a simple chemical reaction network: the well-known repressilator.
-
-We first import the basic packages we'll need:
+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
+
+# name of the environment
+Pkg.activate("catalyst_introduction")
+
+# packages we will use in this tutorial
+Pkg.add("Catalyst")
+Pkg.add("OrdinaryDiffEq")
+Pkg.add("Plots")
+Pkg.add("Latexify")
+Pkg.add("JumpProcesses")
+Pkg.add("StochasticDiffEq")
+```
+We next load the basic packages we'll need for our first example:
```@example tut1
-# If not already installed, first hit "]" within a Julia REPL. Then type:
-# add Catalyst OrdinaryDiffEq Plots Latexify
-
using Catalyst, OrdinaryDiffEq, Plots, Latexify
```
-We now construct the reaction network. The basic types of arrows and predefined
-rate laws one can 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:
+Let's start by using the Catalyst [`@reaction_network`](@ref) macro to specify a
+simple chemical reaction network: the well-known repressilator. We first construct
+the reaction network. The basic types of arrows and predefined rate laws one can
+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
-repressilator = @reaction_network Repressilator begin
+rn = @reaction_network Repressilator begin
hillr(P₃,α,K,n), ∅ --> m₁
hillr(P₁,α,K,n), ∅ --> m₂
hillr(P₂,α,K,n), ∅ --> m₃
@@ -37,37 +50,37 @@ repressilator = @reaction_network Repressilator begin
μ, P₂ --> ∅
μ, P₃ --> ∅
end
-show(stdout, MIME"text/plain"(), repressilator) # hide
+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 `repressilator` variable. It can
+[`ReactionSystem`](@ref), which we saved in the `rn` variable. It can
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). For example, to see the chemical species, parameters,
+[Catalyst.jl API](@ref api). For example, to see the chemical species, parameters,
and reactions we can use
```@example tut1
-species(repressilator)
+species(rn)
```
```@example tut1
-parameters(repressilator)
+parameters(rn)
```
and
```@example tut1
-reactions(repressilator)
+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(repressilator)
+latexify(rn)
```
```@example tut1
-repressilator #hide
+rn #hide
```
Assuming [Graphviz](https://graphviz.org/) is installed and command line
accessible, within a Jupyter notebook we can also graph the reaction network by
```julia
-g = Graph(repressilator)
+g = Graph(rn)
```
giving
@@ -96,8 +109,8 @@ 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
-repressilator = complete(repressilator)
-odesys = convert(ODESystem, repressilator)
+rn = complete(rn)
+odesys = convert(ODESystem, rn)
```
(Here Latexify is used automatically to display `odesys` in Latex within Markdown
documents or notebook environments like Pluto.jl.)
@@ -117,12 +130,10 @@ nothing # hide
Alternatively, we can use ModelingToolkit-based symbolic species variables to
specify these mappings like
```@example tut1
-t = default_t()
-@parameters α K n δ γ β μ
-@species m₁(t) m₂(t) m₃(t) P₁(t) P₂(t) P₃(t)
-psymmap = (α => .5, K => 40, n => 2, δ => log(2)/120,
- γ => 5e-3, β => 20*log(2)/120, μ => log(2)/60)
-u₀symmap = [m₁ => 0., m₂ => 0., m₃ => 0., P₁ => 20., P₂ => 0., P₃ => 0.]
+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)
+u₀symmap = [rn.m₁ => 0., rn.m₂ => 0., rn.m₃ => 0., rn.P₁ => 20.,
+ rn.P₂ => 0., rn.P₃ => 0.]
nothing # hide
```
Knowing these mappings we can set up the `ODEProblem` we want to solve:
@@ -132,11 +143,11 @@ Knowing these mappings we can set up the `ODEProblem` we want to solve:
tspan = (0., 10000.)
# create the ODEProblem we want to solve
-oprob = ODEProblem(repressilator, u₀map, tspan, pmap)
+oprob = ODEProblem(rn, u₀map, tspan, pmap)
nothing # hide
```
-By passing `repressilator` directly to the `ODEProblem`, Catalyst has to
-(internally) call `convert(ODESystem, repressilator)` again to generate the
+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)
@@ -149,13 +160,13 @@ underlying problem.
!!! note
When passing `odesys` to `ODEProblem` we needed to use the symbolic
variable-based parameter mappings, `u₀symmap` and `psymmap`, while when
- directly passing `repressilator` we could use either those or the
+ directly passing `rn` we could use either those or the
`Symbol`-based mappings, `u₀map` and `pmap`. `Symbol`-based mappings can
always be converted to `symbolic` mappings using [`symmap_to_varmap`](@ref).
!!! note
- Above we have used `repressilator = complete(repressilator)` and `odesys = complete(odesys)` to mark these systems as *complete*, indicating to Catalyst and ModelingToolkit that these models are finalized. This must be done before any system is given as input to a `convert` call or some problem type. `ReactionSystem` models created through the @reaction_network` DSL (which is introduced elsewhere, and primarily used throughout these documentation) are always marked as complete when generated. Hence `complete` does not need to be called on them. Symbolically generated `ReactionSystem`s, `ReactionSystem`s generated via the `@network_component` macro, and any ModelingToolkit system generated by `convert` always needs to be manually marked as `complete` as we do for `odesys` above. An expanded description on *completeness* can be found [here](@ref completeness_note).
+ Above we have used `rn = complete(rn)` and `odesys = complete(odesys)` to mark these systems as *complete*, indicating to Catalyst and ModelingToolkit that these models are finalized. This must be done before any system is given as input to a `convert` call or some problem type. `ReactionSystem` models created through the `@reaction_network` DSL (which is introduced elsewhere, and primarily used throughout these documentation) are always marked as complete when generated. Hence `complete` does not need to be called on them. Symbolically generated `ReactionSystem`s, `ReactionSystem`s generated via the `@network_component` macro, and any ModelingToolkit system generated by `convert` always needs to be manually marked as `complete` as we do for `odesys` above. An expanded description on *completeness* can be found [here](@ref completeness_note).
At this point we are all set to solve the ODEs. We can now use any ODE solver
from within the
@@ -164,7 +175,7 @@ package. We'll use the recommended default explicit solver, `Tsit5()`, and then
plot the solutions:
```@example tut1
-sol = solve(oprob, Tsit5(), saveat=10.)
+sol = solve(oprob, Tsit5(), saveat=10.0)
plot(sol)
```
We see the well-known oscillatory behavior of the repressilator! For more on the
@@ -188,14 +199,15 @@ using JumpProcesses
u₀map = [:m₁ => 0, :m₂ => 0, :m₃ => 0, :P₁ => 20, :P₂ => 0, :P₃ => 0]
# next we create a discrete problem to encode that our species are integer-valued:
-dprob = DiscreteProblem(repressilator, u₀map, tspan, pmap)
+dprob = DiscreteProblem(rn, u₀map, tspan, pmap)
# now, we create a JumpProblem, and specify Gillespie's Direct Method as the solver:
-jprob = JumpProblem(repressilator, dprob, Direct(), save_positions=(false,false))
+jprob = JumpProblem(rn, dprob, Direct())
# now, let's solve and plot the jump process:
-sol = solve(jprob, SSAStepper(), saveat=10.)
+sol = solve(jprob, SSAStepper())
plot(sol)
+plot(sol, density = 10000, fmt = :png) # hide
```
We see that oscillations remain, but become much noisier. Note, in constructing
diff --git a/docs/src/inverse_problems/global_sensitivity_analysis.md b/docs/src/inverse_problems/global_sensitivity_analysis.md
index cd65631c2f..e2a10759f9 100644
--- a/docs/src/inverse_problems/global_sensitivity_analysis.md
+++ b/docs/src/inverse_problems/global_sensitivity_analysis.md
@@ -142,7 +142,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 catalyst_citation), please cite the following paper to support the authors of the GlobalSensitivity.jl package:
+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 ae729d2fc2..5834b96823 100644
--- a/docs/src/inverse_problems/optimization_ode_param_fitting.md
+++ b/docs/src/inverse_problems/optimization_ode_param_fitting.md
@@ -170,7 +170,7 @@ nothing # hide
```
---
-## [Citation](@id structural_identifiability_citation)
+## [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,
diff --git a/docs/src/model_creation/compositional_modeling.md b/docs/src/model_creation/compositional_modeling.md
index 1f0bfac0cd..ca0807299c 100644
--- a/docs/src/model_creation/compositional_modeling.md
+++ b/docs/src/model_creation/compositional_modeling.md
@@ -27,7 +27,8 @@ We can test whether a system is complete using the `ModelingToolkit.iscomplete`
```@example ex0
ModelingToolkit.iscomplete(degradation_component)
```
-To mark a system as complete, after which is should be considered as representing a finalized model, use the `complete` function
+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)
@@ -35,8 +36,9 @@ 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 for
-extending a system is the `extend` command
+[`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
diff --git a/docs/src/model_creation/dsl_advanced.md b/docs/src/model_creation/dsl_advanced.md
index 3e42269b28..4edd069ba0 100644
--- a/docs/src/model_creation/dsl_advanced.md
+++ b/docs/src/model_creation/dsl_advanced.md
@@ -85,7 +85,7 @@ Generally, there are four main reasons for specifying species/parameters using t
3. To designate metadata for species/parameters (described [here](@ref dsl_advanced_options_species_and_parameters_metadata)).
4. To designate a species or parameters that do not occur in reactions, but are still part of the model (e.g a [parametric initial condition](@ref dsl_advanced_options_parametric_initial_conditions))
-!!! warn
+!!! warning
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)
@@ -496,7 +496,7 @@ sol = solve(oprob)
plot(sol)
```
-!!! warn
+!!! warning
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)
diff --git a/docs/src/model_creation/dsl_basics.md b/docs/src/model_creation/dsl_basics.md
index a091f49453..409e3b1f95 100644
--- a/docs/src/model_creation/dsl_basics.md
+++ b/docs/src/model_creation/dsl_basics.md
@@ -9,7 +9,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
+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
@@ -93,8 +93,8 @@ Reactants whose stoichiometries are not defined are assumed to have stoichiometr
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
rn6 = @reaction_network begin
- k, 2X + 3(Y + 2Z) --> 5(V + W)
- k, 2X + 3Y + 6Z --> 5V + 5W
+ k, 2X + 3(Y + 2Z) --> 5(V + W)
+ k, 2X + 3Y + 6Z --> 5V + 5W
end
```
@@ -186,7 +186,7 @@ rn12 = @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.
+However, like for the above model, bundling reactions too zealously can reduce (rather than improve) a model's readability.
## [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.
@@ -216,8 +216,8 @@ We can confirm that this generates the same ODE:
latexify(rn_13_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.
-!!! warn
- While `rn_13` and `rn_13_alt` will generate equivalent simulations, for jump simulations, the first model will have reduced performance (which generally are more performant when rates are constant).
+!!! warning
+ While `rn_13` and `rn_13_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.
!!! danger
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).
@@ -232,7 +232,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:
+It is possible for the rate to contain Julia functions. These can either be functions from Julia's standard library:
```@example dsl_basics
rn_16 = @reaction_network begin
d, A --> 0
@@ -281,8 +281,12 @@ rn_15 = @reaction_network begin
end
```
-!!! warn
- Jump simulations cannot be performed for models with time-dependent rates without additional considerations.
+!!! warning
+ Models with explicit time-dependent rates require additional steps to correctly
+ convert to stochastic chemical kinetics jump process representations. See
+ [here](https://github.com/SciML/Catalyst.jl/issues/636#issuecomment-1500311639)
+ for guidance on manually creating such representations. Enabling
+ Catalyst to handle this seamlessly is work in progress.
## [Non-standard stoichiometries](@id dsl_description_stoichiometries)
@@ -327,7 +331,7 @@ end
Catalyst uses `-->`, `<-->`, and `<--` to denote forward, bi-directional, and backwards reactions, respectively. Several unicode representations of these arrows are available. Here,
- `>`, `→`, `↣`, `↦`, `⇾`, `⟶`, `⟼`, `⥟`, `⥟`, `⇀`, and `⇁` can be used to represent forward reactions.
- `↔`, `⟷`, `⇄`, `⇆`, `⇌`, `⇋`, , and `⇔` can be used to represent bi-directional reactions.
-- `<`, `←`, `↢`, `↤`, `⇽`, `⟵`, `⟻`, `⥚`, `⥞`, `↼`, , and `↽` can be used to represent backwards reactions.
+- `<`, `←`, `↢`, `↤`, `⇽`, `⟵`, `⟻`, `⥚`, `⥞`, `↼`, , and `↽` can be used to represent backwards reactions.
E.g. the production/degradation system can alternatively be written as:
```@example dsl_basics
@@ -352,6 +356,7 @@ An example of how this can be used to create a neat-looking model can be found i
(kB,kD), A + σᵛ ↔ Aσᵛ
L, Aσᵛ → σᵛ
end
+nothing # hide
```
This functionality can also be used to create less serious models:
diff --git a/docs/src/model_creation/examples/basic_CRN_library.md b/docs/src/model_creation/examples/basic_CRN_library.md
index 76287686b5..7279fa4f33 100644
--- a/docs/src/model_creation/examples/basic_CRN_library.md
+++ b/docs/src/model_creation/examples/basic_CRN_library.md
@@ -115,12 +115,13 @@ using Plots
oplt = plot(osol; title = "Reaction rate equation (ODE)")
splt = plot(ssol; title = "Chemical Langevin equation (SDE)")
jplt = plot(jsol; title = "Stochastic chemical kinetics (Jump)")
-plot(oplt, splt, jplt; lw = 2, size=(800,800), layout = (3,1))
+plot(oplt, splt, jplt; lw = 2, size=(800,800), layout = (3,1))
+oplt = plot(osol; title = "Reaction rate equation (ODE)", plotdensity = 1000, fmt = :png) # hide
+splt = plot(ssol; title = "Chemical Langevin equation (SDE)", plotdensity = 1000, fmt = :png) # hide
+jplt = plot(jsol; title = "Stochastic chemical kinetics (Jump)", plotdensity = 1000, fmt = :png) # hide
+plot(oplt, splt, jplt; lw = 2, size=(800,800), layout = (3,1), plotdensity = 1000, fmt = :png) # hide
plot!(bottom_margin = 3Plots.Measures.mm) # hide
-nothing # hide
```
-![MM Kinetics](../../assets/long_ploting_times/model_creation/mm_kinetics.svg)
-Note that, due to the large amounts of the species involved, the stochastic trajectories are very similar to the deterministic one.
## [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.
@@ -160,9 +161,11 @@ jplt1 = plot(jsol1; title = "Outbreak")
jplt2 = plot(jsol2; title = "Outbreak")
jplt3 = plot(jsol3; title = "No outbreak")
plot(jplt1, jplt2, jplt3; lw = 3, size=(800,700), layout = (3,1))
-nothing # hide
+jplt1 = plot(jsol1; title = "Outbreak", plotdensity = 1000, fmt = :png) # hide
+jplt2 = plot(jsol2; title = "Outbreak", plotdensity = 1000, fmt = :png) # hide
+jplt3 = plot(jsol3; title = "No outbreak", plotdensity = 1000, fmt = :png) # hide
+plot(jplt1, jplt2, jplt3; lw = 3, size=(800,700), layout = (3,1), plotdensity = 1000, fmt = :png) # hide
```
-![SIR Outbreak](../../assets/long_ploting_times/model_creation/sir_outbreaks.svg)
## [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).
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 813ae5f401..873fbb13ff 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,7 @@
-# Loading Chemical Reaction Network Models from Files
-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 softwares 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.
+# [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
+## [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
@@ -56,7 +56,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
+## [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
@@ -70,7 +70,7 @@ rm("saved_parameters.jls") # hide
loaded_sol # hide
```
-## [Loading .net files using ReactionNetworkImporters.jl](@id file_loading_rni_net)
+## [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
@@ -96,7 +96,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
+## [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:
@@ -104,7 +104,7 @@ SBMLImporter's `load_SBML` function can be used to load SBML files. Here, we loa
using SBMLImporter
prn, cbs = load_SBML("brusselator.xml", massaction = true)
```
-Here, while [ReactionNetworkImporters generates a `ParsedReactionSystem` only](@ref file_loading_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:
+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, OrdinaryDiffEq, Plots
tspan = (0.0, 50.0)
@@ -121,10 +121,10 @@ A more detailed description of SBMLImporter's features can be found in its [docu
!!! note
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
+### [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
+## [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).
@@ -136,7 +136,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 catalyst_citation), please cite the paper(s) corresponding to whichever package(s) you used:
+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},
diff --git a/docs/src/model_creation/network_analysis.md b/docs/src/model_creation/network_analysis.md
index 792ed5477d..f1cf33efc9 100644
--- a/docs/src/model_creation/network_analysis.md
+++ b/docs/src/model_creation/network_analysis.md
@@ -364,7 +364,7 @@ complexgraph(rn)
It is evident from the preceding graph that the network is not reversible.
However, it satisfies a weaker property in that there is a path from each
reaction complex back to itself within its associated subgraph. This is known as
-*weak reversiblity*. One can test a network for weak reversibility by using
+*weak reversibility*. One can test a network for weak reversibility by using
the [`isweaklyreversible`](@ref) function:
```@example s1
# need subnetworks from the reaction network first
diff --git a/docs/src/model_creation/programmatic_CRN_construction.md b/docs/src/model_creation/programmatic_CRN_construction.md
index a8e7f15d86..d5d12911c8 100644
--- a/docs/src/model_creation/programmatic_CRN_construction.md
+++ b/docs/src/model_creation/programmatic_CRN_construction.md
@@ -61,7 +61,7 @@ system to be the same as the name of the variable storing the system.
Alternatively, one can use the `name = :repressilator` keyword argument to the
`ReactionSystem` constructor.
-!!! warn
+!!! warning
All `ReactionSystem`s created via the symbolic interface (i.e. by calling `ReactionSystem` with some input, rather than using `@reaction_network`) are not marked as complete. To simulate them, they must first be marked as *complete*, indicating to Catalyst and ModelingToolkit that they represent finalized models. This can be done using the `complete` function, i.e. by calling `repressilator = complete(repressilator)`. An expanded description on *completeness* can be found [here](@ref completeness_note).
We can check that this is the same model as the one we defined via the DSL as
@@ -180,7 +180,7 @@ This ensured they were properly treated as species and not parameters. See the
## Basic querying of `ReactionSystems`
-The [Catalyst.jl API](@ref) provides a large variety of functionality for
+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
@@ -247,5 +247,5 @@ rx1.prodstoich
rx1.netstoich
```
-See the [Catalyst.jl API](@ref) for much more detail on the various querying and
+See the [Catalyst.jl API](@ref api) for much more detail on the various querying and
analysis functions provided by Catalyst.
diff --git a/docs/src/model_simulation/ode_simulation_performance.md b/docs/src/model_simulation/ode_simulation_performance.md
index 9d6249d5f3..40e699657b 100644
--- a/docs/src/model_simulation/ode_simulation_performance.md
+++ b/docs/src/model_simulation/ode_simulation_performance.md
@@ -31,9 +31,8 @@ oprob = ODEProblem(brusselator, u0, tspan, ps)
sol1 = solve(oprob, Tsit5())
plot(sol1)
-nothing # hide
+plot(sol1, plotdensity = 1000, fmt = :png) # hide
```
-![Incomplete Brusselator Simulation](../assets/long_ploting_times/model_simulation/incomplete_brusselator_simulation.svg)
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
diff --git a/docs/src/model_simulation/sde_simulation_performance.md b/docs/src/model_simulation/sde_simulation_performance.md
new file mode 100644
index 0000000000..d32861ae69
--- /dev/null
+++ b/docs/src/model_simulation/sde_simulation_performance.md
@@ -0,0 +1,58 @@
+# [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).
+
+## [Options for Jacobian computation](@id sde_simulation_performance_jacobian)
+In the section on ODE simulation performance, we describe various [options for computing the system Jacobian](@ref ode_simulation_performance_jacobian), and how these could be used to improve performance for [implicit solvers](@ref ode_simulation_performance_stiffness). These can be used in tandem with implicit SDE solvers (such as `STrapezoid`). However, due to additional considerations during SDE simulations, it is much less certain whether these will actually have any impact on performance. So while these options might be worth reading about and trialling, there is no guarantee that they will be beneficial.
+
+## [Parallelisation on CPUs and GPUs](@id sde_simulation_performance_parallelisation)
+We have previously described how simulation parallelisation can be used to [improve performance when multiple ODE simulations are carried out](@ref ode_simulation_performance_parallelisation). The same approaches can be used for SDE simulations. Indeed, it is often more relevant for SDEs, as these are often re-simulated using identical simulation conditions (to investigate their typical behaviour across many samples). CPU parallelisation of SDE simulations uses the [same approach as ODEs](@ref ode_simulation_performance_parallelisation_CPU). GPU parallelisation requires some additional considerations, which are described below.
+
+### [GPU parallelisation of SDE simulations](@id sde_simulation_performance_parallelisation_GPU)
+GPU parallelisation of SDE simulations uses a similar approach as that for [ODE simulations](@ref ode_simulation_performance_parallelisation_GPU). The main differences are that SDE parallelisation requires a GPU SDE solver (like `GPUEM`) and fixed time stepping.
+
+We will assume that we are using the CUDA GPU hardware, so we will first load the [CUDA.jl](https://github.com/JuliaGPU/CUDA.jl) backend package, as well as DiffEqGPU:
+```julia
+using CUDA, DiffEqGPU
+```
+Which backend package you should use depends on your available hardware, with the alternatives being listed [here](https://docs.sciml.ai/DiffEqGPU/stable/manual/backends/).
+
+Next, we create the `SDEProblem` which we wish to simulate. Like for ODEs, we ensure that all vectors are [static vectors](https://github.com/JuliaArrays/StaticArrays.jl) and that all values are `Float32`s. Here we prepare the parallel simulations of a simple [birth-death process](@ref basic_CRN_library_bd).
+```@example sde_simulation_performance_gpu
+using Catalyst, StochasticDiffEq, StaticArrays
+bd_model = @reaction_network begin
+ (p,d), 0 <--> X
+end
+@unpack X, p, d = bd_model
+
+u0 = @SVector [X => 20.0f0]
+tspan = (0.0f0, 10.0f0)
+ps = @SVector [p => 10.0f0, d => 1.0f0]
+sprob = SDEProblem(bd_model, u0, tspan, ps)
+nothing # hide
+```
+The `SDEProblem` is then used to [create an `EnsembleProblem`](@ref ensemble_simulations_monte_carlo).
+```@example sde_simulation_performance_gpu
+eprob = EnsembleProblem(sprob)
+nothing # hide
+```
+Finally, we can solve our `EnsembleProblem` while:
+- Using a valid GPU SDE solver (either [`GPUEM`](https://docs.sciml.ai/DiffEqGPU/stable/manual/ensemblegpukernel/#DiffEqGPU.GPUEM) or [`GPUSIEA`](https://docs.sciml.ai/DiffEqGPU/stable/manual/ensemblegpukernel/#DiffEqGPU.GPUSIEA)).
+- Designating the GPU ensemble method, `EnsembleGPUKernel` (with the correct GPU backend as input).
+- Designating the number of trajectories we wish to simulate.
+- Designating a fixed time step size.
+
+```julia
+esol = solve(eprob, GPUEM(), EnsembleGPUKernel(CUDA.CUDABackend()); trajectories = 10000, dt = 0.01)
+```
+
+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
diff --git a/docs/src/model_simulation/simulation_introduction.md b/docs/src/model_simulation/simulation_introduction.md
index bf5fc7158e..f2aafd8ec7 100644
--- a/docs/src/model_simulation/simulation_introduction.md
+++ b/docs/src/model_simulation/simulation_introduction.md
@@ -1,7 +1,7 @@
# [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).
+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.
@@ -169,7 +169,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. sBy default, Catalyst generates SDEs from CRN models using the chemical Langevin equation.
+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:
```@example simulation_intro_sde
@@ -222,6 +222,17 @@ sol = solve(sprob, STrapezoid(); seed = 12345, abstol = 1e-1, reltol = 1e-1) # h
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)
+sol = solve(sprob, EM(); dt = 0.001, seed = 1234567) # hide
+plot(sol)
+```
+We note that this approach also enables us to successfully simulate the SDE we previously struggled with.
+
+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
@@ -336,4 +347,54 @@ circadian_model = @reaction_network begin
d, P --> 0
end
```
-This type of model will generate so called *variable rate jumps*. Simulation of such model is non-trivial (and Catalyst currently lacks a good interface for this). A detailed description of how to carry out jump simulations for models with time-dependant rates can be found [here](https://docs.sciml.ai/JumpProcesses/stable/tutorials/simple_poisson_process/#VariableRateJumps-for-processes-that-are-not-constant-between-jumps).
\ No newline at end of file
+This type of model will generate so called *variable rate jumps*. Simulation of such model is non-trivial (and Catalyst currently lacks a good interface for this). A detailed description of how to carry out jump simulations for models with time-dependant rates can be found [here](https://docs.sciml.ai/JumpProcesses/stable/tutorials/simple_poisson_process/#VariableRateJumps-for-processes-that-are-not-constant-between-jumps).
+
+
+---
+## [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,
+ author = {Rackauckas, Christopher and Nie, Qing},
+ doi = {10.5334/jors.151},
+ journal = {The Journal of Open Research Software},
+ keywords = {Applied Mathematics},
+ note = {Exported from https://app.dimensions.ai on 2019/05/05},
+ number = {1},
+ pages = {},
+ title = {DifferentialEquations.jl – A Performant and Feature-Rich Ecosystem for Solving Differential Equations in Julia},
+ url = {https://app.dimensions.ai/details/publication/pub.1085583166 and http://openresearchsoftware.metajnl.com/articles/10.5334/jors.151/galley/245/download/},
+ volume = {5},
+ year = {2017}
+}
+```
+For SDE simulations:
+```
+@article{rackauckas2017adaptive,
+ title={Adaptive methods for stochastic differential equations via natural embeddings and rejection sampling with memory},
+ author={Rackauckas, Christopher and Nie, Qing},
+ journal={Discrete and continuous dynamical systems. Series B},
+ volume={22},
+ number={7},
+ pages={2731},
+ year={2017},
+ publisher={NIH Public Access}
+}
+```
+For jump simulations:
+```
+@misc{2022JumpProcesses,
+ author = {Isaacson, S. A. and Ilin, V. and Rackauckas, C. V.},
+ title = {{JumpProcesses.jl}},
+ howpublished = {\url{https://github.com/SciML/JumpProcesses.jl/}},
+ year = {2022}
+}
+@misc{zagatti_extending_2023,
+ title = {Extending {JumpProcess}.jl for fast point process simulation with time-varying intensities},
+ url = {http://arxiv.org/abs/2306.06992},
+ doi = {10.48550/arXiv.2306.06992},
+ publisher = {arXiv},
+ author = {Zagatti, Guilherme Augusto and Isaacson, Samuel A. and Rackauckas, Christopher and Ilin, Vasily and Ng, See-Kiong and Bressan, Stéphane},
+ year = {2023},
+}
+```
\ No newline at end of file
diff --git a/docs/src/model_simulation/simulation_structure_interfacing.md b/docs/src/model_simulation/simulation_structure_interfacing.md
index d8c9463234..1c37df3672 100644
--- a/docs/src/model_simulation/simulation_structure_interfacing.md
+++ b/docs/src/model_simulation/simulation_structure_interfacing.md
@@ -1,7 +1,7 @@
# [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, and `simulation_struct[:x] = 5.0` to set it to a new 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).
+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).
## [Interfacing problem objects](@id simulation_structure_interfacing_problems)
@@ -35,26 +35,6 @@ To retrieve several species initial condition (or parameter) values, simply give
oprob[[:S₁, :S₂]]
```
-We can change a species's initial condition value using a similar notation. Here we increase the initial concentration of $C$ (and also confirm that the new value is stored in an updated `oprob`):
-```@example structure_indexing
-oprob[:C] = 0.1
-oprob[:C]
-```
-Again, parameter values can be changed using a similar notation, however, again requiring `oprob.ps` notation:
-```@example structure_indexing
-oprob.ps[:k₁] = 10.0
-oprob.ps[:k₁]
-```
-Finally, vectors can be used to update multiple quantities simultaneously
-```@example structure_indexing
-oprob[[:S₁, :S₂]] = [0.5, 0.3]
-oprob[[:S₁, :S₂]]
-```
-Generally, when updating problems, it is often better to use the [`remake` function](@ref simulation_structure_interfacing_problems_remake) (especially when several values are updated).
-
-!!! warn
- Indexing *should not* be used not modify `JumpProblem`s. Here, [remake](@ref simulation_structure_interfacing_problems_remake) should be used exclusively.
-
A problem's time span can be accessed through the `tspan` field:
```@example structure_indexing
oprob.tspan
@@ -64,8 +44,8 @@ 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)
-The `remake` function offers an (to indexing) alternative approach for updating problems. Unlike indexing, `remake` creates a new problem (rather than updating the old one). Furthermore, it permits the updating of several values simultaneously. The `remake` function takes the following inputs:
-- The problem that is remakes.
+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).
- (optionally) `tspan`: An updated time span (using the same format as time spans normally are given in).
- (optionally) `p`: A vector with parameters that should be updated. The vector takes the same form as normal parameter vectors, but does not need to be complete (in which case only a subset of the parameters are updated).
@@ -74,22 +54,28 @@ Here we modify our problem to increase the initial condition concentrations of t
```@example structure_indexing
using OrdinaryDiffEq
oprob_new = remake(oprob; u0 = [:S₁ => 5.0, :S₂ => 2.5])
-oprob_new == oprob
+oprob_new != oprob
```
-Here, we instead use `remake` to simultaneously update a all three fields:
+Here, we instead use `remake` to simultaneously update all three fields:
```@example structure_indexing
oprob_new_2 = remake(oprob; u0 = [:C => 0.2], tspan = (0.0, 20.0), p = [:k₁ => 2.0, :k₂ => 2.0])
nothing # hide
```
+Typically, when using `remake` to update a problem, the common workflow is to overwrite the old one with the output. E.g. to set the value of `k₁` to `5.0` in `oprob`, you would do:
+```@example structure_indexing
+oprob = remake(oprob; p = [:k₁ => 5.0])
+nothing # hide
+```
+
## [Interfacing integrator objects](@id simulation_structure_interfacing_integrators)
-During a simulation, the solution is stored in an integrator object. Here, we will describe how to interface with these. The almost exclusive circumstance when integrator-interfacing is relevant is when simulation events are implemented through callbacks. However, to demonstrate integrator indexing in this tutorial, we will create one through the `init` function (while circumstances where one might [want to use `init` function exist](https://docs.sciml.ai/DiffEqDocs/stable/basics/integrator/#Initialization-and-Stepping), since integrators are automatically created during simulations, these are rare).
+During a simulation, the solution is stored in an integrator object. Here, we will describe how to interface with these. The almost exclusive circumstance when integrator-interfacing is relevant is when simulation events are implemented through [callbacks](https://docs.sciml.ai/DiffEqDocs/stable/features/callback_functions/). However, to demonstrate integrator indexing in this tutorial, we will create one through the `init` function (while circumstances where one might [want to use `init` function exist](https://docs.sciml.ai/DiffEqDocs/stable/basics/integrator/#Initialization-and-Stepping), since integrators are automatically created during simulations, these are rare).
```@example structure_indexing
integrator = init(oprob)
nothing # hide
```
-We can interface with our integrator using an identical syntax as [was used for problems](@ref simulation_structure_interfacing_problems) (with the exception that `remake` is not available). Here we update, and then check the values of, first the species $C$ and then the parameter $k₁$:
+We can interface with our integrator using an identical syntax as [was used for problems](@ref simulation_structure_interfacing_problems). The primary exception is that there is no `remake` function for integrators. Instead, we can update species and parameter values using normal indexing. Here we update, and then check the values of, first the species $C$ and then the parameter $k₁$:
```@example structure_indexing
integrator[:C] = 0.0
integrator[:C]
@@ -164,7 +150,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
+using Catalyst, OrdinaryDiffEq
t = default_t()
@species X1(t) X2(t)
@parameters k1 k2
@@ -180,7 +166,7 @@ tspan = (0.0, 1.0)
ps = [k1 => 1.0, k2 => 2.0]
oprob = ODEProblem(two_state_model, u0, tspan, ps)
-oprob[X1] = 5.0
+oprob = remake(oprob; u0 = [X1 => 5.0])
oprob[X1]
```
Symbolic variables can be used to access or update species or parameters for all the cases when `Symbol`s can (including when using `remake` or e.g. `getu`).
@@ -196,5 +182,5 @@ oprob[two_state_model.X1 + two_state_model.X2]
```
This can be used to form symbolic expressions using model quantities when a model has been created using the DSL (as an alternative to @unpack). Alternatively, [creating an observable](@ref dsl_advanced_options_observables), and then interface using its `Symbol` representation, is also possible.
-!!! warn
- With interfacing with a simulating structure using symbolic variables stored in a `ReactionSystem` model, ensure that the model is complete.
\ No newline at end of file
+!!! warning
+ When accessing a simulation structure using symbolic variables from a `ReactionSystem` model, such as `rn.A` for `rn` a `ReactionSystem` and `A` a species within it, ensure that the model is complete.
diff --git a/docs/src/steady_state_functionality/dynamical_systems.md b/docs/src/steady_state_functionality/dynamical_systems.md
index 5b844e065d..ab48f99bac 100644
--- a/docs/src/steady_state_functionality/dynamical_systems.md
+++ b/docs/src/steady_state_functionality/dynamical_systems.md
@@ -124,7 +124,7 @@ 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 catalyst_citation), please cite the following paper to support the author of the DynamicalSystems.jl package:
+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,
doi = {10.21105/joss.00598},
diff --git a/docs/src/steady_state_functionality/nonlinear_solve.md b/docs/src/steady_state_functionality/nonlinear_solve.md
index 0630da11fa..d5e438d89c 100644
--- a/docs/src/steady_state_functionality/nonlinear_solve.md
+++ b/docs/src/steady_state_functionality/nonlinear_solve.md
@@ -133,7 +133,7 @@ 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 catalyst_citation), please cite the following paper to support the authors of the NonlinearSolve.jl package:
+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,
title={NonlinearSolve. jl: High-Performance and Robust Solvers for Systems of Nonlinear Equations in Julia},
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 b23a0b0d88..c51b55cd2a 100644
--- a/docs/src/steady_state_functionality/steady_state_stability_computation.md
+++ b/docs/src/steady_state_functionality/steady_state_stability_computation.md
@@ -1,10 +1,10 @@
-# Steady state stability computation
+# [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.
-!!! warn
+!!! warning
Catalyst currently computes steady state stabilities using the naive approach of checking whether a system's largest eigenvalue real part is negative. While more advanced stability computation methods exist (and would be a welcome addition to Catalyst), there is no direct plans to implement these. Furthermore, Catalyst uses a tolerance `tol = 10*sqrt(eps())` to determine whether a computed eigenvalue is far away enough from 0 to be reliably used. This threshold can be changed through the `tol` keyword argument.
-## Basic examples
+## [Basic examples](@id steady_state_stability_basics)
Let us consider the following basic example:
```@example stability_1
using Catalyst
@@ -42,7 +42,7 @@ Finally, as described above, Catalyst uses an optional argument, `tol`, to deter
nothing# hide
```
-## Pre-computing the Jacobian to increase performance when computing stability for many steady states
+## [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.
In this example we use the self-activation loop from previously, pre-computes its Jacobian, and uses it to multiple `steady_state_stability` calls:
@@ -59,5 +59,5 @@ stabs_2 = [steady_state_stability(st, sa_loop, ps_2; ss_jac) for st in steady_st
nothing # hide
```
-!!! warn
+!!! warning
For systems with [conservation laws](@ref homotopy_continuation_conservation_laws), `steady_state_jac` must be supplied a `u0` vector (indicating species concentrations for conservation law computation). This is required to eliminate the conserved quantities, preventing a singular Jacobian. These are supplied using the `u0` optional argument.
\ No newline at end of file
diff --git a/docs/src/v14_migration_guide.md b/docs/src/v14_migration_guide.md
new file mode 100644
index 0000000000..ed5074839b
--- /dev/null
+++ b/docs/src/v14_migration_guide.md
@@ -0,0 +1,214 @@
+# Version 14 Migration Guide
+
+Catalyst is built on the [ModelingToolkit.jl](https://github.com/SciML/ModelingToolkit.jl) modelling language. A recent update of ModelingToolkit from version 8 to version 9 has required a corresponding update to Catalyst (from version 13 to 14). This update has introduced a couple of breaking changes, all of which will be detailed below.
+
+!!! note
+ 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).
+
+A model's completeness depends on how it was created:
+- Models created programmatically (using the `ReactionSystem` constructor) are *not marked as complete* by default.
+- Models created using the `@reaction_network` DSL are *automatically marked as complete*.
+- To *use the DSL to create models that are not marked as complete*, use the `@network_component` macro (which in all other aspects is identical to `@reaction_network`).
+- Models generated through the `compose` and `extend` functions are *not marked as complete*.
+
+Furthermore, any systems generated through e.g. `convert(ODESystem, rs)` are *not marked as complete*.
+
+Complete models can be generated from incomplete models through the `complete` function. Here is a workflow where we take completeness into account in the simulation of a simple birth-death process.
+```@example v14_migration_1
+using Catalyst
+t = default_t()
+@species X(t)
+@parameters p d
+rxs = [
+ Reaction(p, [], [X]),
+ Reaction(d, [X], [])
+]
+@named rs = ReactionSystem(rxs, t)
+```
+Here we have created a model that is not marked as complete. If our model is ready (i.e. we do not wish to compose it with additional models) we mark it as complete:
+```@example v14_migration_1
+rs = complete(rs)
+```
+Here, `complete` does not change the input model, but simply creates a new model that is tagged as complete. We hence overwrite our model variable (`rs`) with `complete`'s output. We can confirm that our model is complete using the `Catalyst.iscomplete` function:
+```@example v14_migration_1
+Catalyst.iscomplete(rs)
+```
+We can now go on and use our model for e.g. simulations:
+```@example v14_migration_1
+using OrdinaryDiffEq, Plots
+u0 = [X => 0.1]
+tspan = (0.0, 10.0)
+ps = [p => 1.0, d => 0.2]
+oprob = ODEProblem(rs, u0, tspan, ps)
+sol = solve(oprob)
+plot(sol)
+```
+
+If we wish to first manually convert our `ReactionSystem` to an `ODESystem`, the generated `ODESystem` will *not* be marked as complete
+```@example v14_migration_1
+osys = convert(ODESystem, rs)
+Catalyst.iscomplete(osys)
+```
+(note that `rs` must be complete before it can be converted to an `ODESystem` or any other system type)
+
+If we now wish to create an `ODEProblem` from our `ODESystem`, we must first mark it as complete (using similar syntax as for our `ReactionSystem`):
+```@example v14_migration_1
+osys = complete(osys)
+oprob = ODEProblem(osys, u0, tspan, ps)
+sol = solve(oprob)
+plot(sol)
+```
+
+Note, if we had instead used the [`@reaction_network`](@ref) DSL macro to build
+our model, i.e.
+```@example v14_migration_1
+rs2 = @reaction_network rs begin
+ p, ∅ --> X
+ d, X --> ∅
+end
+```
+then the model is automatically marked as complete
+```@example v14_migration_1
+Catalyst.iscomplete(rs2)
+```
+In contrast, if we used the [`@network_component`](@ref) DSL macro to build our
+model it is not marked as complete, and is equivalent to our original definition of `rs`
+```@example v14_migration_1
+rs3 = @network_component rs begin
+ p, ∅ --> X
+ d, X --> ∅
+end
+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:
+```@example v14_migration_2
+using Catalyst
+t = default_t()
+D = default_time_deriv()
+@species X(t)
+@variables V(t)
+@parameters p d Vmax
+
+eqs = [
+ Reaction(p, [], [X]),
+ Reaction(d, [X], []),
+ D(V) ~ Vmax - V*X*d/p
+]
+@named rs = ReactionSystem(eqs, t)
+```
+We can now use `unknowns` to retrieve all unknowns
+```@example v14_migration_2
+unknowns(rs)
+```
+Meanwhile, `species` and `nonspecies` (like previously) returns all species or non-species unknowns, respectively:
+```@example v14_migration_2
+species(rs)
+```
+```@example v14_migration_2
+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.
+
+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
+@variables t
+nothing # hide
+```
+MTKv9 has introduced a standard global time variable, and as such a new, preferred, interface has been developed:
+```@example v14_migration_3
+t = default_t()
+nothing # hide
+```
+
+Similarly, the time differential (primarily relevant when creating combined reaction-ODE models) used to be declared through
+```@example v14_migration_3
+D = Differential(t)
+nothing # hide
+```
+where the preferred method is now
+```@example v14_migration_3
+D = default_time_deriv()
+nothing # hide
+```
+
+!!! note
+ 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
+rn = @reaction_network begin
+ (p,d), 0 <--> X
+end
+u0 = [:X => 1.0]
+ps = [:p => 1.0, :d => 0.2]
+oprob = ODEProblem(rn, u0, (0.0, 1.0), ps)
+nothing # hide
+```
+```julia
+oprob[:p]
+```
+This is *no longer supported*. When you wish to query a problem (or integrator or solution) for a parameter value (or to update a parameter value), you must append `.ps` to the problem variable name:
+```@example v14_migration_4
+oprob.ps[:p]
+```
+
+Furthermore, a few new functions (`getp`, `getu`, `setp`, `setu`) have been introduced from [SymbolicIndexingInterface](https://github.com/SciML/SymbolicIndexingInterface.jl) to support efficient and systematic querying and/or updating of symbolic unknown/parameter values. Using these can *significantly* improve performance when querying or updating a value multiple times, for example within a callback. These are described in more detail [here](@ref simulation_structure_interfacing_functions).
+
+For more details on how to query various structures for parameter and species values, please read [this documentation page](@ref simulation_structure_interfacing).
+
+## 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, OrdinaryDiffEq
+rn = @reaction_network begin
+ (k1,k2), X1 <--> X2
+end
+u0 = [:X1 => 1.0, :X2 => 2.0]
+ps = [:k1 => 0.5, :k2 => 3.0]
+oprob = ODEProblem(rn, u0, (0.0, 10.0), ps; remove_conserved = true)
+solve(oprob)
+# hide
+```
+is perfectly fine, attempting to then modify any initial conditions or the value of the conservation constant in `oprob` will likely silently fail:
+```@example v14_migration_5
+oprob_remade = remake(oprob; u0 = [:X1 => 5.0]) # NEVER do this.
+solve(oprob_remade)
+# hide
+```
+This might generate a silent error, where the remade problem is different from the intended one (the value of the conserved constant will not be updated correctly).
+
+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.
\ No newline at end of file
diff --git a/docs/unpublished/petab_ode_param_fitting.md b/docs/unpublished/petab_ode_param_fitting.md
index 9e503d7411..64b61df5bf 100644
--- a/docs/unpublished/petab_ode_param_fitting.md
+++ b/docs/unpublished/petab_ode_param_fitting.md
@@ -523,7 +523,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 catalyst_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:
+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},
diff --git a/ext/CatalystBifurcationKitExtension/bifurcation_kit_extension.jl b/ext/CatalystBifurcationKitExtension/bifurcation_kit_extension.jl
index be0becb3a9..6aeafa9bc2 100644
--- a/ext/CatalystBifurcationKitExtension/bifurcation_kit_extension.jl
+++ b/ext/CatalystBifurcationKitExtension/bifurcation_kit_extension.jl
@@ -22,7 +22,8 @@ function BK.BifurcationProblem(rs::ReactionSystem, u0_bif, ps, bif_par, args...;
# Creates NonlinearSystem.
Catalyst.conservationlaw_errorcheck(rs, vcat(ps, u0))
- nsys = convert(NonlinearSystem, rs; remove_conserved = true, defaults = Dict(u0))
+ nsys = convert(NonlinearSystem, rs; defaults = Dict(u0),
+ remove_conserved = true, remove_conserved_warn = false)
nsys = complete(nsys)
# Makes BifurcationProblem (this call goes through the ModelingToolkit-based BifurcationKit extension).
diff --git a/ext/CatalystHomotopyContinuationExtension/homotopy_continuation_extension.jl b/ext/CatalystHomotopyContinuationExtension/homotopy_continuation_extension.jl
index db7a8ff0c4..916a124423 100644
--- a/ext/CatalystHomotopyContinuationExtension/homotopy_continuation_extension.jl
+++ b/ext/CatalystHomotopyContinuationExtension/homotopy_continuation_extension.jl
@@ -49,7 +49,8 @@ end
# For a given reaction system, parameter values, and initial conditions, find the polynomial that HC solves to find steady states.
function steady_state_polynomial(rs::ReactionSystem, ps, u0)
rs = Catalyst.expand_registered_functions(rs)
- ns = complete(convert(NonlinearSystem, rs; remove_conserved = true))
+ ns = complete(convert(NonlinearSystem, rs;
+ remove_conserved = true, remove_conserved_warn = false))
pre_varmap = [symmap_to_varmap(rs, u0)..., symmap_to_varmap(rs, ps)...]
Catalyst.conservationlaw_errorcheck(rs, pre_varmap)
p_vals = ModelingToolkit.varmap_to_vars(pre_varmap, parameters(ns);
diff --git a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl
index c5906eaf6e..29269b91eb 100644
--- a/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl
+++ b/ext/CatalystStructuralIdentifiabilityExtension/structural_identifiability_extension.jl
@@ -167,7 +167,7 @@ function make_osys(rs::ReactionSystem; remove_conserved = true)
error("Identifiability should only be computed for complete systems. A ReactionSystem can be marked as complete using the `complete` function.")
end
rs = complete(Catalyst.expand_registered_functions(flatten(rs)))
- osys = complete(convert(ODESystem, rs; remove_conserved))
+ osys = complete(convert(ODESystem, rs; remove_conserved, remove_conserved_warn = false))
vars = [unknowns(rs); parameters(rs)]
# Computes equations for system conservation laws.
diff --git a/src/Catalyst.jl b/src/Catalyst.jl
index add5e6c7ae..e8181933ff 100644
--- a/src/Catalyst.jl
+++ b/src/Catalyst.jl
@@ -9,7 +9,7 @@ using LaTeXStrings, Latexify, Requires
using LinearAlgebra, Combinatorics
using JumpProcesses: JumpProcesses, JumpProblem,
MassActionJump, ConstantRateJump, VariableRateJump,
- SpatialMassActionJump
+ SpatialMassActionJump, CartesianGrid, CartesianGridRej
# ModelingToolkit imports and convenience functions we use
using ModelingToolkit
@@ -128,7 +128,8 @@ export @reaction_network, @network_component, @reaction, @species
include("network_analysis.jl")
export reactioncomplexmap, reactioncomplexes, incidencemat
export complexstoichmat
-export complexoutgoingmat, incidencematgraph, linkageclasses, deficiency, subnetworks
+export complexoutgoingmat, incidencematgraph, linkageclasses, stronglinkageclasses,
+ terminallinkageclasses, deficiency, subnetworks
export linkagedeficiencies, isreversible, isweaklyreversible
export conservationlaws, conservedquantities, conservedequations, conservationlaw_constants
@@ -172,18 +173,25 @@ include("spatial_reaction_systems/spatial_reactions.jl")
export TransportReaction, TransportReactions, @transport_reaction
export isedgeparameter
-# Lattice reaction systems
+# Lattice reaction systems.
include("spatial_reaction_systems/lattice_reaction_systems.jl")
export LatticeReactionSystem
export spatial_species, vertex_parameters, edge_parameters
-
-# Various utility functions
-include("spatial_reaction_systems/utility.jl")
+export CartesianGrid, CartesianGridReJ # (Implemented in JumpProcesses)
+export has_cartesian_lattice, has_masked_lattice, has_grid_lattice, has_graph_lattice,
+ grid_dims, grid_size
+export make_edge_p_values, make_directed_edge_values
+include("spatial_reaction_systems/lattice_solution_interfacing.jl")
+export get_lrs_vals
# Specific spatial problem types.
include("spatial_reaction_systems/spatial_ODE_systems.jl")
+export rebuild_lat_internals!
include("spatial_reaction_systems/lattice_jump_systems.jl")
+# General spatial modelling utility functions.
+include("spatial_reaction_systems/utility.jl")
+
### ReactionSystem Serialisation ###
# Has to be at the end (because it uses records of all metadata declared by Catalyst).
include("reactionsystem_serialisation/serialisation_support.jl")
diff --git a/src/dsl.jl b/src/dsl.jl
index ded8a854ea..337593a01f 100644
--- a/src/dsl.jl
+++ b/src/dsl.jl
@@ -138,8 +138,8 @@ end
"""
@network_component
-As the @reaction_network macro (see it for more information), but the output system
-*is not* complete.
+Equivalent to `@reaction_network` except the generated `ReactionSystem` is not marked as
+complete.
"""
macro network_component(name::Symbol, network_expr::Expr)
make_rs_expr(QuoteNode(name), network_expr; complete = false)
@@ -632,7 +632,7 @@ end
# When compound species are declared using the "@compound begin ... end" option, get a list of the compound species, and also the expression that crates them.
function read_compound_options(opts)
- # If the compound option is used retrive a list of compound species (need to be added to the reaction system's species), and the option that creates them (used to declare them as compounds at the end).
+ # If the compound option is used retrieve a list of compound species (need to be added to the reaction system's species), and the option that creates them (used to declare them as compounds at the end).
if haskey(opts, :compounds)
compound_expr = opts[:compounds]
# Find compound species names, and append the independent variable.
@@ -645,7 +645,7 @@ function read_compound_options(opts)
return compound_expr, compound_species
end
-# Read the events (continious or discrete) provided as options to the DSL. Returns an expression which evalutes to these.
+# Read the events (continuous or discrete) provided as options to the DSL. Returns an expression which evaluates to these.
function read_events_option(options, event_type::Symbol)
# Prepares the events, if required to, converts them to block form.
if event_type ∉ [:continuous_events, :discrete_events]
@@ -655,7 +655,7 @@ function read_events_option(options, event_type::Symbol)
MacroTools.striplines(:(begin end))
events_input = option_block_form(events_input)
- # Goes throgh the events, checks for errors, and adds them to the output vector.
+ # Goes through the events, checks for errors, and adds them to the output vector.
events_expr = :([])
for arg in events_input.args
# Formatting error checks.
@@ -666,7 +666,7 @@ function read_events_option(options, event_type::Symbol)
end
if (arg isa Expr) && (arg.args[2] isa Expr) && (arg.args[2].head != :vect) &&
(event_type == :continuous_events)
- error("The condition part of continious events (the left-hand side) must be a vector. This is not the case for: $(arg).")
+ error("The condition part of continuous events (the left-hand side) must be a vector. This is not the case for: $(arg).")
end
if (arg isa Expr) && (arg.args[3] isa Expr) && (arg.args[3].head != :vect)
error("The affect part of all events (the righ-hand side) must be a vector. This is not the case for: $(arg).")
@@ -681,10 +681,10 @@ end
# Reads the variables options. Outputs:
# `vars_extracted`: A vector with extracted variables (lhs in pure differential equations only).
-# `dtexpr`: If a differentialequation is defined, the default derrivative (D ~ Differential(t)) must be defined.
+# `dtexpr`: If a differential equation is defined, the default derivative (D ~ Differential(t)) must be defined.
# `equations`: a vector with the equations provided.
function read_equations_options(options, variables_declared)
- # Prepares the equations. First, extracts equations from provided option (converting to block form if requried).
+ # Prepares the equations. First, extracts equations from provided option (converting to block form if required).
# Next, uses MTK's `parse_equations!` function to split input into a vector with the equations.
eqs_input = haskey(options, :equations) ? options[:equations].args[3] : :(begin end)
eqs_input = option_block_form(eqs_input)
@@ -738,12 +738,12 @@ function create_differential_expr(options, add_default_diff, used_syms, tiv)
(dexpr.args[1] isa Symbol) ||
error("Differential left-hand side must be a single symbol, instead \"$(dexpr.args[1])\" was given.")
in(dexpr.args[1], used_syms) &&
- error("Differential name ($(dexpr.args[1])) is also a species, variable, or parameter. This is ambigious and not allowed.")
+ error("Differential name ($(dexpr.args[1])) is also a species, variable, or parameter. This is ambiguous and not allowed.")
in(dexpr.args[1], forbidden_symbols_error) &&
error("A forbidden symbol ($(dexpr.args[1])) was used as a differential name.")
end
- # If the default differential D has been used, but not pre-declared using the @differenitals
+ # If the default differential D has been used, but not pre-declared using the @differentials
# options, add this declaration to the list of declared differentials.
if add_default_diff && !any(diff_dec.args[1] == :D for diff_dec in diffexpr.args)
push!(diffexpr.args, :(D = Differential($(tiv))))
@@ -767,7 +767,7 @@ end
function read_observed_options(options, species_n_vars_declared, ivs_sorted)
if haskey(options, :observables)
# Gets list of observable equations and prepares variable declaration expression.
- # (`options[:observables]` inlucdes `@observables`, `.args[3]` removes this part)
+ # (`options[:observables]` includes `@observables`, `.args[3]` removes this part)
observed_eqs = make_observed_eqs(options[:observables].args[3])
observed_expr = Expr(:block, :(@variables))
obs_syms = :([])
@@ -785,11 +785,11 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted)
error("A forbidden symbol ($(obs_eq.args[2])) was used as an observable name.")
end
if (obs_name in species_n_vars_declared) && is_escaped_expr(obs_eq.args[2])
- error("An interpoalted observable have been used, which has also been explicitly delcared within the system using eitehr @species or @variables. This is not permited.")
+ error("An interpolated observable have been used, which has also been explicitly declared within the system using either @species or @variables. This is not permitted.")
end
if ((obs_name in species_n_vars_declared) || is_escaped_expr(obs_eq.args[2])) &&
!isnothing(metadata)
- error("Metadata was provided to observable $obs_name in the `@observables` macro. However, the obervable was also declared separately (using either @species or @variables). When this is done, metadata should instead be provided within the original @species or @variable declaration.")
+ error("Metadata was provided to observable $obs_name in the `@observables` macro. However, the observable was also declared separately (using either @species or @variables). When this is done, metadata should instead be provided within the original @species or @variable declaration.")
end
# This bits adds the observables to the @variables vector which is given as output.
@@ -820,7 +820,7 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted)
# Adds the observable to the list of observable names.
# This is required for filtering away so these are not added to the ReactionSystem's species list.
- # Again, avoid this check if we have interpoalted the variable.
+ # Again, avoid this check if we have interpolated the variable.
is_escaped_expr(obs_eq.args[2]) || push!(obs_syms.args, obs_name)
end
diff --git a/src/network_analysis.jl b/src/network_analysis.jl
index f60bfcbc4a..1b0a18c1bf 100644
--- a/src/network_analysis.jl
+++ b/src/network_analysis.jl
@@ -260,25 +260,19 @@ end
Construct a directed simple graph where nodes correspond to reaction complexes and directed
edges to reactions converting between two complexes.
-Notes:
-- Requires the `incidencemat` to already be cached in `rn` by a previous call to
- `reactioncomplexes`.
-
For example,
```julia
sir = @reaction_network SIR begin
β, S + I --> 2I
ν, I --> R
end
-complexes,incidencemat = reactioncomplexes(sir)
incidencematgraph(sir)
```
"""
function incidencematgraph(rn::ReactionSystem)
nps = get_networkproperties(rn)
if Graphs.nv(nps.incidencegraph) == 0
- isempty(nps.incidencemat) &&
- error("Please call reactioncomplexes(rn) first to construct the incidence matrix.")
+ isempty(nps.incidencemat) && reactioncomplexes(rn)
nps.incidencegraph = incidencematgraph(nps.incidencemat)
end
nps.incidencegraph
@@ -329,17 +323,12 @@ Given the incidence graph of a reaction network, return a vector of the
connected components of the graph (i.e. sub-groups of reaction complexes that
are connected in the incidence graph).
-Notes:
-- Requires the `incidencemat` to already be cached in `rn` by a previous call to
- `reactioncomplexes`.
-
For example,
```julia
sir = @reaction_network SIR begin
β, S + I --> 2I
ν, I --> R
end
-complexes,incidencemat = reactioncomplexes(sir)
linkageclasses(sir)
```
gives
@@ -359,6 +348,56 @@ end
linkageclasses(incidencegraph) = Graphs.connected_components(incidencegraph)
+"""
+ stronglinkageclasses(rn::ReactionSystem)
+
+ Return the strongly connected components of a reaction network's incidence graph (i.e. sub-groups of reaction complexes such that every complex is reachable from every other one in the sub-group).
+"""
+
+function stronglinkageclasses(rn::ReactionSystem)
+ nps = get_networkproperties(rn)
+ if isempty(nps.stronglinkageclasses)
+ nps.stronglinkageclasses = stronglinkageclasses(incidencematgraph(rn))
+ end
+ nps.stronglinkageclasses
+end
+
+stronglinkageclasses(incidencegraph) = Graphs.strongly_connected_components(incidencegraph)
+
+"""
+ terminallinkageclasses(rn::ReactionSystem)
+
+ Return the terminal strongly connected components of a reaction network's incidence graph (i.e. sub-groups of reaction complexes that are 1) strongly connected and 2) every outgoing reaction from a complex in the component produces a complex also in the component).
+"""
+
+function terminallinkageclasses(rn::ReactionSystem)
+ nps = get_networkproperties(rn)
+ if isempty(nps.terminallinkageclasses)
+ slcs = stronglinkageclasses(rn)
+ tslcs = filter(lc -> isterminal(lc, rn), slcs)
+ nps.terminallinkageclasses = tslcs
+ end
+ nps.terminallinkageclasses
+end
+
+# Helper function for terminallinkageclasses. Given a linkage class and a reaction network, say whether the linkage class is terminal,
+# i.e. all outgoing reactions from complexes in the linkage class produce a complex also in the linkage class
+function isterminal(lc::Vector, rn::ReactionSystem)
+ imat = incidencemat(rn)
+
+ for r in 1:size(imat, 2)
+ # Find the index of the reactant complex for a given reaction
+ s = findfirst(==(-1), @view imat[:, r])
+
+ # If the reactant complex is in the linkage class, check whether the product complex is also in the linkage class. If any of them are not, return false.
+ if s in Set(lc)
+ p = findfirst(==(1), @view imat[:, r])
+ p in Set(lc) ? continue : return false
+ end
+ end
+ true
+end
+
@doc raw"""
deficiency(rn::ReactionSystem)
@@ -371,17 +410,12 @@ Here the deficiency, ``\delta``, of a network with ``n`` reaction complexes,
\delta = n - \ell - s
```
-Notes:
-- Requires the `incidencemat` to already be cached in `rn` by a previous call to
- `reactioncomplexes`.
-
For example,
```julia
sir = @reaction_network SIR begin
β, S + I --> 2I
ν, I --> R
end
-rcs,incidencemat = reactioncomplexes(sir)
δ = deficiency(sir)
```
"""
@@ -419,17 +453,12 @@ end
Find subnetworks corresponding to each linkage class of the reaction network.
-Notes:
-- Requires the `incidencemat` to already be cached in `rn` by a previous call to
- `reactioncomplexes`.
-
For example,
```julia
sir = @reaction_network SIR begin
β, S + I --> 2I
ν, I --> R
end
-complexes,incidencemat = reactioncomplexes(sir)
subnetworks(sir)
```
"""
@@ -456,17 +485,12 @@ end
Calculates the deficiency of each sub-reaction network within `network`.
-Notes:
-- Requires the `incidencemat` to already be cached in `rn` by a previous call to
- `reactioncomplexes`.
-
For example,
```julia
sir = @reaction_network SIR begin
β, S + I --> 2I
ν, I --> R
end
-rcs,incidencemat = reactioncomplexes(sir)
linkage_deficiencies = linkagedeficiencies(sir)
```
"""
@@ -487,17 +511,12 @@ end
Given a reaction network, returns if the network is reversible or not.
-Notes:
-- Requires the `incidencemat` to already be cached in `rn` by a previous call to
- `reactioncomplexes`.
-
For example,
```julia
sir = @reaction_network SIR begin
β, S + I --> 2I
ν, I --> R
end
-rcs,incidencemat = reactioncomplexes(sir)
isreversible(sir)
```
"""
@@ -511,30 +530,27 @@ end
Determine if the reaction network with the given subnetworks is weakly reversible or not.
-Notes:
-- Requires the `incidencemat` to already be cached in `rn` by a previous call to
- `reactioncomplexes`.
-
For example,
```julia
sir = @reaction_network SIR begin
β, S + I --> 2I
ν, I --> R
end
-rcs,incidencemat = reactioncomplexes(sir)
subnets = subnetworks(rn)
isweaklyreversible(rn, subnets)
```
"""
function isweaklyreversible(rn::ReactionSystem, subnets)
- im = get_networkproperties(rn).incidencemat
- isempty(im) &&
- error("Error, please call reactioncomplexes(rn::ReactionSystem) to ensure the incidence matrix has been cached.")
- sparseig = issparse(im)
+ nps = get_networkproperties(rn)
+ isempty(nps.incidencemat) && reactioncomplexes(rn)
+ sparseig = issparse(nps.incidencemat)
+
for subnet in subnets
- nps = get_networkproperties(subnet)
- isempty(nps.incidencemat) && reactioncomplexes(subnet; sparse = sparseig)
+ subnps = get_networkproperties(subnet)
+ isempty(subnps.incidencemat) && reactioncomplexes(subnet; sparse = sparseig)
end
+
+ # A network is weakly reversible if all of its subnetworks are strongly connected
all(Graphs.is_strongly_connected ∘ incidencematgraph, subnets)
end
@@ -709,7 +725,7 @@ conservedquantities(state, cons_laws) = cons_laws * state
# If u0s are not given while conservation laws are present, throws an error.
# Used in HomotopyContinuation and BifurcationKit extensions.
# Currently only checks if any u0s are given
-# (not whether these are enough for computing conserved quantitites, this will yield a less informative error).
+# (not whether these are enough for computing conserved quantities, this will yield a less informative error).
function conservationlaw_errorcheck(rs, pre_varmap)
vars_with_vals = Set(p[1] for p in pre_varmap)
any(s -> s in vars_with_vals, species(rs)) && return
diff --git a/src/reaction.jl b/src/reaction.jl
index 592e448613..165bbeff37 100644
--- a/src/reaction.jl
+++ b/src/reaction.jl
@@ -445,10 +445,10 @@ end
"""
getmetadata_dict(reaction::Reaction)
-Retrives the `ImmutableDict` containing all of the metadata associated with a specific reaction.
+Retrieves the `ImmutableDict` containing all of the metadata associated with a specific reaction.
Arguments:
-- `reaction`: The reaction for which we wish to retrive all metadata.
+- `reaction`: The reaction for which we wish to retrieve all metadata.
Example:
```julia
@@ -482,11 +482,11 @@ end
"""
getmetadata(reaction::Reaction, md_key::Symbol)
-Retrives a certain metadata value from a `Reaction`. If the metadata does not exists, throws an error.
+Retrieves a certain metadata value from a `Reaction`. If the metadata does not exist, throws an error.
Arguments:
-- `reaction`: The reaction for which we wish to retrive a specific metadata value.
-- `md_key`: The metadata for which we wish to retrive.
+- `reaction`: The reaction for which we wish to retrieve a specific metadata value.
+- `md_key`: The metadata for which we wish to retrieve.
Example:
```julia
@@ -622,7 +622,7 @@ getmisc(reaction)
Notes:
- The `misc` field can contain any valid Julia structure. This mean that Catalyst cannot check it
-for symbolci variables that are added here. This means that symbolic variables (e.g. parameters of
+for symbolic variables that are added here. This means that symbolic variables (e.g. parameters of
species) that are stored here are not accessible to Catalyst. This can cause troubles when e.g.
creating a `ReactionSystem` programmatically (in which case any symbolic variables stored in the
`misc` metadata field should also be explicitly provided to the `ReactionSystem` constructor).
diff --git a/src/reactionsystem.jl b/src/reactionsystem.jl
index a84d41c522..7bd09f555b 100644
--- a/src/reactionsystem.jl
+++ b/src/reactionsystem.jl
@@ -78,6 +78,7 @@ Base.@kwdef mutable struct NetworkProperties{I <: Integer, V <: BasicSymbolic{Re
isempty::Bool = true
netstoichmat::Union{Matrix{Int}, SparseMatrixCSC{Int, Int}} = Matrix{Int}(undef, 0, 0)
conservationmat::Matrix{I} = Matrix{I}(undef, 0, 0)
+ cyclemat::Matrix{I} = Matrix{I}(undef, 0, 0)
col_order::Vector{Int} = Int[]
rank::Int = 0
nullity::Int = 0
@@ -93,6 +94,8 @@ Base.@kwdef mutable struct NetworkProperties{I <: Integer, V <: BasicSymbolic{Re
complexoutgoingmat::Union{Matrix{Int}, SparseMatrixCSC{Int, Int}} = Matrix{Int}(undef, 0, 0)
incidencegraph::Graphs.SimpleDiGraph{Int} = Graphs.DiGraph()
linkageclasses::Vector{Vector{Int}} = Vector{Vector{Int}}(undef, 0)
+ stronglinkageclasses::Vector{Vector{Int}} = Vector{Vector{Int}}(undef, 0)
+ terminallinkageclasses::Vector{Vector{Int}} = Vector{Vector{Int}}(undef, 0)
deficiency::Int = 0
end
#! format: on
@@ -116,6 +119,7 @@ function reset!(nps::NetworkProperties{I, V}) where {I, V}
nps.isempty && return
nps.netstoichmat = Matrix{Int}(undef, 0, 0)
nps.conservationmat = Matrix{I}(undef, 0, 0)
+ nps.cyclemat = Matrix{Int}(undef, 0, 0)
empty!(nps.col_order)
nps.rank = 0
nps.nullity = 0
@@ -131,6 +135,8 @@ function reset!(nps::NetworkProperties{I, V}) where {I, V}
nps.complexoutgoingmat = Matrix{Int}(undef, 0, 0)
nps.incidencegraph = Graphs.DiGraph()
empty!(nps.linkageclasses)
+ empty!(nps.stronglinkageclasses)
+ empty!(nps.terminallinkageclasses)
nps.deficiency = 0
# this needs to be last due to setproperty! setting it to false
@@ -195,7 +201,7 @@ end
function find_event_vars!(ps, us, events::Vector, ivs, vars)
foreach(event -> find_event_vars!(ps, us, event, ivs, vars), events)
end
-# For a single event, adds quantitites from its condition and affect expression(s) to `ps` and `us`.
+# For a single event, adds quantities from its condition and affect expression(s) to `ps` and `us`.
# Applies `findvars!` to the event's condition (`event[1])` and affec (`event[2]`).
function find_event_vars!(ps, us, event, ivs, vars)
findvars!(ps, us, event[1], ivs, vars)
@@ -406,7 +412,7 @@ function ReactionSystem(eqs, iv, unknowns, ps;
error("Catalyst reserves the symbols $forbidden_symbols_error for internal use. Please do not use these symbols as parameters or unknowns/species.")
end
- # Handles reactions and equations. Sorts so that reactions are before equaions in the equations vector.
+ # Handles reactions and equations. Sorts so that reactions are before equations in the equations vector.
eqs′ = CatalystEqType[eq for eq in eqs]
sort!(eqs′; by = eqsortby)
rxs = Reaction[rx for rx in eqs if rx isa Reaction]
@@ -443,7 +449,7 @@ function ReactionSystem(eqs, iv, unknowns, ps;
networkproperties
end
- # Creates the continious and discrete callbacks.
+ # Creates the continuous and discrete callbacks.
ccallbacks = MT.SymbolicContinuousCallbacks(continuous_events)
dcallbacks = MT.SymbolicDiscreteCallbacks(discrete_events)
@@ -464,7 +470,7 @@ function ReactionSystem(iv; kwargs...)
ReactionSystem(Reaction[], iv, [], []; kwargs...)
end
-# Called internally (whether DSL-based or programmtic model creation is used).
+# Called internally (whether DSL-based or programmatic model creation is used).
# Creates a sorted reactions + equations vector, also ensuring reaction is first in this vector.
# Extracts potential species, variables, and parameters from the input (if not provided as part of
# the model creation) and creates the corresponding vectors.
@@ -474,12 +480,12 @@ function make_ReactionSystem_internal(rxs_and_eqs::Vector, iv, us_in, ps_in;
spatial_ivs = nothing, continuous_events = [], discrete_events = [],
observed = [], kwargs...)
- # Filters away any potential obervables from `states` and `spcs`.
+ # Filters away any potential observables from `states` and `spcs`.
obs_vars = [obs_eq.lhs for obs_eq in observed]
us_in = filter(u -> !any(isequal(u, obs_var) for obs_var in obs_vars), us_in)
# Creates a combined iv vector (iv and sivs). This is used later in the function (so that
- # independent variables can be exluded when encountered quantities are added to `us` and `ps`).
+ # independent variables can be excluded when encountered quantities are added to `us` and `ps`).
t = value(iv)
ivs = Set([t])
if (spatial_ivs !== nothing)
@@ -501,17 +507,17 @@ function make_ReactionSystem_internal(rxs_and_eqs::Vector, iv, us_in, ps_in;
# Loops through all reactions, adding encountered quantities to the unknown and parameter vectors.
# Starts by looping through substrates + products only (so these are added to the vector first).
- # Next, the otehr components of reactions (e.g. rates and stoichiometries) are added.
+ # Next, the other components of reactions (e.g. rates and stoichiometries) are added.
for rx in rxs
for reactants in (rx.substrates, rx.products), spec in reactants
MT.isparameter(spec) ? push!(ps, spec) : push!(us, spec)
end
end
for rx in rxs
- # Adds all quantitites encountered in the reaction's rate.
+ # Adds all quantities encountered in the reaction's rate.
findvars!(ps, us, rx.rate, ivs, vars)
- # Extracts all quantitites encountered within stoichiometries.
+ # Extracts all quantities encountered within stoichiometries.
for stoichiometry in (rx.substoich, rx.prodstoich), sym in stoichiometry
(sym isa Symbolic) && findvars!(ps, us, sym, ivs, vars)
end
@@ -531,7 +537,7 @@ function make_ReactionSystem_internal(rxs_and_eqs::Vector, iv, us_in, ps_in;
fulleqs = rxs
end
- # Loops through all events, adding encountered quantities to the unknwon and parameter vectors.
+ # Loops through all events, adding encountered quantities to the unknown and parameter vectors.
find_event_vars!(ps, us, continuous_events, ivs, vars)
find_event_vars!(ps, us, discrete_events, ivs, vars)
@@ -633,7 +639,7 @@ get_networkproperties(sys::ReactionSystem) = getfield(sys, :networkproperties)
Returns true if the default for the system is to rescale ratelaws, see
https://docs.sciml.ai/Catalyst/stable/introduction_to_catalyst/introduction_to_catalyst/#Reaction-rate-laws-used-in-simulations
-for details. Can be overriden via passing `combinatoric_ratelaws` to `convert` or the
+for details. Can be overridden via passing `combinatoric_ratelaws` to `convert` or the
`*Problem` functions.
"""
get_combinatoric_ratelaws(sys::ReactionSystem) = getfield(sys, :combinatoric_ratelaws)
@@ -642,7 +648,7 @@ get_combinatoric_ratelaws(sys::ReactionSystem) = getfield(sys, :combinatoric_rat
combinatoric_ratelaws(sys::ReactionSystem)
Returns the effective (default) `combinatoric_ratelaw` value for a compositional system,
-calculated by taking the logical or of each component `ReactionSystem`. Can be overriden
+calculated by taking the logical or of each component `ReactionSystem`. Can be overridden
during calls to `convert` of problem constructors.
"""
function combinatoric_ratelaws(sys::ReactionSystem)
@@ -805,7 +811,7 @@ end
"""
nonreactions(network)
-Return the non-reaction equations within the network (i.e. algebraic and differnetial equations).
+Return the non-reaction equations within the network (i.e. algebraic and differential equations).
Notes:
- Allocates a new array to store the non-species variables.
@@ -833,7 +839,7 @@ isspatial(rn::ReactionSystem) = !isempty(get_sivs(rn))
### ModelingToolkit Function Dispatches ###
-# Retrives events.
+# Retrieves events.
MT.get_continuous_events(sys::ReactionSystem) = getfield(sys, :continuous_events)
# `MT.get_discrete_events(sys::ReactionSystem) = getfield(sys, :get_discrete_events)` should be added here.
@@ -1044,10 +1050,10 @@ end
# Checks if the `ReactionSystem` structure have been updated without also updating the
# `reactionsystem_fields` constant. If this is the case, returns `false`. This is used in
# certain functionalities which would break if the `ReactionSystem` structure is updated without
-# also updating tehse functionalities.
+# also updating these functionalities.
function reactionsystem_uptodate_check()
if fieldnames(ReactionSystem) != reactionsystem_fields
- @warn "The `ReactionSystem` strcuture have been modified without this being taken into account in the functionality you are attempting to use. Please report this at https://github.com/SciML/Catalyst.jl/issues. Proceed with cautioun, as there might be errors in whichever funcionality you are attempting to use."
+ @warn "The `ReactionSystem` structure have been modified without this being taken into account in the functionality you are attempting to use. Please report this at https://github.com/SciML/Catalyst.jl/issues. Proceed with caution, as there might be errors in whichever functionality you are attempting to use."
end
end
@@ -1230,7 +1236,7 @@ default reaction metadata is currently the only supported feature.
Arguments:
- `rs::ReactionSystem`: The `ReactionSystem` which you wish to remake.
- `default_reaction_metadata::Vector{Pair{Symbol, T}}`: A vector with default `Reaction` metadata values.
- Each metadata in each `Reaction` of the updated `ReactionSystem` will have the value desiganted in
+ Each metadata in each `Reaction` of the updated `ReactionSystem` will have the value designated in
`default_reaction_metadata` (however, `Reaction`s that already have that metadata designated will not
have their value updated).
"""
diff --git a/src/reactionsystem_conversions.jl b/src/reactionsystem_conversions.jl
index 8e49896854..c46854454f 100644
--- a/src/reactionsystem_conversions.jl
+++ b/src/reactionsystem_conversions.jl
@@ -109,7 +109,7 @@ function assemble_diffusion(rs, sts, ispcs; combinatoric_ratelaws = true,
num_bcsts = count(isbc, get_unknowns(rs))
# we make a matrix sized by the number of reactions
- eqs = Matrix{Any}(undef, length(sts) + num_bcsts, length(get_rxs(rs)))
+ eqs = Matrix{Num}(undef, length(sts) + num_bcsts, length(get_rxs(rs)))
eqs .= 0
species_to_idx = Dict((x => i for (i, x) in enumerate(ispcs)))
nps = get_networkproperties(rs)
@@ -433,7 +433,7 @@ end
### Utility ###
-# Throws an error when attempting to convert a spatial system to an unssuported type.
+# Throws an error when attempting to convert a spatial system to an unsupported type.
function spatial_convert_err(rs::ReactionSystem, systype)
isspatial(rs) && error("Conversion to $systype is not supported for spatial networks.")
end
@@ -450,6 +450,22 @@ diff_2_zero(expr) = (Symbolics.is_derivative(expr) ? 0 : expr)
COMPLETENESS_ERROR = "A ReactionSystem must be complete before it can be converted to other system types. A ReactionSystem can be marked as complete using the `complete` function."
+# Used to, when required, display a warning about conservation law removal and remake.
+function check_cons_warning(remove_conserved, remove_conserved_warn)
+ (remove_conserved && remove_conserved_warn) || return
+ @warn "You are creating a system or problem while eliminating conserved quantities. Please note,
+ due to limitations / design choices in ModelingToolkit if you use the created system to
+ create a problem (e.g. an `ODEProblem`), or are directly creating a problem, you *should not*
+ modify that problem's initial conditions for species (e.g. using `remake`). Changing initial
+ conditions must be done by creating a new Problem from your reaction system or the
+ ModelingToolkit system you converted it into with the new initial condition map.
+ Modification of parameter values is still possible, *except* for the modification of any
+ conservation law constants ($CONSERVED_CONSTANT_SYMBOL), which is not possible. You might
+ get this warning when creating a problem directly.
+
+ You can remove this warning by setting `remove_conserved_warn = false`."
+end
+
### System Conversions ###
"""
@@ -467,15 +483,20 @@ Keyword args and default values:
- `remove_conserved=false`, if set to `true` will calculate conservation laws of the
underlying set of reactions (ignoring constraint equations), and then apply them to reduce
the number of equations.
+- `remove_conserved_warn = true`: If `true`, if also `remove_conserved = true`, there will be
+ a warning regarding limitations of modifying problems generated from the created system.
"""
function Base.convert(::Type{<:ODESystem}, rs::ReactionSystem; name = nameof(rs),
combinatoric_ratelaws = get_combinatoric_ratelaws(rs),
- include_zero_odes = true, remove_conserved = false, checks = false,
- default_u0 = Dict(), default_p = Dict(),
+ include_zero_odes = true, remove_conserved = false, remove_conserved_warn = true,
+ checks = false, default_u0 = Dict(), default_p = Dict(),
defaults = _merge(Dict(default_u0), Dict(default_p)),
kwargs...)
+ # Error checks.
iscomplete(rs) || error(COMPLETENESS_ERROR)
spatial_convert_err(rs::ReactionSystem, ODESystem)
+ check_cons_warning(remove_conserved, remove_conserved_warn)
+
fullrs = Catalyst.flatten(rs)
remove_conserved && conservationlaws(fullrs)
ists, ispcs = get_indep_sts(fullrs, remove_conserved)
@@ -509,16 +530,19 @@ Keyword args and default values:
- `remove_conserved=false`, if set to `true` will calculate conservation laws of the
underlying set of reactions (ignoring constraint equations), and then apply them to reduce
the number of equations.
+- `remove_conserved_warn = true`: If `true`, if also `remove_conserved = true`, there will be
+ a warning regarding limitations of modifying problems generated from the created system.
"""
function Base.convert(::Type{<:NonlinearSystem}, rs::ReactionSystem; name = nameof(rs),
combinatoric_ratelaws = get_combinatoric_ratelaws(rs),
include_zero_odes = true, remove_conserved = false, checks = false,
- default_u0 = Dict(), default_p = Dict(),
+ remove_conserved_warn = true, default_u0 = Dict(), default_p = Dict(),
defaults = _merge(Dict(default_u0), Dict(default_p)),
all_differentials_permitted = false, kwargs...)
# Error checks.
iscomplete(rs) || error(COMPLETENESS_ERROR)
spatial_convert_err(rs::ReactionSystem, NonlinearSystem)
+ check_cons_warning(remove_conserved, remove_conserved_warn)
if !isautonomous(rs)
error("Attempting to convert a non-autonomous `ReactionSystem` (e.g. where some rate depend on $(get_iv(rs))) to a `NonlinearSystem`. This is not possible. if you are intending to compute system steady states, consider creating and solving a `SteadyStateProblem.")
end
@@ -546,7 +570,7 @@ end
# Ideally, when `ReactionSystem`s are converted to `NonlinearSystem`s, any coupled ODEs should be
# on the form D(X) ~ ..., where lhs is the time derivative w.r.t. a single variable, and the rhs
-# does not contain any differentials. If this is not teh case, we throw a warning to let the user
+# does not contain any differentials. If this is not the case, we throw a warning to let the user
# know that they should be careful.
function nonlinear_convert_differentials_check(rs::ReactionSystem)
for eq in filter(is_diff_equation, equations(rs))
@@ -554,7 +578,7 @@ function nonlinear_convert_differentials_check(rs::ReactionSystem)
# If there is a differential on the right hand side.
# If the lhs is not on the form D(...).
# If the lhs upper level function is not a differential w.r.t. time.
- # If the contenct of the differential is not a variable (and nothing more).
+ # If the content of the differential is not a variable (and nothing more).
# If either of this is a case, throws the warning.
if hasnode(Symbolics.is_derivative, eq.rhs) ||
!Symbolics.is_derivative(eq.lhs) ||
@@ -566,8 +590,8 @@ function nonlinear_convert_differentials_check(rs::ReactionSystem)
(2) The right-hand side does not contain any differentials.
This is generally not permitted.
- If you still would like to perform this conversions, please use the `all_differentials_permitted = true` option. In this case, all differential will be set to `0`.
- However, it is recommended to proceed with caution to ensure that the produced nonlinear equation makes sense for you intended application."
+ If you still would like to perform this conversion, please use the `all_differentials_permitted = true` option. In this case, all differentials will be set to `0`.
+ However, it is recommended to proceed with caution to ensure that the produced nonlinear equation makes sense for your intended application."
)
end
end
@@ -589,17 +613,19 @@ Notes:
- `remove_conserved=false`, if set to `true` will calculate conservation laws of the
underlying set of reactions (ignoring constraint equations), and then apply them to reduce
the number of equations.
-- Does not currently support `ReactionSystem`s that include coupled algebraic or
- differential equations.
+- `remove_conserved_warn = true`: If `true`, if also `remove_conserved = true`, there will be
+ a warning regarding limitations of modifying problems generated from the created system.
"""
function Base.convert(::Type{<:SDESystem}, rs::ReactionSystem;
name = nameof(rs), combinatoric_ratelaws = get_combinatoric_ratelaws(rs),
include_zero_odes = true, checks = false, remove_conserved = false,
- default_u0 = Dict(), default_p = Dict(),
+ remove_conserved_warn = true, default_u0 = Dict(), default_p = Dict(),
defaults = _merge(Dict(default_u0), Dict(default_p)),
kwargs...)
+ # Error checks.
iscomplete(rs) || error(COMPLETENESS_ERROR)
spatial_convert_err(rs::ReactionSystem, SDESystem)
+ check_cons_warning(remove_conserved, remove_conserved_warn)
flatrs = Catalyst.flatten(rs)
@@ -651,7 +677,6 @@ function Base.convert(::Type{<:JumpSystem}, rs::ReactionSystem; name = nameof(rs
kwargs...)
iscomplete(rs) || error(COMPLETENESS_ERROR)
spatial_convert_err(rs::ReactionSystem, JumpSystem)
-
(remove_conserved !== nothing) &&
throw(ArgumentError("Catalyst does not support removing conserved species when converting to JumpSystems."))
@@ -684,12 +709,12 @@ function DiffEqBase.ODEProblem(rs::ReactionSystem, u0, tspan,
p = DiffEqBase.NullParameters(), args...;
check_length = false, name = nameof(rs),
combinatoric_ratelaws = get_combinatoric_ratelaws(rs),
- include_zero_odes = true, remove_conserved = false,
+ include_zero_odes = true, remove_conserved = false, remove_conserved_warn = true,
checks = false, structural_simplify = false, kwargs...)
u0map = symmap_to_varmap(rs, u0)
pmap = symmap_to_varmap(rs, p)
osys = convert(ODESystem, rs; name, combinatoric_ratelaws, include_zero_odes, checks,
- remove_conserved)
+ remove_conserved, remove_conserved_warn)
# Handles potential differential algebraic equations (which requires `structural_simplify`).
if structural_simplify
@@ -708,12 +733,12 @@ function DiffEqBase.NonlinearProblem(rs::ReactionSystem, u0,
p = DiffEqBase.NullParameters(), args...;
name = nameof(rs), include_zero_odes = true,
combinatoric_ratelaws = get_combinatoric_ratelaws(rs),
- remove_conserved = false, checks = false,
+ remove_conserved = false, remove_conserved_warn = true, checks = false,
check_length = false, all_differentials_permitted = false, kwargs...)
u0map = symmap_to_varmap(rs, u0)
pmap = symmap_to_varmap(rs, p)
nlsys = convert(NonlinearSystem, rs; name, combinatoric_ratelaws, include_zero_odes,
- checks, all_differentials_permitted, remove_conserved)
+ checks, all_differentials_permitted, remove_conserved, remove_conserved_warn)
nlsys = complete(nlsys)
return NonlinearProblem(nlsys, u0map, pmap, args...; check_length,
kwargs...)
@@ -723,12 +748,12 @@ end
function DiffEqBase.SDEProblem(rs::ReactionSystem, u0, tspan,
p = DiffEqBase.NullParameters(), args...;
name = nameof(rs), combinatoric_ratelaws = get_combinatoric_ratelaws(rs),
- include_zero_odes = true, checks = false, check_length = false,
- remove_conserved = false, structural_simplify = false, kwargs...)
+ include_zero_odes = true, checks = false, check_length = false, remove_conserved = false,
+ remove_conserved_warn = true, structural_simplify = false, kwargs...)
u0map = symmap_to_varmap(rs, u0)
pmap = symmap_to_varmap(rs, p)
sde_sys = convert(SDESystem, rs; name, combinatoric_ratelaws,
- include_zero_odes, checks, remove_conserved)
+ include_zero_odes, checks, remove_conserved, remove_conserved_warn)
# Handles potential differential algebraic equations (which requires `structural_simplify`).
if structural_simplify
@@ -772,12 +797,12 @@ function DiffEqBase.SteadyStateProblem(rs::ReactionSystem, u0,
p = DiffEqBase.NullParameters(), args...;
check_length = false, name = nameof(rs),
combinatoric_ratelaws = get_combinatoric_ratelaws(rs),
- remove_conserved = false, include_zero_odes = true,
+ remove_conserved = false, remove_conserved_warn = true, include_zero_odes = true,
checks = false, structural_simplify = false, kwargs...)
u0map = symmap_to_varmap(rs, u0)
pmap = symmap_to_varmap(rs, p)
osys = convert(ODESystem, rs; name, combinatoric_ratelaws, include_zero_odes, checks,
- remove_conserved)
+ remove_conserved, remove_conserved_warn)
# Handles potential differential algebraic equations (which requires `structural_simplify`).
if structural_simplify
@@ -798,7 +823,7 @@ function _symbol_to_var(sys, sym)
if hasproperty(sys, sym)
var = getproperty(sys, sym, namespace = false)
else
- strs = split(String(sym), "₊") # need to check if this should be split of not!!!
+ strs = split(String(sym), ModelingToolkit.NAMESPACE_SEPARATOR) # need to check if this should be split of not!!!
if length(strs) > 1
var = getproperty(sys, Symbol(strs[1]), namespace = false)
for str in view(strs, 2:length(strs))
diff --git a/src/registered_functions.jl b/src/registered_functions.jl
index e5305482e4..2c6bbebad4 100644
--- a/src/registered_functions.jl
+++ b/src/registered_functions.jl
@@ -109,16 +109,37 @@ function Symbolics.derivative(::typeof(hillar), args::NTuple{5, Any}, ::Val{5})
(args[1]^args[5] + args[2]^args[5] + args[4]^args[5])^2
end
+# Tuple storing all registered function (for use in various functionalities).
+const registered_funcs = (mm, mmr, hill, hillr, hillar)
+
### Custom CRN FUnction-related Functions ###
"""
-expand_registered_functions(expr)
+expand_registered_functions(in)
Takes an expression, and expands registered function expressions. E.g. `mm(X,v,K)` is replaced
-with v*X/(X+K). Currently supported functions: `mm`, `mmr`, `hill`, `hillr`, and `hill`.
+with v*X/(X+K). Currently supported functions: `mm`, `mmr`, `hill`, `hillr`, and `hill`. Can
+be applied to a reaction system, a reaction, an equation, or a symbolic expression. The input
+is not modified, while an output with any functions expanded is returned. If applied to a
+reaction system model, any cached network properties are reset.
"""
function expand_registered_functions(expr)
- iscall(expr) || return expr
+ if hasnode(is_catalyst_function, expr)
+ expr = replacenode(expr, expand_catalyst_function)
+ end
+ return expr
+end
+
+# Checks whether an expression corresponds to a catalyst function call (e.g. `mm(X,v,K)`).
+function is_catalyst_function(expr)
+ iscall(expr) || (return false)
+ return operation(expr) in registered_funcs
+end
+
+# If the input expression corresponds to a catalyst function call (e.g. `mm(X,v,K)`), returns
+# it in its expanded form. If not, returns the input expression.
+function expand_catalyst_function(expr)
+ is_catalyst_function(expr) || (return expr)
args = arguments(expr)
if operation(expr) == Catalyst.mm
return args[2] * args[1] / (args[1] + args[3])
@@ -132,27 +153,50 @@ function expand_registered_functions(expr)
return args[3] * (args[1]^args[5]) /
((args[1])^args[5] + (args[2])^args[5] + (args[4])^args[5])
end
- for i in 1:length(args)
- args[i] = expand_registered_functions(args[i])
- end
- return expr
end
-# If applied to a `Reaction`, return a reaction with its rate modified.
+# If applied to a Reaction, return a reaction with its rate modified.
function expand_registered_functions(rx::Reaction)
Reaction(expand_registered_functions(rx.rate), rx.substrates, rx.products,
rx.substoich, rx.prodstoich, rx.netstoich, rx.only_use_rate, rx.metadata)
end
-# If applied to an `Equation`, returns it with it applied to lhs and rhs
+# If applied to a Equation, returns it with it applied to lhs and rhs.
function expand_registered_functions(eq::Equation)
return expand_registered_functions(eq.lhs) ~ expand_registered_functions(eq.rhs)
end
-# If applied to a `ReactionSystem`, applied function to all `Reaction`s and other `Equation`s,
-# and return updated system.
+# If applied to a continuous event, returns it applied to eqs and affect.
+function expand_registered_functions(ce::ModelingToolkit.SymbolicContinuousCallback)
+ eqs = expand_registered_functions(ce.eqs)
+ affect = expand_registered_functions(ce.affect)
+ return ModelingToolkit.SymbolicContinuousCallback(eqs, affect)
+end
+
+# If applied to a discrete event, returns it applied to condition and affects.
+function expand_registered_functions(de::ModelingToolkit.SymbolicDiscreteCallback)
+ condition = expand_registered_functions(de.condition)
+ affects = expand_registered_functions(de.affects)
+ return ModelingToolkit.SymbolicDiscreteCallback(condition, affects)
+end
+
+# If applied to a vector, applies it to every element in the vector.
+function expand_registered_functions(vec::Vector)
+ return [Catalyst.expand_registered_functions(element) for element in vec]
+end
+
+# If applied to a ReactionSystem, applied function to all Reactions and other Equations, and return updated system.
+# Currently, `ModelingToolkit.has_X_events` returns `true` even if event vector is empty (hence
+# this function cannot be used).
function expand_registered_functions(rs::ReactionSystem)
- @set! rs.eqs = [Catalyst.expand_registered_functions(eq) for eq in get_eqs(rs)]
- @set! rs.rxs = [Catalyst.expand_registered_functions(rx) for rx in get_rxs(rs)]
+ @set! rs.eqs = Catalyst.expand_registered_functions(get_eqs(rs))
+ @set! rs.rxs = Catalyst.expand_registered_functions(get_rxs(rs))
+ if !isempty(ModelingToolkit.get_continuous_events(rs))
+ @set! rs.continuous_events = Catalyst.expand_registered_functions(ModelingToolkit.get_continuous_events(rs))
+ end
+ if !isempty(ModelingToolkit.get_discrete_events(rs))
+ @set! rs.discrete_events = Catalyst.expand_registered_functions(ModelingToolkit.get_discrete_events(rs))
+ end
+ reset_networkproperties!(rs)
return rs
end
diff --git a/src/spatial_reaction_systems/lattice_jump_systems.jl b/src/spatial_reaction_systems/lattice_jump_systems.jl
index fae7bb9e2d..ebbe367cf1 100644
--- a/src/spatial_reaction_systems/lattice_jump_systems.jl
+++ b/src/spatial_reaction_systems/lattice_jump_systems.jl
@@ -7,123 +7,123 @@ function DiffEqBase.DiscreteProblem(lrs::LatticeReactionSystem, u0_in, tspan,
error("Currently lattice Jump simulations only supported when all spatial reactions are transport reactions.")
end
- # Converts potential symmaps to varmaps
- # Vertex and edge parameters may be given in a tuple, or in a common vector, making parameter case complicated.
+ # Converts potential symmaps to varmaps.
u0_in = symmap_to_varmap(lrs, u0_in)
- p_in = (p_in isa Tuple{<:Any, <:Any}) ?
- (symmap_to_varmap(lrs, p_in[1]), symmap_to_varmap(lrs, p_in[2])) :
- symmap_to_varmap(lrs, p_in)
+ p_in = symmap_to_varmap(lrs, p_in)
# Converts u0 and p to their internal forms.
+ # u0 is simply a vector with all the species' initial condition values across all vertices.
# u0 is [spec 1 at vert 1, spec 2 at vert 1, ..., spec 1 at vert 2, ...].
- u0 = lattice_process_u0(u0_in, species(lrs), lrs.num_verts)
- # Both vert_ps and edge_ps becomes vectors of vectors. Each have 1 element for each parameter.
- # These elements are length 1 vectors (if the parameter is uniform),
- # or length num_verts/nE, with unique values for each vertex/edge (for vert_ps/edge_ps, respectively).
+ u0 = lattice_process_u0(u0_in, species(lrs), lrs)
+ # vert_ps and `edge_ps` are vector maps, taking each parameter's Symbolics representation to its value(s).
+ # vert_ps values are vectors. Here, index (i) is a parameter's value in vertex i.
+ # edge_ps values are sparse matrices. Here, index (i,j) is a parameter's value in the edge from vertex i to vertex j.
+ # Uniform vertex/edge parameters store only a single value (a length 1 vector, or size 1x1 sparse matrix).
vert_ps, edge_ps = lattice_process_p(p_in, vertex_parameters(lrs),
edge_parameters(lrs), lrs)
- # Returns a DiscreteProblem.
- # Previously, a Tuple was used for (vert_ps, edge_ps), but this was converted to a Vector internally.
- return DiscreteProblem(u0, tspan, [vert_ps, edge_ps], args...; kwargs...)
+ # Returns a DiscreteProblem (which basically just stores the processed input).
+ return DiscreteProblem(u0, tspan, [vert_ps; edge_ps], args...; kwargs...)
end
-# Builds a spatial JumpProblem from a DiscreteProblem containg a Lattice Reaction System.
-function JumpProcesses.JumpProblem(lrs::LatticeReactionSystem, dprob, aggregator,
- args...; name = nameof(lrs.rs),
- combinatoric_ratelaws = get_combinatoric_ratelaws(lrs.rs), kwargs...)
+# Builds a spatial JumpProblem from a DiscreteProblem containing a `LatticeReactionSystem`.
+function JumpProcesses.JumpProblem(lrs::LatticeReactionSystem, dprob, aggregator, args...;
+ combinatoric_ratelaws = get_combinatoric_ratelaws(reactionsystem(lrs)),
+ name = nameof(reactionsystem(lrs)), kwargs...)
# Error checks.
if !isnothing(dprob.f.sys)
- error("Unexpected `DiscreteProblem` passed into `JumpProblem`. Was a `LatticeReactionSystem` used as input to the initial `DiscreteProblem`?")
+ throw(ArgumentError("Unexpected `DiscreteProblem` passed into `JumpProblem`. Was a `LatticeReactionSystem` used as input to the initial `DiscreteProblem`?"))
end
# Computes hopping constants and mass action jumps (requires some internal juggling).
- # Currently, JumpProcesses requires uniform vertex parameters (hence `p=first.(dprob.p[1])`).
# Currently, the resulting JumpProblem does not depend on parameters (no way to incorporate these).
- # Hence the parameters of this one does nto actually matter. If at some point JumpProcess can
+ # Hence the parameters of this one do not actually matter. If at some point JumpProcess can
# handle parameters this can be updated and improved.
# The non-spatial DiscreteProblem have a u0 matrix with entries for all combinations of species and vertexes.
hopping_constants = make_hopping_constants(dprob, lrs)
sma_jumps = make_spatial_majumps(dprob, lrs)
- non_spat_dprob = DiscreteProblem(
- reshape(dprob.u0, lrs.num_species, lrs.num_verts), dprob.tspan, first.(dprob.p[1]))
+ non_spat_dprob = DiscreteProblem(reshape(dprob.u0, num_species(lrs), num_verts(lrs)),
+ dprob.tspan, first.(dprob.p[1]))
+ # Creates and returns a spatial JumpProblem (masked lattices are not supported by these).
+ spatial_system = has_masked_lattice(lrs) ? get_lattice_graph(lrs) : lattice(lrs)
return JumpProblem(non_spat_dprob, aggregator, sma_jumps;
- hopping_constants, spatial_system = lrs.lattice, name, kwargs...)
+ hopping_constants, spatial_system, name, kwargs...)
end
# Creates the hopping constants from a discrete problem and a lattice reaction system.
function make_hopping_constants(dprob::DiscreteProblem, lrs::LatticeReactionSystem)
# Creates the all_diff_rates vector, containing for each species, its transport rate across all edges.
- # If transport rate is uniform for one species, the vector have a single element, else one for each edge.
- spatial_rates_dict = Dict(compute_all_transport_rates(dprob.p[1], dprob.p[2], lrs))
+ # If the transport rate is uniform for one species, the vector has a single element, else one for each edge.
+ spatial_rates_dict = Dict(compute_all_transport_rates(Dict(dprob.p), lrs))
all_diff_rates = [haskey(spatial_rates_dict, s) ? spatial_rates_dict[s] : [0.0]
for s in species(lrs)]
- # Creates the hopping constant Matrix. It contains one element for each combination of species and vertex.
- # Each element is a Vector, containing the outgoing hopping rates for that species, from that vertex, on that edge.
- hopping_constants = [Vector{Float64}(undef, length(lrs.lattice.fadjlist[j]))
- for i in 1:(lrs.num_species), j in 1:(lrs.num_verts)]
-
- # For each edge, finds each position in `hopping_constants`.
- for (e_idx, e) in enumerate(edges(lrs.lattice))
- dst_idx = findfirst(isequal(e.dst), lrs.lattice.fadjlist[e.src])
- # For each species, sets that hopping rate.
- for s_idx in 1:(lrs.num_species)
- hopping_constants[s_idx, e.src][dst_idx] = get_component_value(
- all_diff_rates[s_idx], e_idx)
- end
+ # Creates an array (of the same size as the hopping constant array) containing all edges.
+ # First the array is a NxM matrix (number of species x number of vertices). Each element is a
+ # vector containing all edges leading out from that vertex (sorted by destination index).
+ edge_array = [Pair{Int64, Int64}[] for _1 in 1:num_species(lrs), _2 in 1:num_verts(lrs)]
+ for e in edge_iterator(lrs), s_idx in 1:num_species(lrs)
+ push!(edge_array[s_idx, e[1]], e)
end
+ foreach(e_vec -> sort!(e_vec; by = e -> e[2]), edge_array)
+ # Creates the hopping constants array. It has the same shape as the edge array, but each
+ # element is that species transportation rate along that edge
+ hopping_constants = [[Catalyst.get_edge_value(all_diff_rates[s_idx], e)
+ for e in edge_array[s_idx, src_idx]]
+ for s_idx in 1:num_species(lrs), src_idx in 1:num_verts(lrs)]
return hopping_constants
end
# Creates a SpatialMassActionJump struct from a (spatial) DiscreteProblem and a LatticeReactionSystem.
-# Could implementation a version which, if all reaction's rates are uniform, returns a MassActionJump.
-# Not sure if there is any form of performance improvement from that though. Possibly is not the case.
+# Could implement a version which, if all reactions' rates are uniform, returns a MassActionJump.
+# Not sure if there is any form of performance improvement from that though. Likely not the case.
function make_spatial_majumps(dprob, lrs::LatticeReactionSystem)
# Creates a vector, storing which reactions have spatial components.
- is_spatials = [Catalyst.has_spatial_vertex_component(rx.rate, lrs;
- vert_ps = dprob.p[1]) for rx in reactions(lrs.rs)]
+ is_spatials = [has_spatial_vertex_component(rx.rate, dprob.p)
+ for rx in reactions(reactionsystem(lrs))]
# Creates templates for the rates (uniform and spatial) and the stoichiometries.
# We cannot fetch reactant_stoich and net_stoich from a (non-spatial) MassActionJump.
# The reason is that we need to re-order the reactions so that uniform appears first, and spatial next.
- u_rates = Vector{Float64}(undef, length(reactions(lrs.rs)) - count(is_spatials))
- s_rates = Matrix{Float64}(undef, count(is_spatials), lrs.num_verts)
- reactant_stoich = Vector{Vector{Pair{Int64, Int64}}}(undef, length(reactions(lrs.rs)))
- net_stoich = Vector{Vector{Pair{Int64, Int64}}}(undef, length(reactions(lrs.rs)))
+ num_rxs = length(reactions(reactionsystem(lrs)))
+ u_rates = Vector{Float64}(undef, num_rxs - count(is_spatials))
+ s_rates = Matrix{Float64}(undef, count(is_spatials), num_verts(lrs))
+ reactant_stoich = Vector{Vector{Pair{Int64, Int64}}}(undef, num_rxs)
+ net_stoich = Vector{Vector{Pair{Int64, Int64}}}(undef, num_rxs)
# Loops through reactions with non-spatial rates, computes their rates and stoichiometries.
cur_rx = 1
- for (is_spat, rx) in zip(is_spatials, reactions(lrs.rs))
+ for (is_spat, rx) in zip(is_spatials, reactions(reactionsystem(lrs)))
is_spat && continue
- u_rates[cur_rx] = compute_vertex_value(rx.rate, lrs; vert_ps = dprob.p[1])[1]
+ u_rates[cur_rx] = compute_vertex_value(rx.rate, lrs; ps = dprob.p)[1]
substoich_map = Pair.(rx.substrates, rx.substoich)
- reactant_stoich[cur_rx] = int_map(substoich_map, lrs.rs)
- net_stoich[cur_rx] = int_map(rx.netstoich, lrs.rs)
+ reactant_stoich[cur_rx] = int_map(substoich_map, reactionsystem(lrs))
+ net_stoich[cur_rx] = int_map(rx.netstoich, reactionsystem(lrs))
cur_rx += 1
end
# Loops through reactions with spatial rates, computes their rates and stoichiometries.
- for (is_spat, rx) in zip(is_spatials, reactions(lrs.rs))
+ for (is_spat, rx) in zip(is_spatials, reactions(reactionsystem(lrs)))
is_spat || continue
- s_rates[cur_rx - length(u_rates), :] = compute_vertex_value(rx.rate, lrs;
- vert_ps = dprob.p[1])
+ s_rates[cur_rx - length(u_rates), :] .= compute_vertex_value(rx.rate, lrs;
+ ps = dprob.p)
substoich_map = Pair.(rx.substrates, rx.substoich)
- reactant_stoich[cur_rx] = int_map(substoich_map, lrs.rs)
- net_stoich[cur_rx] = int_map(rx.netstoich, lrs.rs)
+ reactant_stoich[cur_rx] = int_map(substoich_map, reactionsystem(lrs))
+ net_stoich[cur_rx] = int_map(rx.netstoich, reactionsystem(lrs))
cur_rx += 1
end
# SpatialMassActionJump expects empty rate containers to be nothing.
isempty(u_rates) && (u_rates = nothing)
(count(is_spatials) == 0) && (s_rates = nothing)
- return SpatialMassActionJump(u_rates, s_rates, reactant_stoich, net_stoich)
+ return SpatialMassActionJump(u_rates, s_rates, reactant_stoich, net_stoich, nothing)
end
### Extra ###
-# Temporary. Awaiting implementation in SII, or proper implementation withinCatalyst (with more general functionality).
+# Temporary. Awaiting implementation in SII, or proper implementation within Catalyst (with
+# more general functionality).
function int_map(map_in, sys)
return [ModelingToolkit.variable_index(sys, pair[1]) => pair[2] for pair in map_in]
end
@@ -133,7 +133,7 @@ end
# function make_majumps(non_spat_dprob, rs::ReactionSystem)
# # Computes various required inputs for assembling the mass action jumps.
# js = convert(JumpSystem, rs)
-# statetoid = Dict(ModelingToolkit.value(state) => i for (i, state) in enumerate(states(rs)))
+# statetoid = Dict(ModelingToolkit.value(state) => i for (i, state) in enumerate(unknowns(rs)))
# eqs = equations(js)
# invttype = non_spat_dprob.tspan[1] === nothing ? Float64 : typeof(1 / non_spat_dprob.tspan[2])
#
@@ -142,3 +142,13 @@ end
# majpmapper = ModelingToolkit.JumpSysMajParamMapper(js, p; jseqs = eqs, rateconsttype = invttype)
# return ModelingToolkit.assemble_maj(eqs.x[1], statetoid, majpmapper)
# end
+
+### Problem & Integrator Rebuilding ###
+
+# Currently not implemented.
+function rebuild_lat_internals!(dprob::DiscreteProblem)
+ error("Modification and/or rebuilding of `DiscreteProblem`s is currently not supported. Please create a new problem instead.")
+end
+function rebuild_lat_internals!(jprob::JumpProblem)
+ error("Modification and/or rebuilding of `JumpProblem`s is currently not supported. Please create a new problem instead.")
+end
diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl
index 102b586a7c..7f9e2923cf 100644
--- a/src/spatial_reaction_systems/lattice_reaction_systems.jl
+++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl
@@ -1,49 +1,80 @@
+### New Type Unions ###
+
+# Cartesian and masked grids share several traits, hence we declare a common (union) type for them.
+const GridLattice{N, T} = Union{Array{Bool, N}, CartesianGridRej{N, T}}
+
### Lattice Reaction Network Structure ###
-# Describes a spatial reaction network over a graph.
-# Adding the "<: MT.AbstractTimeDependentSystem" part messes up show, disabling me from creating LRSs.
-struct LatticeReactionSystem{S, T} # <: MT.AbstractTimeDependentSystem
+
+# Describes a spatial reaction network over a lattice.
+struct LatticeReactionSystem{Q, R, S, T} <: MT.AbstractTimeDependentSystem
# Input values.
- """The reaction system within each compartment."""
- rs::ReactionSystem{S}
- """The spatial reactions defined between individual nodes."""
- spatial_reactions::Vector{T}
- """The graph on which the lattice is defined."""
- lattice::SimpleDiGraph{Int64}
+ """The (non-spatial) reaction system within each vertex."""
+ reactionsystem::ReactionSystem{Q}
+ """The spatial reactions defined between individual vertices."""
+ spatial_reactions::Vector{R}
+ """The lattice on which the (discrete) spatial system is defined."""
+ lattice::S
# Derived values.
- """The number of compartments."""
+ """The number of vertices (compartments)."""
num_verts::Int64
"""The number of edges."""
num_edges::Int64
"""The number of species."""
num_species::Int64
- """Whenever the initial input was a digraph."""
- init_digraph::Bool
- """Species that may move spatially."""
- spat_species::Vector{BasicSymbolic{Real}}
+
+ """List of species that may move spatially."""
+ spatial_species::Vector{BasicSymbolic{Real}}
"""
All parameters related to the lattice reaction system
- (both with spatial and non-spatial effects).
+ (both those whose values are tied to vertices and edges).
"""
- parameters::Vector{BasicSymbolic{Real}}
+ parameters::Vector{Any}
"""
- Parameters which values are tied to vertexes (adjacencies),
- e.g. (possibly) have an unique value at each vertex of the system.
+ Parameters which values are tied to vertices,
+ e.g. that possibly could have unique values at each vertex of the system.
"""
- vertex_parameters::Vector{BasicSymbolic{Real}}
+ vertex_parameters::Vector{Any}
"""
- Parameters which values are tied to edges (adjacencies),
- e.g. (possibly) have an unique value at each edge of the system.
+ Parameters whose values are tied to edges (adjacencies),
+ e.g. that possibly could have unique values at each edge of the system.
"""
- edge_parameters::Vector{BasicSymbolic{Real}}
+ edge_parameters::Vector{Any}
+ """
+ An iterator over all the lattice's edges. Currently, the format is always a Vector{Pair{Int64,Int64}}.
+ However, in the future, different types could potentially be used for different types of lattice
+ (E.g. for a Cartesian grid, we do not technically need to enumerate each edge)
+ """
+ edge_iterator::T
- function LatticeReactionSystem(rs::ReactionSystem{S}, spatial_reactions::Vector{T},
- lattice::DiGraph; init_digraph = true) where {S, T}
- # There probably some better way to ascertain that T has that type. Not sure how.
- if !(T <: AbstractSpatialReaction)
- error("The second argument must be a vector of AbstractSpatialReaction subtypes.")
+ function LatticeReactionSystem(rs::ReactionSystem{Q}, spatial_reactions::Vector{R},
+ lattice::S, num_verts::Int64, num_edges::Int64,
+ edge_iterator::T) where {Q, R, S, T}
+ # Error checks.
+ if !(R <: AbstractSpatialReaction)
+ throw(ArgumentError("The second argument must be a vector of AbstractSpatialReaction subtypes."))
+ end
+ if !iscomplete(rs)
+ throw(ArgumentError("A non-complete `ReactionSystem` was used as input, this is not permitted."))
+ end
+ if !isempty(MT.get_systems(rs))
+ throw(ArgumentError("A non-flattened (hierarchical) `ReactionSystem` was used as input. `LatticeReactionSystem`s can only be based on non-hierarchical `ReactionSystem`s."))
+ end
+ if length(species(rs)) != length(unknowns(rs))
+ throw(ArgumentError("The `ReactionSystem` used as input contain variable unknowns (in addition to species unknowns). This is not permitted (the input `ReactionSystem` must contain species unknowns only)."))
+ end
+ if length(reactions(rs)) != length(equations(rs))
+ throw(ArgumentError("The `ReactionSystem` used as input contain equations (in addition to reactions). This is not permitted."))
+ end
+ if !isempty(MT.continuous_events(rs)) || !isempty(MT.discrete_events(rs))
+ throw(ArgumentError("The `ReactionSystem` used as input to `LatticeReactionSystem contain events. These will be ignored in any simulations based on the created `LatticeReactionSystem`."))
+ end
+ if !isempty(observed(rs))
+ @warn "The `ReactionSystem` used as input to `LatticeReactionSystem contain observables. It will not be possible to access these from the created `LatticeReactionSystem`."
end
+ # Computes the species which are parts of spatial reactions. Also counts the total number of
+ # species types.
if isempty(spatial_reactions)
spat_species = Vector{BasicSymbolic{Real}}[]
else
@@ -51,6 +82,8 @@ struct LatticeReactionSystem{S, T} # <: MT.AbstractTimeDependentSystem
[spatial_species(sr) for sr in spatial_reactions]))
end
num_species = length(unique([species(rs); spat_species]))
+
+ # Computes the sets of vertex, edge, and all, parameters.
rs_edge_parameters = filter(isedgeparameter, parameters(rs))
if isempty(spatial_reactions)
srs_edge_parameters = Vector{BasicSymbolic{Real}}[]
@@ -60,39 +93,435 @@ struct LatticeReactionSystem{S, T} # <: MT.AbstractTimeDependentSystem
end
edge_parameters = unique([rs_edge_parameters; srs_edge_parameters])
vertex_parameters = filter(!isedgeparameter, parameters(rs))
+
# Ensures the parameter order begins similarly to in the non-spatial ReactionSystem.
ps = [parameters(rs); setdiff([edge_parameters; vertex_parameters], parameters(rs))]
- for sr in spatial_reactions
- check_spatial_reaction_validity(rs, sr; edge_parameters = edge_parameters)
+ # Checks that all spatial reactions are valid for this reaction system.
+ foreach(
+ sr -> check_spatial_reaction_validity(rs, sr; edge_parameters = edge_parameters),
+ spatial_reactions)
+
+ return new{Q, R, S, T}(
+ rs, spatial_reactions, lattice, num_verts, num_edges, num_species,
+ spat_species, ps, vertex_parameters, edge_parameters, edge_iterator)
+ end
+end
+
+# Creates a LatticeReactionSystem from a (directed) Graph lattice (graph grid).
+function LatticeReactionSystem(rs, srs, lattice::DiGraph)
+ num_verts = nv(lattice)
+ num_edges = ne(lattice)
+ edge_iterator = [e.src => e.dst for e in edges(lattice)]
+ return LatticeReactionSystem(rs, srs, lattice, num_verts, num_edges, edge_iterator)
+end
+# Creates a LatticeReactionSystem from a (undirected) Graph lattice (graph grid).
+function LatticeReactionSystem(rs, srs, lattice::SimpleGraph)
+ LatticeReactionSystem(rs, srs, DiGraph(lattice))
+end
+
+# Creates a LatticeReactionSystem from a CartesianGrid lattice (cartesian grid) or a Boolean Array
+# lattice (masked grid). These two are quite similar, so much code can be reused in a single interface.
+function LatticeReactionSystem(rs, srs, lattice::GridLattice{N, T};
+ diagonal_connections = false) where {N, T}
+ # Error checks.
+ (N > 3) && error("Grids of higher dimension than 3 is currently not supported.")
+
+ # Computes the number of vertices and edges. The two grid types (Cartesian and masked) each
+ # uses their own function for this.
+ num_verts = count_verts(lattice)
+ num_edges = count_edges(lattice; diagonal_connections)
+
+ # Finds all the grid's edges. First computer `flat_to_grid_idx` which is a vector which takes
+ # each vertex's flat (scalar) index to its grid index (e.g. (3,5) for a 2d grid). Next compute
+ # `grid_to_flat_idx` which is an array (of the same size as the grid) that does the reverse conversion.
+ # Especially for masked grids these have non-trivial forms.
+ flat_to_grid_idx, grid_to_flat_idx = get_index_converters(lattice, num_verts)
+ # Simultaneously iterates through all vertices' flat and grid indices. Finds the (grid) indices
+ # of their neighbours (different approaches for the two grid types). Converts these to flat
+ # indices and adds the edges to `edge_iterator`.
+ cur_vert = 0
+ g_size = grid_size(lattice)
+ edge_iterator = Vector{Pair{Int64, Int64}}(undef, num_edges)
+ for (flat_idx, grid_idx) in enumerate(flat_to_grid_idx)
+ for neighbour_grid_idx in get_neighbours(lattice, grid_idx, g_size;
+ diagonal_connections)
+ cur_vert += 1
+ edge_iterator[cur_vert] = flat_idx => grid_to_flat_idx[neighbour_grid_idx...]
end
- return new{S, T}(
- rs, spatial_reactions, lattice, nv(lattice), ne(lattice), num_species,
- init_digraph, spat_species, ps, vertex_parameters, edge_parameters)
end
+
+ return LatticeReactionSystem(rs, srs, lattice, num_verts, num_edges, edge_iterator)
end
-function LatticeReactionSystem(rs, srs, lat::SimpleGraph)
- return LatticeReactionSystem(rs, srs, DiGraph(lat); init_digraph = false)
+
+### LatticeReactionSystem Helper Functions ###
+# Note, most of these are specifically for (Cartesian or masked) grids, we call them `grid`, not `lattice`.
+
+# Counts the number of vertices on a (Cartesian or masked) grid.
+count_verts(grid::CartesianGridRej{N, T}) where {N, T} = prod(grid_size(grid))
+count_verts(grid::Array{Bool, N}) where {N} = count(grid)
+
+# Counts and edges on a Cartesian grid. The formula counts the number of internal, side, edge, and
+# corner vertices (on the grid). `l,m,n = grid_dims(grid),1,1` ensures that "extra" dimensions get
+# length 1. The formula holds even if one or more of l, m, and n are 1.
+function count_edges(grid::CartesianGridRej{N, T};
+ diagonal_connections = false) where {N, T}
+ l, m, n = grid_size(grid)..., 1, 1
+ (ni, ns, ne, nc) = diagonal_connections ? (26, 17, 11, 7) : (6, 5, 4, 3)
+ num_edges = ni * (l - 2) * (m - 2) * (n - 2) + # Edges from internal vertices.
+ ns * (2(l - 2) * (m - 2) + 2(l - 2) * (n - 2) + 2(m - 2) * (n - 2)) + # Edges from side vertices.
+ ne * (4(l - 2) + 4(m - 2) + 4(n - 2)) + # Edges from edge vertices.
+ nc * 8 # Edges from corner vertices.
+ return num_edges
end
-### Lattice ReactionSystem Getters ###
+# Counts and edges on a masked grid. Does so by looping through all the vertices of the grid,
+# finding their neighbours, and updating the edge count accordingly.
+function count_edges(grid::Array{Bool, N}; diagonal_connections = false) where {N}
+ g_size = grid_size(grid)
+ num_edges = 0
+ for grid_idx in get_grid_indices(grid)
+ grid[grid_idx] || continue
+ num_edges += length(get_neighbours(grid, Tuple(grid_idx), g_size;
+ diagonal_connections))
+ end
+ return num_edges
+end
-# Get all species.
-species(lrs::LatticeReactionSystem) = unique([species(lrs.rs); lrs.spat_species])
-# Get all species that may be transported.
-spatial_species(lrs::LatticeReactionSystem) = lrs.spat_species
+# For a (1d, 2d, or 3d) (Cartesian or masked) grid, returns a vector and an array, permitting the
+# conversion between a vertex's flat (scalar) and grid indices. E.g. for a 2d grid, if grid point (3,2)
+# corresponds to the fifth vertex, then `flat_to_grid_idx[5] = (3,2)` and `grid_to_flat_idx[3,2] = 5`.
+function get_index_converters(grid::GridLattice{N, T}, num_verts) where {N, T}
+ flat_to_grid_idx = Vector{typeof(grid_size(grid))}(undef, num_verts)
+ grid_to_flat_idx = Array{Int64}(undef, grid_size(grid))
+
+ # Loops through the flat and grid indices simultaneously, adding them to their respective converters.
+ cur_flat_idx = 0
+ for grid_idx in get_grid_indices(grid)
+ # For a masked grid, grid points with `false` values are skipped.
+ (grid isa Array{Bool}) && (!grid[grid_idx]) && continue
+
+ cur_flat_idx += 1
+ flat_to_grid_idx[cur_flat_idx] = grid_idx
+ grid_to_flat_idx[grid_idx] = cur_flat_idx
+ end
+ return flat_to_grid_idx, grid_to_flat_idx
+end
+
+# For a vertex's grid index, and a lattice, returns the grid indices of all its (valid) neighbours.
+function get_neighbours(grid::GridLattice{N, T}, grid_idx, g_size;
+ diagonal_connections = false) where {N, T}
+ # Depending on the grid's dimension, find all potential neighbours.
+ if grid_dims(grid) == 1
+ potential_neighbours = [grid_idx .+ (i) for i in -1:1]
+ elseif grid_dims(grid) == 2
+ potential_neighbours = [grid_idx .+ (i, j) for i in -1:1 for j in -1:1]
+ else
+ potential_neighbours = [grid_idx .+ (i, j, k) for i in -1:1 for j in -1:1
+ for k in -1:1]
+ end
+
+ # Depending on whether diagonal connections are used or not, find valid neighbours.
+ if diagonal_connections
+ filter!(n_idx -> n_idx !== grid_idx, potential_neighbours)
+ else
+ filter!(n_idx -> count(n_idx .== grid_idx) == (length(g_size) - 1),
+ potential_neighbours)
+ end
+
+ # Removes neighbours outside of the grid, and returns the full list.
+ return filter(n_idx -> is_valid_grid_point(grid, n_idx, g_size), potential_neighbours)
+end
+
+# Checks if a grid index corresponds to a valid grid point. First, check that each dimension of the
+# index is within the grid's bounds. Next, perform an extra check for the masked grid.
+function is_valid_grid_point(grid::GridLattice{N, T}, grid_idx, g_size) where {N, T}
+ if !all(0 < g_idx <= dim_leng for (g_idx, dim_leng) in zip(grid_idx, g_size))
+ return false
+ end
+ return (grid isa Array{Bool}) ? grid[grid_idx...] : true
+end
-# Get all parameters.
-ModelingToolkit.parameters(lrs::LatticeReactionSystem) = lrs.parameters
-# Get all parameters which values are tied to vertexes (compartments).
-vertex_parameters(lrs::LatticeReactionSystem) = lrs.vertex_parameters
-# Get all parameters which values are tied to edges (adjacencies).
-edge_parameters(lrs::LatticeReactionSystem) = lrs.edge_parameters
+# Gets an iterator over a grid's grid indices. Separate function so we can handle the two grid types
+# separately (i.e. not calling `CartesianIndices(ones(grid_size(grid)))` unnecessarily for masked grids).
+function get_grid_indices(grid::CartesianGridRej{N, T}) where {N, T}
+ CartesianIndices(ones(grid_size(grid)))
+end
+get_grid_indices(grid::Array{Bool, N}) where {N} = CartesianIndices(grid)
+
+### LatticeReactionSystem-specific Getters ###
-# Gets the lrs name (same as rs name).
-ModelingToolkit.nameof(lrs::LatticeReactionSystem) = nameof(lrs.rs)
+# Basic getters (because `LatticeReactionSystem`s are `AbstractSystem`s), normal `lrs.field` does not
+# work and these getters must be used throughout all code.
+reactionsystem(lrs::LatticeReactionSystem) = getfield(lrs, :reactionsystem)
+spatial_reactions(lrs::LatticeReactionSystem) = getfield(lrs, :spatial_reactions)
+lattice(lrs::LatticeReactionSystem) = getfield(lrs, :lattice)
+num_verts(lrs::LatticeReactionSystem) = getfield(lrs, :num_verts)
+num_edges(lrs::LatticeReactionSystem) = getfield(lrs, :num_edges)
+num_species(lrs::LatticeReactionSystem) = getfield(lrs, :num_species)
+spatial_species(lrs::LatticeReactionSystem) = getfield(lrs, :spatial_species)
+MT.parameters(lrs::LatticeReactionSystem) = getfield(lrs, :parameters)
+vertex_parameters(lrs::LatticeReactionSystem) = getfield(lrs, :vertex_parameters)
+edge_parameters(lrs::LatticeReactionSystem) = getfield(lrs, :edge_parameters)
+edge_iterator(lrs::LatticeReactionSystem) = getfield(lrs, :edge_iterator)
-# Checks if a lattice reaction system is a pure (linear) transport reaction system.
+# Non-trivial getters.
+"""
+ is_transport_system(lrs::LatticeReactionSystem)
+
+Returns `true` if all spatial reactions in `lrs` are `TransportReaction`s.
+"""
function is_transport_system(lrs::LatticeReactionSystem)
- all(sr -> sr isa TransportReaction, lrs.spatial_reactions)
+ return all(sr -> sr isa TransportReaction, spatial_reactions(lrs))
+end
+
+"""
+ has_cartesian_lattice(lrs::LatticeReactionSystem)
+
+Returns `true` if `lrs` was created using a cartesian grid lattice (e.g. created via `CartesianGrid(5,5)`).
+Otherwise, returns `false`.
+"""
+has_cartesian_lattice(lrs::LatticeReactionSystem) = lattice(lrs) isa
+ CartesianGridRej{N, T} where {N, T}
+
+"""
+ has_masked_lattice(lrs::LatticeReactionSystem)
+
+Returns `true` if `lrs` was created using a masked grid lattice (e.g. created via `[true true; true false]`).
+Otherwise, returns `false`.
+"""
+has_masked_lattice(lrs::LatticeReactionSystem) = lattice(lrs) isa Array{Bool, N} where {N}
+
+"""
+ has_grid_lattice(lrs::LatticeReactionSystem)
+
+Returns `true` if `lrs` was created using a cartesian or masked grid lattice. Otherwise, returns `false`.
+"""
+function has_grid_lattice(lrs::LatticeReactionSystem)
+ return has_cartesian_lattice(lrs) || has_masked_lattice(lrs)
+end
+
+"""
+ has_graph_lattice(lrs::LatticeReactionSystem)
+
+Returns `true` if `lrs` was created using a graph grid lattice (e.g. created via `path_graph(5)`).
+Otherwise, returns `false`.
+"""
+has_graph_lattice(lrs::LatticeReactionSystem) = lattice(lrs) isa SimpleDiGraph
+
+"""
+ grid_size(lrs::LatticeReactionSystem)
+
+Returns the size of `lrs`'s lattice (only if it is a cartesian or masked grid lattice).
+E.g. for a lattice `CartesianGrid(4,6)`, `(4,6)` is returned.
+"""
+grid_size(lrs::LatticeReactionSystem) = grid_size(lattice(lrs))
+grid_size(lattice::CartesianGridRej{N, T}) where {N, T} = lattice.dims
+grid_size(lattice::Array{Bool, N}) where {N} = size(lattice)
+function grid_size(lattice::Graphs.AbstractGraph)
+ throw(ArgumentError("Grid size is only defined for LatticeReactionSystems with grid-based lattices (not graph-based)."))
+end
+
+"""
+ grid_dims(lrs::LatticeReactionSystem)
+
+Returns the number of dimensions of `lrs`'s lattice (only if it is a cartesian or masked grid lattice).
+The output is either `1`, `2`, or `3`.
+"""
+grid_dims(lrs::LatticeReactionSystem) = grid_dims(lattice(lrs))
+grid_dims(lattice::GridLattice{N, T}) where {N, T} = return N
+function grid_dims(lattice::Graphs.AbstractGraph)
+ throw(ArgumentError("Grid dimensions is only defined for LatticeReactionSystems with grid-based lattices (not graph-based)."))
+end
+
+"""
+ get_lattice_graph(lrs::LatticeReactionSystem)
+
+Returns lrs's lattice, but in as a graph. Currently does not work for Cartesian lattices.
+"""
+function get_lattice_graph(lrs::LatticeReactionSystem)
+ has_graph_lattice(lrs) && return lattice(lrs)
+ return Graphs.SimpleGraphFromIterator(Graphs.SimpleEdge(e[1], e[2])
+ for e in edge_iterator(lrs))
+end
+
+### Catalyst-based Getters ###
+
+# Get all species.
+function species(lrs::LatticeReactionSystem)
+ unique([species(reactionsystem(lrs)); spatial_species(lrs)])
+end
+
+# Generic ones (simply forwards call to the non-spatial system).
+reactions(lrs::LatticeReactionSystem) = reactions(reactionsystem(lrs))
+
+### ModelingToolkit-based Getters ###
+
+# Generic ones (simply forwards call to the non-spatial system)
+# The `parameters` MTK getter have a specialised accessor for LatticeReactionSystems.
+MT.nameof(lrs::LatticeReactionSystem) = MT.nameof(reactionsystem(lrs))
+MT.get_iv(lrs::LatticeReactionSystem) = MT.get_iv(reactionsystem(lrs))
+MT.equations(lrs::LatticeReactionSystem) = MT.equations(reactionsystem(lrs))
+MT.unknowns(lrs::LatticeReactionSystem) = MT.unknowns(reactionsystem(lrs))
+MT.get_metadata(lrs::LatticeReactionSystem) = MT.get_metadata(reactionsystem(lrs))
+
+# Lattice reaction systems should not be combined with compositional modelling.
+# Maybe these should be allowed anyway? Still feel a bit weird
+function MT.get_eqs(lrs::LatticeReactionSystem)
+ MT.get_eqs(reactionsystem(lrs))
+end
+function MT.get_unknowns(lrs::LatticeReactionSystem)
+ MT.get_unknowns(reactionsystem(lrs))
+end
+function MT.get_ps(lrs::LatticeReactionSystem)
+ MT.get_ps(reactionsystem(lrs))
+end
+
+# Technically should not be used, but has to be declared for the `show` function to work.
+function MT.get_systems(lrs::LatticeReactionSystem)
+ return []
+end
+
+# Other non-relevant getters.
+function MT.independent_variables(lrs::LatticeReactionSystem)
+ MT.independent_variables(reactionsystem(lrs))
+end
+
+### Edge Parameter Value Generators ###
+
+"""
+ make_edge_p_values(lrs::LatticeReactionSystem, make_edge_p_value::Function)
+
+Generates edge parameter values for a lattice reaction system. Only work for (Cartesian or masked)
+grid lattices (without diagonal adjacencies).
+
+Input:
+- `lrs`: The lattice reaction system for which values should be generated.
+ - `make_edge_p_value`: a function describing a rule for generating the edge parameter values.
+
+Output:
+ - `ep_vals`: A sparse matrix of size (num_verts,num_verts) (where num_verts is the number of
+ vertices in `lrs`). Here, `eps[i,j]` is filled only if there is an edge going from vertex i to
+ vertex j. The value of `eps[i,j]` is determined by `make_edge_p_value`.
+
+Here, `make_edge_p_value` should take two arguments, `src_vert` and `dst_vert`, which correspond to
+the grid indices of an edge's source and destination vertices, respectively. It outputs a single value,
+which is the value assigned to that edge.
+
+Example:
+ In the following example, we assign the value `0.1` to all edges, except for the one leading from
+ vertex (1,1) to vertex (1,2), to which we assign the value `1.0`.
+```julia
+using Catalyst
+rn = @reaction_network begin
+ (p,d), 0 <--> X
+end
+tr = @transport_reaction D X
+lattice = CartesianGrid((5,5))
+lrs = LatticeReactionSystem(rn, [tr], lattice)
+
+function make_edge_p_value(src_vert, dst_vert)
+ if src_vert == (1,1) && dst_vert == (1,2)
+ return 1.0
+ else
+ return 0.1
+ end
+end
+
+D_vals = make_edge_p_values(lrs, make_edge_p_value)
+```
+"""
+function make_edge_p_values(lrs::LatticeReactionSystem, make_edge_p_value::Function)
+ if has_graph_lattice(lrs)
+ error("The `make_edge_p_values` function is only meant for lattices with (Cartesian or masked) grid structures. It cannot be applied to graph lattices.")
+ end
+
+ # Makes the flat to index grid converts. Predeclared the edge parameter value sparse matrix.
+ flat_to_grid_idx = get_index_converters(lattice(lrs), num_verts(lrs))[1]
+ values = spzeros(num_verts(lrs), num_verts(lrs))
+
+ # Loops through all edges, and applies the value function to these.
+ for e in edge_iterator(lrs)
+ # This extra step is needed to ensure that `0` is stored if make_edge_p_value yields a 0.
+ # If not, then the sparse matrix simply becomes empty in that position.
+ values[e[1], e[2]] = eps()
+
+ values[e[1], e[2]] = make_edge_p_value(flat_to_grid_idx[e[1]],
+ flat_to_grid_idx[e[2]])
+ end
+
+ return values
+end
+
+"""
+ make_directed_edge_values(lrs::LatticeReactionSystem, x_vals::Tuple{T,T}, y_vals::Tuple{T,T} = (undef,undef),
+ z_vals::Tuple{T,T} = (undef,undef)) where {T}
+
+Generates edge parameter values for a lattice reaction system. Only work for (Cartesian or masked)
+grid lattices (without diagonal adjacencies). Each dimension (x, and possibly y and z), and
+direction has assigned its own constant edge parameter value.
+
+Input:
+ - `lrs`: The lattice reaction system for which values should be generated.
+ - `x_vals::Tuple{T,T}`: The values in the increasing (from a lower x index to a higher x index)
+ and decreasing (from a higher x index to a lower x index) direction along the x dimension.
+ - `y_vals::Tuple{T,T}`: The values in the increasing and decreasing direction along the y dimension.
+ Should only be used for 2 and 3-dimensional grids.
+ - `z_vals::Tuple{T,T}`: The values in the increasing and decreasing direction along the z dimension.
+ Should only be used for 3-dimensional grids.
+
+Output:
+ - `ep_vals`: A sparse matrix of size (num_verts,num_verts) (where num_verts is the number of
+ vertices in `lrs`). Here, `eps[i,j]` is filled only if there is an edge going from vertex i to
+ vertex j. The value of `eps[i,j]` is determined by the `x_vals`, `y_vals`, and `z_vals` Tuples,
+ and vertices i and j's relative position in the grid.
+
+It should be noted that two adjacent vertices will always be different in exactly a single dimension
+(x, y, or z). The corresponding tuple determines which value is assigned.
+
+Example:
+ In the following example, we wish to have diffusion in the x dimension, but a constant flow from
+ low y values to high y values (so not transportation from high to low y). We achieve it in the
+ following manner:
+```julia
+using Catalyst
+rn = @reaction_network begin
+ (p,d), 0 <--> X
+end
+tr = @transport_reaction D X
+lattice = CartesianGrid((5,5))
+lrs = LatticeReactionSystem(rn, [tr], lattice)
+
+D_vals = make_directed_edge_values(lrs, (0.1, 0.1), (0.1, 0.0))
+```
+Here, since we have a 2d grid, we only provide the first two Tuples to `make_directed_edge_values`.
+"""
+function make_directed_edge_values(lrs::LatticeReactionSystem, x_vals::Tuple{T, T},
+ y_vals::Union{Nothing, Tuple{T, T}} = nothing,
+ z_vals::Union{Nothing, Tuple{T, T}} = nothing) where {T}
+ # Error checks.
+ if has_graph_lattice(lrs)
+ error("The `make_directed_edge_values` function is only meant for lattices with (Cartesian or masked) grid structures. It cannot be applied to graph lattices.")
+ end
+ if count(!isnothing(flow) for flow in [x_vals, y_vals, z_vals]) != grid_dims(lrs)
+ error("You must provide flows in the same number of dimensions as your lattice has dimensions. The lattice have $(grid_dims(lrs)), and flows where provided for $(count(isnothing(flow) for flow in [x_vals, y_vals, z_vals])) dimensions.")
+ end
+
+ # Declares a function that assigns the correct flow value for a given edge.
+ function directed_vals_func(src_vert, dst_vert)
+ if count(src_vert .== dst_vert) != (grid_dims(lrs) - 1)
+ error("The `make_directed_edge_values` function can only be applied to lattices with rigid (non-diagonal) grid structure. It is being evaluated for the edge from $(src_vert) to $(dst_vert), which does not seem directly adjacent on a grid.")
+ elseif src_vert[1] != dst_vert[1]
+ return (src_vert[1] < dst_vert[1]) ? x_vals[1] : x_vals[2]
+ elseif src_vert[2] != dst_vert[2]
+ return (src_vert[2] < dst_vert[2]) ? y_vals[1] : y_vals[2]
+ elseif src_vert[3] != dst_vert[3]
+ return (src_vert[3] < dst_vert[3]) ? z_vals[1] : z_vals[2]
+ else
+ error("Problem when evaluating adjacency type for the edge from $(src_vert) to $(dst_vert).")
+ end
+ end
+
+ # Uses the make_edge_p_values function to compute the output.
+ return make_edge_p_values(lrs, directed_vals_func)
end
diff --git a/src/spatial_reaction_systems/lattice_solution_interfacing.jl b/src/spatial_reaction_systems/lattice_solution_interfacing.jl
new file mode 100644
index 0000000000..3a286a8a02
--- /dev/null
+++ b/src/spatial_reaction_systems/lattice_solution_interfacing.jl
@@ -0,0 +1,121 @@
+### Rudimentary Interfacing Function ###
+# A single function, `get_lrs_vals`, which contains all interfacing functionality. However,
+# long-term it should be replaced with a sleeker interface. Ideally as MTK-wide support for
+# lattice problems and solutions is introduced.
+
+"""
+ get_lrs_vals(sol, sp, lrs::LatticeReactionSystem; t = nothing)
+
+A function for retrieving the solution of a `LatticeReactionSystem`-based simulation on various
+desired forms. Generally, for `LatticeReactionSystem`s, the values in `sol` is ordered in a
+way which is not directly interpretable by the user. Furthermore, the normal Catalyst interface
+for solutions (e.g. `sol[:X]`) does not work for these solutions. Hence this function is used instead.
+
+The output is a vector, which in each position contains sp's value (either at a time step of time,
+depending on the input `t`). Its shape depends on the lattice (using a similar form as heterogeneous
+initial conditions). I.e. for a NxM cartesian grid, the values are NxM matrices. For a masked grid,
+the values are sparse matrices. For a graph lattice, the values are vectors (where the value in
+the n'th position corresponds to sp's value in the n'th vertex).
+
+Arguments:
+- `sol`: The solution from which we wish to retrieve some values.
+- `sp`: The species which values we wish to retrieve. Can be either a symbol (e.g. `:X`) or a symbolic
+variable (e.g. `X`).
+- `lrs`: The `LatticeReactionSystem` which was simulated to generate the solution.
+- `t = nothing`: If `nothing`, we simply return the solution across all saved time steps. If `t`
+instead is a vector (or range of values), returns the solutions interpolated at these time points.
+
+Notes:
+- The `get_lrs_vals` is not optimised for performance. However, it should still be quite performant,
+but there might be some limitations if called a very large number of times.
+- Long-term it is likely that this function gets replaced with a sleeker interface.
+
+Example:
+```julia
+using Catalyst, OrdinaryDiffEq
+
+# Prepare `LatticeReactionSystem`s.
+rs = @reaction_network begin
+ (k1,k2), X1 <--> X2
+end
+tr = @transport_reaction D X1
+lrs = LatticeReactionSystem(rs, [tr], CartesianGrid((2,2)))
+
+# Create problems.
+u0 = [:X1 => 1, :X2 => 2]
+tspan = (0.0, 10.0)
+ps = [:k1 => 1, :k2 => 2.0, :D => 0.1]
+
+oprob = ODEProblem(lrs1, u0, tspan, ps)
+osol = solve(oprob1, Tsit5())
+get_lrs_vals(osol, :X1, lrs) # Returns the value of X1 at each time step.
+get_lrs_vals(osol, :X1, lrs; t = 0.0:10.0) # Returns the value of X1 at times 0.0, 1.0, ..., 10.0
+```
+"""
+function get_lrs_vals(sol, sp, lrs::LatticeReactionSystem; t = nothing)
+ # Figures out which species we wish to fetch information about.
+ (sp isa Symbol) && (sp = Catalyst._symbol_to_var(lrs, sp))
+ sp_idx = findfirst(isequal(sp), species(lrs))
+ sp_tot = length(species(lrs))
+
+ # Extracts the lattice and calls the next function. Masked grids (Array of Bools) are converted
+ # to sparse array using the same template size as we wish to shape the data to.
+ lattice = Catalyst.lattice(lrs)
+ if has_masked_lattice(lrs)
+ if grid_dims(lrs) == 3
+ error("The `get_lrs_vals` function is not defined for systems based on 3d sparse arrays. Please raise an issue at the Catalyst GitHub site if this is something which would be useful to you.")
+ end
+ lattice = sparse(lattice)
+ end
+ get_lrs_vals(sol, lattice, t, sp_idx, sp_tot)
+end
+
+# Function which handles the input in the case where `t` is `nothing` (i.e. return `sp`s value
+# across all sample points).
+function get_lrs_vals(sol, lattice, t::Nothing, sp_idx, sp_tot)
+ # ODE simulations contain, in each data point, all values in a single vector. Jump simulations
+ # instead in a matrix (NxM, where N is the number of species and M the number of vertices). We
+ # must consider each case separately.
+ if sol.prob isa ODEProblem
+ return [reshape_vals(vals[sp_idx:sp_tot:end], lattice) for vals in sol.u]
+ elseif sol.prob isa DiscreteProblem
+ return [reshape_vals(vals[sp_idx, :], lattice) for vals in sol.u]
+ else
+ error("Unknown type of solution provided to `get_lrs_vals`. Only ODE or Jump solutions are supported.")
+ end
+end
+
+# Function which handles the input in the case where `t` is a range of values (i.e. return `sp`s
+# value at all designated time points.
+function get_lrs_vals(
+ sol, lattice, t::AbstractVector{T}, sp_idx, sp_tot) where {T <: Number}
+ if (minimum(t) < sol.t[1]) || (maximum(t) > sol.t[end])
+ error("The range of the t values provided for sampling, ($(minimum(t)),$(maximum(t))) is not fully within the range of the simulation time span ($(sol.t[1]),$(sol.t[end])).")
+ end
+
+ # ODE simulations contain, in each data point, all values in a single vector. Jump simulations
+ # instead in a matrix (NxM, where N is the number of species and M the number of vertices). We
+ # must consider each case separately.
+ if sol.prob isa ODEProblem
+ return [reshape_vals(sol(ti)[sp_idx:sp_tot:end], lattice) for ti in t]
+ elseif sol.prob isa DiscreteProblem
+ return [reshape_vals(sol(ti)[sp_idx, :], lattice) for ti in t]
+ else
+ error("Unknown type of solution provided to `get_lrs_vals`. Only ODE or Jump solutions are supported.")
+ end
+end
+
+# Functions which in each sample point reshape the vector of values to the correct form (depending
+# on the type of lattice used).
+function reshape_vals(vals, lattice::CartesianGridRej{N, T}) where {N, T}
+ return reshape(vals, lattice.dims...)
+end
+function reshape_vals(vals, lattice::AbstractSparseArray{Bool, Int64, 1})
+ return SparseVector(lattice.n, lattice.nzind, vals)
+end
+function reshape_vals(vals, lattice::AbstractSparseArray{Bool, Int64, 2})
+ return SparseMatrixCSC(lattice.m, lattice.n, lattice.colptr, lattice.rowval, vals)
+end
+function reshape_vals(vals, lattice::DiGraph)
+ return vals
+end
diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl
index 73f23c4624..3dced1e573 100644
--- a/src/spatial_reaction_systems/spatial_ODE_systems.jl
+++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl
@@ -1,114 +1,173 @@
-### Spatial ODE Functor Structures ###
+### Spatial ODE Functor Structure ###
-# Functor with information for the forcing function of a spatial ODE with spatial movement on a lattice.
-struct LatticeTransportODEf{Q, R, S, T}
- """The ODEFunction of the (non-spatial) reaction system which generated this function."""
- ofunc::Q
- """The number of vertices."""
+# Functor with information about a spatial Lattice Reaction ODEs forcing and Jacobian functions.
+# Also used as ODE Function input to corresponding `ODEProblem`.
+struct LatticeTransportODEFunction{P, Q, R, S, T}
+ """
+ The ODEFunction of the (non-spatial) ReactionSystem that generated this
+ LatticeTransportODEFunction instance.
+ """
+ ofunc::P
+ """The lattice's number of vertices."""
num_verts::Int64
- """The number of species."""
+ """The system's number of species."""
num_species::Int64
- """The values of the parameters which values are tied to vertexes."""
- vert_ps::Vector{Vector{R}}
"""
- Temporary vector. For parameters which values are identical across the lattice,
- at some point these have to be converted of a length num_verts vector.
- To avoid re-allocation they are written to this vector.
+ Stores an index for each heterogeneous vertex parameter (i.e. vertex parameter which value is
+ not identical across the lattice). Each index corresponds to its position in the full parameter
+ vector (`parameters(lrs)`).
"""
- work_vert_ps::Vector{R}
+ heterogeneous_vert_p_idxs::Vector{Int64}
"""
- For each parameter in vert_ps, its value is a vector with length either num_verts or 1.
- To know whenever a parameter's value need expanding to the work_vert_ps array, its length needs checking.
- This check is done once, and the value stored to this array.
- This field (specifically) is an enumerate over that array.
+ The MTKParameters structure which corresponds to the non-spatial `ReactionSystem`. During
+ simulations, as we loop through each vertex, this is updated to correspond to the vertex
+ parameters of that specific vertex.
"""
- v_ps_idx_types::Vector{Bool}
+ mtk_ps::Q
"""
- A vector of pairs, with a value for each species with transportation.
- The first value is the species index (in the species(::ReactionSystem) vector),
- and the second is a vector with its transport rate values.
- If the transport rate is uniform (across all edges), that value is the only value in the vector.
- Else, there is one value for each edge in the lattice.
+ Stores a SymbolicIndexingInterface `setp` function for each heterogeneous vertex parameter (i.e.
+ vertex parameter whose value is not identical across the lattice). The `setp` function at index
+ i of `p_setters` corresponds to the parameter in index i of `heterogeneous_vert_p_idxs`.
"""
- transport_rates::Vector{Pair{Int64, Vector{R}}}
+ p_setters::R
"""
- A matrix, NxM, where N is the number of species with transportation and M the number of vertexes.
- Each value is the total rate at which that species leaves that vertex
- (e.g. for a species with constant diffusion rate D, in a vertex with n neighbours, this value is n*D).
+ A vector that stores, for each species with transportation, its transportation rate(s).
+ Each entry is a pair from (the index of) the transported species (in the `species(lrs)` vector)
+ to its transportation rate (each species only has a single transportation rate, the sum of all
+ its transportation reactions' rates). If the transportation rate is uniform across all edges,
+ stores a single value (in a size (1,1) sparse matrix). Otherwise, stores these in a sparse
+ matrix where value (i,j) is the species transportation rate from vertex i to vertex j.
"""
- leaving_rates::Matrix{R}
- """An (enumerate'ed) iterator over all the edges of the lattice."""
- edges::S
+ transport_rates::Vector{Pair{Int64, SparseMatrixCSC{S, Int64}}}
"""
- The edge parameters used to create the spatial ODEProblem. Currently unused,
- but will be needed to support changing these (e.g. due to events).
- Contain one vector for each edge parameter (length one if uniform, else one value for each edge).
+ For each transport rate in transport_rates, its value is a (sparse) matrix with a size of either
+ (num_verts,num_verts) or (1,1). In the second case, the transportation rate is uniform across
+ all edges. To avoid having to check which case holds for each transportation rate, we store the
+ corresponding case in this value. `true` means that a species has a uniform transportation rate.
"""
- edge_ps::Vector{Vector{T}}
-
- function LatticeTransportODEf(ofunc::Q, vert_ps::Vector{Vector{R}},
- transport_rates::Vector{Pair{Int64, Vector{R}}},
- edge_ps::Vector{Vector{T}}, lrs::LatticeReactionSystem) where {Q, R, T}
- leaving_rates = zeros(length(transport_rates), lrs.num_verts)
- for (s_idx, trpair) in enumerate(transport_rates)
- rates = last(trpair)
- for (e_idx, e) in enumerate(edges(lrs.lattice))
- # Updates the exit rate for species s_idx from vertex e.src
- leaving_rates[s_idx, e.src] += get_component_value(rates, e_idx)
- end
- end
- work_vert_ps = zeros(lrs.num_verts)
- # 1 if ps are constant across the graph, 0 else.
- v_ps_idx_types = map(vp -> length(vp) == 1, vert_ps)
- eds = edges(lrs.lattice)
- new{Q, R, typeof(eds), T}(ofunc, lrs.num_verts, lrs.num_species, vert_ps,
- work_vert_ps, v_ps_idx_types, transport_rates, leaving_rates, eds, edge_ps)
- end
-end
-
-# Functor with information for the Jacobian function of a spatial ODE with spatial movement on a lattice.
-struct LatticeTransportODEjac{Q, R, S, T}
- """The ODEFunction of the (non-spatial) reaction system which generated this function."""
- ofunc::Q
- """The number of vertices."""
- num_verts::Int64
- """The number of species."""
- num_species::Int64
- """The values of the parameters which values are tied to vertexes."""
- vert_ps::Vector{Vector{R}}
+ t_rate_idx_types::Vector{Bool}
"""
- Temporary vector. For parameters which values are identical across the lattice,
- at some point these have to be converted of a length(num_verts) vector.
- To avoid re-allocation they are written to this vector.
+ A matrix, NxM, where N is the number of species with transportation and M is the number of
+ vertices. Each value is the total rate at which that species leaves that vertex (e.g. for a
+ species with constant diffusion rate D, in a vertex with n neighbours, this value is n*D).
"""
- work_vert_ps::Vector{R}
+ leaving_rates::Matrix{S}
+ """An iterator over all the edges of the lattice."""
+ edge_iterator::Vector{Pair{Int64, Int64}}
"""
- For each parameter in vert_ps, it either have length num_verts or 1.
- To know whenever a parameter's value need expanding to the work_vert_ps array,
- its length needs checking. This check is done once, and the value stored to this array.
- This field (specifically) is an enumerate over that array.
+ The transport rates. This is a dense or sparse matrix (depending on what type of Jacobian is
+ used).
"""
- v_ps_idx_types::Vector{Bool}
- """Whether the Jacobian is sparse or not."""
+ jac_transport::T
+ """ Whether sparse jacobian representation is used. """
sparse::Bool
- """The transport rates. Can be a dense matrix (for non-sparse) or as the "nzval" field if sparse."""
- jac_transport::S
- """
- The edge parameters used to create the spatial ODEProblem. Currently unused,
- but will be needed to support changing these (e.g. due to events).
- Contain one vector for each edge parameter (length one if uniform, else one value for each edge).
- """
- edge_ps::Vector{Vector{T}}
-
- function LatticeTransportODEjac(
- ofunc::R, vert_ps::Vector{Vector{S}}, lrs::LatticeReactionSystem,
- jac_transport::Union{Nothing, SparseMatrixCSC{Float64, Int64}},
- edge_ps::Vector{Vector{T}}, sparse::Bool) where {R, S, T}
- work_vert_ps = zeros(lrs.num_verts)
- v_ps_idx_types = map(vp -> length(vp) == 1, vert_ps)
- new{R, S, typeof(jac_transport), T}(ofunc, lrs.num_verts, lrs.num_species, vert_ps,
- work_vert_ps, v_ps_idx_types, sparse, jac_transport, edge_ps)
+ """Remove when we add this as problem metadata"""
+ lrs::LatticeReactionSystem
+
+ function LatticeTransportODEFunction(ofunc::P, ps::Vector{<:Pair},
+ lrs::LatticeReactionSystem, sparse::Bool,
+ jac_transport::Union{Nothing, Matrix{S}, SparseMatrixCSC{S, Int64}},
+ transport_rates::Vector{Pair{Int64, SparseMatrixCSC{S, Int64}}}) where {P, S}
+ # Computes `LatticeTransportODEFunction` functor fields.
+ heterogeneous_vert_p_idxs = make_heterogeneous_vert_p_idxs(ps, lrs)
+ mtk_ps, p_setters = make_mtk_ps_structs(ps, lrs, heterogeneous_vert_p_idxs)
+ t_rate_idx_types, leaving_rates = make_t_types_and_leaving_rates(transport_rates,
+ lrs)
+
+ # Creates and returns the `LatticeTransportODEFunction` functor.
+ new{P, typeof(mtk_ps), typeof(p_setters), S, typeof(jac_transport)}(ofunc,
+ num_verts(lrs), num_species(lrs), heterogeneous_vert_p_idxs, mtk_ps, p_setters,
+ transport_rates, t_rate_idx_types, leaving_rates, Catalyst.edge_iterator(lrs),
+ jac_transport, sparse, lrs)
+ end
+end
+
+# `LatticeTransportODEFunction` helper functions (re-used by rebuild function later on).
+
+# Creates a vector with the heterogeneous vertex parameters' indexes in the full parameter vector.
+function make_heterogeneous_vert_p_idxs(ps, lrs)
+ p_dict = Dict(ps)
+ return findall((p_dict[p] isa Vector) && (length(p_dict[p]) > 1)
+ for p in parameters(lrs))
+end
+
+# Creates the MTKParameters structure and `p_setters` vector (which are used to manage
+# the vertex parameter values during the simulations).
+function make_mtk_ps_structs(ps, lrs, heterogeneous_vert_p_idxs)
+ p_dict = Dict(ps)
+ nonspatial_osys = complete(convert(ODESystem, reactionsystem(lrs)))
+ p_init = [p => p_dict[p][1] for p in parameters(nonspatial_osys)]
+ mtk_ps = MT.MTKParameters(nonspatial_osys, p_init)
+ p_setters = [MT.setp(nonspatial_osys, p)
+ for p in parameters(lrs)[heterogeneous_vert_p_idxs]]
+ return mtk_ps, p_setters
+end
+
+# Computes the transport rate type vector and leaving rate matrix.
+function make_t_types_and_leaving_rates(transport_rates, lrs)
+ t_rate_idx_types = [size(tr[2]) == (1, 1) for tr in transport_rates]
+ leaving_rates = zeros(length(transport_rates), num_verts(lrs))
+ for (s_idx, tr_pair) in enumerate(transport_rates)
+ for e in Catalyst.edge_iterator(lrs)
+ # Updates the exit rate for species s_idx from vertex e.src.
+ leaving_rates[s_idx, e[1]] += get_transport_rate(tr_pair[2], e,
+ t_rate_idx_types[s_idx])
+ end
+ end
+ return t_rate_idx_types, leaving_rates
+end
+
+### Spatial ODE Functor Functions ###
+
+# Defines the functor's effect when applied as a forcing function.
+function (lt_ofun::LatticeTransportODEFunction)(du::AbstractVector, u, p, t)
+ # Updates for non-spatial reactions.
+ for vert_i in 1:(lt_ofun.num_verts)
+ # Gets the indices of all the species at vertex i.
+ idxs = get_indexes(vert_i, lt_ofun.num_species)
+
+ # Updates the functors vertex parameter tracker (`mtk_ps`) to contain the vertex parameter
+ # values for vertex vert_i. Then evaluates the reaction contributions to du at vert_i.
+ update_mtk_ps!(lt_ofun, p, vert_i)
+ lt_ofun.ofunc((@view du[idxs]), (@view u[idxs]), lt_ofun.mtk_ps, t)
+ end
+
+ # s_idx is the species index among transport species, s is the index among all species.
+ # rates are the species' transport rates.
+ for (s_idx, (s, rates)) in enumerate(lt_ofun.transport_rates)
+ # Rate for leaving source vertex vert_i.
+ for vert_i in 1:(lt_ofun.num_verts)
+ idx_src = get_index(vert_i, s, lt_ofun.num_species)
+ du[idx_src] -= lt_ofun.leaving_rates[s_idx, vert_i] * u[idx_src]
+ end
+ # Add rates for entering a destination vertex via an incoming edge.
+ for e in lt_ofun.edge_iterator
+ idx_src = get_index(e[1], s, lt_ofun.num_species)
+ idx_dst = get_index(e[2], s, lt_ofun.num_species)
+ du[idx_dst] += get_transport_rate(rates, e, lt_ofun.t_rate_idx_types[s_idx]) *
+ u[idx_src]
+ end
+ end
+end
+
+# Defines the functor's effect when applied as a Jacobian.
+function (lt_ofun::LatticeTransportODEFunction)(J::AbstractMatrix, u, p, t)
+ # Resets the Jacobian J's values.
+ J .= 0.0
+
+ # Update the Jacobian from non-spatial reaction terms.
+ for vert_i in 1:(lt_ofun.num_verts)
+ # Gets the indices of all the species at vertex i.
+ idxs = get_indexes(vert_i, lt_ofun.num_species)
+
+ # Updates the functors vertex parameter tracker (`mtk_ps`) to contain the vertex parameter
+ # values for vertex vert_i. Then evaluates the reaction contributions to J at vert_i.
+ update_mtk_ps!(lt_ofun, p, vert_i)
+ lt_ofun.ofunc.jac((@view J[idxs, idxs]), (@view u[idxs]), lt_ofun.mtk_ps, t)
end
+
+ # Updates for the spatial reactions (adds the Jacobian values from the transportation reactions).
+ J .+= lt_ofun.jac_transport
end
### ODEProblem ###
@@ -118,198 +177,260 @@ function DiffEqBase.ODEProblem(lrs::LatticeReactionSystem, u0_in, tspan,
p_in = DiffEqBase.NullParameters(), args...;
jac = false, sparse = false,
name = nameof(lrs), include_zero_odes = true,
- combinatoric_ratelaws = get_combinatoric_ratelaws(lrs.rs),
+ combinatoric_ratelaws = get_combinatoric_ratelaws(reactionsystem(lrs)),
remove_conserved = false, checks = false, kwargs...)
if !is_transport_system(lrs)
error("Currently lattice ODE simulations are only supported when all spatial reactions are TransportReactions.")
end
- # Converts potential symmaps to varmaps
- # Vertex and edge parameters may be given in a tuple, or in a common vector, making parameter case complicated.
+ # Converts potential symmaps to varmaps.
u0_in = symmap_to_varmap(lrs, u0_in)
- p_in = (p_in isa Tuple{<:Any, <:Any}) ?
- (symmap_to_varmap(lrs, p_in[1]), symmap_to_varmap(lrs, p_in[2])) :
- symmap_to_varmap(lrs, p_in)
+ p_in = symmap_to_varmap(lrs, p_in)
# Converts u0 and p to their internal forms.
+ # u0 is simply a vector with all the species' initial condition values across all vertices.
# u0 is [spec 1 at vert 1, spec 2 at vert 1, ..., spec 1 at vert 2, ...].
- u0 = lattice_process_u0(u0_in, species(lrs), lrs.num_verts)
- # Both vert_ps and edge_ps becomes vectors of vectors. Each have 1 element for each parameter.
- # These elements are length 1 vectors (if the parameter is uniform),
- # or length num_verts/nE, with unique values for each vertex/edge (for vert_ps/edge_ps, respectively).
- vert_ps, edge_ps = lattice_process_p(
- p_in, vertex_parameters(lrs), edge_parameters(lrs), lrs)
-
- # Creates ODEProblem.
+ u0 = lattice_process_u0(u0_in, species(lrs), lrs)
+ # vert_ps and `edge_ps` are vector maps, taking each parameter's Symbolics representation to its value(s).
+ # vert_ps values are vectors. Here, index (i) is a parameter's value in vertex i.
+ # edge_ps values are sparse matrices. Here, index (i,j) is a parameter's value in the edge from vertex i to vertex j.
+ # Uniform vertex/edge parameters store only a single value (a length 1 vector, or size 1x1 sparse matrix).
+ # In the `ODEProblem` vert_ps and edge_ps are merged (but for building the ODEFunction, they are separate).
+ vert_ps, edge_ps = lattice_process_p(p_in, vertex_parameters(lrs),
+ edge_parameters(lrs), lrs)
+
+ # Creates the ODEFunction.
ofun = build_odefunction(lrs, vert_ps, edge_ps, jac, sparse, name, include_zero_odes,
combinatoric_ratelaws, remove_conserved, checks)
- return ODEProblem(ofun, u0, tspan, vert_ps, args...; kwargs...)
+
+ # Combines `vert_ps` and `edge_ps` to a single vector with values only (not a map). Creates ODEProblem.
+ pval_dict = Dict([vert_ps; edge_ps])
+ ps = [pval_dict[p] for p in parameters(lrs)]
+ return ODEProblem(ofun, u0, tspan, ps, args...; kwargs...)
end
# Builds an ODEFunction for a spatial ODEProblem.
-function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Vector{T}},
- edge_ps::Vector{Vector{T}}, jac::Bool, sparse::Bool,
- name, include_zero_odes, combinatoric_ratelaws, remove_conserved, checks) where {T}
+function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{R, Vector{T}}},
+ edge_ps::Vector{Pair{S, SparseMatrixCSC{T, Int64}}},
+ jac::Bool, sparse::Bool, name, include_zero_odes, combinatoric_ratelaws,
+ remove_conserved, checks) where {R, S, T}
+ # Error check.
if remove_conserved
- error("Removal of conserved quantities is currently not supported for `LatticeReactionSystem`s")
+ throw(ArgumentError("Removal of conserved quantities is currently not supported for `LatticeReactionSystem`s"))
end
- # Creates a map, taking (the index in species(lrs) each species (with transportation)
- # to its transportation rate (uniform or one value for each edge).
+ # Prepares the inputs to the `LatticeTransportODEFunction` functor.
+ osys = complete(convert(ODESystem, reactionsystem(lrs);
+ name, combinatoric_ratelaws, include_zero_odes, checks))
+ ofunc_dense = ODEFunction(osys; jac = true, sparse = false)
+ ofunc_sparse = ODEFunction(osys; jac = true, sparse = true)
transport_rates = make_sidxs_to_transrate_map(vert_ps, edge_ps, lrs)
- # Prepares the Jacobian and forcing functions (depending on jacobian and sparsity selection).
- osys = complete(convert(ODESystem, lrs.rs;
- name, combinatoric_ratelaws, include_zero_odes, checks))
- if jac
- # `build_jac_prototype` currently assumes a sparse (non-spatial) Jacobian. Hence compute this.
- # `LatticeTransportODEjac` currently assumes a dense (non-spatial) Jacobian. Hence compute this.
- # Long term we could write separate version of these functions for generic input.
- ofunc_dense = ODEFunction(osys; jac = true, sparse = false)
- ofunc_sparse = ODEFunction(osys; jac = true, sparse = true)
- jac_vals = build_jac_prototype(ofunc_sparse.jac_prototype, transport_rates, lrs;
- set_nonzero = true)
- if sparse
- f = LatticeTransportODEf(ofunc_sparse, vert_ps, transport_rates, edge_ps, lrs)
- jac_vals = build_jac_prototype(ofunc_sparse.jac_prototype, transport_rates,
- lrs; set_nonzero = true)
- J = LatticeTransportODEjac(ofunc_dense, vert_ps, lrs, jac_vals, edge_ps, true)
- jac_prototype = jac_vals
- else
- f = LatticeTransportODEf(ofunc_dense, vert_ps, transport_rates, edge_ps, lrs)
- J = LatticeTransportODEjac(ofunc_dense, vert_ps, lrs, jac_vals, edge_ps, false)
- jac_prototype = nothing
- end
+ # Depending on Jacobian and sparsity options, compute the Jacobian transport matrix and prototype.
+ if !sparse && !jac
+ jac_transport = nothing
+ jac_prototype = nothing
else
- if sparse
- ofunc_sparse = ODEFunction(osys; jac = false, sparse = true)
- f = LatticeTransportODEf(ofunc_sparse, vert_ps, transport_rates, edge_ps, lrs)
- jac_prototype = build_jac_prototype(ofunc_sparse.jac_prototype,
- transport_rates, lrs; set_nonzero = false)
- else
- ofunc_dense = ODEFunction(osys; jac = false, sparse = false)
- f = LatticeTransportODEf(ofunc_dense, vert_ps, transport_rates, edge_ps, lrs)
- jac_prototype = nothing
- end
- J = nothing
+ jac_sparse = build_jac_prototype(ofunc_sparse.jac_prototype, transport_rates, lrs;
+ set_nonzero = jac)
+ jac_dense = Matrix(jac_sparse)
+ jac_transport = (jac ? (sparse ? jac_sparse : jac_dense) : nothing)
+ jac_prototype = (sparse ? jac_sparse : nothing)
end
- return ODEFunction(f; jac = J, jac_prototype = jac_prototype)
+ # Creates the `LatticeTransportODEFunction` functor (if `jac`, sets it as the Jacobian as well).
+ f = LatticeTransportODEFunction(ofunc_dense, [vert_ps; edge_ps], lrs, sparse,
+ jac_transport, transport_rates)
+ J = (jac ? f : nothing)
+
+ # Extracts the `Symbol` form for parameters (but not species). Creates and returns the `ODEFunction`.
+ paramsyms = [MT.getname(p) for p in parameters(lrs)]
+ sys = SciMLBase.SymbolCache([], paramsyms, [])
+ return ODEFunction(f; jac = J, jac_prototype, sys)
end
-# Builds a jacobian prototype. If requested, populate it with the Jacobian's (constant) values as well.
-function build_jac_prototype(
- ns_jac_prototype::SparseMatrixCSC{Float64, Int64}, trans_rates,
- lrs::LatticeReactionSystem; set_nonzero = false)
- # Finds the indexes of the transport species, and the species with transport only (and no non-spatial dynamics).
- trans_species = first.(trans_rates)
- trans_only_species = filter(
- s_idx -> !Base.isstored(ns_jac_prototype, s_idx, s_idx), trans_species)
+# Builds a Jacobian prototype.
+# If requested, populate it with the constant values of the Jacobian's transportation part.
+function build_jac_prototype(ns_jac_prototype::SparseMatrixCSC{Float64, Int64},
+ transport_rates::Vector{Pair{Int64, SparseMatrixCSC{T, Int64}}},
+ lrs::LatticeReactionSystem; set_nonzero = false) where {T}
+ # Finds the indices of both the transport species,
+ # and the species with transport only (that is, with no non-spatial dynamics but with spatial dynamics).
+ trans_species = [tr[1] for tr in transport_rates]
+ trans_only_species = filter(s_idx -> !Base.isstored(ns_jac_prototype, s_idx, s_idx),
+ trans_species)
- # Finds the indexes of all terms in the non-spatial jacobian.
+ # Finds the indices of all terms in the non-spatial jacobian.
ns_jac_prototype_idxs = findnz(ns_jac_prototype)
ns_i_idxs = ns_jac_prototype_idxs[1]
ns_j_idxs = ns_jac_prototype_idxs[2]
- # Prepares vectors to store i and j indexes of Jacobian entries.
+ # Prepares vectors to store i and j indices of Jacobian entries.
idx = 1
- num_entries = lrs.num_verts * length(ns_i_idxs) +
- lrs.num_edges * (length(trans_only_species) + length(trans_species))
+ num_entries = num_verts(lrs) * length(ns_i_idxs) +
+ num_edges(lrs) * (length(trans_only_species) + length(trans_species))
i_idxs = Vector{Int}(undef, num_entries)
j_idxs = Vector{Int}(undef, num_entries)
- # Indexes of elements due to non-spatial dynamics.
- for vert in 1:(lrs.num_verts)
+ # Indices of elements caused by non-spatial dynamics.
+ for vert in 1:num_verts(lrs)
for n in 1:length(ns_i_idxs)
- i_idxs[idx] = get_index(vert, ns_i_idxs[n], lrs.num_species)
- j_idxs[idx] = get_index(vert, ns_j_idxs[n], lrs.num_species)
+ i_idxs[idx] = get_index(vert, ns_i_idxs[n], num_species(lrs))
+ j_idxs[idx] = get_index(vert, ns_j_idxs[n], num_species(lrs))
idx += 1
end
end
- # Indexes of elements due to spatial dynamics.
- for e in edges(lrs.lattice)
- # Indexes due to terms for a species leaves its current vertex (but does not have
+ # Indices of elements caused by spatial dynamics.
+ for e in edge_iterator(lrs)
+ # Indexes due to terms for a species leaving its source vertex (but does not have
# non-spatial dynamics). If the non-spatial Jacobian is fully dense, these would already
# be accounted for.
for s_idx in trans_only_species
- i_idxs[idx] = get_index(e.src, s_idx, lrs.num_species)
+ i_idxs[idx] = get_index(e[1], s_idx, num_species(lrs))
j_idxs[idx] = i_idxs[idx]
idx += 1
end
- # Indexes due to terms for species arriving into a new vertex.
+ # Indexes due to terms for species arriving into a destination vertex.
for s_idx in trans_species
- i_idxs[idx] = get_index(e.src, s_idx, lrs.num_species)
- j_idxs[idx] = get_index(e.dst, s_idx, lrs.num_species)
+ i_idxs[idx] = get_index(e[1], s_idx, num_species(lrs))
+ j_idxs[idx] = get_index(e[2], s_idx, num_species(lrs))
idx += 1
end
end
- # Create sparse jacobian prototype with 0-valued entries.
- jac_prototype = sparse(i_idxs, j_idxs, zeros(num_entries))
+ # Create a sparse Jacobian prototype with 0-valued entries. If requested,
+ # updates values with non-zero entries.
+ jac_prototype = sparse(i_idxs, j_idxs, zeros(T, num_entries))
+ set_nonzero && set_jac_transport_values!(jac_prototype, transport_rates, lrs)
- # Set element values.
- if set_nonzero
- for (s, rates) in trans_rates, (e_idx, e) in enumerate(edges(lrs.lattice))
- idx_src = get_index(e.src, s, lrs.num_species)
- idx_dst = get_index(e.dst, s, lrs.num_species)
- val = get_component_value(rates, e_idx)
+ return jac_prototype
+end
- # Term due to species leaving source vertex.
- jac_prototype[idx_src, idx_src] -= val
+# For a Jacobian prototype with zero-valued entries. Set entry values according to a set of
+# transport reaction values.
+function set_jac_transport_values!(jac_prototype, transport_rates, lrs)
+ for (s, rates) in transport_rates, e in edge_iterator(lrs)
+ idx_src = get_index(e[1], s, num_species(lrs))
+ idx_dst = get_index(e[2], s, num_species(lrs))
+ val = get_transport_rate(rates, e, size(rates) == (1, 1))
- # Term due to species arriving to destination vertex.
- jac_prototype[idx_src, idx_dst] += val
- end
+ # Term due to species leaving source vertex.
+ jac_prototype[idx_src, idx_src] -= val
+
+ # Term due to species arriving to destination vertex.
+ jac_prototype[idx_src, idx_dst] += val
end
+end
- return jac_prototype
+### Functor Updating Functionality ###
+
+"""
+ rebuild_lat_internals!(sciml_struct)
+
+Rebuilds the internal functions for simulating a LatticeReactionSystem. Wenever a problem or
+integrator has had its parameter values updated, this function should be called for the update to
+be taken into account. For ODE simulations, `rebuild_lat_internals!` needs only to be called when
+- An edge parameter has been updated.
+- When a parameter with spatially homogeneous values has been given spatially heterogeneous values
+(or vice versa).
+
+Arguments:
+- `sciml_struct`: The problem (e.g. an `ODEProblem`) or an integrator which we wish to rebuild.
+
+Notes:
+- Currently does not work for `DiscreteProblem`s, `JumpProblem`s, or their integrators.
+- The function is not built with performance in mind, so avoid calling it multiple times in
+performance-critical applications.
+
+Example:
+```julia
+# Creates an initial `ODEProblem`
+rs = @reaction_network begin
+ (k1,k2), X1 <--> X2
end
+tr = @transport_reaction D X1
+grid = CartesianGrid((2,2))
+lrs = LatticeReactionSystem(rs, [tr], grid)
-# Defines the forcing functor's effect on the (spatial) ODE system.
-function (f_func::LatticeTransportODEf)(du, u, p, t)
- # Updates for non-spatial reactions.
- for vert_i in 1:(f_func.num_verts)
- # gets the indices of species at vertex i
- idxs = get_indexes(vert_i, f_func.num_species)
+u0 = [:X1 => 2, :X2 => [5 6; 7 8]]
+tspan = (0.0, 10.0)
+ps = [:k1 => 1.5, :k2 => [1.0 1.5; 2.0 3.5], :D => 0.1]
+
+oprob = ODEProblem(lrs, u0, tspan, ps)
+
+# Updates parameter values.
+oprob.ps[:ks] = [2.0 2.5; 3.0 4.5]
+oprob.ps[:D] = 0.05
+
+# Rebuilds `ODEProblem` to make changes have an effect.
+rebuild_lat_internals!(oprob)
+```
+"""
+function rebuild_lat_internals!(oprob::ODEProblem)
+ rebuild_lat_internals!(oprob.f.f, oprob.p, oprob.f.f.lrs)
+end
+
+# Function for rebuilding a `LatticeReactionSystem` integrator after it has been updated.
+# We could specify `integrator`'s type, but that required adding OrdinaryDiffEq as a direct
+# dependency of Catalyst.
+function rebuild_lat_internals!(integrator)
+ rebuild_lat_internals!(integrator.f.f, integrator.p, integrator.f.f.lrs)
+end
- # vector of vertex ps at vert_i
- vert_i_ps = view_vert_ps_vector!(
- f_func.work_vert_ps, p, vert_i, enumerate(f_func.v_ps_idx_types))
+# Function which rebuilds a `LatticeTransportODEFunction` functor for a new parameter set.
+function rebuild_lat_internals!(lt_ofun::LatticeTransportODEFunction, ps_new,
+ lrs::LatticeReactionSystem)
+ # Computes Jacobian properties.
+ jac = !isnothing(lt_ofun.jac_transport)
+ sparse = lt_ofun.sparse
- # evaluate reaction contributions to du at vert_i
- f_func.ofunc((@view du[idxs]), (@view u[idxs]), vert_i_ps, t)
+ # Recreates the new parameters on the requisite form.
+ ps_new = [(length(p) == 1) ? p[1] : p for p in deepcopy(ps_new)]
+ ps_new = [p => p_val for (p, p_val) in zip(parameters(lrs), deepcopy(ps_new))]
+ vert_ps, edge_ps = lattice_process_p(ps_new, vertex_parameters(lrs),
+ edge_parameters(lrs), lrs)
+ ps_new = [vert_ps; edge_ps]
+
+ # Creates the new transport rates and transport Jacobian part.
+ transport_rates = make_sidxs_to_transrate_map(vert_ps, edge_ps, lrs)
+ if !isnothing(lt_ofun.jac_transport)
+ lt_ofun.jac_transport .= 0.0
+ set_jac_transport_values!(lt_ofun.jac_transport, transport_rates, lrs)
end
- # s_idx is species index among transport species, s is index among all species
- # rates are the species' transport rates
- for (s_idx, (s, rates)) in enumerate(f_func.transport_rates)
- # Rate for leaving vert_i
- for vert_i in 1:(f_func.num_verts)
- idx = get_index(vert_i, s, f_func.num_species)
- du[idx] -= f_func.leaving_rates[s_idx, vert_i] * u[idx]
- end
- # Add rates for entering a given vertex via an incoming edge
- for (e_idx, e) in enumerate(f_func.edges)
- idx_dst = get_index(e.dst, s, f_func.num_species)
- idx_src = get_index(e.src, s, f_func.num_species)
- du[idx_dst] += get_component_value(rates, e_idx) * u[idx_src]
- end
+ # Computes new field values.
+ heterogeneous_vert_p_idxs = make_heterogeneous_vert_p_idxs(ps_new, lrs)
+ mtk_ps, p_setters = make_mtk_ps_structs(ps_new, lrs, heterogeneous_vert_p_idxs)
+ t_rate_idx_types, leaving_rates = make_t_types_and_leaving_rates(transport_rates, lrs)
+
+ # Updates functor fields.
+ replace_vec!(lt_ofun.heterogeneous_vert_p_idxs, heterogeneous_vert_p_idxs)
+ replace_vec!(lt_ofun.p_setters, p_setters)
+ replace_vec!(lt_ofun.transport_rates, transport_rates)
+ replace_vec!(lt_ofun.t_rate_idx_types, t_rate_idx_types)
+ lt_ofun.leaving_rates .= leaving_rates
+
+ # Updating the `MTKParameters` structure is a bit more complicated.
+ p_dict = Dict(ps_new)
+ osys = complete(convert(ODESystem, reactionsystem(lrs)))
+ for p in parameters(osys)
+ MT.setp(osys, p)(lt_ofun.mtk_ps, (p_dict[p] isa Number) ? p_dict[p] : p_dict[p][1])
end
+
+ return nothing
end
-# Defines the jacobian functor's effect on the (spatial) ODE system.
-function (jac_func::LatticeTransportODEjac)(J, u, p, t)
- J .= 0.0
+# Specialised function which replaced one vector in another in a mutating way.
+# Required to update the vectors in the `LatticeTransportODEFunction` functor.
+function replace_vec!(vec1, vec2)
+ l1 = length(vec1)
+ l2 = length(vec2)
- # Update the Jacobian from reaction terms
- for vert_i in 1:(jac_func.num_verts)
- idxs = get_indexes(vert_i, jac_func.num_species)
- vert_ps = view_vert_ps_vector!(jac_func.work_vert_ps, p, vert_i,
- enumerate(jac_func.v_ps_idx_types))
- jac_func.ofunc.jac((@view J[idxs, idxs]), (@view u[idxs]), vert_ps, t)
+ # Updates the fields, then deletes superfluous fields, or additional ones.
+ for (i, v) in enumerate(vec2[1:min(l1, l2)])
+ vec1[i] = v
end
-
- # Updates for the spatial reactions (adds the Jacobian values from the diffusion reactions).
- J .+= jac_func.jac_transport
+ foreach(idx -> deleteat!(vec1, idx), l1:-1:(l2 + 1))
+ foreach(val -> push!(vec1, val), vec2[(l1 + 1):l2])
end
diff --git a/src/spatial_reaction_systems/spatial_reactions.jl b/src/spatial_reaction_systems/spatial_reactions.jl
index b897cd11fd..204d94992a 100644
--- a/src/spatial_reaction_systems/spatial_reactions.jl
+++ b/src/spatial_reaction_systems/spatial_reactions.jl
@@ -3,7 +3,7 @@
# Abstract spatial reaction structures.
abstract type AbstractSpatialReaction end
-### EdgeParameter Metadata ###
+### Edge Parameter Metadata ###
# Implements the edgeparameter metadata field.
struct EdgeParameter end
@@ -22,7 +22,7 @@ end
# A transport reaction. These are simple to handle, and should cover most types of spatial reactions.
# Only permit constant rates (possibly consisting of several parameters).
struct TransportReaction <: AbstractSpatialReaction
- """The rate function (excluding mass action terms). Currently only constants supported"""
+ """The rate function (excluding mass action terms). Currently, only constants supported"""
rate::Any
"""The species that is subject to diffusion."""
species::BasicSymbolic{Real}
@@ -40,7 +40,7 @@ function TransportReactions(transport_reactions)
[TransportReaction(tr[1], tr[2]) for tr in transport_reactions]
end
-# Macro for creating a transport reaction.
+# Macro for creating a TransportReactions.
macro transport_reaction(rateex::ExprValues, species::ExprValues)
make_transport_reaction(MacroTools.striplines(rateex), species)
end
@@ -62,6 +62,11 @@ function make_transport_reaction(rateex, species)
iv = :(@variables $(DEFAULT_IV_SYM))
trxexpr = :(TransportReaction($rateex, $species))
+ # Appends `edgeparameter` metadata to all declared parameters.
+ for idx in 4:2:(2 + 2 * length(parameters))
+ insert!(pexprs.args, idx, :([edgeparameter = true]))
+ end
+
quote
$pexprs
$iv
@@ -70,18 +75,18 @@ function make_transport_reaction(rateex, species)
end
end
-# Gets the parameters in a transport reaction.
+# Gets the parameters in a TransportReactions.
ModelingToolkit.parameters(tr::TransportReaction) = Symbolics.get_variables(tr.rate)
-# Gets the species in a transport reaction.
+# Gets the species in a TransportReactions.
spatial_species(tr::TransportReaction) = [tr.species]
-# Checks that a transport reaction is valid for a given reaction system.
+# Checks that a TransportReactions is valid for a given reaction system.
function check_spatial_reaction_validity(rs::ReactionSystem, tr::TransportReaction;
edge_parameters = [])
# Checks that the species exist in the reaction system.
# (ODE simulation code becomes difficult if this is not required,
- # as non-spatial jacobian and f function generated from rs is of wrong size).
+ # as non-spatial jacobian and f function generated from rs are of the wrong size).
if !any(isequal(tr.species), species(rs))
error("Currently, species used in TransportReactions must have previously been declared within the non-spatial ReactionSystem. This is not the case for $(tr.species).")
end
@@ -94,15 +99,15 @@ function check_spatial_reaction_validity(rs::ReactionSystem, tr::TransportReacti
# Checks that the species does not exist in the system with different metadata.
if any(isequal(tr.species, s) && !isequivalent(tr.species, s) for s in species(rs))
- error("A transport reaction used a species, $(tr.species), with metadata not matching its lattice reaction system. Please fetch this species from the reaction system and used in transport reaction creation.")
+ error("A transport reaction used a species, $(tr.species), with metadata not matching its lattice reaction system. Please fetch this species from the reaction system and use it during transport reaction creation.")
end
# No `for` loop, just weird formatting by the formatter.
if any(isequal(rs_p, tr_p) && !isequivalent(rs_p, tr_p)
for rs_p in parameters(rs), tr_p in Symbolics.get_variables(tr.rate))
- error("A transport reaction used a parameter with metadata not matching its lattice reaction system. Please fetch this parameter from the reaction system and used in transport reaction creation.")
+ error("A transport reaction used a parameter with metadata not matching its lattice reaction system. Please fetch this parameter from the reaction system and use it during transport reaction creation.")
end
- # Checks that no edge parameter occur among rates of non-spatial reactions.
+ # Checks that no edge parameter occurs among rates of non-spatial reactions.
# No `for` loop, just weird formatting by the formatter.
if any(!isempty(intersect(Symbolics.get_variables(r.rate), edge_parameters))
for r in reactions(rs))
@@ -144,7 +149,8 @@ function hash(tr::TransportReaction, h::UInt)
end
### Utility ###
-# Loops through a rate and extract all parameters.
+
+# Loops through a rate and extracts all parameters.
function find_parameters_in_rate!(parameters, rateex::ExprValues)
if rateex isa Symbol
if rateex in [:t, :∅, :im, :nothing, CONSERVED_CONSTANT_SYMBOL]
@@ -153,10 +159,10 @@ function find_parameters_in_rate!(parameters, rateex::ExprValues)
push!(parameters, rateex)
end
elseif rateex isa Expr
- # Note, this (correctly) skips $(...) expressions
+ # Note, this (correctly) skips $(...) expressions.
for i in 2:length(rateex.args)
find_parameters_in_rate!(parameters, rateex.args[i])
end
end
- nothing
+ return nothing
end
diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl
index bf6e0c2a2e..e00f753d8c 100644
--- a/src/spatial_reaction_systems/utility.jl
+++ b/src/spatial_reaction_systems/utility.jl
@@ -13,397 +13,302 @@ function _symbol_to_var(lrs::LatticeReactionSystem, sym)
error("Could not find property parameter/species $sym in lattice reaction system.")
end
-# From u0 input, extracts their values and store them in the internal format.
-# Internal format: a vector on the form [spec 1 at vert 1, spec 2 at vert 1, ..., spec 1 at vert 2, ...]).
-function lattice_process_u0(u0_in, u0_syms, num_verts)
- # u0 values can be given in various forms. This converts it to a Vector{Vector{}} form.
- # Top-level vector: Contains one vector for each species.
- # Second-level vector: contain one value if species uniform across lattice, else one value for each vertex).
- u0 = lattice_process_input(u0_in, u0_syms, num_verts)
-
- # Perform various error checks on the (by the user provided) initial conditions.
- check_vector_lengths(u0, length(u0_syms), num_verts)
-
- # Converts the Vector{Vector{}} format to a single Vector (with one values for each species and vertex).
- expand_component_values(u0, num_verts)
+# From u0 input, extract their values and store them in the internal format.
+# Internal format: a vector on the form [spec 1 at vert 1, spec 2 at vert 1, ..., spec 1 at vert 2, ...]).
+function lattice_process_u0(u0_in, u0_syms::Vector, lrs::LatticeReactionSystem)
+ # u0 values can be given in various forms. This converts it to a Vector{Pair{Symbolics,...}} form.
+ # Top-level vector: Maps each species to its value(s).
+ u0 = lattice_process_input(u0_in, u0_syms)
+
+ # Species' initial condition values can be given in different forms (also depending on the lattice).
+ # This converts each species's values to a Vector. In it, for species with uniform initial conditions,
+ # it holds that value only. For spatially heterogeneous initial conditions,
+ # the vector has the same length as the number of vertices (storing one value for each).
+ u0 = vertex_value_map(u0, lrs)
+
+ # Converts the initial condition to a single Vector (with one value for each species and vertex).
+ return expand_component_values([entry[2] for entry in u0], num_verts(lrs))
end
-# From p input, splits it into diffusion parameters and compartment parameters.
+# From a parameter input, split it into vertex parameters and edge parameters.
# Store these in the desired internal format.
-function lattice_process_p(p_in, p_vertex_syms, p_edge_syms, lrs::LatticeReactionSystem)
- # If the user provided parameters as a single map (mixing vertex and edge parameters):
- # Split into two separate vectors.
- vert_ps_in, edge_ps_in = split_parameters(p_in, p_vertex_syms, p_edge_syms)
+function lattice_process_p(ps_in, ps_vertex_syms::Vector,
+ ps_edge_syms::Vector, lrs::LatticeReactionSystem)
+ # p values can be given in various forms. This converts it to a Vector{Pair{Symbolics,...}} form.
+ # Top-level vector: Maps each parameter to its value(s).
+ # Second-level: Contains either a vector (vertex parameters) or a sparse matrix (edge parameters).
+ # For uniform parameters these have size 1/(1,1). Else, they have size num_verts/(num_verts,num_verts).
+ ps = lattice_process_input(ps_in, [ps_vertex_syms; ps_edge_syms])
+
+ # Split the parameter vector into one for vertex parameters and one for edge parameters.
+ # Next, convert their values to the correct form (vectors for vert_ps and sparse matrices for edge_ps).
+ vert_ps, edge_ps = split_parameters(ps, ps_vertex_syms, ps_edge_syms)
+ vert_ps = vertex_value_map(vert_ps, lrs)
+ edge_ps = edge_value_map(edge_ps, lrs)
- # Parameter values can be given in various forms. This converts it to the Vector{Vector{}} form.
- vert_ps = lattice_process_input(vert_ps_in, p_vertex_syms, lrs.num_verts)
-
- # Parameter values can be given in various forms. This converts it to the Vector{Vector{}} form.
- edge_ps = lattice_process_input(edge_ps_in, p_edge_syms, lrs.num_edges)
+ return vert_ps, edge_ps
+end
- # If the lattice defined as (N edge) undirected graph, and we provides N/2 values for some edge parameter:
- # Presume they want to expand that parameters value so it has the same value in both directions.
- lrs.init_digraph || duplicate_trans_params!(edge_ps, lrs)
+# The input (parameters or initial conditions) may either be a dictionary (symbolics to value(s).)
+# or a map (in vector or tuple form) from symbolics to value(s). This converts the input to a
+# (Vector) map from symbolics to value(s), where the entries have the same order as `syms`.
+function lattice_process_input(input::Dict{<:Any, T}, syms::Vector) where {T}
+ # Error checks
+ if !isempty(setdiff(keys(input), syms))
+ throw(ArgumentError("You have provided values for the following unrecognised parameters/initial conditions: $(setdiff(keys(input), syms))."))
+ end
+ if !isempty(setdiff(syms, keys(input)))
+ throw(ArgumentError("You have not provided values for the following parameters/initial conditions: $(setdiff(syms, keys(input)))."))
+ end
- # Perform various error checks on the (by the user provided) vertex and edge parameters.
- check_vector_lengths(vert_ps, length(p_vertex_syms), lrs.num_verts)
- check_vector_lengths(edge_ps, length(p_edge_syms), lrs.num_edges)
+ return [sym => input[sym] for sym in syms]
+end
+function lattice_process_input(input, syms::Vector)
+ if ((input isa Vector) || (input isa Tuple)) && all(entry isa Pair for entry in input)
+ return lattice_process_input(Dict(input), syms)
+ end
+ throw(ArgumentError("Input parameters/initial conditions have the wrong format ($(typeof(input))). These should either be a Dictionary, or a Tuple or a Vector (where each entry is a Pair taking a parameter/species to its value)."))
+end
+# Splits parameters into vertex and edge parameters.
+function split_parameters(ps, p_vertex_syms::Vector, p_edge_syms::Vector)
+ vert_ps = [p for p in ps if any(isequal(p[1]), p_vertex_syms)]
+ edge_ps = [p for p in ps if any(isequal(p[1]), p_edge_syms)]
return vert_ps, edge_ps
end
-# Splits parameters into those for the vertexes and those for the edges.
-
-# If they are already split, return that.
-split_parameters(ps::Tuple{<:Any, <:Any}, args...) = ps
-# Providing parameters to a spatial reaction system as a single vector of values (e.g. [1.0, 4.0, 0.1]) is not allowed.
-# Either use tuple (e.g. ([1.0, 4.0], [0.1])) or map format (e.g. [A => 1.0, B => 4.0, D => 0.1]).
-function split_parameters(ps::Vector{<:Number}, args...)
- error("When providing parameters for a spatial system as a single vector, the paired form (e.g :D =>1.0) must be used.")
+# Converts the values for the initial conditions/vertex parameters to the correct form:
+# A map vector from symbolics to vectors of either length 1 (for uniform values) or num_verts.
+function vertex_value_map(values, lrs::LatticeReactionSystem)
+ isempty(values) && (return Pair{BasicSymbolic{Real}, Vector{Float64}}[])
+ return [entry[1] => vertex_value_form(entry[2], lrs, entry[1]) for entry in values]
end
-# Splitting is only done for Vectors of Pairs (where the first value is a Symbols, and the second a value).
-function split_parameters(ps::Vector{<:Pair}, p_vertex_syms::Vector, p_edge_syms::Vector)
- vert_ps_in = [p for p in ps if any(isequal(p[1]), p_vertex_syms)]
- edge_ps_in = [p for p in ps if any(isequal(p[1]), p_edge_syms)]
-
- # Error check, in case some input parameters where neither recognised as vertex or edge parameters.
- if (sum(length.([vert_ps_in, edge_ps_in])) != length(ps))
- error("These input parameters are not recognised: $(setdiff(first.(ps), vcat(first.([vert_ps_in, edge_ps_in]))))")
+
+# Converts the values for an individual species/vertex parameter to its correct vector form.
+function vertex_value_form(values, lrs::LatticeReactionSystem, sym::BasicSymbolic)
+ # If the value is a scalar (i.e. uniform across the lattice), return it in vector form.
+ (values isa AbstractArray) || (return [values])
+
+ # If the value is a vector (something all three lattice types accept).
+ if values isa Vector
+ # For the case where we have a 1d (Cartesian or masked) grid, and the vector's values
+ # correspond to individual grid points.
+ if has_grid_lattice(lrs) && (size(values) == grid_size(lrs))
+ return vertex_value_form(values, num_verts(lrs), lattice(lrs), sym)
+ end
+
+ # For the case where the i'th value of the vector corresponds to the value in the i'th vertex.
+ # This is the only (non-uniform) case possible for graph grids.
+ if (length(values) != num_verts(lrs))
+ throw(ArgumentError("You have provided ($(length(values))) values for $sym. This is not equal to the number of vertices ($(num_verts(lrs)))."))
+ end
+ return values
end
- return vert_ps_in, edge_ps_in
+ # (2d and 3d) Cartesian and masked grids can take non-vector, non-scalar, values input.
+ return vertex_value_form(values, num_verts(lrs), lattice(lrs), sym)
end
-# Input may have the following forms (after potential Symbol maps to Symbolic maps conversions):
-# - A vector of values, where the i'th value corresponds to the value of the i'th
-# initial condition value (for u0_in), vertex parameter value (for vert_ps_in), or edge parameter value (for edge_ps_in).
-# - A vector of vectors of values. The same as previously,
-# but here the species/parameter can have different values across the spatial structure.
-# - A map of Symbols to values. These can either be a single value (if uniform across the spatial structure)
-# or a vector (with different values for each vertex/edge).
-# These can be mixed (e.g. [X => 1.0, Y => [1.0, 2.0, 3.0, 4.0]] is allowed).
-# - A matrix. E.g. for initial conditions you can have a num_species * num_vertex matrix,
-# indicating the value of each species at each vertex.
-
-# The lattice_process_input function takes input initial conditions/vertex parameters/edge parameters
-# of whichever form the user have used, and converts them to the Vector{Vector{}} form used internally.
-# E.g. for parameters the top-level vector contain one vector for each parameter (same order as in parameters(::ReactionSystem)).
-# If a parameter is uniformly-values across the spatial structure, its vector has a single value.
-# Else, it has a number of values corresponding to the number of vertexes/edges (for edge/vertex parameters).
-# Initial conditions works similarly.
-
-# If the input is given in a map form, the vector needs sorting and the first value removed.
-# The creates a Vector{Vector{Value}} or Vector{value} form, which is then again sent to lattice_process_input for reprocessing.
-function lattice_process_input(input::Vector{<:Pair}, syms::Vector{BasicSymbolic{Real}},
- args...)
- if !isempty(setdiff(first.(input), syms))
- error("Some input symbols are not recognised: $(setdiff(first.(input), syms)).")
+# Converts values to the correct vector form for a Cartesian grid lattice.
+function vertex_value_form(values::AbstractArray, num_verts::Int64,
+ lattice::CartesianGridRej{N, T}, sym::BasicSymbolic) where {N, T}
+ if size(values) != lattice.dims
+ throw(ArgumentError("The values for $sym did not have the same format as the lattice. Expected a $(lattice.dims) array, got one of size $(size(values))"))
end
- sorted_input = sort(input; by = p -> findfirst(isequal(p[1]), syms))
- return lattice_process_input(last.(sorted_input), syms, args...)
-end
-# If the input is a matrix: Processes the input and gives it in a form where it is a vector of vectors
-# (some of which may have a single value). Sends it back to lattice_process_input for reprocessing.
-function lattice_process_input(input::Matrix{<:Number}, args...)
- lattice_process_input([vec(input[i, :]) for i in 1:size(input, 1)], args...)
-end
-# Possibly we want to support this type of input at some point.
-function lattice_process_input(input::Array{<:Number, 3}, args...)
- error("3 dimensional array parameter input currently not supported.")
-end
-# If the input is a Vector containing both vectors and single values, converts it to the Vector{<:Vector} form.
-# Technically this last lattice_process_input is probably not needed.
-function lattice_process_input(input::Vector{<:Any}, args...)
- isempty(input) ? Vector{Vector{Float64}}() :
- lattice_process_input([(val isa Vector{<:Number}) ? val : [val] for val in input],
- args...)
-end
-# If the input is of the correct form already, return it.
-function lattice_process_input(input::Vector{<:Vector}, syms::Vector{BasicSymbolic{Real}},
- n::Int64)
- return input
+ if (length(values) != num_verts)
+ throw(ArgumentError("You have provided ($(length(values))) values for $sym. This is not equal to the number of vertices ($(num_verts))."))
+ end
+ return [values[flat_idx] for flat_idx in 1:num_verts]
end
-# Checks that a value vector have the right length, as well as that of all its sub vectors.
-# Error check if e.g. the user does not provide values for all species/parameters,
-# or for one: provides a vector of values, but that has the wrong length
-# (e.g providing 7 values for one species, but there are 8 vertexes).
-function check_vector_lengths(input::Vector{<:Vector}, n_syms, n_locations)
- if (length(input) != n_syms)
- error("Missing values for some initial conditions/parameters. Expected $n_syms values, got $(length(input)).")
+# Converts values to the correct vector form for a masked grid lattice.
+function vertex_value_form(values::AbstractArray, num_verts::Int64,
+ lattice::Array{Bool, T}, sym::BasicSymbolic) where {T}
+ if size(values) != size(lattice)
+ throw(ArgumentError("The values for $sym did not have the same format as the lattice. Expected a $(size(lattice)) array, got one of size $(size(values))"))
end
- if !isempty(setdiff(unique(length.(input)), [1, n_locations]))
- error("Some inputs where given values of inappropriate length.")
+
+ # Pre-declares a vector with the values in each vertex (return_values).
+ # Loops through the lattice and the values, adding these to the return_values.
+ return_values = Vector{typeof(values[1])}(undef, num_verts)
+ cur_idx = 0
+ for (idx, val) in enumerate(values)
+ lattice[idx] || continue
+ return_values[cur_idx += 1] = val
end
-end
-# For transport parameters, if the lattice was given as an undirected graph of size n:
-# this is converted to a directed graph of size 2n.
-# If transport parameters are given with n values, we want to use the same value for both directions.
-# Since the order of edges in the new graph is non-trivial, this function
-# distributes the n input values to a 2n length vector, putting the correct value in each position.
-function duplicate_trans_params!(edge_ps::Vector{Vector{Float64}},
- lrs::LatticeReactionSystem)
- cum_adjacency_counts = [0; cumsum(length.(lrs.lattice.fadjlist[1:(end - 1)]))]
- for idx in 1:length(edge_ps)
- # If the edge parameter already values for each directed edge, we can continue.
- (2length(edge_ps[idx]) == lrs.num_edges) || continue #
-
- # This entire thing depends on the fact that, in the edges(lattice) iterator, the edges are sorted by:
- # (1) Their source node
- # (2) Their destination node.
-
- # A vector where we will put the edge parameters new values.
- # Has the correct length (the number of directed edges in the lattice).
- new_vals = Vector{Float64}(undef, lrs.num_edges)
- # As we loop through the edges of the di-graph, this keeps track of each edge's index in the original graph.
- original_edge_count = 0
- for edge in edges(lrs.lattice) # For each edge.
- # The digraph conversion only adds edges so that src > dst.
- (edge.src < edge.dst) ? (original_edge_count += 1) : continue
- # For original edge i -> j, finds the index of i -> j in DiGraph.
- idx_fwd = cum_adjacency_counts[edge.src] +
- findfirst(isequal(edge.dst), lrs.lattice.fadjlist[edge.src])
- # For original edge i -> j, finds the index of j -> i in DiGraph.
- idx_bwd = cum_adjacency_counts[edge.dst] +
- findfirst(isequal(edge.src), lrs.lattice.fadjlist[edge.dst])
- new_vals[idx_fwd] = edge_ps[idx][original_edge_count]
- new_vals[idx_bwd] = edge_ps[idx][original_edge_count]
- end
- # Replaces the edge parameters values with the updated value vector.
- edge_ps[idx] = new_vals
+ # Checks that the correct number of values was provided, and returns the values.
+ if (length(return_values) != num_verts)
+ throw(ArgumentError("You have provided ($(length(return_values))) values for $sym. This is not equal to the number of vertices ($(num_verts))."))
end
+ return return_values
end
-# For a set of input values on the given forms, and their symbolics, convert into a dictionary.
-vals_to_dict(syms::Vector, vals::Vector{<:Vector}) = Dict(zip(syms, vals))
-# Produces a dictionary with all parameter values.
-function param_dict(vert_ps, edge_ps, lrs)
- merge(vals_to_dict(vertex_parameters(lrs), vert_ps),
- vals_to_dict(edge_parameters(lrs), edge_ps))
+# Converts the values for the edge parameters to the correct form:
+# A map vector from symbolics to sparse matrices of size either (1,1) or (num_verts,num_verts).
+function edge_value_map(values, lrs::LatticeReactionSystem)
+ isempty(values) && (return Pair{BasicSymbolic{Real}, SparseMatrixCSC{Float64, Int64}}[])
+ return [entry[1] => edge_value_form(entry[2], lrs, entry[1]) for entry in values]
+end
+
+# Converts the values for an individual edge parameter to its correct sparse matrix form.
+function edge_value_form(values, lrs::LatticeReactionSystem, sym)
+ # If the value is a scalar (i.e. uniform across the lattice), return it in sparse matrix form.
+ (values isa SparseMatrixCSC) || (return sparse([1], [1], [values]))
+
+ # Error checks.
+ if nnz(values) != num_edges(lrs)
+ throw(ArgumentError("You have provided ($(nnz(values))) values for $sym. This is not equal to the number of edges ($(num_edges(lrs)))."))
+ end
+ if !all(Base.isstored(values, e[1], e[2]) for e in edge_iterator(lrs))
+ throw(ArgumentError("Values was not provided for some edges for edge parameter $sym."))
+ end
+
+ # Unlike initial conditions/vertex parameters, (unless uniform) edge parameters' values are
+ # always provided in the same (sparse matrix) form.
+ return values
end
-# Computes the transport rates and stores them in a desired format
-# (a Dictionary from species index to rates across all edges).
-function compute_all_transport_rates(vert_ps::Vector{Vector{Float64}},
- edge_ps::Vector{Vector{Float64}}, lrs::LatticeReactionSystem)
- # Creates a dict, allowing us to access the values of wll parameters.
- p_val_dict = param_dict(vert_ps, edge_ps, lrs)
+# Creates a map, taking each species (with transportation) to its transportation rate.
+# The species is represented by its index (in species(lrs).
+# If the rate is uniform across all edges, the transportation rate will be a size (1,1) sparse matrix.
+# Else, the rate will be a size (num_verts,num_verts) sparse matrix.
+function make_sidxs_to_transrate_map(vert_ps::Vector{Pair{R, Vector{T}}},
+ edge_ps::Vector{Pair{S, SparseMatrixCSC{T, Int64}}},
+ lrs::LatticeReactionSystem) where {R, S, T}
+ # Creates a dictionary with each parameter's value(s).
+ p_val_dict = Dict(vcat(vert_ps, edge_ps))
+
+ # First, compute a map from species in their symbolics form to their values.
+ # Next, convert to map from species index to values.
+ transport_rates_speciesmap = compute_all_transport_rates(p_val_dict, lrs)
+ return Pair{Int64, SparseMatrixCSC{T, Int64}}[
+ speciesmap(reactionsystem(lrs))[spat_rates[1]] => spat_rates[2]
+ for spat_rates in transport_rates_speciesmap
+ ]
+end
+# Computes the transport rates for all species with transportation rates. Output is a map
+# taking each species' symbolics form to its transportation rates across all edges.
+function compute_all_transport_rates(p_val_dict, lrs::LatticeReactionSystem)
# For all species with transportation, compute their transportation rate (across all edges).
# This is a vector, pairing each species to these rates.
- unsorted_rates = [s => compute_transport_rates(
- get_transport_rate_law(s, lrs), p_val_dict, lrs.num_edges)
+ unsorted_rates = [s => compute_transport_rates(s, p_val_dict, lrs)
for s in spatial_species(lrs)]
- # Sorts all the species => rate pairs according to their species index in species(::ReactionSystem).
+ # Sorts all the species => rate pairs according to their species index in species(lrs).
return sort(unsorted_rates; by = rate -> findfirst(isequal(rate[1]), species(lrs)))
end
-# For a species, retrieves the symbolic expression for its transportation rate
-# (likely only a single parameter, such as `D`, but could be e.g. L*D, where L and D are parameters).
-# We could allows several transportation reactions for one species and simply sum them though, easy change.
-function get_transport_rate_law(s::BasicSymbolic{Real}, lrs::LatticeReactionSystem)
- rates = filter(sr -> isequal(s, sr.species), lrs.spatial_reactions)
- (length(rates) > 1) && error("Species $s have more than one diffusion reaction.")
- return rates[1].rate
-end
-# For the numeric expression describing the rate of transport (likely only a single parameter, e.g. `D`),
-# and the values of all our parameters, computes the transport rate(s).
-# If all parameters the rate depend on are uniform all edges, this becomes a length 1 vector.
-# Else a vector with each value corresponding to the rate at one specific edge.
-function compute_transport_rates(rate_law::Num,
- p_val_dict::Dict{BasicSymbolic{Real}, Vector{Float64}}, num_edges::Int64)
- # Finds parameters involved in rate and create a function evaluating the rate law.
+
+# For the expression describing the rate of transport (likely only a single parameter, e.g. `D`),
+# and the values of all our parameters, compute the transport rate(s).
+# If all parameters that the rate depends on are uniform across all edges, this becomes a length-1 vector.
+# Else it becomes a vector where each value corresponds to the rate at one specific edge.
+function compute_transport_rates(s::BasicSymbolic, p_val_dict, lrs::LatticeReactionSystem)
+ # Find parameters involved in the rate and create a function evaluating the rate law.
+ rate_law = get_transport_rate_law(s, lrs)
relevant_ps = Symbolics.get_variables(rate_law)
rate_law_func = drop_expr(@RuntimeGeneratedFunction(build_function(
rate_law, relevant_ps...)))
- # If all these parameters are spatially uniform. `rates` becomes a vector with 1 value.
- if all(length(p_val_dict[P]) == 1 for P in relevant_ps)
- return [rate_law_func([p_val_dict[p][1] for p in relevant_ps]...)]
- # If at least on parameter the rate depends on have a value varying across all edges,
- # we have to compute one rate value for each edge.
+ # If all these parameters are spatially uniform, the rates become a size (1,1) sparse matrix.
+ # Else, the rates become a size (num_verts,num_verts) sparse matrix.
+ if all(size(p_val_dict[p]) == (1, 1) for p in relevant_ps)
+ relevant_p_vals = [get_edge_value(p_val_dict[p], 1 => 1) for p in relevant_ps]
+ return sparse([1], [1], rate_law_func(relevant_p_vals...))
else
- return [rate_law_func([get_component_value(p_val_dict[p], idxE)
- for p in relevant_ps]...)
- for idxE in 1:num_edges]
+ transport_rates = spzeros(num_verts(lrs), num_verts(lrs))
+ for e in edge_iterator(lrs)
+ relevant_p_vals = [get_edge_value(p_val_dict[p], e) for p in relevant_ps]
+ transport_rates[e...] = rate_law_func(relevant_p_vals...)[1]
+ end
+ return transport_rates
end
end
-# Creates a map, taking each species (with transportation) to its transportation rate.
-# The species is represented by its index (in species(lrs).
-# If the rate is uniform across all edges, the vector will be length 1 (with this value),
-# else there will be a separate value for each edge.
-# Pair{Int64, Vector{T}}[] is required in case vector is empty (otherwise it becomes Any[], causing type error later).
-function make_sidxs_to_transrate_map(vert_ps::Vector{Vector{Float64}},
- edge_ps::Vector{Vector{T}}, lrs::LatticeReactionSystem) where {T}
- transport_rates_speciesmap = compute_all_transport_rates(vert_ps, edge_ps, lrs)
- return Pair{Int64, Vector{T}}[speciesmap(lrs.rs)[spat_rates[1]] => spat_rates[2]
- for spat_rates in transport_rates_speciesmap]
+# For a species, retrieve the symbolic expression for its transportation rate
+# (likely only a single parameter, such as `D`, but could be e.g. L*D, where L and D are parameters).
+# If there are several transportation reactions for the species, their sum is used.
+function get_transport_rate_law(s::BasicSymbolic, lrs::LatticeReactionSystem)
+ rates = filter(sr -> isequal(s, sr.species), spatial_reactions(lrs))
+ return sum(getfield.(rates, :rate))
end
### Accessing Unknown & Parameter Array Values ###
-# Gets the index in the u array of species s in vertex vert (when their are num_species species).
+# Converts a vector of vectors to a single, long, vector.
+# These are used when the initial condition is converted to a single vector (from vector of vector form).
+function expand_component_values(values::Vector{Vector{T}}, num_verts::Int64) where {T}
+ vcat([get_vertex_value.(values, vert) for vert in 1:num_verts]...)
+end
+
+# Gets the index in the u array of species s in vertex vert (when there are num_species species).
get_index(vert::Int64, s::Int64, num_species::Int64) = (vert - 1) * num_species + s
-# Gets the indexes in the u array of all species in vertex vert (when their are num_species species).
+# Gets the indices in the u array of all species in vertex vert (when there are num_species species).
function get_indexes(vert::Int64, num_species::Int64)
return ((vert - 1) * num_species + 1):(vert * num_species)
end
-# For vectors of length 1 or n, we want to get value idx (or the one value, if length is 1).
-# This function gets that. Here:
-# - values is the vector with the values of the component across all locations
-# (where the internal vectors may or may not be of size 1).
-# - component_idx is the initial condition species/vertex parameter/edge parameters's index.
-# This is predominantly used for parameters, for initial conditions,
-# it is only used once (at initialisation) to re-process the input vector.
-# - location_idx is the index of the vertex or edge for which we wish to access a initial condition or parameter values.
-# The first two function takes the full value vector, and call the function of at the components specific index.
-function get_component_value(values::Vector{<:Vector}, component_idx::Int64,
- location_idx::Int64)
- return get_component_value(values[component_idx], location_idx)
-end
-# Sometimes we have pre-computed, for each component, whether it's vector is length 1 or not.
-# This is stored in location_types.
-function get_component_value(values::Vector{<:Vector}, component_idx::Int64,
- location_idx::Int64, location_types::Vector{Bool})
- return get_component_value(values[component_idx], location_idx,
- location_types[component_idx])
-end
-# For a components value (which is a vector of either length 1 or some other length), retrieves its value.
-function get_component_value(values::Vector{<:Number}, location_idx::Int64)
- return get_component_value(values, location_idx, length(values) == 1)
+# Returns the value of a parameter in an edge. For vertex parameters, use their values in the source.
+function get_edge_value(values::Vector{T}, edge::Pair{Int64, Int64}) where {T}
+ return (length(values) == 1) ? values[1] : values[edge[1]]
end
-# Again, the location type (length of the value vector) may be pre-computed.
-function get_component_value(values::Vector{<:Number}, location_idx::Int64,
- location_type::Bool)
- return location_type ? values[1] : values[location_idx]
+function get_edge_value(values::SparseMatrixCSC{T, Int64},
+ edge::Pair{Int64, Int64}) where {T}
+ return (size(values) == (1, 1)) ? values[1, 1] : values[edge[1], edge[2]]
end
-# Converts a vector of vectors to a long vector.
-# These are used when the initial condition is converted to a single vector (from vector of vector form).
-function expand_component_values(values::Vector{<:Vector}, n)
- return vcat([get_component_value.(values, comp) for comp in 1:n]...)
-end
-function expand_component_values(values::Vector{<:Vector}, n, location_types::Vector{Bool})
- return vcat([get_component_value.(values, comp, location_types) for comp in 1:n]...)
-end
-
-# Creates a view of the vert_ps vector at a given location.
-# Provides a work vector to which the converted vector is written.
-function view_vert_ps_vector!(work_vert_ps, vert_ps, comp, enumerated_vert_ps_idx_types)
- # Loops through all parameters.
- for (idx, loc_type) in enumerated_vert_ps_idx_types
- # If the parameter is uniform across the spatial structure, it will have a length-1 value vector
- # (which value we write to the work vector).
- # Else, we extract it value at the specific location.
- work_vert_ps[idx] = (loc_type ? vert_ps[idx][1] : vert_ps[idx][comp])
- end
- return work_vert_ps
+# Returns the value of an initial condition of vertex parameter in a vertex.
+function get_vertex_value(values::Vector{T}, vert_idx::Int64) where {T}
+ return (length(values) == 1) ? values[1] : values[vert_idx]
end
-# Expands a u0/p information stored in Vector{Vector{}} for to Matrix form
-# (currently only used in Spatial Jump systems).
-function matrix_expand_component_values(values::Vector{<:Vector}, n)
- return reshape(expand_component_values(values, n), length(values), n)
+# Finds the transport rate of a parameter along a specific edge.
+function get_transport_rate(transport_rate::SparseMatrixCSC{T, Int64},
+ edge::Pair{Int64, Int64}, t_rate_idx_types::Bool) where {T}
+ return t_rate_idx_types ? transport_rate[1, 1] : transport_rate[edge[1], edge[2]]
end
-# For an expression, computes its values using the provided state and parameter vectors.
-# The expression is assumed to be valid in edges (and can have edges parameter components).
-# If some component is non-uniform, output is a vector of length equal to the number of vertexes.
-# If all components are uniform, the output is a length one vector.
-function compute_edge_value(exp, lrs::LatticeReactionSystem, edge_ps)
- # Finds the symbols in the expression. Checks that all correspond to edge parameters.
- relevant_syms = Symbolics.get_variables(exp)
- if !all(any(isequal(sym, p) for p in edge_parameters(lrs)) for sym in relevant_syms)
- error("An non-edge parameter was encountered in expressions: $exp. Here, only edge parameters are expected.")
+# For a `LatticeTransportODEFunction`, update its stored parameters (in `mtk_ps`) so that they
+# the heterogeneous parameters' values correspond to the values in the specified vertex.
+function update_mtk_ps!(lt_ofun::LatticeTransportODEFunction, all_ps::Vector{T},
+ vert::Int64) where {T}
+ for (setp, idx) in zip(lt_ofun.p_setters, lt_ofun.heterogeneous_vert_p_idxs)
+ setp(lt_ofun.mtk_ps, all_ps[idx][vert])
end
-
- # Creates a Function tha computes the expressions value for a parameter set.
- exp_func = drop_expr(@RuntimeGeneratedFunction(build_function(exp, relevant_syms...)))
- # Creates a dictionary with the value(s) for all edge parameters.
- sym_val_dict = vals_to_dict(edge_parameters(lrs), edge_ps)
-
- # If all values are uniform, compute value once. Else, do it at all edges.
- if !has_spatial_edge_component(exp, lrs, edge_ps)
- return [exp_func([sym_val_dict[sym][1] for sym in relevant_syms]...)]
- end
- return [exp_func([get_component_value(sym_val_dict[sym], idxE) for sym in relevant_syms]...)
- for idxE in 1:(lrs.num_edges)]
end
-# For an expression, computes its values using the provided state and parameter vectors.
+# For an expression, compute its values using the provided state and parameter vectors.
# The expression is assumed to be valid in vertexes (and can have vertex parameter and state components).
# If at least one component is non-uniform, output is a vector of length equal to the number of vertexes.
# If all components are uniform, the output is a length one vector.
-function compute_vertex_value(
- exp, lrs::LatticeReactionSystem; u = nothing, vert_ps = nothing)
- # Finds the symbols in the expression. Checks that all correspond to states or vertex parameters.
+function compute_vertex_value(exp, lrs::LatticeReactionSystem; u = [], ps = [])
+ # Finds the symbols in the expression. Checks that all correspond to unknowns or vertex parameters.
relevant_syms = Symbolics.get_variables(exp)
if any(any(isequal(sym) in edge_parameters(lrs)) for sym in relevant_syms)
- error("An edge parameter was encountered in expressions: $exp. Here, on vertex-based components are expected.")
+ throw(ArgumentError("An edge parameter was encountered in expressions: $exp. Here, only vertex-based components are expected."))
end
- # Creates a Function tha computes the expressions value for a parameter set.
+
+ # Creates a Function that computes the expression value for a parameter set.
exp_func = drop_expr(@RuntimeGeneratedFunction(build_function(exp, relevant_syms...)))
+
# Creates a dictionary with the value(s) for all edge parameters.
- if !isnothing(u) && !isnothing(vert_ps)
- all_syms = [species(lrs); vertex_parameters(lrs)]
- all_vals = [u; vert_ps]
- elseif !isnothing(u) && isnothing(vert_ps)
- all_syms = species(lrs)
- all_vals = u
-
- elseif isnothing(u) && !isnothing(vert_ps)
- all_syms = vertex_parameters(lrs)
- all_vals = vert_ps
- else
- error("Either u or vertex_ps have to be provided to has_spatial_vertex_component.")
- end
- sym_val_dict = vals_to_dict(all_syms, all_vals)
+ value_dict = Dict(vcat(u, ps))
# If all values are uniform, compute value once. Else, do it at all edges.
- if !has_spatial_vertex_component(exp, lrs; u, vert_ps)
- return [exp_func([sym_val_dict[sym][1] for sym in relevant_syms]...)]
+ if all(length(value_dict[sym]) == 1 for sym in relevant_syms)
+ return [exp_func([value_dict[sym][1] for sym in relevant_syms]...)]
end
- return [exp_func([get_component_value(sym_val_dict[sym], idxV) for sym in relevant_syms]...)
- for idxV in 1:(lrs.num_verts)]
+ return [exp_func([get_vertex_value(value_dict[sym], vert_idx) for sym in relevant_syms]...)
+ for vert_idx in 1:num_verts(lrs)]
end
### System Property Checks ###
-# For a Symbolic expression, a LatticeReactionSystem, and a parameter list of the internal format:
-# Checks if any edge parameter in the expression have a spatial component (that is, is not uniform).
-function has_spatial_edge_component(exp, lrs::LatticeReactionSystem, edge_ps)
- # Finds the edge parameters in the expression. Computes their indexes.
- exp_syms = Symbolics.get_variables(exp)
- exp_edge_ps = filter(sym -> any(isequal(sym), edge_parameters(lrs)), exp_syms)
- p_idxs = [findfirst(isequal(sym, edge_p) for edge_p in edge_parameters(lrs))
- for sym in exp_syms]
- # Checks if any of the corresponding value vectors have length != 1 (that is, is not uniform).
- return any(length(edge_ps[p_idx]) != 1 for p_idx in p_idxs)
-end
-
-# For a Symbolic expression, a LatticeReactionSystem, and a parameter list of the internal format (vector of vectors):
-# Checks if any vertex parameter in the expression have a spatial component (that is, is not uniform).
-function has_spatial_vertex_component(exp, lrs::LatticeReactionSystem;
- u = nothing, vert_ps = nothing)
- # Finds all the symbols in the expression.
- exp_syms = Symbolics.get_variables(exp)
-
- # If vertex parameter values where given, checks if any of these have non-uniform values.
- if !isnothing(vert_ps)
- exp_vert_ps = filter(sym -> any(isequal(sym), vertex_parameters(lrs)), exp_syms)
- p_idxs = [ModelingToolkit.parameter_index(lrs.rs, sym) for sym in exp_vert_ps]
- any(length(vert_ps[p_idx]) != 1 for p_idx in p_idxs) && return true
- end
-
- # If states values where given, checks if any of these have non-uniform values.
- if !isnothing(u)
- exp_u = filter(sym -> any(isequal(sym), species(lrs)), exp_syms)
- u_idxs = [ModelingToolkit.variable_index(lrs.rs, sym) for sym in exp_u]
- any(length(u[u_idx]) != 1 for u_idx in u_idxs) && return true
- end
-
- return false
+# For a Symbolic expression, and a parameter set, check if any relevant parameters have a
+# spatial component. Filters out any parameters that are edge parameters.
+function has_spatial_vertex_component(exp, ps)
+ relevant_syms = Symbolics.get_variables(exp)
+ value_dict = Dict(filter(p -> p[2] isa Vector, ps))
+ return any(length(value_dict[sym]) > 1 for sym in relevant_syms)
end
diff --git a/src/steady_state_stability.jl b/src/steady_state_stability.jl
index 42ffec2d0e..de9a229ef8 100644
--- a/src/steady_state_stability.jl
+++ b/src/steady_state_stability.jl
@@ -117,8 +117,8 @@ function steady_state_jac(rs::ReactionSystem; u0 = [sp => 0.0 for sp in unknowns
# Creates an `ODEProblem` with a Jacobian. Dummy values for `u0` and `ps` must be provided.
ps = [p => 0.0 for p in parameters(rs)]
- return ODEProblem(rs, u0, 0, ps; jac = true, remove_conserved = true,
- combinatoric_ratelaws = combinatoric_ratelaws)
+ return ODEProblem(rs, u0, 0, ps; jac = true, combinatoric_ratelaws,
+ remove_conserved = true, remove_conserved_warn = false)
end
# Converts a `u` vector from a vector of values to a map.
diff --git a/test/dsl/dsl_options.jl b/test/dsl/dsl_options.jl
index 9aff7afe52..819a887427 100644
--- a/test/dsl/dsl_options.jl
+++ b/test/dsl/dsl_options.jl
@@ -493,7 +493,7 @@ let
@test plot(sol; idxs=:X).series_list[1].plotattributes[:y][end] ≈ 10.0
@test plot(sol; idxs=[X, Y]).series_list[2].plotattributes[:y][end] ≈ 3.0
@test plot(sol; idxs=[rn.X, rn.Y]).series_list[2].plotattributes[:y][end] ≈ 3.0
- @test_broken plot(sol; idxs=[:X, :Y]).series_list[2].plotattributes[:y][end] ≈ 3.0 # (https://github.com/SciML/ModelingToolkit.jl/issues/2778)
+ @test plot(sol; idxs=[:X, :Y]).series_list[2].plotattributes[:y][end] ≈ 3.0 # (https://github.com/SciML/ModelingToolkit.jl/issues/2778)
end
# Compares programmatic and DSL system with observables.
@@ -950,4 +950,4 @@ let
rl = oderatelaw(reactions(rn3)[1]; combinatoric_ratelaw)
@unpack k1, A = rn3
@test isequal(rl, k1*A^2)
-end
\ No newline at end of file
+end
diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl
index 7a260e3b59..3a59f0f3a8 100644
--- a/test/extensions/homotopy_continuation.jl
+++ b/test/extensions/homotopy_continuation.jl
@@ -25,17 +25,18 @@ let
u0 = [:X1 => 2.0, :X2 => 2.0, :X3 => 2.0, :X2_2X3 => 2.0]
# Computes the single steady state, checks that when given to the ODE rhs, all are evaluated to 0.
- hc_ss = hc_steady_states(rs, ps; u0=u0, show_progress=false)
+ hc_ss = hc_steady_states(rs, ps; u0 = u0, show_progress = false, seed = 0x000004d1)
hc_ss = Pair.(unknowns(rs), hc_ss[1])
- @test maximum(abs.(f_eval(rs, hc_ss, ps, 0.0))) ≈ 0.0 atol=1e-12
+ @test maximum(abs.(f_eval(rs, hc_ss, ps, 0.0))) ≈ 0.0 atol = 1e-12
# Checks that not giving a `u0` argument yields an error for systems with conservation laws.
- @test_throws Exception hc_steady_states(rs, ps; show_progress=false)
+ @test_throws Exception hc_steady_states(rs, ps; show_progress = false)
end
# Tests for network with multiple steady state.
# Tests for Symbol parameter input.
-# Tests that passing kwargs to HC.solve does not error.
+# Tests that passing kwargs to HC.solve does not error and have an effect (i.e. modifying the seed
+# slightly modified the output in some way).
let
wilhelm_2009_model = @reaction_network begin
k1, Y --> 2X
@@ -43,13 +44,13 @@ let
k3, X + Y --> Y
k4, X --> 0
end
- ps = [:k3 => 1.0, :k2 => 2.0, :k4 => 1.5, :k1=>8.0]
+ ps = [:k3 => 1.0, :k2 => 2.0, :k4 => 1.5, :k1 => 8.0]
- hc_ss_1 = hc_steady_states(wilhelm_2009_model, ps; seed=0x000004d1, show_progress=false)
- @test sort(hc_ss_1, by=sol->sol[1]) ≈ [[0.0, 0.0], [0.5, 2.0], [4.5, 6.0]]
+ hc_ss_1 = hc_steady_states(wilhelm_2009_model, ps; seed = 0x000004d1, show_progress = false)
+ @test sort(hc_ss_1, by = sol->sol[1]) ≈ [[0.0, 0.0], [0.5, 2.0], [4.5, 6.0]]
- hc_ss_2 = hc_steady_states(wilhelm_2009_model, ps; seed=0x000004d2, show_progress=false)
- hc_ss_3 = hc_steady_states(wilhelm_2009_model, ps; seed=0x000004d2, show_progress=false)
+ hc_ss_2 = hc_steady_states(wilhelm_2009_model, ps; seed = 0x000004d2, show_progress = false)
+ hc_ss_3 = hc_steady_states(wilhelm_2009_model, ps; seed = 0x000004d2, show_progress = false)
@test hc_ss_1 != hc_ss_2
@test hc_ss_2 == hc_ss_3
end
@@ -69,7 +70,7 @@ let
ps = (:kY1 => 1.0, :kY2 => 3, :kZ1 => 1.0, :kZ2 => 4.0)
u0_1 = (:Y1 => 1.0, :Y2 => 3, :Z1 => 10, :Z2 =>40.0)
- ss_1 = sort(hc_steady_states(rs_1, ps; u0=u0_1, show_progress=false), by=sol->sol[1])
+ ss_1 = sort(hc_steady_states(rs_1, ps; u0 = u0_1, show_progress = false, seed = 0x000004d1), by = sol->sol[1])
@test ss_1 ≈ [[0.2, 0.1, 3.0, 1.0, 40.0, 10.0]]
rs_2 = @reaction_network begin
@@ -81,7 +82,7 @@ let
end
u0_2 = [:B2 => 1.0, :B1 => 3.0, :A2 => 10.0, :A1 =>40.0]
- ss_2 = sort(hc_steady_states(rs_2, ps; u0=u0_2, show_progress=false), by=sol->sol[1])
+ ss_2 = sort(hc_steady_states(rs_2, ps; u0 = u0_2, show_progress = false, seed = 0x000004d1), by = sol->sol[1])
@test ss_1 ≈ ss_2
end
@@ -96,14 +97,15 @@ let
d, X --> 0
end
ps = [:v => 5.0, :K => 2.5, :n => 3, :d => 1.0]
- sss = hc_steady_states(rs, ps; filter_negative=false, show_progress=false)
+ sss = hc_steady_states(rs, ps; filter_negative = false, show_progress = false, seed = 0x000004d1)
@test length(sss) == 4
for ss in sss
- @test f_eval(rs,sss[1], last.(ps), 0.0)[1] ≈ 0.0 atol=1e-12
+ @test f_eval(rs,sss[1], last.(ps), 0.0)[1] ≈ 0.0 atol = 1e-12
end
- @test_throws Exception hc_steady_states(rs, [:v => 5.0, :K => 2.5, :n => 2.7, :d => 1.0]; show_progress=false)
+ ps = [:v => 5.0, :K => 2.5, :n => 2.7, :d => 1.0]
+ @test_throws Exception hc_steady_states(rs, ps; show_progress = false, seed = 0x000004d1)
end
@@ -124,7 +126,7 @@ let
# Checks that homotopy continuation correctly find the system's single steady state.
ps = [:p => 2.0, :d => 1.0, :k => 5.0]
- hc_ss = hc_steady_states(rs, ps)
+ hc_ss = hc_steady_states(rs, ps; show_progress = false, seed = 0x000004d1)
@test hc_ss ≈ [[2.0, 0.2, 10.0]]
end
@@ -137,7 +139,7 @@ let
p_start = [:p => 1.0, :d => 0.2]
# Computes bifurcation diagram.
- @test_throws Exception hc_steady_states(incomplete_network, p_start)
+ @test_throws Exception hc_steady_states(incomplete_network, p_start; show_progress = false, seed = 0x000004d1)
end
# Tests that non-autonomous system throws an error
@@ -146,5 +148,5 @@ let
(k,t), 0 <--> X
end
ps = [:k => 1.0]
- @test_throws Exception hc_steady_states(rs, ps)
+ @test_throws Exception hc_steady_states(rs, ps; show_progress = false, seed = 0x000004d1)
end
\ No newline at end of file
diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl
index 105d990d48..c4becbc5e4 100644
--- a/test/extensions/structural_identifiability.jl
+++ b/test/extensions/structural_identifiability.jl
@@ -316,6 +316,7 @@ end
### Other Tests ###
# Checks that identifiability can be assessed for coupled CRN/DAE systems.
+# `remove_conserved = false` is used to remove info print statement from log.
let
rs = @reaction_network begin
@parameters k c1 c2
@@ -329,9 +330,10 @@ let
@unpack p, d, k, c1, c2 = rs
# Tests identifiability assessment when all unknowns are measured.
- gi_1 = assess_identifiability(rs; measured_quantities = [:X, :V, :C], loglevel)
- li_1 = assess_local_identifiability(rs; measured_quantities = [:X, :V, :C], loglevel)
- ifs_1 = find_identifiable_functions(rs; measured_quantities = [:X, :V, :C], loglevel)
+ remove_conserved = false
+ gi_1 = assess_identifiability(rs; measured_quantities = [:X, :V, :C], loglevel, remove_conserved)
+ li_1 = assess_local_identifiability(rs; measured_quantities = [:X, :V, :C], loglevel, remove_conserved)
+ ifs_1 = find_identifiable_functions(rs; measured_quantities = [:X, :V, :C], loglevel, remove_conserved)
@test sym_dict(gi_1) == Dict([:X => :globally, :C => :globally, :V => :globally, :k => :globally,
:c1 => :nonidentifiable, :c2 => :nonidentifiable, :p => :globally, :d => :globally])
@test sym_dict(li_1) == Dict([:X => 1, :C => 1, :V => 1, :k => 1, :c1 => 0, :c2 => 0, :p => 1, :d => 1])
@@ -339,9 +341,9 @@ let
# Tests identifiability assessment when only variables are measured.
# Checks that a parameter in an equation can be set as known.
- gi_2 = assess_identifiability(rs; measured_quantities = [:V, :C], known_p = [:c1], loglevel)
- li_2 = assess_local_identifiability(rs; measured_quantities = [:V, :C], known_p = [:c1], loglevel)
- ifs_2 = find_identifiable_functions(rs; measured_quantities = [:V, :C], known_p = [:c1], loglevel)
+ gi_2 = assess_identifiability(rs; measured_quantities = [:V, :C], known_p = [:c1], loglevel, remove_conserved)
+ li_2 = assess_local_identifiability(rs; measured_quantities = [:V, :C], known_p = [:c1], loglevel, remove_conserved)
+ ifs_2 = find_identifiable_functions(rs; measured_quantities = [:V, :C], known_p = [:c1], loglevel, remove_conserved)
@test sym_dict(gi_2) == Dict([:X => :nonidentifiable, :C => :globally, :V => :globally, :k => :nonidentifiable,
:c1 => :globally, :c2 => :nonidentifiable, :p => :nonidentifiable, :d => :globally])
@test sym_dict(li_2) == Dict([:X => 0, :C => 1, :V => 1, :k => 0, :c1 => 1, :c2 => 0, :p => 0, :d => 1])
diff --git a/test/miscellaneous_tests/api.jl b/test/miscellaneous_tests/api.jl
index 2a54807930..9f2cddbb20 100644
--- a/test/miscellaneous_tests/api.jl
+++ b/test/miscellaneous_tests/api.jl
@@ -314,7 +314,7 @@ end
# Test defaults.
# Uses mutating stuff (`setdefaults!`) and order dependent input (`species(rn) .=> u0`).
-# If you want to test this here @Sam I can write a new one that simualtes using defaults.
+# If you want to test this here @Sam I can write a new one that simulates using defaults.
# If so, tell me if you have anything specific you want to check though, or I will just implement
# it as I would.
let
diff --git a/test/miscellaneous_tests/reactionsystem_serialisation.jl b/test/miscellaneous_tests/reactionsystem_serialisation.jl
index 06684fdcf2..79e06c4ab1 100644
--- a/test/miscellaneous_tests/reactionsystem_serialisation.jl
+++ b/test/miscellaneous_tests/reactionsystem_serialisation.jl
@@ -462,4 +462,67 @@ let
rs_incomplete_loaded = include("../serialised_rs_incomplete.jl")
@test !ModelingToolkit.iscomplete(rs_incomplete_loaded)
rm("serialised_rs_incomplete.jl")
-end
\ No newline at end of file
+end
+
+# Tests network without species, reactions, and parameters.
+let
+ # Creates model.
+ rs = @reaction_network begin
+ @equations D(V) ~ -V
+ end
+
+ # Checks its serialisation.
+ save_reactionsystem("test_serialisation.jl", rs; safety_check = false)
+ isequal(rs, include("../test_serialisation.jl"))
+ rm("test_serialisation.jl")
+end
+
+# Tests various corner cases (multiple observables, species observables, non-default combinatoric
+# rate law, and rate law disabling)
+let
+ # Creates model.
+ rs = @reaction_network begin
+ @combinatoric_ratelaws false
+ @species Xcount(t)
+ @observables begin
+ Xtot ~ X + 2X2
+ Xcount ~ X + X2
+ end
+ p, 0 --> X
+ d*X2, X => 0
+ (k1,k2), 2X <--> X2
+ end
+
+ # Checks its serialisation.
+ save_reactionsystem("test_serialisation.jl", rs; safety_check = false)
+ isequal(rs, include("../test_serialisation.jl"))
+ rm("test_serialisation.jl")
+end
+
+# Tests saving of empty network.
+let
+ rs = @reaction_network
+ save_reactionsystem("test_serialisation.jl", rs; safety_check = false)
+ isequal(rs, include("../test_serialisation.jl"))
+ rm("test_serialisation.jl")
+end
+
+# Test that serialisation of unknown type (here a function) yields an error.
+let
+ rs = @reaction_network begin
+ d, X --> 0, [misc = x -> 2x]
+ end
+ @test_throws Exception save_reactionsystem("test_serialisation.jl", rs)
+end
+
+# Test connection field.
+# Not really used for `ReactionSystem`s right now, so tests the direct function and its warning.
+let
+ rs = @reaction_network begin
+ d, X --> 0
+ end
+ @test (@test_logs (:warn, ) match_mode=:any Catalyst.get_connection_type_string(rs)) == ""
+ @test Catalyst.get_connection_type_annotation(rs) == "Connection types:: (OBS: Currently not supported, and hence empty)"
+end
+
+
diff --git a/test/network_analysis/conservation_laws.jl b/test/network_analysis/conservation_laws.jl
index 0bc563642c..e962252ae0 100644
--- a/test/network_analysis/conservation_laws.jl
+++ b/test/network_analysis/conservation_laws.jl
@@ -11,6 +11,9 @@ seed = rand(rng, 1:100)
# Fetch test networks.
include("../test_networks.jl")
+# Except where we test the warnings, we do not want to print this warning.
+remove_conserved_warn = false
+
### Basic Tests ###
# Tests basic functionality on system with known conservation laws.
@@ -47,7 +50,7 @@ end
# Tests conservation law computation on large number of networks where we know which have conservation laws.
let
- # networks for whch we know there is no conservation laws.
+ # networks for which we know there is no conservation laws.
Cs_standard = map(conservationlaws, reaction_networks_standard)
Cs_hill = map(conservationlaws, reaction_networks_hill)
@test all(size(C, 1) == 0 for C in Cs_standard)
@@ -114,23 +117,23 @@ let
tspan = (0.0, 20.0)
# Simulates model using ODEs and checks that simulations are identical.
- osys = complete(convert(ODESystem, rn; remove_conserved = true))
+ osys = complete(convert(ODESystem, rn; remove_conserved = true, remove_conserved_warn))
oprob1 = ODEProblem(osys, u0, tspan, p)
oprob2 = ODEProblem(rn, u0, tspan, p)
- oprob3 = ODEProblem(rn, u0, tspan, p; remove_conserved = true)
+ oprob3 = ODEProblem(rn, u0, tspan, p; remove_conserved = true, remove_conserved_warn)
osol1 = solve(oprob1, Tsit5(); abstol = 1e-8, reltol = 1e-8, saveat= 0.2)
osol2 = solve(oprob2, Tsit5(); abstol = 1e-8, reltol = 1e-8, saveat= 0.2)
osol3 = solve(oprob3, Tsit5(); abstol = 1e-8, reltol = 1e-8, saveat= 0.2)
@test osol1[sps] ≈ osol2[sps] ≈ osol3[sps]
# Checks that steady states found using nonlinear solving and steady state simulations are identical.
- nsys = complete(convert(NonlinearSystem, rn; remove_conserved = true))
+ nsys = complete(convert(NonlinearSystem, rn; remove_conserved = true, remove_conserved_warn))
nprob1 = NonlinearProblem{true}(nsys, u0, p)
nprob2 = NonlinearProblem(rn, u0, p)
- nprob3 = NonlinearProblem(rn, u0, p; remove_conserved = true)
+ nprob3 = NonlinearProblem(rn, u0, p; remove_conserved = true, remove_conserved_warn)
ssprob1 = SteadyStateProblem{true}(osys, u0, p)
ssprob2 = SteadyStateProblem(rn, u0, p)
- ssprob3 = SteadyStateProblem(rn, u0, p; remove_conserved = true)
+ ssprob3 = SteadyStateProblem(rn, u0, p; remove_conserved = true, remove_conserved_warn)
nsol1 = solve(nprob1, NewtonRaphson(); abstol = 1e-8)
# Nonlinear problems cannot find steady states properly without removing conserved species.
nsol3 = solve(nprob3, NewtonRaphson(); abstol = 1e-8)
@@ -142,10 +145,10 @@ let
# Creates SDEProblems using various approaches.
u0_sde = [A => 100.0, B => 20.0, C => 5.0, D => 10.0, E => 3.0, F1 => 8.0, F2 => 2.0,
F3 => 20.0]
- ssys = complete(convert(SDESystem, rn; remove_conserved = true))
+ ssys = complete(convert(SDESystem, rn; remove_conserved = true, remove_conserved_warn))
sprob1 = SDEProblem(ssys, u0_sde, tspan, p)
sprob2 = SDEProblem(rn, u0_sde, tspan, p)
- sprob3 = SDEProblem(rn, u0_sde, tspan, p; remove_conserved = true)
+ sprob3 = SDEProblem(rn, u0_sde, tspan, p; remove_conserved = true, remove_conserved_warn)
# Checks that the SDEs f and g function evaluates to the same thing.
ind_us = ModelingToolkit.get_unknowns(ssys)
@@ -186,9 +189,9 @@ let
u0_2 = [rn.X => 2.0, rn.Y => 3.0, rn.XY => 4.0]
u0_3 = [:X => 2.0, :Y => 3.0, :XY => 4.0]
ps = (kB => 2, kD => 1.5)
- oprob1 = ODEProblem(rn, u0_1, 10.0, ps; remove_conserved = true)
- oprob2 = ODEProblem(rn, u0_2, 10.0, ps; remove_conserved = true)
- oprob3 = ODEProblem(rn, u0_3, 10.0, ps; remove_conserved = true)
+ oprob1 = ODEProblem(rn, u0_1, 10.0, ps; remove_conserved = true, remove_conserved_warn)
+ oprob2 = ODEProblem(rn, u0_2, 10.0, ps; remove_conserved = true, remove_conserved_warn)
+ oprob3 = ODEProblem(rn, u0_3, 10.0, ps; remove_conserved = true, remove_conserved_warn)
@test solve(oprob1)[sps] ≈ solve(oprob2)[sps] ≈ solve(oprob3)[sps]
end
@@ -200,7 +203,7 @@ let
end
u0 = Dict([:X1 => 100.0, :X2 => 120.0])
ps = [:k1 => 0.2, :k2 => 0.15]
- sprob = SDEProblem(rn, u0, 10.0, ps; remove_conserved = true)
+ sprob = SDEProblem(rn, u0, 10.0, ps; remove_conserved = true, remove_conserved_warn)
# Checks that conservation laws hold in all simulations.
sol = solve(sprob, ImplicitEM(); seed)
@@ -213,7 +216,7 @@ let
rn = @reaction_network begin
(k1,k2), X1 <--> X2
end
- osys = complete(convert(ODESystem, rn; remove_conserved = true))
+ osys = complete(convert(ODESystem, rn; remove_conserved = true, remove_conserved_warn))
u0 = [osys.X1 => 1.0, osys.X2 => 1.0]
ps_1 = [osys.k1 => 2.0, osys.k2 => 3.0]
ps_2 = [osys.k1 => 2.0, osys.k2 => 3.0, osys.Γ[1] => 4.0]
@@ -227,6 +230,55 @@ let
@test all(sol2[osys.X1 + osys.X2] .== 4.0)
end
+# Tests system problem updating when conservation laws are eliminated.
+# Checks that the correct values are used after the conservation law species are updated.
+# Here is an issue related to the broken tests: https://github.com/SciML/Catalyst.jl/issues/952
+let
+ # Create model and fetch the conservation parameter (Γ).
+ t = default_t()
+ @parameters k1 k2
+ @species X1(t) X2(t)
+ rxs = [
+ Reaction(k1, [X1], [X2]),
+ Reaction(k2, [X2], [X1])
+ ]
+ @named rs = ReactionSystem(rxs, t)
+ osys = convert(ODESystem, complete(rs); remove_conserved = true, remove_conserved_warn = false)
+ osys = complete(osys)
+ @unpack Γ = osys
+
+ # Creates an `ODEProblem`.
+ u0 = [X1 => 1.0, X2 => 2.0]
+ ps = [k1 => 0.1, k2 => 0.2]
+ oprob = ODEProblem(osys, u0, (0.0, 1.0), ps)
+
+ # Check `ODEProblem` content.
+ @test oprob[X1] == 1.0
+ @test oprob[X2] == 2.0
+ @test oprob.ps[k1] == 0.1
+ @test oprob.ps[k2] == 0.2
+ @test oprob.ps[Γ[1]] == 3.0
+
+ # Currently, any kind of updating of species or the conservation parameter(s) is not possible.
+
+ # Update problem parameters using `remake`.
+ oprob_new = remake(oprob; p = [k1 => 0.3, k2 => 0.4])
+ @test oprob_new.ps[k1] == 0.3
+ @test oprob_new.ps[k2] == 0.4
+ integrator = init(oprob_new, Tsit5())
+ @test integrator.ps[k1] == 0.3
+ @test integrator.ps[k2] == 0.4
+
+ # Update problem parameters using direct indexing.
+ oprob.ps[k1] = 0.5
+ oprob.ps[k2] = 0.6
+ @test oprob.ps[k1] == 0.5
+ @test oprob.ps[k2] == 0.6
+ integrator = init(oprob, Tsit5())
+ @test integrator.ps[k1] == 0.5
+ @test integrator.ps[k2] == 0.6
+end
+
### Other Tests ###
# Checks that `JumpSystem`s with conservation laws cannot be generated.
@@ -234,7 +286,7 @@ let
rn = @reaction_network begin
(k1,k2), X1 <--> X2
end
- @test_throws ArgumentError convert(JumpSystem, rn; remove_conserved = true)
+ @test_throws ArgumentError convert(JumpSystem, rn; remove_conserved = true, remove_conserved_warn)
end
# Checks that `conserved` metadata is added correctly to parameters.
@@ -245,7 +297,7 @@ let
(k1,k2), X1 <--> X2
(k1,k2), Y1 <--> Y2
end
- osys = convert(ODESystem, rs; remove_conserved = true)
+ osys = convert(ODESystem, rs; remove_conserved = true, remove_conserved_warn)
# Checks that the correct parameters have the `conserved` metadata.
@test Catalyst.isconserved(osys.Γ[1])
@@ -253,3 +305,59 @@ let
@test !Catalyst.isconserved(osys.k1)
@test !Catalyst.isconserved(osys.k2)
end
+
+# Checks that conservation law elimination warnings are generated in the correct cases.
+let
+ # Prepare model.
+ rn = @reaction_network begin
+ (k1,k2), X1 <--> X2
+ end
+ u0 = [:X1 => 1.0, :X2 => 2.0]
+ tspan = (0.0, 1.0)
+ ps = [:k1 => 3.0, :k2 => 4.0]
+
+ # Check warnings in system conversion.
+ for XSystem in [ODESystem, SDESystem, NonlinearSystem]
+ @test_nowarn convert(XSystem, rn)
+ @test_logs (:warn, r"You are creating a system or problem while eliminating conserved quantities. Please *") convert(XSystem, rn; remove_conserved = true)
+ @test_nowarn convert(XSystem, rn; remove_conserved_warn = false)
+ @test_nowarn convert(XSystem, rn; remove_conserved = true, remove_conserved_warn = false)
+ end
+
+ # Checks during problem creation (separate depending on whether they have a time span or not).
+ for XProblem in [ODEProblem, SDEProblem]
+ @test_nowarn XProblem(rn, u0, tspan, ps)
+ @test_logs (:warn, r"You are creating a system or problem while eliminating conserved quantities. Please *") XProblem(rn, u0, tspan, ps; remove_conserved = true)
+ @test_nowarn XProblem(rn, u0, tspan, ps; remove_conserved_warn = false)
+ @test_nowarn XProblem(rn, u0, tspan, ps; remove_conserved = true, remove_conserved_warn = false)
+ end
+ for XProblem in [NonlinearProblem, SteadyStateProblem]
+ @test_nowarn XProblem(rn, u0, ps)
+ @test_logs (:warn, r"You are creating a system or problem while eliminating conserved quantities. Please *") XProblem(rn, u0, ps; remove_conserved = true)
+ @test_nowarn XProblem(rn, u0, ps; remove_conserved_warn = false)
+ @test_nowarn XProblem(rn, u0, ps; remove_conserved = true, remove_conserved_warn = false)
+ end
+end
+
+# Conservation law simulations for vectorised species.
+let
+ # Prepares the model.
+ t = default_t()
+ @species X(t)[1:2]
+ @parameters k[1:2]
+ rxs = [
+ Reaction(k[1], [X[1]], [X[2]]),
+ Reaction(k[2], [X[2]], [X[1]])
+ ]
+ @named rs = ReactionSystem(rxs, t)
+ rs = complete(rs)
+
+ # Checks that simulation reaches known equilibrium.
+ @test_broken false # Currently broken on MTK .
+ # u0 = [:X => [3.0, 9.0]]
+ # ps = [:k => [1.0, 2.0]]
+ # oprob = ODEProblem(rs, u0, (0.0, 1000.0), ps; remove_conserved = true)
+ # sol = solve(oprob, Vern7())
+ # @test sol[X[1]][end] ≈ 8.0
+ # @test sol[X[2]][end] ≈ 4.0
+end
diff --git a/test/network_analysis/network_properties.jl b/test/network_analysis/network_properties.jl
index 5507a9f655..811d6418cc 100644
--- a/test/network_analysis/network_properties.jl
+++ b/test/network_analysis/network_properties.jl
@@ -326,3 +326,86 @@ let
@test Catalyst.iscomplexbalanced(rn, rates) == true
end
+### STRONG LINKAGE CLASS TESTS
+
+
+# a) Checks that strong/terminal linkage classes are correctly found. Should identify the (A, B+C) linkage class as non-terminal, since B + C produces D
+let
+ rn = @reaction_network begin
+ (k1, k2), A <--> B + C
+ k3, B + C --> D
+ k4, D --> E
+ (k5, k6), E <--> 2F
+ k7, 2F --> D
+ (k8, k9), D + E <--> G
+ end
+
+ rcs, D = reactioncomplexes(rn)
+ slcs = stronglinkageclasses(rn)
+ tslcs = terminallinkageclasses(rn)
+ @test length(slcs) == 3
+ @test length(tslcs) == 2
+ @test issubset([[1,2], [3,4,5], [6,7]], slcs)
+ @test issubset([[3,4,5], [6,7]], tslcs)
+end
+
+# b) Makes the D + E --> G reaction irreversible. Thus, (D+E) becomes a non-terminal linkage class. Checks whether correctly identifies both (A, B+C) and (D+E) as non-terminal
+let
+ rn = @reaction_network begin
+ (k1, k2), A <--> B + C
+ k3, B + C --> D
+ k4, D --> E
+ (k5, k6), E <--> 2F
+ k7, 2F --> D
+ (k8, k9), D + E --> G
+ end
+
+ rcs, D = reactioncomplexes(rn)
+ slcs = stronglinkageclasses(rn)
+ tslcs = terminallinkageclasses(rn)
+ @test length(slcs) == 4
+ @test length(tslcs) == 2
+ @test issubset([[1,2], [3,4,5], [6], [7]], slcs)
+ @test issubset([[3,4,5], [7]], tslcs)
+end
+
+# From a), makes the B + C <--> D reaction reversible. Thus, the non-terminal (A, B+C) linkage class gets absorbed into the terminal (A, B+C, D, E, 2F) linkage class, and the terminal linkage classes and strong linkage classes coincide.
+let
+ rn = @reaction_network begin
+ (k1, k2), A <--> B + C
+ (k3, k4), B + C <--> D
+ k5, D --> E
+ (k6, k7), E <--> 2F
+ k8, 2F --> D
+ (k9, k10), D + E <--> G
+ end
+
+ rcs, D = reactioncomplexes(rn)
+ slcs = stronglinkageclasses(rn)
+ tslcs = terminallinkageclasses(rn)
+ @test length(slcs) == 2
+ @test length(tslcs) == 2
+ @test issubset([[1,2,3,4,5], [6,7]], slcs)
+ @test issubset([[1,2,3,4,5], [6,7]], tslcs)
+end
+
+# Simple test for strong and terminal linkage classes
+let
+ rn = @reaction_network begin
+ (k1, k2), A <--> 2B
+ k3, A --> C + D
+ (k4, k5), C + D <--> E
+ k6, 2B --> F
+ (k7, k8), F <--> 2G
+ (k9, k10), 2G <--> H
+ k11, H --> F
+ end
+
+ rcs, D = reactioncomplexes(rn)
+ slcs = stronglinkageclasses(rn)
+ tslcs = terminallinkageclasses(rn)
+ @test length(slcs) == 3
+ @test length(tslcs) == 2
+ @test issubset([[1,2], [3,4], [5,6,7]], slcs)
+ @test issubset([[3,4], [5,6,7]], tslcs)
+end
diff --git a/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl b/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl
index c3c801c603..804dc4bc05 100644
--- a/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl
+++ b/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl
@@ -19,11 +19,11 @@ include("../spatial_test_networks.jl")
# Small grid, small, non-stiff, system.
let
- lrs = LatticeReactionSystem(SIR_system, SIR_srs_2, small_2d_grid)
- u0 = [:S => 990.0, :I => 20.0 * rand_v_vals(lrs.lattice), :R => 0.0]
+ lrs = LatticeReactionSystem(SIR_system, SIR_srs_2, small_2d_graph_grid)
+ u0 = [:S => 990.0, :I => 20.0 * rand_v_vals(lrs), :R => 0.0]
pV = SIR_p
pE = [:dS => 0.01, :dI => 0.01, :dR => 0.01]
- oprob = ODEProblem(lrs, u0, (0.0, 500.0), (pV, pE); jac = false)
+ oprob = ODEProblem(lrs, u0, (0.0, 500.0), [pV; pE]; jac = false)
@test SciMLBase.successful_retcode(solve(oprob, Tsit5()))
runtime_target = 0.00027
@@ -35,10 +35,10 @@ end
# Large grid, small, non-stiff, system.
let
lrs = LatticeReactionSystem(SIR_system, SIR_srs_2, large_2d_grid)
- u0 = [:S => 990.0, :I => 20.0 * rand_v_vals(lrs.lattice), :R => 0.0]
+ u0 = [:S => 990.0, :I => 20.0 * rand_v_vals(lrs), :R => 0.0]
pV = SIR_p
pE = [:dS => 0.01, :dI => 0.01, :dR => 0.01]
- oprob = ODEProblem(lrs, u0, (0.0, 500.0), (pV, pE); jac = false)
+ oprob = ODEProblem(lrs, u0, (0.0, 500.0), [pV; pE]; jac = false)
@test SciMLBase.successful_retcode(solve(oprob, Tsit5()))
runtime_target = 0.12
@@ -49,11 +49,11 @@ end
# Small grid, small, stiff, system.
let
- lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_2d_grid)
- u0 = [:X => rand_v_vals(lrs.lattice, 10), :Y => rand_v_vals(lrs.lattice, 10)]
+ lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_2d_graph_grid)
+ u0 = [:X => rand_v_vals(lrs, 10), :Y => rand_v_vals(lrs, 10)]
pV = brusselator_p
pE = [:dX => 0.2]
- oprob = ODEProblem(lrs, u0, (0.0, 100.0), (pV, pE))
+ oprob = ODEProblem(lrs, u0, (0.0, 100.0), [pV; pE])
@test SciMLBase.successful_retcode(solve(oprob, CVODE_BDF(linear_solver=:GMRES)))
runtime_target = 0.013
@@ -65,10 +65,10 @@ end
# Large grid, small, stiff, system.
let
lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, large_2d_grid)
- u0 = [:X => rand_v_vals(lrs.lattice, 10), :Y => rand_v_vals(lrs.lattice, 10)]
+ u0 = [:X => rand_v_vals(lrs, 10), :Y => rand_v_vals(lrs, 10)]
pV = brusselator_p
pE = [:dX => 0.2]
- oprob = ODEProblem(lrs, u0, (0.0, 100.0), (pV, pE))
+ oprob = ODEProblem(lrs, u0, (0.0, 100.0), [pV; pE])
@test SciMLBase.successful_retcode(solve(oprob, CVODE_BDF(linear_solver=:GMRES)))
runtime_target = 11.
@@ -80,12 +80,12 @@ end
# Small grid, mid-sized, non-stiff, system.
let
lrs = LatticeReactionSystem(CuH_Amination_system, CuH_Amination_srs_2,
- small_2d_grid)
+ small_2d_graph_grid)
u0 = [
- :CuoAc => 0.005 .+ rand_v_vals(lrs.lattice, 0.005),
- :Ligand => 0.005 .+ rand_v_vals(lrs.lattice, 0.005),
+ :CuoAc => 0.005 .+ rand_v_vals(lrs, 0.005),
+ :Ligand => 0.005 .+ rand_v_vals(lrs, 0.005),
:CuoAcLigand => 0.0,
- :Silane => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
+ :Silane => 0.5 .+ rand_v_vals(lrs, 0.5),
:CuHLigand => 0.0,
:SilaneOAc => 0.0,
:Styrene => 0.16,
@@ -99,7 +99,7 @@ let
]
pV = CuH_Amination_p
pE = [:D1 => 0.1, :D2 => 0.1, :D3 => 0.1, :D4 => 0.1, :D5 => 0.1]
- oprob = ODEProblem(lrs, u0, (0.0, 10.0), (pV, pE); jac = false)
+ oprob = ODEProblem(lrs, u0, (0.0, 10.0), [pV; pE]; jac = false)
@test SciMLBase.successful_retcode(solve(oprob, Tsit5()))
runtime_target = 0.0012
@@ -113,10 +113,10 @@ let
lrs = LatticeReactionSystem(CuH_Amination_system, CuH_Amination_srs_2,
large_2d_grid)
u0 = [
- :CuoAc => 0.005 .+ rand_v_vals(lrs.lattice, 0.005),
- :Ligand => 0.005 .+ rand_v_vals(lrs.lattice, 0.005),
+ :CuoAc => 0.005 .+ rand_v_vals(lrs, 0.005),
+ :Ligand => 0.005 .+ rand_v_vals(lrs, 0.005),
:CuoAcLigand => 0.0,
- :Silane => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
+ :Silane => 0.5 .+ rand_v_vals(lrs, 0.5),
:CuHLigand => 0.0,
:SilaneOAc => 0.0,
:Styrene => 0.16,
@@ -130,7 +130,7 @@ let
]
pV = CuH_Amination_p
pE = [:D1 => 0.1, :D2 => 0.1, :D3 => 0.1, :D4 => 0.1, :D5 => 0.1]
- oprob = ODEProblem(lrs, u0, (0.0, 10.0), (pV, pE); jac = false)
+ oprob = ODEProblem(lrs, u0, (0.0, 10.0), [pV; pE]; jac = false)
@test SciMLBase.successful_retcode(solve(oprob, Tsit5()))
runtime_target = 0.56
@@ -141,22 +141,22 @@ end
# Small grid, mid-sized, stiff, system.
let
- lrs = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_2d_grid)
+ lrs = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_2d_graph_grid)
u0 = [
- :w => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
- :w2 => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
- :w2v => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
- :v => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
- :w2v2 => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
- :vP => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
- :σB => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
- :w2σB => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
+ :w => 0.5 .+ rand_v_vals(lrs, 0.5),
+ :w2 => 0.5 .+ rand_v_vals(lrs, 0.5),
+ :w2v => 0.5 .+ rand_v_vals(lrs, 0.5),
+ :v => 0.5 .+ rand_v_vals(lrs, 0.5),
+ :w2v2 => 0.5 .+ rand_v_vals(lrs, 0.5),
+ :vP => 0.5 .+ rand_v_vals(lrs, 0.5),
+ :σB => 0.5 .+ rand_v_vals(lrs, 0.5),
+ :w2σB => 0.5 .+ rand_v_vals(lrs, 0.5),
:vPp => 0.0,
:phos => 0.4,
]
pV = sigmaB_p
pE = [:DσB => 0.1, :Dw => 0.1, :Dv => 0.1]
- oprob = ODEProblem(lrs, u0, (0.0, 50.0), (pV, pE))
+ oprob = ODEProblem(lrs, u0, (0.0, 50.0), [pV; pE])
@test SciMLBase.successful_retcode(solve(oprob, CVODE_BDF(linear_solver=:GMRES)))
runtime_target = 0.61
@@ -169,20 +169,20 @@ end
let
lrs = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, large_2d_grid)
u0 = [
- :w => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
- :w2 => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
- :w2v => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
- :v => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
- :w2v2 => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
- :vP => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
- :σB => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
- :w2σB => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
+ :w => 0.5 .+ rand_v_vals(lrs, 0.5),
+ :w2 => 0.5 .+ rand_v_vals(lrs, 0.5),
+ :w2v => 0.5 .+ rand_v_vals(lrs, 0.5),
+ :v => 0.5 .+ rand_v_vals(lrs, 0.5),
+ :w2v2 => 0.5 .+ rand_v_vals(lrs, 0.5),
+ :vP => 0.5 .+ rand_v_vals(lrs, 0.5),
+ :σB => 0.5 .+ rand_v_vals(lrs, 0.5),
+ :w2σB => 0.5 .+ rand_v_vals(lrs, 0.5),
:vPp => 0.0,
:phos => 0.4,
]
pV = sigmaB_p
pE = [:DσB => 0.1, :Dw => 0.1, :Dv => 0.1]
- oprob = ODEProblem(lrs, u0, (0.0, 10.0), (pV, pE)) # Time reduced from 50.0 (which casues Julai to crash).
+ oprob = ODEProblem(lrs, u0, (0.0, 10.0), [pV; pE]) # Time reduced from 50.0 (which casues Julai to crash).
@test SciMLBase.successful_retcode(solve(oprob, CVODE_BDF(linear_solver=:GMRES)))
runtime_target = 59.
diff --git a/test/reactionsystem_core/custom_crn_functions.jl b/test/reactionsystem_core/custom_crn_functions.jl
index 5d1070d65a..c4f115d7c3 100644
--- a/test/reactionsystem_core/custom_crn_functions.jl
+++ b/test/reactionsystem_core/custom_crn_functions.jl
@@ -2,6 +2,7 @@
# Fetch packages.
using Catalyst, Test
+using ModelingToolkit: get_continuous_events, get_discrete_events
using Symbolics: derivative
# Sets stable rng number.
@@ -154,4 +155,63 @@ let
@test isequal(Catalyst.expand_registered_functions(eq3), 0 ~ V * (X^N) / (X^N + K^N))
@test isequal(Catalyst.expand_registered_functions(eq4), 0 ~ V * (K^N) / (X^N + K^N))
@test isequal(Catalyst.expand_registered_functions(eq5), 0 ~ V * (X^N) / (X^N + Y^N + K^N))
-end
\ No newline at end of file
+end
+
+# Ensures that original system is not modified.
+let
+ # Create model with a registered function.
+ @species X(t)
+ @variables V(t)
+ @parameters v K
+ eqs = [
+ Reaction(mm(X,v,K), [], [X]),
+ mm(V,v,K) ~ V + 1
+ ]
+ @named rs = ReactionSystem(eqs, t)
+
+ # Check that `expand_registered_functions` does not mutate original model.
+ rs_expanded_funcs = Catalyst.expand_registered_functions(rs)
+ @test isequal(only(Catalyst.get_rxs(rs)).rate, Catalyst.mm(X,v,K))
+ @test isequal(only(Catalyst.get_rxs(rs_expanded_funcs)).rate, v*X/(X + K))
+ @test isequal(last(Catalyst.get_eqs(rs)).lhs, Catalyst.mm(V,v,K))
+ @test isequal(last(Catalyst.get_eqs(rs_expanded_funcs)).lhs, v*V/(V + K))
+end
+
+# Tests on model with events.
+let
+ # Creates a model, saves it, and creates an expanded version.
+ rs = @reaction_network begin
+ @continuous_events begin
+ [mm(X,v,K) ~ 1.0] => [X ~ X]
+ end
+ @discrete_events begin
+ [1.0] => [X ~ mmr(X,v,K) + Y*(v + K)]
+ 1.0 => [X ~ X]
+ (hill(X,v,K,n) > 1000.0) => [X ~ hillr(X,v,K,n) + 2]
+ end
+ v0 + hillar(X,Y,v,K,n), X --> Y
+ end
+ rs_saved = deepcopy(rs)
+ rs_expanded = Catalyst.expand_registered_functions(rs)
+
+ # Checks that the original model is unchanged (equality currently does not consider events).
+ @test rs == rs_saved
+ @test get_continuous_events(rs) == get_continuous_events(rs_saved)
+ @test get_discrete_events(rs) == get_discrete_events(rs_saved)
+
+ # Checks that the new system is expanded.
+ @unpack v0, X, Y, v, K, n = rs
+ continuous_events = [
+ [v*X/(X + K) ~ 1.0] => [X ~ X]
+ ]
+ discrete_events = [
+ [1.0] => [X ~ v*K/(X + K) + Y*(v + K)]
+ 1.0 => [X ~ X]
+ (v * (X^n) / (X^n + K^n) > 1000.0) => [X ~ v * (K^n) / (X^n + K^n) + 2]
+ ]
+ continuous_events = ModelingToolkit.SymbolicContinuousCallback.(continuous_events)
+ discrete_events = ModelingToolkit.SymbolicDiscreteCallback.(discrete_events)
+ @test isequal(only(Catalyst.get_rxs(rs_expanded)).rate, v0 + v * (X^n) / (X^n + Y^n + K^n))
+ @test isequal(get_continuous_events(rs_expanded), continuous_events)
+ @test isequal(get_discrete_events(rs_expanded), discrete_events)
+end
diff --git a/test/reactionsystem_core/reactionsystem.jl b/test/reactionsystem_core/reactionsystem.jl
index 234f995801..538ce0f174 100644
--- a/test/reactionsystem_core/reactionsystem.jl
+++ b/test/reactionsystem_core/reactionsystem.jl
@@ -262,6 +262,70 @@ let
end
end
+### Nich Model Declarations ###
+
+# Checks model with vector species and parameters.
+# Checks that it works for programmatic/dsl-based modelling.
+# Checks that all forms of model input (parameter/initial condition and vector/non-vector) are
+# handled properly.
+let
+ # Declares programmatic model.
+ @parameters p[1:2] k d1 d2
+ @species (X(t))[1:2] Y1(t) Y2(t)
+ rxs = [
+ Reaction(p[1], [], [X[1]]),
+ Reaction(p[2], [], [X[2]]),
+ Reaction(k, [X[1]], [Y1]),
+ Reaction(k, [X[2]], [Y2]),
+ Reaction(d1, [Y1], []),
+ Reaction(d2, [Y2], []),
+ ]
+ rs_prog = complete(ReactionSystem(rxs, t; name = :rs))
+
+ # Declares DSL-based model.
+ rs_dsl = @reaction_network rs begin
+ @parameters p[1:2] k d1 d2
+ @species (X(t))[1:2] Y1(t) Y2(t)
+ (p[1],p[2]), 0 --> (X[1],X[2])
+ k, (X[1],X[2]) --> (Y1,Y2)
+ (d1,d2), (Y1,Y2) --> 0
+ end
+
+ # Checks equivalence.
+ rs_dsl == rs_prog
+
+ # Creates all possible initial conditions and parameter values.
+ u0_alts = [
+ [X => [2.0, 5.0], Y1 => 0.2, Y2 => 0.5],
+ [X[1] => 2.0, X[2] => 5.0, Y1 => 0.2, Y2 => 0.5],
+ [rs_dsl.X => [2.0, 5.0], rs_dsl.Y1 => 0.2, rs_dsl.Y2 => 0.5],
+ [rs_dsl.X[1] => 2.0, X[2] => 5.0, rs_dsl.Y1 => 0.2, rs_dsl.Y2 => 0.5],
+ [:X => [2.0, 5.0], :Y1 => 0.2, :Y2 => 0.5]
+ ]
+ ps_alts = [
+ [p => [1.0, 10.0], d1 => 5.0, d2 => 4.0, k => 2.0],
+ [p[1] => 1.0, p[2] => 10.0, d1 => 5.0, d2 => 4.0, k => 2.0],
+ [rs_dsl.p => [1.0, 10.0], rs_dsl.d1 => 5.0, rs_dsl.d2 => 4.0, rs_dsl.k => 2.0],
+ [rs_dsl.p[1] => 1.0, p[2] => 10.0, rs_dsl.d1 => 5.0, rs_dsl.d2 => 4.0, rs_dsl.k => 2.0],
+ [:p => [1.0, 10.0], :d1 => 5.0, :d2 => 4.0, :k => 2.0]
+ ]
+
+ # Loops through all inputs and check that the correct steady state is reached
+ # Target steady state: (X1, X2, Y1, Y2) = (p1/k, p2/k, p1/d1, p2/d2).
+ # Technically only one model needs to be check. However, "equivalent" models in MTK can still
+ # have slight differences, so checking for both here to be certain.
+ for rs in [rs_prog, rs_dsl]
+ oprob = ODEProblem(rs, u0_alts[1], (0.0, 10000.), ps_alts[1])
+ @test_broken false # Cannot currently `remake` this problem/
+ # for rs in [rs_prog, rs_dsl], u0 in u0_alts, p in ps_alts
+ # oprob_remade = remake(oprob; u0, p)
+ # sol = solve(oprob_remade, Vern7(); abstol = 1e-8, reltol = 1e-8)
+ # @test sol[end] ≈ [0.5, 5.0, 0.2, 2.5]
+ # end
+ end
+end
+
+### Other Tests ###
### Test Show ###
@@ -746,9 +810,48 @@ let
end
# Checks that the `reactionsystem_uptodate` function work. If it does not, the ReactionSystem
-# strcuture's fields have been updated, without updating the `reactionsystem_fields` costant. If so,
+# structure's fields have been updated, without updating the `reactionsystem_fields` constant. If so,
# there are several places in the code where the `reactionsystem_uptodate` function is called, here
# the code might need adaptation to take the updated reaction system into account.
let
@test_nowarn Catalyst.reactionsystem_uptodate_check()
-end
\ No newline at end of file
+end
+
+# Test that functions using the incidence matrix properly cache it
+let
+ rn = @reaction_network begin
+ k1, A --> B
+ k2, B --> C
+ k3, C --> A
+ end
+
+ nps = Catalyst.get_networkproperties(rn)
+ @test isempty(nps.incidencemat) == true
+
+ img = incidencematgraph(rn)
+ @test size(nps.incidencemat) == (3,3)
+
+ Catalyst.reset!(nps)
+ lcs = linkageclasses(rn)
+ @test size(nps.incidencemat) == (3,3)
+
+ Catalyst.reset!(nps)
+ sns = subnetworks(rn)
+ @test size(nps.incidencemat) == (3,3)
+
+ Catalyst.reset!(nps)
+ δ = deficiency(rn)
+ @test size(nps.incidencemat) == (3,3)
+
+ Catalyst.reset!(nps)
+ δ_l = linkagedeficiencies(rn)
+ @test size(nps.incidencemat) == (3,3)
+
+ Catalyst.reset!(nps)
+ rev = isreversible(rn)
+ @test size(nps.incidencemat) == (3,3)
+
+ Catalyst.reset!(nps)
+ weakrev = isweaklyreversible(rn, sns)
+ @test size(nps.incidencemat) == (3,3)
+end
diff --git a/test/runtests.jl b/test/runtests.jl
index bf6be2bbfd..4197706e0d 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -50,12 +50,6 @@ using SafeTestsets, Test
@time @safetestset "MTK Structure Indexing" begin include("upstream/mtk_structure_indexing.jl") end
@time @safetestset "MTK Problem Inputs" begin include("upstream/mtk_problem_inputs.jl") end
- # Tests spatial modelling and simulations.
- @time @safetestset "PDE Systems Simulations" begin include("spatial_modelling/simulate_PDEs.jl") end
- @time @safetestset "Lattice Reaction Systems" begin include("spatial_modelling/lattice_reaction_systems.jl") end
- @time @safetestset "ODE Lattice Systems Simulations" begin include("spatial_modelling/lattice_reaction_systems_ODEs.jl") end
- @time @safetestset "Jump Lattice Systems Simulations" begin include("spatial_reaction_systems/lattice_reaction_systems_jumps.jl") end
-
# Tests network visualisation.
@time @safetestset "Latexify" begin include("visualisation/latexify.jl") end
# Disable on Macs as can't install GraphViz via jll
@@ -68,7 +62,16 @@ using SafeTestsets, Test
@time @safetestset "HomotopyContinuation Extension" begin include("extensions/homotopy_continuation.jl") end
@time @safetestset "Structural Identifiability Extension" begin include("extensions/structural_identifiability.jl") end
- # test stability (uses HomotopyContinuation extension)
+ # Tests stability computation (uses HomotopyContinuation extension).
@time @safetestset "Steady State Stability Computations" begin include("miscellaneous_tests/stability_computation.jl") end
+ # Tests spatial modelling and simulations.
+ @time @safetestset "PDE Systems Simulations" begin include("spatial_modelling/simulate_PDEs.jl") end
+ @time @safetestset "Spatial Reactions" begin include("spatial_modelling/spatial_reactions.jl") end
+ @time @safetestset "Lattice Reaction Systems" begin include("spatial_modelling/lattice_reaction_systems.jl") end
+ @time @safetestset "Spatial Lattice Variants" begin include("spatial_modelling/lattice_reaction_systems_lattice_types.jl") end
+ @time @safetestset "ODE Lattice Systems Simulations" begin include("spatial_modelling/lattice_reaction_systems_ODEs.jl") end
+ @time @safetestset "Jump Lattice Systems Simulations" begin include("spatial_modelling/lattice_reaction_systems_jumps.jl") end
+ @time @safetestset "Jump Solution Interfacing" begin include("spatial_modelling/lattice_solution_interfacing.jl") end
+
end # @time
diff --git a/test/simulation_and_solving/solve_nonlinear.jl b/test/simulation_and_solving/solve_nonlinear.jl
index 1dcb8cd5c1..0315a65ac5 100644
--- a/test/simulation_and_solving/solve_nonlinear.jl
+++ b/test/simulation_and_solving/solve_nonlinear.jl
@@ -90,7 +90,7 @@ let
# Creates NonlinearProblem.
u0 = [steady_state_network_3.X => rand(), steady_state_network_3.Y => rand() + 1.0, steady_state_network_3.Y2 => rand() + 3.0, steady_state_network_3.XY2 => 0.0]
p = [:p => rand()+1.0, :d => 0.5, :k1 => 1.0, :k2 => 2.0, :k3 => 3.0, :k4 => 4.0]
- nl_prob_1 = NonlinearProblem(steady_state_network_3, u0, p; remove_conserved = true)
+ nl_prob_1 = NonlinearProblem(steady_state_network_3, u0, p; remove_conserved = true, remove_conserved_warn = false)
nl_prob_2 = NonlinearProblem(steady_state_network_3, u0, p)
# Solves it using standard algorithm and simulation based algorithm.
diff --git a/test/spatial_modelling/lattice_reaction_systems.jl b/test/spatial_modelling/lattice_reaction_systems.jl
index d25c72694c..f36a0a5d4c 100644
--- a/test/spatial_modelling/lattice_reaction_systems.jl
+++ b/test/spatial_modelling/lattice_reaction_systems.jl
@@ -1,12 +1,13 @@
### Preparations ###
# Fetch packages.
-using Catalyst, Graphs, Test
-using Symbolics: BasicSymbolic, unwrap
-t = default_t()
+using Catalyst, Graphs, OrdinaryDiffEq, Test
-# Pre declares a grid.
-grid = Graphs.grid([2, 2])
+# Fetch test networks.
+include("../spatial_test_networks.jl")
+
+# Pre-declares a set of grids.
+grids = [very_small_2d_cartesian_grid, very_small_2d_masked_grid, very_small_2d_graph_grid]
### Tests LatticeReactionSystem Getters Correctness ###
@@ -16,16 +17,18 @@ let
rs = @reaction_network begin
(p, 1), 0 <--> X
end
- tr = @transport_reaction d X
- lrs = LatticeReactionSystem(rs, [tr], grid)
-
- @unpack X, p = rs
- d = edge_parameters(lrs)[1]
- @test issetequal(species(lrs), [X])
- @test issetequal(spatial_species(lrs), [X])
- @test issetequal(parameters(lrs), [p, d])
- @test issetequal(vertex_parameters(lrs), [p])
- @test issetequal(edge_parameters(lrs), [d])
+ tr = @transport_reaction d X
+ for grid in grids
+ lrs = LatticeReactionSystem(rs, [tr], grid)
+
+ @unpack X, p = rs
+ d = edge_parameters(lrs)[1]
+ @test issetequal(species(lrs), [X])
+ @test issetequal(spatial_species(lrs), [X])
+ @test issetequal(parameters(lrs), [p, d])
+ @test issetequal(vertex_parameters(lrs), [p])
+ @test issetequal(edge_parameters(lrs), [d])
+ end
end
# Test case 2.
@@ -48,14 +51,16 @@ let
end
tr_1 = @transport_reaction dX X
tr_2 = @transport_reaction dY Y
- lrs = LatticeReactionSystem(rs, [tr_1, tr_2], grid)
-
- @unpack X, Y, pX, pY, dX, dY = rs
- @test issetequal(species(lrs), [X, Y])
- @test issetequal(spatial_species(lrs), [X, Y])
- @test issetequal(parameters(lrs), [pX, pY, dX, dY])
- @test issetequal(vertex_parameters(lrs), [pX, pY, dY])
- @test issetequal(edge_parameters(lrs), [dX])
+ for grid in grids
+ lrs = LatticeReactionSystem(rs, [tr_1, tr_2], grid)
+
+ @unpack X, Y, pX, pY, dX, dY = rs
+ @test issetequal(species(lrs), [X, Y])
+ @test issetequal(spatial_species(lrs), [X, Y])
+ @test issetequal(parameters(lrs), [pX, pY, dX, dY])
+ @test issetequal(vertex_parameters(lrs), [pX, pY, dY])
+ @test issetequal(edge_parameters(lrs), [dX])
+ end
end
# Test case 4.
@@ -66,14 +71,16 @@ let
(pY, 1), 0 <--> Y
end
tr_1 = @transport_reaction dX X
- lrs = LatticeReactionSystem(rs, [tr_1], grid)
-
- @unpack dX, p, X, Y, pX, pY = rs
- @test issetequal(species(lrs), [X, Y])
- @test issetequal(spatial_species(lrs), [X])
- @test issetequal(parameters(lrs), [dX, p, pX, pY])
- @test issetequal(vertex_parameters(lrs), [dX, p, pX, pY])
- @test issetequal(edge_parameters(lrs), [])
+ for grid in grids
+ lrs = LatticeReactionSystem(rs, [tr_1], grid)
+
+ @unpack dX, p, X, Y, pX, pY = rs
+ @test issetequal(species(lrs), [X, Y])
+ @test issetequal(spatial_species(lrs), [X])
+ @test issetequal(parameters(lrs), [dX, p, pX, pY])
+ @test issetequal(vertex_parameters(lrs), [dX, p, pX, pY])
+ @test issetequal(edge_parameters(lrs), [])
+ end
end
# Test case 5.
@@ -88,21 +95,24 @@ let
end
@unpack dX, X, V = rs
@parameters dV dW
+ @variables t
@species W(t)
tr_1 = TransportReaction(dX, X)
tr_2 = @transport_reaction dY Y
tr_3 = @transport_reaction dZ Z
tr_4 = TransportReaction(dV, V)
- tr_5 = TransportReaction(dW, W)
- lrs = LatticeReactionSystem(rs, [tr_1, tr_2, tr_3, tr_4, tr_5], grid)
-
- @unpack pX, pY, pZ, pV, dX, dY, X, Y, Z, V = rs
- dZ, dV, dW = edge_parameters(lrs)[2:end]
- @test issetequal(species(lrs), [W, X, Y, Z, V])
- @test issetequal(spatial_species(lrs), [X, Y, Z, V, W])
- @test issetequal(parameters(lrs), [pX, pY, dX, dY, pZ, pV, dZ, dV, dW])
- @test issetequal(vertex_parameters(lrs), [pX, pY, dY, pZ, pV])
- @test issetequal(edge_parameters(lrs), [dX, dZ, dV, dW])
+ tr_5 = TransportReaction(dW, W)
+ for grid in grids
+ lrs = LatticeReactionSystem(rs, [tr_1, tr_2, tr_3, tr_4, tr_5], grid)
+
+ @unpack pX, pY, pZ, pV, dX, dY, X, Y, Z, V = rs
+ dZ, dV, dW = edge_parameters(lrs)[2:end]
+ @test issetequal(species(lrs), [W, X, Y, Z, V])
+ @test issetequal(spatial_species(lrs), [X, Y, Z, V, W])
+ @test issetequal(parameters(lrs), [pX, pY, dX, dY, pZ, pV, dZ, dV, dW])
+ @test issetequal(vertex_parameters(lrs), [pX, pY, dY, pZ, pV])
+ @test issetequal(edge_parameters(lrs), [dX, dZ, dV, dW])
+ end
end
# Test case 6.
@@ -111,131 +121,55 @@ let
(p, 1), 0 <--> X
end
tr = @transport_reaction d X
- lrs = LatticeReactionSystem(rs, [tr], grid)
-
- @test nameof(lrs) == :customname
-end
-
-### Tests Spatial Reactions Getters Correctness ###
-
-# Test case 1.
-let
- tr_1 = @transport_reaction dX X
- tr_2 = @transport_reaction dY1*dY2 Y
-
- # @test ModelingToolkit.getname.(species(tr_1)) == ModelingToolkit.getname.(spatial_species(tr_1)) == [:X] # species(::TransportReaction) currently not supported.
- # @test ModelingToolkit.getname.(species(tr_2)) == ModelingToolkit.getname.(spatial_species(tr_2)) == [:Y]
- @test ModelingToolkit.getname.(spatial_species(tr_1)) == [:X]
- @test ModelingToolkit.getname.(spatial_species(tr_2)) == [:Y]
- @test ModelingToolkit.getname.(parameters(tr_1)) == [:dX]
- @test ModelingToolkit.getname.(parameters(tr_2)) == [:dY1, :dY2]
-
- # @test issetequal(species(tr_1), [tr_1.species])
- # @test issetequal(species(tr_2), [tr_2.species])
- @test issetequal(spatial_species(tr_1), [tr_1.species])
- @test issetequal(spatial_species(tr_2), [tr_2.species])
-end
+ for grid in grids
+ lrs = LatticeReactionSystem(rs, [tr], grid)
-# Test case 2.
-let
- rs = @reaction_network begin
- @species X(t) Y(t)
- @parameters dX dY1 dY2
- end
- @unpack X, Y, dX, dY1, dY2 = rs
- tr_1 = TransportReaction(dX, X)
- tr_2 = TransportReaction(dY1*dY2, Y)
- # @test isequal(species(tr_1), [X])
- # @test isequal(species(tr_1), [X])
- @test issetequal(spatial_species(tr_2), [Y])
- @test issetequal(spatial_species(tr_2), [Y])
- @test issetequal(parameters(tr_1), [dX])
- @test issetequal(parameters(tr_2), [dY1, dY2])
-end
-
-### Tests Spatial Reactions Generation ###
-
-# Tests TransportReaction with non-trivial rate.
-let
- rs = @reaction_network begin
- @parameters dV dE [edgeparameter=true]
- (p,1), 0 <--> X
- end
- @unpack dV, dE, X = rs
-
- tr = TransportReaction(dV*dE, X)
- @test isequal(tr.rate, dV*dE)
-end
-
-# Tests transport_reactions function for creating TransportReactions.
-let
- rs = @reaction_network begin
- @parameters d
- (p,1), 0 <--> X
+ @test nameof(lrs) == :customname
end
- @unpack d, X = rs
- trs = TransportReactions([(d, X), (d, X)])
- @test isequal(trs[1], trs[2])
end
-# Test reactions with constants in rate.
-let
- @species X(t) Y(t)
-
- tr_1 = TransportReaction(1.5, X)
- tr_1_macro = @transport_reaction 1.5 X
- @test isequal(tr_1.rate, tr_1_macro.rate)
- @test isequal(tr_1.species, tr_1_macro.species)
-
- tr_2 = TransportReaction(π, Y)
- tr_2_macro = @transport_reaction π Y
- @test isequal(tr_2.rate, tr_2_macro.rate)
- @test isequal(tr_2.species, tr_2_macro.species)
-end
-
-### Test Interpolation ###
-
-# Does not currently work. The 3 tr_macro_ lines generate errors.
-# Test case 1.
+# Tests using various more obscure types of getters.
let
- rs = @reaction_network begin
- @species X(t) Y(t) Z(t)
- @parameters dX dY1 dY2 dZ
- end
- @unpack X, Y, Z, dX, dY1, dY2, dZ = rs
- rate1 = dX
- rate2 = dY1*dY2
- species3 = Z
- tr_1 = TransportReaction(dX, X)
- tr_2 = TransportReaction(dY1*dY2, Y)
- tr_3 = TransportReaction(dZ, Z)
- tr_macro_1 = @transport_reaction $dX X
- tr_macro_2 = @transport_reaction $(rate2) Y
- # tr_macro_3 = @transport_reaction dZ $species3 # Currently does not work, something with meta programming.
-
- @test isequal(tr_1, tr_macro_1)
- @test isequal(tr_2, tr_macro_2) # Unsure why these fails, since for components equality hold: `isequal(tr_1.species, tr_macro_1.species)` and `isequal(tr_1.rate, tr_macro_1.rate)` are both true.
- # @test isequal(tr_3, tr_macro_3)
+ # Create LatticeReactionsSystems.
+ t = default_t()
+ @parameters p d kB kD
+ @species X(t) X2(t)
+ rxs = [
+ Reaction(p, [], [X])
+ Reaction(d, [X], [])
+ Reaction(kB, [X], [X2], [2], [1])
+ Reaction(kD, [X2], [X], [1], [2])
+ ]
+ @named rs = ReactionSystem(rxs, t; metadata = "Metadata string")
+ rs = complete(rs)
+ tr = @transport_reaction D X2
+ lrs = LatticeReactionSystem(rs, [tr], small_2d_cartesian_grid)
+
+ # Generic ones (simply forwards call to the non-spatial system).
+ @test isequal(reactions(lrs), rxs)
+ @test isequal(nameof(lrs), :rs)
+ @test isequal(ModelingToolkit.get_iv(lrs), t)
+ @test isequal(equations(lrs), rxs)
+ @test isequal(unknowns(lrs), [X, X2])
+ @test isequal(ModelingToolkit.get_metadata(lrs), "Metadata string")
+ @test isequal(ModelingToolkit.get_eqs(lrs), rxs)
+ @test isequal(ModelingToolkit.get_unknowns(lrs), [X, X2])
+ @test isequal(ModelingToolkit.get_ps(lrs), [p, d, kB, kD])
+ @test isequal(ModelingToolkit.get_systems(lrs), [])
+ @test isequal(independent_variables(lrs), [t])
end
### Tests Error generation ###
-# Test creation of TransportReaction with non-parameters in rate.
-# Tests that it works even when rate is highly nested.
-let
- @species X(t) Y(t)
- @parameters D1 D2 D3
- @test_throws ErrorException TransportReaction(D1 + D2*(D3 + Y), X)
- @test_throws ErrorException TransportReaction(Y, X)
-end
-
# Network where diffusion species is not declared in non-spatial network.
let
rs = @reaction_network begin
(p, d), 0 <--> X
end
tr = @transport_reaction D Y
- @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid)
+ for grid in grids
+ @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid)
+ end
end
# Network where the rate depend on a species
@@ -245,7 +179,9 @@ let
(p, d), 0 <--> X
end
tr = @transport_reaction D*Y X
- @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid)
+ for grid in grids
+ @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid)
+ end
end
# Network with edge parameter in non-spatial reaction rate.
@@ -255,7 +191,9 @@ let
(p, d), 0 <--> X
end
tr = @transport_reaction D X
- @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid)
+ for grid in grids
+ @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid)
+ end
end
# Network where metadata has been added in rs (which is not seen in transport reaction).
@@ -265,104 +203,193 @@ let
(p, d), 0 <--> X
end
tr = @transport_reaction D X
- @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid)
+ for grid in grids
+ @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid)
- rs = @reaction_network begin
- @parameters D [description="Parameter with added metadata"]
- (p, d), 0 <--> X
+ rs = @reaction_network begin
+ @parameters D [description="Parameter with added metadata"]
+ (p, d), 0 <--> X
+ end
+ tr = @transport_reaction D X
+ @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid)
+ end
+end
+
+# Tests various networks with non-permitted content.
+ let
+ tr = @transport_reaction D X
+
+ # Variable unknowns.
+ rs1 = @reaction_network begin
+ @variables V(t)
+ (p,d), 0 <--> X
+ end
+ @test_throws ArgumentError LatticeReactionSystem(rs1, [tr], short_path)
+
+ # Non-reaction equations.
+ rs2 = @reaction_network begin
+ @equations D(V) ~ X - V
+ (p,d), 0 <--> X
end
+ @test_throws ArgumentError LatticeReactionSystem(rs2, [tr], short_path)
+
+ # Events.
+ rs3 = @reaction_network begin
+ @discrete_events [1.0] => [p ~ p + 1]
+ (p,d), 0 <--> X
+ end
+ @test_throws ArgumentError LatticeReactionSystem(rs3, [tr], short_path)
+
+ # Observables (only generates a warning).
+ rs4 = @reaction_network begin
+ @observables X2 ~ 2X
+ (p,d), 0 <--> X
+ end
+ @test_logs (:warn, r"The `ReactionSystem` used as input to `LatticeReactionSystem contain observables. It *") match_mode=:any LatticeReactionSystem(rs4, [tr], short_path)
+end
+
+# Tests for hierarchical input system.
+let
+ t = default_t()
+ @parameters d D
+ @species X(t)
+ rxs = [Reaction(d, [X], [])]
+ @named rs1 = ReactionSystem(rxs, t)
+ @named rs2 = ReactionSystem(rxs, t; systems = [rs1])
+ rs2 = complete(rs2)
+ @test_throws ArgumentError LatticeReactionSystem(rs2, [TransportReaction(D, X)], CartesianGrid((2,2)))
+end
+
+# Tests for non-complete input `ReactionSystem`.
+let
tr = @transport_reaction D X
- @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid)
+ rs = @network_component begin
+ (p,d), 0 <--> X
+ end
+ @test_throws ArgumentError LatticeReactionSystem(rs, [tr], CartesianGrid((2,2)))
end
+### Tests Grid Vertex and Edge Number Computation ###
-### Test Designation of Parameter Types ###
-# Currently not supported. Won't be until the LatticeReactionSystem internal update is merged.
+# Tests that the correct numbers are computed for num_edges.
+let
+ # Function counting the values in an iterator by stepping through it.
+ function iterator_count(iterator)
+ count = 0
+ foreach(e -> count+=1, iterator)
+ return count
+ end
-# Checks that parameter types designated in the non-spatial `ReactionSystem` is handled correctly.
-# Broken lattice tests have local branches that fixes them.
-@test_broken let
- # Declares LatticeReactionSystem with designated parameter types.
- rs = @reaction_network begin
- @parameters begin
- k1
- l1
- k2::Float64 = 2.0
- l2::Float64
- k3::Int64 = 2, [description="A parameter"]
- l3::Int64
- k4::Float32, [description="Another parameter"]
- l4::Float32
- k5::Rational{Int64}
- l5::Rational{Int64}
- D1::Float32
- D2, [edgeparameter=true]
- D3::Rational{Int64}, [edgeparameter=true]
- end
- (k1,l1), X1 <--> Y1
- (k2,l2), X2 <--> Y2
- (k3,l3), X3 <--> Y3
- (k4,l4), X4 <--> Y4
- (k5,l5), X5 <--> Y5
- end
- tr1 = @transport_reaction $(rs.D1) X1
- tr2 = @transport_reaction $(rs.D2) X2
- tr3 = @transport_reaction $(rs.D3) X3
- lrs = LatticeReactionSystem(rs, [tr1, tr2, tr3], grid)
-
- # Loops through all parameters, ensuring that they have the correct type
- p_types = Dict([ModelingToolkit.nameof(p) => typeof(unwrap(p)) for p in parameters(lrs)])
- @test p_types[:k1] == BasicSymbolic{Real}
- @test p_types[:l1] == BasicSymbolic{Real}
- @test p_types[:k2] == BasicSymbolic{Float64}
- @test p_types[:l2] == BasicSymbolic{Float64}
- @test p_types[:k3] == BasicSymbolic{Int64}
- @test p_types[:l3] == BasicSymbolic{Int64}
- @test p_types[:k4] == BasicSymbolic{Float32}
- @test p_types[:l4] == BasicSymbolic{Float32}
- @test p_types[:k5] == BasicSymbolic{Rational{Int64}}
- @test p_types[:l5] == BasicSymbolic{Rational{Int64}}
- @test p_types[:D1] == BasicSymbolic{Float32}
- @test p_types[:D2] == BasicSymbolic{Real}
- @test p_types[:D3] == BasicSymbolic{Rational{Int64}}
+ # Cartesian and masked grid (test diagonal edges as well).
+ for lattice in [small_1d_cartesian_grid, small_2d_cartesian_grid, small_3d_cartesian_grid,
+ random_1d_masked_grid, random_2d_masked_grid, random_3d_masked_grid]
+ lrs1 = LatticeReactionSystem(SIR_system, SIR_srs_1, lattice)
+ lrs2 = LatticeReactionSystem(SIR_system, SIR_srs_1, lattice; diagonal_connections=true)
+ @test num_edges(lrs1) == iterator_count(edge_iterator(lrs1))
+ @test num_edges(lrs2) == iterator_count(edge_iterator(lrs2))
+ end
+
+ # Graph grids (cannot test diagonal connections).
+ for lattice in [small_2d_graph_grid, small_3d_graph_grid, undirected_cycle, small_directed_cycle, unconnected_graph]
+ lrs1 = LatticeReactionSystem(SIR_system, SIR_srs_1, lattice)
+ @test num_edges(lrs1) == iterator_count(edge_iterator(lrs1))
+ end
end
-# Checks that programmatically declared parameters (with types) can be used in `TransportReaction`s.
-# Checks that LatticeReactionSystem with non-default parameter types can be simulated.
-@test_broken let
- rs = @reaction_network begin
- @parameters p::Float32
+### Tests Edge Value Computation Helper Functions ###
+
+# Checks that we compute the correct values across various types of grids.
+let
+ # Prepares the model and the function that determines the edge values.
+ rn = @reaction_network begin
(p,d), 0 <--> X
end
- @parameters D::Rational{Int64}
- tr = TransportReaction(D, rs.X)
- lrs = LatticeReactionSystem(rs, [tr], grid)
+ tr = @transport_reaction D X
+ function make_edge_p_value(src_vert, dst_vert)
+ return prod(src_vert) + prod(dst_vert)
+ end
+
+ # Loops through a variety of grids, checks that `make_edge_p_values` yields the correct values.
+ for grid in [small_1d_cartesian_grid, small_2d_cartesian_grid, small_3d_cartesian_grid,
+ small_1d_masked_grid, small_2d_masked_grid, small_3d_masked_grid,
+ random_1d_masked_grid, random_2d_masked_grid, random_3d_masked_grid]
+ lrs = LatticeReactionSystem(rn, [tr], grid)
+ flat_to_grid_idx = Catalyst.get_index_converters(lattice(lrs), num_verts(lrs))[1]
+ edge_values = make_edge_p_values(lrs, make_edge_p_value)
- p_types = Dict([ModelingToolkit.nameof(p) => typeof(unwrap(p)) for p in parameters(lrs)])
- @test p_types[:p] == BasicSymbolic{Float32}
- @test p_types[:d] == BasicSymbolic{Real}
- @test p_types[:D] == BasicSymbolic{Rational{Int64}}
-
- u0 = [:X => [0.25, 0.5, 2.0, 4.0]]
- ps = [rs.p => 2.0, rs.d => 1.0, D => 1//2]
-
- # Currently broken. This requires some non-trivial reworking of internals.
- # However, spatial internals have already been reworked (and greatly improved) in an unmerged PR.
- # This will be sorted out once that has finished.
- @test_broken false
- # oprob = ODEProblem(lrs, u0, (0.0, 10.0), ps)
- # sol = solve(oprob, Tsit5())
- # @test sol[end] == [1.0, 1.0, 1.0, 1.0]
+ for e in edge_iterator(lrs)
+ @test edge_values[e[1], e[2]] == make_edge_p_value(flat_to_grid_idx[e[1]], flat_to_grid_idx[e[2]])
+ end
+ end
+end
+
+# Checks that all species end up in the correct place in a pure flow system (checking various dimensions).
+let
+ # Prepares a system with a single species which is transported only.
+ rn = @reaction_network begin
+ @species X(t)
+ end
+ n = 5
+ tr = @transport_reaction D X
+ tspan = (0.0, 1000.0)
+ u0 = [:X => 1.0]
+
+ # Checks the 1d case.
+ lrs = LatticeReactionSystem(rn, [tr], CartesianGrid(n))
+ ps = [:D => make_directed_edge_values(lrs, (10.0, 0.0))]
+ oprob = ODEProblem(lrs, u0, tspan, ps)
+ @test isapprox(solve(oprob, Tsit5()).u[end][5], n, rtol=1e-6)
+
+ # Checks the 2d case (both with 1d and 2d flow).
+ lrs = LatticeReactionSystem(rn, [tr], CartesianGrid((n,n)))
+
+ ps = [:D => make_directed_edge_values(lrs, (1.0, 0.0), (0.0, 0.0))]
+ oprob = ODEProblem(lrs, u0, tspan, ps)
+ @test all(isapprox.(solve(oprob, Tsit5()).u[end][5:5:25], n, rtol=1e-6))
+
+ ps = [:D => make_directed_edge_values(lrs, (1.0, 0.0), (1.0, 0.0))]
+ oprob = ODEProblem(lrs, u0, tspan, ps)
+ @test isapprox(solve(oprob, Tsit5()).u[end][25], n^2, rtol=1e-6)
+
+ # Checks the 3d case (both with 1d and 2d flow).
+ lrs = LatticeReactionSystem(rn, [tr], CartesianGrid((n,n,n)))
+
+ ps = [:D => make_directed_edge_values(lrs, (1.0, 0.0), (0.0, 0.0), (0.0, 0.0))]
+ oprob = ODEProblem(lrs, u0, tspan, ps)
+ @test all(isapprox.(solve(oprob, Tsit5()).u[end][5:5:125], n, rtol=1e-6))
+
+ ps = [:D => make_directed_edge_values(lrs, (1.0, 0.0), (1.0, 0.0), (0.0, 0.0))]
+ oprob = ODEProblem(lrs, u0, tspan, ps)
+ @test all(isapprox.(solve(oprob, Tsit5()).u[end][25:25:125], n^2, rtol=1e-6))
+
+ ps = [:D => make_directed_edge_values(lrs, (1.0, 0.0), (1.0, 0.0), (1.0, 0.0))]
+ oprob = ODEProblem(lrs, u0, tspan, ps)
+ @test isapprox(solve(oprob, Tsit5()).u[end][125], n^3, rtol=1e-6)
end
-# Tests that LatticeReactionSystem cannot be generated where transport reactions depend on parameters
-# that have a type designated in the non-spatial `ReactionSystem`.
-@test_broken false
-# let
-# rs = @reaction_network begin
-# @parameters D::Int64
-# (p,d), 0 <--> X
-# end
-# tr = @transport_reaction D X
-# @test_throws Exception LatticeReactionSystem(rs, tr, grid)
-# end
\ No newline at end of file
+# Checks that erroneous input yields errors.
+let
+ rn = @reaction_network begin
+ (p,d), 0 <--> X
+ end
+ tr = @transport_reaction D X
+ tspan = (0.0, 10000.0)
+ make_edge_p_value(src_vert, dst_vert) = rand()
+
+ # Graph grids.
+ lrs = LatticeReactionSystem(rn, [tr], path_graph(5))
+ @test_throws Exception make_edge_p_values(lrs, make_edge_p_value,)
+ @test_throws Exception make_directed_edge_values(lrs, (1.0, 0.0))
+
+ # Wrong dimensions to `make_directed_edge_values`.
+ lrs_1d = LatticeReactionSystem(rn, [tr], CartesianGrid(5))
+ lrs_2d = LatticeReactionSystem(rn, [tr], fill(true,5,5))
+ lrs_3d = LatticeReactionSystem(rn, [tr], CartesianGrid((5,5,5)))
+
+ @test_throws Exception make_directed_edge_values(lrs_1d, (1.0, 0.0), (1.0, 0.0))
+ @test_throws Exception make_directed_edge_values(lrs_1d, (1.0, 0.0), (1.0, 0.0), (1.0, 0.0))
+ @test_throws Exception make_directed_edge_values(lrs_2d, (1.0, 0.0))
+ @test_throws Exception make_directed_edge_values(lrs_2d, (1.0, 0.0), (1.0, 0.0), (1.0, 0.0))
+ @test_throws Exception make_directed_edge_values(lrs_3d, (1.0, 0.0))
+ @test_throws Exception make_directed_edge_values(lrs_3d, (1.0, 0.0), (1.0, 0.0))
+end
\ No newline at end of file
diff --git a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl
index cdefc6ebc9..26da7ba3fc 100644
--- a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl
+++ b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl
@@ -14,78 +14,34 @@ rng = StableRNG(12345)
# Sets defaults
t = default_t()
-### Tests Simulations Don't Error ###
-for grid in [small_2d_grid, short_path, small_directed_cycle]
- # Non-stiff case
- for srs in [Vector{TransportReaction}(), SIR_srs_1, SIR_srs_2]
- lrs = LatticeReactionSystem(SIR_system, srs, grid)
- u0_1 = [:S => 999.0, :I => 1.0, :R => 0.0]
- u0_2 = [:S => 500.0 .+ 500.0 * rand_v_vals(lrs.lattice), :I => 1.0, :R => 0.0]
- u0_3 = [
- :S => 950.0,
- :I => 50 * rand_v_vals(lrs.lattice),
- :R => 50 * rand_v_vals(lrs.lattice),
- ]
- u0_4 = [
- :S => 500.0 .+ 500.0 * rand_v_vals(lrs.lattice),
- :I => 50 * rand_v_vals(lrs.lattice),
- :R => 50 * rand_v_vals(lrs.lattice),
- ]
- u0_5 = make_u0_matrix(u0_3, vertices(lrs.lattice),
- map(s -> Symbol(s.f), species(lrs.rs)))
- for u0 in [u0_1, u0_2, u0_3, u0_4, u0_5]
- p1 = [:α => 0.1 / 1000, :β => 0.01]
- p2 = [:α => 0.1 / 1000, :β => 0.02 * rand_v_vals(lrs.lattice)]
- p3 = [
- :α => 0.1 / 2000 * rand_v_vals(lrs.lattice),
- :β => 0.02 * rand_v_vals(lrs.lattice),
- ]
- p4 = make_u0_matrix(p1, vertices(lrs.lattice), Symbol.(parameters(lrs.rs)))
- for pV in [p1, p2, p3, p4]
- pE_1 = map(sp -> sp => 0.01, spatial_param_syms(lrs))
- pE_2 = map(sp -> sp => 0.01, spatial_param_syms(lrs))
- pE_3 = map(sp -> sp => rand_e_vals(lrs.lattice, 0.01),
- spatial_param_syms(lrs))
- pE_4 = make_u0_matrix(pE_3, edges(lrs.lattice), spatial_param_syms(lrs))
- for pE in [pE_1, pE_2, pE_3, pE_4]
- oprob = ODEProblem(lrs, u0, (0.0, 500.0), (pV, pE))
- @test SciMLBase.successful_retcode(solve(oprob, Tsit5()))
-
- oprob = ODEProblem(lrs, u0, (0.0, 10.0), (pV, pE); jac = false)
- @test SciMLBase.successful_retcode(solve(oprob, Tsit5()))
- end
- end
- end
- end
-
- # Stiff case
- for srs in [Vector{TransportReaction}(), brusselator_srs_1, brusselator_srs_2]
- lrs = LatticeReactionSystem(brusselator_system, srs, grid)
- u0_1 = [:X => 1.0, :Y => 20.0]
- u0_2 = [:X => rand_v_vals(lrs.lattice, 10.0), :Y => 2.0]
- u0_3 = [:X => rand_v_vals(lrs.lattice, 20), :Y => rand_v_vals(lrs.lattice, 10)]
- u0_4 = make_u0_matrix(u0_3, vertices(lrs.lattice),
- map(s -> Symbol(s.f), species(lrs.rs)))
- for u0 in [u0_1, u0_2, u0_3, u0_4]
- p1 = [:A => 1.0, :B => 4.0]
- p2 = [:A => 0.5 .+ rand_v_vals(lrs.lattice, 0.5), :B => 4.0]
- p3 = [
- :A => 0.5 .+ rand_v_vals(lrs.lattice, 0.5),
- :B => 4.0 .+ rand_v_vals(lrs.lattice, 1.0),
+### Tests Simulations Do Not Error ###
+let
+ for grid in [small_1d_cartesian_grid, small_1d_masked_grid, small_1d_graph_grid]
+ for srs in [Vector{TransportReaction}(), SIR_srs_1, SIR_srs_2]
+ lrs = LatticeReactionSystem(SIR_system, srs, grid)
+ u0_1 = [:S => 999.0, :I => 1.0, :R => 0.0]
+ u0_2 = [:S => 500.0 .+ 500.0 * rand_v_vals(lrs), :I => 1.0, :R => 0.0]
+ u0_3 = [
+ :S => 500.0 .+ 500.0 * rand_v_vals(lrs),
+ :I => 50 * rand_v_vals(lrs),
+ :R => 50 * rand_v_vals(lrs),
]
- p4 = make_u0_matrix(p2, vertices(lrs.lattice), Symbol.(parameters(lrs.rs)))
- for pV in [p1, p2, p3, p4]
- pE_1 = map(sp -> sp => 0.2, spatial_param_syms(lrs))
- pE_2 = map(sp -> sp => rand(rng), spatial_param_syms(lrs))
- pE_3 = map(sp -> sp => rand_e_vals(lrs.lattice, 0.2),
- spatial_param_syms(lrs))
- pE_4 = make_u0_matrix(pE_3, edges(lrs.lattice), spatial_param_syms(lrs))
- for pE in [pE_1, pE_2, pE_3, pE_4]
- oprob = ODEProblem(lrs, u0, (0.0, 10.0), (pV, pE))
- @test SciMLBase.successful_retcode(solve(oprob, QNDF()))
-
- oprob = ODEProblem(lrs, u0, (0.0, 10.0), (pV, pE); sparse = false)
- @test SciMLBase.successful_retcode(solve(oprob, QNDF()))
+ for u0 in [u0_1, u0_2, u0_3]
+ pV_1 = [:α => 0.1 / 1000, :β => 0.01]
+ pV_2 = [:α => 0.1 / 1000, :β => 0.02 * rand_v_vals(lrs)]
+ pV_3 = [
+ :α => 0.1 / 2000 * rand_v_vals(lrs),
+ :β => 0.02 * rand_v_vals(lrs),
+ ]
+ for pV in [pV_1, pV_2, pV_3]
+ pE_1 = map(sp -> sp => 0.01, spatial_param_syms(lrs))
+ pE_2 = map(sp -> sp => 0.01, spatial_param_syms(lrs))
+ pE_3 = map(sp -> sp => rand_e_vals(lrs, 0.01), spatial_param_syms(lrs))
+ for pE in [pE_1, pE_2, pE_3]
+ isempty(spatial_param_syms(lrs)) && (pE = Vector{Pair{Symbol, Float64}}())
+ oprob = ODEProblem(lrs, u0, (0.0, 500.0), [pV; pE]; jac = false, sparse = false)
+ @test SciMLBase.successful_retcode(solve(oprob, Tsit5()))
+ end
end
end
end
@@ -94,24 +50,23 @@ end
### Tests Simulation Correctness ###
-# Checks that non-spatial brusselator simulation is identical to all on an unconnected lattice.
+# Tests with non-Float64 parameter values.
let
- lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, unconnected_graph)
- u0 = [:X => 2.0 + 2.0 * rand(rng), :Y => 10.0 + 10.0 * rand(rng)]
- pV = brusselator_p
- pE = [:dX => 0.2]
- oprob_nonspatial = ODEProblem(brusselator_system, u0, (0.0, 100.0), pV)
- oprob_spatial = ODEProblem(lrs, u0, (0.0, 100.0), (pV, pE))
- sol_nonspatial = solve(oprob_nonspatial, QNDF(); abstol = 1e-12, reltol = 1e-12)
- sol_spatial = solve(oprob_spatial, QNDF(); abstol = 1e-12, reltol = 1e-12)
-
- for i in 1:nv(unconnected_graph)
- @test all(isapprox.(sol_nonspatial.u[end],
- sol_spatial.u[end][((i - 1) * 2 + 1):((i - 1) * 2 + 2)]))
+ lrs = LatticeReactionSystem(SIR_system, SIR_srs_2, very_small_2d_cartesian_grid)
+ u0 = [:S => 990.0, :I => rand_v_vals(lrs), :R => 0.0]
+ ps_1 = [:α => 0.1, :β => 0.01, :dS => 0.01, :dI => 0.01, :dR => 0.01]
+ ps_2 = [:α => 1//10, :β => 1//100, :dS => 1//100, :dI => 1//100, :dR => 1//100]
+ ps_3 = [:α => 1//10, :β => 0.01, :dS => 0.01, :dI => 1//100, :dR => 0.01]
+ sol_base = solve(ODEProblem(lrs, u0, (0.0, 100.0), ps_1), Rosenbrock23(); saveat = 0.1)
+ for ps in [ps_1, ps_2, ps_3]
+ for jac in [true, false], sparse in [true, false]
+ oprob = ODEProblem(lrs, u0, (0.0, 100.0), ps; jac, sparse)
+ @test sol_base ≈ solve(oprob, Rosenbrock23(); saveat = 0.1)
+ end
end
end
-# Compares Jacobian and forcing functions of spatial system to analytically computed on.
+# Compares Jacobian and forcing functions of spatial system to analytically computed ones.
let
# Creates LatticeReactionNetwork ODEProblem.
rs = @reaction_network begin
@@ -124,9 +79,11 @@ let
lattice = path_graph(3)
lrs = LatticeReactionSystem(rs, [tr], lattice);
- D_vals = [0.2, 0.2, 0.3, 0.3]
+ D_vals = spzeros(3,3)
+ D_vals[1,2] = 0.2; D_vals[2,1] = 0.2;
+ D_vals[2,3] = 0.3; D_vals[3,2] = 0.3;
u0 = [:X => [1.0, 2.0, 3.0], :Y => 1.0]
- ps = [:pX => [2.0, 2.5, 3.0], :pY => 0.5, :d => 0.1, :D => D_vals]
+ ps = [:pX => [2.0, 2.5, 3.0], :d => 0.1, :pY => 0.5, :D => D_vals]
oprob = ODEProblem(lrs, u0, (0.0, 0.0), ps; jac=true, sparse=true)
# Creates manual f and jac functions.
@@ -136,7 +93,8 @@ let
pX1, pX2, pX3 = pX
pY, = pY
d, = d
- D1, D2, D3, D4 = D_vals
+ D1 = D_vals[1,2]; D2 = D_vals[2,1];
+ D3 = D_vals[2,3]; D4 = D_vals[3,2];
du[1] = pX1 - d*X1 - D1*X1 + D2*X2
du[2] = pY*X1 - d*Y1
du[3] = pX2 - d*X2 + D1*X1 - (D2+D3)*X2 + D4*X3
@@ -150,7 +108,8 @@ let
pX1, pX2, pX3 = pX
pY, = pY
d, = d
- D1, D2, D3, D4 = D_vals
+ D1 = D_vals[1,2]; D2 = D_vals[2,1];
+ D3 = D_vals[2,3]; D4 = D_vals[3,2];
J .= 0.0
@@ -177,7 +136,7 @@ let
# Sets test input values.
u = rand(rng, 6)
- p = [rand(rng, 3), rand(rng, 1), rand(rng, 1)]
+ p = [rand(rng, 3), ps[2][2], ps[3][2]]
# Tests forcing function.
du1 = fill(0.0, 6)
@@ -198,9 +157,9 @@ end
let
lrs = LatticeReactionSystem(binding_system, binding_srs, undirected_cycle)
u0 = [
- :X => 1.0 .+ rand_v_vals(lrs.lattice),
- :Y => 2.0 * rand_v_vals(lrs.lattice),
- :XY => 0.5,
+ :X => 1.0 .+ rand_v_vals(lrs),
+ :Y => 2.0 * rand_v_vals(lrs),
+ :XY => 0.5
]
oprob = ODEProblem(lrs, u0, (0.0, 1000.0), binding_p; tstops = 0.1:0.1:1000.0)
ss = solve(oprob, Tsit5()).u[end]
@@ -212,61 +171,236 @@ end
# Checks that various combinations of jac and sparse gives the same result.
let
- lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_2d_grid)
- u0 = [:X => rand_v_vals(lrs.lattice, 10), :Y => rand_v_vals(lrs.lattice, 10)]
+ lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_2d_graph_grid)
+ u0 = [:X => rand_v_vals(lrs, 10), :Y => rand_v_vals(lrs, 10)]
pV = brusselator_p
pE = [:dX => 0.2]
- oprob = ODEProblem(lrs, u0, (0.0, 50.0), (pV, pE); jac = false, sparse = false)
- oprob_sparse = ODEProblem(lrs, u0, (0.0, 50.0), (pV, pE); jac = false, sparse = true)
- oprob_jac = ODEProblem(lrs, u0, (0.0, 50.0), (pV, pE); jac = true, sparse = false)
- oprob_sparse_jac = ODEProblem(lrs, u0, (0.0, 50.0), (pV, pE); jac = true, sparse = true)
+ oprob = ODEProblem(lrs, u0, (0.0, 5.0), [pV; pE]; jac = false, sparse = false)
+ oprob_sparse = ODEProblem(lrs, u0, (0.0, 5.0), [pV; pE]; jac = false, sparse = true)
+ oprob_jac = ODEProblem(lrs, u0, (0.0, 5.0), [pV; pE]; jac = true, sparse = false)
+ oprob_sparse_jac = ODEProblem(lrs, u0, (0.0, 5.0), [pV; pE]; jac = true, sparse = true)
ss = solve(oprob, Rosenbrock23(); abstol = 1e-10, reltol = 1e-10).u[end]
- @test all(isapprox.(ss,
- solve(oprob_sparse, Rosenbrock23(); abstol = 1e-10, reltol = 1e-10).u[end];
- rtol = 0.0001))
- @test all(isapprox.(ss,
- solve(oprob_jac, Rosenbrock23(); abstol = 1e-10, reltol = 1e-10).u[end];
- rtol = 0.0001))
- @test all(isapprox.(ss,
- solve(oprob_sparse_jac, Rosenbrock23(); abstol = 1e-10, reltol = 1e-10).u[end];
- rtol = 0.0001))
+ @test all(isapprox.(ss, solve(oprob_sparse, Rosenbrock23(); abstol = 1e-10, reltol = 1e-10).u[end]; rtol = 0.0001))
+ @test all(isapprox.(ss, solve(oprob_jac, Rosenbrock23(); abstol = 1e-10, reltol = 1e-10).u[end]; rtol = 0.0001))
+ @test all(isapprox.(ss, solve(oprob_sparse_jac, Rosenbrock23(); abstol = 1e-10, reltol = 1e-10).u[end]; rtol = 0.0001))
end
-# Checks that, when non directed graphs are provided, the parameters are re-ordered correctly.
+# Compares Catalyst-generated to hand-written one for the Brusselator for a line of cells.
+let
+ function spatial_brusselator_f(du, u, p, t)
+ # Non-spatial
+ for i in 1:2:(length(u) - 1)
+ du[i] = p[1] + 0.5 * (u[i]^2) * u[i + 1] - u[i] - p[2] * u[i]
+ du[i + 1] = p[2] * u[i] - 0.5 * (u[i]^2) * u[i + 1]
+ end
+
+ # Spatial
+ du[1] += p[3] * (u[3] - u[1])
+ du[end - 1] += p[3] * (u[end - 3] - u[end - 1])
+ for i in 3:2:(length(u) - 3)
+ du[i] += p[3] * (u[i - 2] + u[i + 2] - 2u[i])
+ end
+ end
+ function spatial_brusselator_jac(J, u, p, t)
+ J .= 0
+ # Non-spatial
+ for i in 1:2:(length(u) - 1)
+ J[i, i] = u[i] * u[i + 1] - 1 - p[2]
+ J[i, i + 1] = 0.5 * (u[i]^2)
+ J[i + 1, i] = p[2] - u[i] * u[i + 1]
+ J[i + 1, i + 1] = -0.5 * (u[i]^2)
+ end
+
+ # Spatial
+ J[1, 1] -= p[3]
+ J[1, 3] += p[3]
+ J[end - 1, end - 1] -= p[3]
+ J[end - 1, end - 3] += p[3]
+ for i in 3:2:(length(u) - 3)
+ J[i, i] -= 2 * p[3]
+ J[i, i - 2] += p[3]
+ J[i, i + 2] += p[3]
+ end
+ end
+ function spatial_brusselator_jac_sparse(J, u, p, t)
+ # Spatial
+ J.nzval .= 0.0
+ J.nzval[7:6:(end - 9)] .= -2p[3]
+ J.nzval[1] = -p[3]
+ J.nzval[end - 3] = -p[3]
+ J.nzval[3:3:(end - 4)] .= p[3]
+
+ # Non-spatial
+ for i in 1:1:Int64(lenth(u) / 2 - 1)
+ j = 6(i - 1) + 1
+ J.nzval[j] = u[i] * u[i + 1] - 1 - p[2]
+ J.nzval[j + 1] = 0.5 * (u[i]^2)
+ J.nzval[j + 3] = p[2] - u[i] * u[i + 1]
+ J.nzval[j + 4] = -0.5 * (u[i]^2)
+ end
+ J.nzval[end - 3] = u[end - 1] * u[end] - 1 - p[end - 1]
+ J.nzval[end - 2] = 0.5 * (u[end - 1]^2)
+ J.nzval[end - 1] = p[2] - u[end - 1] * u[end]
+ J.nzval[end] = -0.5 * (u[end - 1]^2)
+ end
+ function make_jac_prototype(u0)
+ jac_prototype_pre = zeros(length(u0), length(u0))
+ for i in 1:2:(length(u0) - 1)
+ jac_prototype_pre[i, i] = 1
+ jac_prototype_pre[i + 1, i] = 1
+ jac_prototype_pre[i, i + 1] = 1
+ jac_prototype_pre[i + 1, i + 1] = 1
+ end
+ for i in 3:2:(length(u0) - 1)
+ jac_prototype_pre[i - 2, i] = 1
+ jac_prototype_pre[i, i - 2] = 1
+ end
+ return sparse(jac_prototype_pre)
+ end
+
+ num_verts = 100
+ u0 = 2 * rand(rng, 2*num_verts)
+ p = [1.0, 4.0, 0.1]
+ tspan = (0.0, 100.0)
+
+ ofun_hw_dense = ODEFunction(spatial_brusselator_f; jac = spatial_brusselator_jac)
+ ofun_hw_sparse = ODEFunction(spatial_brusselator_f; jac = spatial_brusselator_jac,
+ jac_prototype = make_jac_prototype(u0))
+
+ lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, path_graph(num_verts))
+ u0_map = [:X => u0[1:2:(end - 1)], :Y => u0[2:2:end]]
+ ps_map = [:A => p[1], :B => p[2], :dX => p[3]]
+ oprob_aut_dense = ODEProblem(lrs, u0_map, tspan, ps_map; jac = true, sparse = false)
+ oprob_aut_sparse = ODEProblem(lrs, u0_map, tspan, ps_map; jac = true, sparse = true)
+ ofun_aut_dense = oprob_aut_dense.f
+ ofun_aut_sparse = oprob_aut_sparse.f
+
+ du_hw_dense = deepcopy(u0)
+ du_hw_sparse = deepcopy(u0)
+ du_aut_dense = deepcopy(u0)
+ du_aut_sparse = deepcopy(u0)
+
+ ofun_hw_dense(du_hw_dense, u0, p, 0.0)
+ ofun_hw_sparse(du_hw_sparse, u0, p, 0.0)
+ ofun_aut_dense(du_aut_dense, u0, oprob_aut_dense.p, 0.0)
+ ofun_aut_sparse(du_aut_sparse, u0, oprob_aut_dense.p, 0.0)
+
+ @test isapprox(du_hw_dense, du_aut_dense)
+ @test isapprox(du_hw_sparse, du_aut_sparse)
+
+ J_hw_dense = deepcopy(zeros(length(u0), length(u0)))
+ J_hw_sparse = deepcopy(make_jac_prototype(u0))
+ J_aut_dense = deepcopy(zeros(length(u0), length(u0)))
+ J_aut_sparse = deepcopy(make_jac_prototype(u0))
+
+ ofun_hw_dense.jac(J_hw_dense, u0, p, 0.0)
+ ofun_hw_sparse.jac(J_hw_sparse, u0, p, 0.0)
+ ofun_aut_dense.jac(J_aut_dense, u0, oprob_aut_dense.p, 0.0)
+ ofun_aut_sparse.jac(J_aut_sparse, u0, oprob_aut_dense.p, 0.0)
+
+ @test isapprox(J_hw_dense, J_aut_dense)
+ @test isapprox(J_hw_sparse, J_aut_sparse)
+end
+
+
+### Test Grid Types ###
+
+# Tests that identical lattices (using different types of lattices) give identical results.
let
- # Create the same lattice (one as digraph, one not). Algorithm depends on Graphs.jl reordering edges, hence the jumbled order.
- lattice_1 = SimpleGraph(5)
- lattice_2 = SimpleDiGraph(5)
-
- add_edge!(lattice_1, 5, 2)
- add_edge!(lattice_1, 1, 4)
- add_edge!(lattice_1, 1, 3)
- add_edge!(lattice_1, 4, 3)
- add_edge!(lattice_1, 4, 5)
-
- add_edge!(lattice_2, 4, 1)
- add_edge!(lattice_2, 3, 4)
- add_edge!(lattice_2, 5, 4)
- add_edge!(lattice_2, 5, 2)
- add_edge!(lattice_2, 4, 3)
- add_edge!(lattice_2, 4, 5)
- add_edge!(lattice_2, 3, 1)
- add_edge!(lattice_2, 2, 5)
- add_edge!(lattice_2, 1, 4)
- add_edge!(lattice_2, 1, 3)
-
- lrs_1 = LatticeReactionSystem(SIR_system, SIR_srs_2, lattice_1)
- lrs_2 = LatticeReactionSystem(SIR_system, SIR_srs_2, lattice_2)
-
- u0 = [:S => 990.0, :I => 20.0 * rand_v_vals(lrs_1.lattice), :R => 0.0]
- pV = [:α => 0.1 / 1000, :β => 0.01]
+ # Declares the diffusion parameters.
+ sigmaB_p_spat = [:DσB => 0.05, :Dw => 0.04, :Dv => 0.03]
+
+ # 1d lattices.
+ lrs1_cartesian = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, very_small_1d_cartesian_grid)
+ lrs1_masked = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, very_small_1d_masked_grid)
+ lrs1_graph = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, very_small_1d_graph_grid)
+
+ oprob1_cartesian = ODEProblem(lrs1_cartesian, sigmaB_u0, (0.0,1.0), [sigmaB_p; sigmaB_p_spat])
+ oprob1_masked = ODEProblem(lrs1_masked, sigmaB_u0, (0.0,1.0), [sigmaB_p; sigmaB_p_spat])
+ oprob1_graph = ODEProblem(lrs1_graph, sigmaB_u0, (0.0,1.0), [sigmaB_p; sigmaB_p_spat])
+ @test solve(oprob1_cartesian, QNDF()) ≈ solve(oprob1_masked, QNDF()) ≈ solve(oprob1_graph, QNDF())
+
+ # 2d lattices.
+ lrs2_cartesian = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, very_small_2d_cartesian_grid)
+ lrs2_masked = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, very_small_2d_masked_grid)
+ lrs2_graph = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, very_small_2d_graph_grid)
+
+ oprob2_cartesian = ODEProblem(lrs2_cartesian, sigmaB_u0, (0.0,1.0), [sigmaB_p; sigmaB_p_spat])
+ oprob2_masked = ODEProblem(lrs2_masked, sigmaB_u0, (0.0,1.0), [sigmaB_p; sigmaB_p_spat])
+ oprob2_graph = ODEProblem(lrs2_graph, sigmaB_u0, (0.0,1.0), [sigmaB_p; sigmaB_p_spat])
+ @test solve(oprob2_cartesian, QNDF()) ≈ solve(oprob2_masked, QNDF()) ≈ solve(oprob2_graph, QNDF())
+
+ # 3d lattices.
+ lrs3_cartesian = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, very_small_3d_cartesian_grid)
+ lrs3_masked = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, very_small_3d_masked_grid)
+ lrs3_graph = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, very_small_3d_graph_grid)
+
+ oprob3_cartesian = ODEProblem(lrs3_cartesian, sigmaB_u0, (0.0,1.0), [sigmaB_p; sigmaB_p_spat])
+ oprob3_masked = ODEProblem(lrs3_masked, sigmaB_u0, (0.0,1.0), [sigmaB_p; sigmaB_p_spat])
+ oprob3_graph = ODEProblem(lrs3_graph, sigmaB_u0, (0.0,1.0), [sigmaB_p; sigmaB_p_spat])
+ @test solve(oprob3_cartesian, QNDF()) ≈ solve(oprob3_masked, QNDF()) ≈ solve(oprob3_graph, QNDF())
+end
- pE_1 = [:dS => [1.3, 1.4, 2.5, 3.4, 4.5], :dI => 0.01, :dR => 0.02]
- pE_2 = [:dS => [1.3, 1.4, 2.5, 1.3, 3.4, 1.4, 3.4, 4.5, 2.5, 4.5], :dI => 0.01, :dR => 0.02]
- ss_1 = solve(ODEProblem(lrs_1, u0, (0.0, 500.0), (pV, pE_1)), Tsit5()).u[end]
- ss_2 = solve(ODEProblem(lrs_2, u0, (0.0, 500.0), (pV, pE_2)), Tsit5()).u[end]
- @test all(isapprox.(ss_1, ss_2))
+# Tests that input parameter and u0 values can be given using different types of input for 2d lattices.
+# Tries both for cartesian and masked (where all vertices are `true`).
+# Tries for Vector, Tuple, and Dictionary inputs.
+let
+ for lattice in [CartesianGrid((4,3)), fill(true, 4, 3)]
+ lrs = LatticeReactionSystem(SIR_system, SIR_srs_1, lattice)
+
+ # Initial condition values.
+ S_vals_vec = [100., 100., 200., 300., 200., 100., 200., 300., 300., 100., 200., 300.]
+ S_vals_mat = [100. 200. 300.; 100. 100. 100.; 200. 200. 200.; 300. 300. 300.]
+ SIR_u0_vec = [:S => S_vals_vec, :I => 1.0, :R => 0.0]
+ SIR_u0_mat = [:S => S_vals_mat, :I => 1.0, :R => 0.0]
+
+ # Parameter values.
+ β_vals_vec = [0.01, 0.01, 0.02, 0.03, 0.02, 0.01, 0.02, 0.03, 0.03, 0.01, 0.02, 0.03]
+ β_vals_mat = [0.01 0.02 0.03; 0.01 0.01 0.01; 0.02 0.02 0.02; 0.03 0.03 0.03]
+ SIR_p_vec = [:α => 0.1 / 1000, :β => β_vals_vec, :dS => 0.01]
+ SIR_p_mat = [:α => 0.1 / 1000, :β => β_vals_mat, :dS => 0.01]
+
+ oprob = ODEProblem(lrs, SIR_u0_vec, (0.0, 10.0), SIR_p_vec)
+ sol_base = solve(oprob, Tsit5())
+ for u0_base in [SIR_u0_vec, SIR_u0_mat], ps_base in [SIR_p_vec, SIR_p_mat]
+ for u0 in [u0_base, Tuple(u0_base), Dict(u0_base)], ps in [ps_base, Tuple(ps_base), Dict(ps_base)]
+ sol = solve(ODEProblem(lrs, u0, (0.0, 10.0), ps), Tsit5())
+ @test sol == sol_base
+ end
+ end
+ end
+end
+
+# Tests that input parameter and u0 values can be given using different types of input for 2d masked grid.
+# Tries when several of the mask values are `false`.
+let
+ lattice = [true true false; true false false; true true true; false true true]
+ lrs = LatticeReactionSystem(SIR_system, SIR_srs_1, lattice)
+
+ # Initial condition values. 999 is used for empty points.
+ S_vals_vec = [100.0, 100.0, 200.0, 200.0, 200.0, 300.0, 200.0, 300.0]
+ S_vals_mat = [100.0 200.0 999.0; 100.0 999.0 999.0; 200.0 200.0 200.0; 999.0 300.0 300.0]
+ S_vals_sparse_mat = sparse(S_vals_mat .* lattice)
+ SIR_u0_vec = [:S => S_vals_vec, :I => 1.0, :R => 0.0]
+ SIR_u0_mat = [:S => S_vals_mat, :I => 1.0, :R => 0.0]
+ SIR_u0_sparse_mat = [:S => S_vals_sparse_mat, :I => 1.0, :R => 0.0]
+
+ # Parameter values. 9.99 is used for empty points.
+ β_vals_vec = [0.01, 0.01, 0.02, 0.02, 0.02, 0.03, 0.02, 0.03]
+ β_vals_mat = [0.01 0.02 9.99; 0.01 9.99 9.99; 0.02 0.02 0.02; 9.99 0.03 0.03]
+ β_vals_sparse_mat = sparse(β_vals_mat .* lattice)
+ SIR_p_vec = [:α => 0.1 / 1000, :β => β_vals_vec, :dS => 0.01]
+ SIR_p_mat = [:α => 0.1 / 1000, :β => β_vals_mat, :dS => 0.01]
+ SIR_p_sparse_mat = [:α => 0.1 / 1000, :β => β_vals_sparse_mat, :dS => 0.01]
+
+ oprob = ODEProblem(lrs, SIR_u0_vec, (0.0, 10.0), SIR_p_vec)
+ sol = solve(oprob, Tsit5())
+ for u0 in [SIR_u0_vec, SIR_u0_mat, SIR_u0_sparse_mat]
+ for p in [SIR_p_vec, SIR_p_mat, SIR_p_sparse_mat]
+ @test sol == solve(ODEProblem(lrs, u0, (0.0, 10.0), p), Tsit5())
+ end
+ end
end
### Test Transport Reaction Types ###
@@ -280,13 +414,13 @@ let
tr_macros_1 = @transport_reaction dS S
tr_macros_2 = @transport_reaction dI I
- lrs_1 = LatticeReactionSystem(SIR_system, [tr_1, tr_2], small_2d_grid)
- lrs_2 = LatticeReactionSystem(SIR_system, [tr_macros_1, tr_macros_2], small_2d_grid)
- u0 = [:S => 990.0, :I => 20.0 * rand_v_vals(lrs_1.lattice), :R => 0.0]
+ lrs_1 = LatticeReactionSystem(SIR_system, [tr_1, tr_2], small_2d_graph_grid)
+ lrs_2 = LatticeReactionSystem(SIR_system, [tr_macros_1, tr_macros_2], small_2d_graph_grid)
+ u0 = [:S => 990.0, :I => 20.0 * rand_v_vals(lrs_1), :R => 0.0]
pV = [:α => 0.1 / 1000, :β => 0.01]
pE = [:dS => 0.01, :dI => 0.01]
- ss_1 = solve(ODEProblem(lrs_1, u0, (0.0, 500.0), (pV, pE)), Tsit5()).u[end]
- ss_2 = solve(ODEProblem(lrs_2, u0, (0.0, 500.0), (pV, pE)), Tsit5()).u[end]
+ ss_1 = solve(ODEProblem(lrs_1, u0, (0.0, 500.0), [pV; pE]), Tsit5()).u[end]
+ ss_2 = solve(ODEProblem(lrs_2, u0, (0.0, 500.0), [pV; pE]), Tsit5()).u[end]
@test all(isapprox.(ss_1, ss_2))
end
@@ -296,17 +430,17 @@ let
SIR_tr_I_alt = @transport_reaction dI1*dI2 I
SIR_tr_R_alt = @transport_reaction log(dR1)+dR2 R
SIR_srs_2_alt = [SIR_tr_S_alt, SIR_tr_I_alt, SIR_tr_R_alt]
- lrs_1 = LatticeReactionSystem(SIR_system, SIR_srs_2, small_2d_grid)
- lrs_2 = LatticeReactionSystem(SIR_system, SIR_srs_2_alt, small_2d_grid)
-
- u0 = [:S => 990.0, :I => 20.0 * rand_v_vals(lrs_1.lattice), :R => 0.0]
+ lrs_1 = LatticeReactionSystem(SIR_system, SIR_srs_2, small_2d_graph_grid)
+ lrs_2 = LatticeReactionSystem(SIR_system, SIR_srs_2_alt, small_2d_graph_grid)
+
+ u0 = [:S => 990.0, :I => 20.0 * rand_v_vals(lrs_1), :R => 0.0]
pV = [:α => 0.1 / 1000, :β => 0.01]
pE_1 = [:dS => 0.01, :dI => 0.01, :dR => 0.01]
- pE_2 = [:dS1 => 0.005, :dS1 => 0.005, :dI1 => 2, :dI2 => 0.005, :dR1 => 1.010050167084168, :dR2 => 1.0755285551056204e-16]
-
- ss_1 = solve(ODEProblem(lrs_1, u0, (0.0, 500.0), (pV, pE_1)), Tsit5()).u[end]
- ss_2 = solve(ODEProblem(lrs_2, u0, (0.0, 500.0), (pV, pE_2)), Tsit5()).u[end]
- @test all(isapprox.(ss_1, ss_2))
+ pE_2 = [:dS1 => 0.003, :dS2 => 0.007, :dI1 => 2, :dI2 => 0.005, :dR1 => 1.010050167084168, :dR2 => 1.0755285551056204e-16]
+
+ ss_1 = solve(ODEProblem(lrs_1, u0, (0.0, 500.0), [pV; pE_1]), Tsit5()).u[end]
+ ss_2 = solve(ODEProblem(lrs_2, u0, (0.0, 500.0), [pV; pE_2]), Tsit5()).u[end]
+ @test ss_1 == ss_2
end
# Tries various ways of creating TransportReactions.
@@ -335,7 +469,7 @@ let
tr_alt_1_6 = @transport_reaction dCu_ELigand Cu_ELigand
tr_alt_1_7 = @transport_reaction dNewspecies2 Newspecies2
CuH_Amination_srs_alt_1 = [tr_alt_1_1, tr_alt_1_2, tr_alt_1_3, tr_alt_1_4, tr_alt_1_5, tr_alt_1_6, tr_alt_1_7]
- lrs_1 = LatticeReactionSystem(CuH_Amination_system_alt_1, CuH_Amination_srs_alt_1, small_2d_grid)
+ lrs_1 = LatticeReactionSystem(CuH_Amination_system_alt_1, CuH_Amination_srs_alt_1, small_2d_graph_grid)
CuH_Amination_system_alt_2 = @reaction_network begin
@species Newspecies1(t) Newspecies2(t)
@@ -361,53 +495,146 @@ let
tr_alt_2_6 = TransportReaction(dCu_ELigand, Cu_ELigand)
tr_alt_2_7 = TransportReaction(dNewspecies2, Newspecies2)
CuH_Amination_srs_alt_2 = [tr_alt_2_1, tr_alt_2_2, tr_alt_2_3, tr_alt_2_4, tr_alt_2_5, tr_alt_2_6, tr_alt_2_7]
- lrs_2 = LatticeReactionSystem(CuH_Amination_system_alt_2, CuH_Amination_srs_alt_2, small_2d_grid)
+ lrs_2 = LatticeReactionSystem(CuH_Amination_system_alt_2, CuH_Amination_srs_alt_2, small_2d_graph_grid)
u0 = [CuH_Amination_u0; :Newspecies1 => 0.1; :Newspecies2 => 0.1]
pV = [CuH_Amination_p; :dLigand => 0.01; :dSilane => 0.01; :dCu_ELigand => 0.009; :dStyrene => -10000.0]
pE = [:dAmine_E => 0.011, :dNewspecies1 => 0.013, :dDecomposition => 0.015, :dNewspecies2 => 0.016, :dCuoAc => -10000.0]
- ss_1 = solve(ODEProblem(lrs_1, u0, (0.0, 500.0), (pV, pE)), Tsit5()).u[end]
- ss_2 = solve(ODEProblem(lrs_2, u0, (0.0, 500.0), (pV, pE)), Tsit5()).u[end]
+ ss_1 = solve(ODEProblem(lrs_1, u0, (0.0, 500.0), [pV; pE]), Tsit5()).u[end]
+ ss_2 = solve(ODEProblem(lrs_2, u0, (0.0, 500.0), [pV; pE]), Tsit5()).u[end]
@test all(isequal.(ss_1, ss_2))
end
+### ODEProblem & Integrator Interfacing ###
+
+# Checks that basic interfacing with ODEProblem parameters (getting and setting) works.
+let
+ # Creates an initial `ODEProblem`.
+ lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_1d_cartesian_grid)
+ u0 = [:X => 1.0, :Y => 2.0]
+ ps = [:A => 1.0, :B => [1.0, 2.0, 3.0, 4.0, 5.0], :dX => 0.1]
+ oprob = ODEProblem(lrs, u0, (0.0, 10.0), ps)
+
+ # Checks that retrieved parameters are correct.
+ @test oprob.ps[:A] == [1.0]
+ @test oprob.ps[:B] == [1.0, 2.0, 3.0, 4.0, 5.0]
+ @test oprob.ps[:dX] == sparse([1], [1], [0.1])
+
+ # Updates content.
+ oprob.ps[:A] = [10.0, 20.0, 30.0, 40.0, 50.0]
+ oprob.ps[:B] = [10.0]
+ oprob.ps[:dX] = [0.01]
+
+ # Checks that content is correct.
+ @test oprob.ps[:A] == [10.0, 20.0, 30.0, 40.0, 50.0]
+ @test oprob.ps[:B] == [10.0]
+ @test oprob.ps[:dX] == [0.01]
+end
+
+# Checks that the `rebuild_lat_internals!` function is correctly applied to an ODEProblem.
+let
+ # Creates a Brusselator `LatticeReactionSystem`.
+ lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_2, very_small_2d_cartesian_grid)
+
+ # Checks for all combinations of Jacobian and sparsity.
+ for jac in [false, true], sparse in [false, true]
+ # Creates an initial ODEProblem.
+ u0 = [:X => 1.0, :Y => [1.0 2.0; 3.0 4.0]]
+ dY_vals = spzeros(4,4)
+ dY_vals[1,2] = 0.1; dY_vals[2,1] = 0.1;
+ dY_vals[1,3] = 0.2; dY_vals[3,1] = 0.2;
+ dY_vals[2,4] = 0.3; dY_vals[4,2] = 0.3;
+ dY_vals[3,4] = 0.4; dY_vals[4,3] = 0.4;
+ ps = [:A => 1.0, :B => [4.0 5.0; 6.0 7.0], :dX => 0.1, :dY => dY_vals]
+ oprob_1 = ODEProblem(lrs, u0, (0.0, 10.0), ps; jac, sparse)
+
+ # Creates an alternative version of the ODEProblem.
+ dX_vals = spzeros(4,4)
+ dX_vals[1,2] = 0.01; dX_vals[2,1] = 0.01;
+ dX_vals[1,3] = 0.02; dX_vals[3,1] = 0.02;
+ dX_vals[2,4] = 0.03; dX_vals[4,2] = 0.03;
+ dX_vals[3,4] = 0.04; dX_vals[4,3] = 0.04;
+ ps = [:A => [1.1 1.2; 1.3 1.4], :B => 5.0, :dX => dX_vals, :dY => 0.01]
+ oprob_2 = ODEProblem(lrs, u0, (0.0, 10.0), ps; jac, sparse)
+
+ # Modifies the initial ODEProblem to be identical to the new one.
+ oprob_1.ps[:A] = [1.1 1.2; 1.3 1.4]
+ oprob_1.ps[:B] = [5.0]
+ oprob_1.ps[:dX] = dX_vals
+ oprob_1.ps[:dY] = [0.01]
+ rebuild_lat_internals!(oprob_1)
+
+ # Checks that simulations of the two `ODEProblem`s are identical.
+ @test solve(oprob_1, Rodas5P()) ≈ solve(oprob_2, Rodas5P())
+ end
+end
+
+# Checks that the `rebuild_lat_internals!` function is correctly applied to an integrator.
+# Does through by applying it within a callback, and compare to simulations without callback.
+# To keep test faster, only check for `jac = sparse = true` only.
+let
+ # Prepares problem inputs.
+ lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_2, very_small_2d_cartesian_grid)
+ u0 = [:X => 1.0, :Y => [1.0 2.0; 3.0 4.0]]
+ A1 = 1.0
+ B1 = [4.0 5.0; 6.0 7.0]
+ A2 = [1.1 1.2; 1.3 1.4]
+ B2 = 5.0
+ dY_vals = spzeros(4,4)
+ dY_vals[1,2] = 0.1; dY_vals[2,1] = 0.1;
+ dY_vals[1,3] = 0.2; dY_vals[3,1] = 0.2;
+ dY_vals[2,4] = 0.3; dY_vals[4,2] = 0.3;
+ dY_vals[3,4] = 0.4; dY_vals[4,3] = 0.4;
+ dX_vals = spzeros(4,4)
+ dX_vals[1,2] = 0.01; dX_vals[2,1] = 0.01;
+ dX_vals[1,3] = 0.02; dX_vals[3,1] = 0.02;
+ dX_vals[2,4] = 0.03; dX_vals[4,2] = 0.03;
+ dX_vals[3,4] = 0.04; dX_vals[4,3] = 0.04;
+ dX1 = 0.1
+ dY1 = dY_vals
+ dX2 = dX_vals
+ dY2 = 0.01
+ ps_1 = [:A => A1, :B => B1, :dX => dX1, :dY => dY1]
+ ps_2 = [:A => A2, :B => B2, :dX => dX2, :dY => dY2]
+
+ # Creates simulation through two different separate simulations.
+ oprob_1_1 = ODEProblem(lrs, u0, (0.0, 5.0), ps_1; jac = true, sparse = true)
+ sol_1_1 = solve(oprob_1_1, Rosenbrock23(); saveat = 1.0, abstol = 1e-8, reltol = 1e-8)
+ u0_1_2 = [:X => sol_1_1.u[end][1:2:end], :Y => sol_1_1.u[end][2:2:end]]
+ oprob_1_2 = ODEProblem(lrs, u0_1_2, (0.0, 5.0), ps_2; jac = true, sparse = true)
+ sol_1_2 = solve(oprob_1_2, Rosenbrock23(); saveat = 1.0, abstol = 1e-8, reltol = 1e-8)
+
+ # Creates simulation through a single simulation with a callback
+ oprob_2 = ODEProblem(lrs, u0, (0.0, 10.0), ps_1; jac = true, sparse = true)
+ condition(u, t, integrator) = (t == 5.0)
+ function affect!(integrator)
+ integrator.ps[:A] = A2
+ integrator.ps[:B] = [B2]
+ integrator.ps[:dX] = dX2
+ integrator.ps[:dY] = [dY2]
+ rebuild_lat_internals!(integrator)
+ end
+ callback = DiscreteCallback(condition, affect!)
+ sol_2 = solve(oprob_2, Rosenbrock23(); saveat = 1.0, tstops = [5.0], callback, abstol = 1e-8, reltol = 1e-8)
+
+ # Check that trajectories are equivalent.
+ @test [sol_1_1.u; sol_1_2.u] ≈ sol_2.u
+end
+
### Tests Special Cases ###
-# Create network with various combinations of graph/di-graph and parameters.
+# Create networks using either graphs or di-graphs.
let
lrs_digraph = LatticeReactionSystem(SIR_system, SIR_srs_2, complete_digraph(3))
lrs_graph = LatticeReactionSystem(SIR_system, SIR_srs_2, complete_graph(3))
- u0 = [:S => 990.0, :I => 20.0 * rand_v_vals(lrs_digraph.lattice), :R => 0.0]
+ u0 = [:S => 990.0, :I => 20.0 * rand_v_vals(lrs_digraph), :R => 0.0]
pV = SIR_p
- pE_digraph_1 = [:dS => [0.10, 0.12, 0.10, 0.14, 0.12, 0.14], :dI => 0.01, :dR => 0.01]
- pE_digraph_2 = [[0.10, 0.12, 0.10, 0.14, 0.12, 0.14], 0.01, 0.01]
- pE_digraph_3 = [0.10 0.12 0.10 0.14 0.12 0.14; 0.01 0.01 0.01 0.01 0.01 0.01; 0.01 0.01 0.01 0.01 0.01 0.01]
- pE_graph_1 = [:dS => [0.10, 0.12, 0.14], :dI => 0.01, :dR => 0.01]
- pE_graph_2 = [[0.10, 0.12, 0.14], 0.01, 0.01]
- pE_graph_3 = [0.10 0.12 0.14; 0.01 0.01 0.01; 0.01 0.01 0.01]
- oprob_digraph_1 = ODEProblem(lrs_digraph, u0, (0.0, 500.0), (pV, pE_digraph_1))
- oprob_digraph_2 = ODEProblem(lrs_digraph, u0, (0.0, 500.0), (pV, pE_digraph_2))
- oprob_digraph_3 = ODEProblem(lrs_digraph, u0, (0.0, 500.0), (pV, pE_digraph_3))
- oprob_graph_11 = ODEProblem(lrs_graph, u0, (0.0, 500.0), (pV, pE_digraph_1))
- oprob_graph_12 = ODEProblem(lrs_graph, u0, (0.0, 500.0), (pV, pE_graph_1))
- oprob_graph_21 = ODEProblem(lrs_graph, u0, (0.0, 500.0), (pV, pE_digraph_2))
- oprob_graph_22 = ODEProblem(lrs_graph, u0, (0.0, 500.0), (pV, pE_graph_2))
- oprob_graph_31 = ODEProblem(lrs_graph, u0, (0.0, 500.0), (pV, pE_digraph_3))
- oprob_graph_32 = ODEProblem(lrs_graph, u0, (0.0, 500.0), (pV, pE_graph_3))
- sim_end_digraph_1 = solve(oprob_digraph_1, Tsit5()).u[end]
- sim_end_digraph_2 = solve(oprob_digraph_2, Tsit5()).u[end]
- sim_end_digraph_3 = solve(oprob_digraph_3, Tsit5()).u[end]
- sim_end_graph_11 = solve(oprob_graph_11, Tsit5()).u[end]
- sim_end_graph_12 = solve(oprob_graph_12, Tsit5()).u[end]
- sim_end_graph_21 = solve(oprob_graph_21, Tsit5()).u[end]
- sim_end_graph_22 = solve(oprob_graph_22, Tsit5()).u[end]
- sim_end_graph_31 = solve(oprob_graph_31, Tsit5()).u[end]
- sim_end_graph_32 = solve(oprob_graph_32, Tsit5()).u[end]
-
- @test all(sim_end_digraph_1 .== sim_end_digraph_2 .== sim_end_digraph_3 .==
- sim_end_graph_11 .== sim_end_graph_12 .== sim_end_graph_21 .==
- sim_end_graph_22 .== sim_end_graph_31 .== sim_end_graph_32)
+ pE = [:dS => 0.10, :dI => 0.01, :dR => 0.01]
+ oprob_digraph = ODEProblem(lrs_digraph, u0, (0.0, 500.0), [pV; pE])
+ oprob_graph = ODEProblem(lrs_graph, u0, (0.0, 500.0), [pV; pE])
+
+ @test solve(oprob_digraph, Tsit5()) == solve(oprob_graph, Tsit5())
end
# Creates networks where some species or parameters have no effect on the system.
@@ -424,194 +651,131 @@ let
TransportReaction(dZ, Z),
TransportReaction(dV, V),
]
- lrs_alt = LatticeReactionSystem(binding_system_alt, binding_srs_alt, small_2d_grid)
+ lrs_alt = LatticeReactionSystem(binding_system_alt, binding_srs_alt, small_2d_graph_grid)
u0_alt = [
:X => 1.0,
- :Y => 2.0 * rand_v_vals(lrs_alt.lattice),
+ :Y => 2.0 * rand_v_vals(lrs_alt),
:XY => 0.5,
- :Z => 2.0 * rand_v_vals(lrs_alt.lattice),
+ :Z => 2.0 * rand_v_vals(lrs_alt),
:V => 0.5,
:W => 1.0,
]
p_alt = [
:k1 => 2.0,
- :k2 => 0.1 .+ rand_v_vals(lrs_alt.lattice),
- :dX => 1.0 .+ rand_e_vals(lrs_alt.lattice),
+ :k2 => 0.1 .+ rand_v_vals(lrs_alt),
+ :dX => rand_e_vals(lrs_alt),
:dXY => 3.0,
- :dZ => rand_e_vals(lrs_alt.lattice),
+ :dZ => rand_e_vals(lrs_alt),
:dV => 0.2,
:p1 => 1.0,
- :p2 => rand_v_vals(lrs_alt.lattice),
+ :p2 => rand_v_vals(lrs_alt),
]
oprob_alt = ODEProblem(lrs_alt, u0_alt, (0.0, 10.0), p_alt)
- ss_alt = solve(oprob_alt, Tsit5()).u[end]
-
+ ss_alt = solve(oprob_alt, Tsit5(); abstol=1e-9, reltol=1e-9).u[end]
+
binding_srs_main = [TransportReaction(dX, X), TransportReaction(dXY, XY)]
- lrs = LatticeReactionSystem(binding_system, binding_srs_main, small_2d_grid)
+ lrs = LatticeReactionSystem(binding_system, binding_srs_main, small_2d_graph_grid)
u0 = u0_alt[1:3]
p = p_alt[1:4]
oprob = ODEProblem(lrs, u0, (0.0, 10.0), p)
- ss = solve(oprob, Tsit5()).u[end]
-
+ ss = solve(oprob, Tsit5(); abstol=1e-9, reltol=1e-9).u[end]
+
+ i = 3
+ ss_alt[((i - 1) * 6 + 1):((i - 1) * 6 + 3)] ≈ ss[((i - 1) * 3 + 1):((i - 1) * 3 + 3)]
+
for i in 1:25
- @test isapprox(ss_alt[((i - 1) * 6 + 1):((i - 1) * 6 + 3)],
- ss[((i - 1) * 3 + 1):((i - 1) * 3 + 3)]) < 1e-3
+ @test ss_alt[((i - 1) * 6 + 1):((i - 1) * 6 + 3)] ≈ ss[((i - 1) * 3 + 1):((i - 1) * 3 + 3)]
end
end
-# Provides initial conditions and parameters in various different ways.
+# Tests with non-Float64 parameter values.
+# Tests for all Jacobian/sparsity combinations.
+# Tests for parameters with/without uniform values.
let
- lrs = LatticeReactionSystem(SIR_system, SIR_srs_2, very_small_2d_grid)
- u0_1 = [:S => 990.0, :I => [1.0, 3.0, 2.0, 5.0], :R => 0.0]
- u0_2 = [990.0, [1.0, 3.0, 2.0, 5.0], 0.0]
- u0_3 = [990.0 990.0 990.0 990.0; 1.0 3.0 2.0 5.0; 0.0 0.0 0.0 0.0]
- pV_1 = [:α => 0.1 / 1000, :β => [0.01, 0.02, 0.01, 0.03]]
- pV_2 = [0.1 / 1000, [0.01, 0.02, 0.01, 0.03]]
- pV_3 = [0.1/1000 0.1/1000 0.1/1000 0.1/1000; 0.01 0.02 0.01 0.03]
- pE_1 = [:dS => [0.01, 0.02, 0.03, 0.04], :dI => 0.01, :dR => 0.01]
- pE_2 = [[0.01, 0.02, 0.03, 0.04], :0.01, 0.01]
- pE_3 = [0.01 0.02 0.03 0.04; 0.01 0.01 0.01 0.01; 0.01 0.01 0.01 0.01]
-
- p1 = [
- :α => 0.1 / 1000,
- :β => [0.01, 0.02, 0.01, 0.03],
- :dS => [0.01, 0.02, 0.03, 0.04],
- :dI => 0.01,
- :dR => 0.01,
- ]
- ss_1_1 = solve(ODEProblem(lrs, u0_1, (0.0, 1.0), p1), Tsit5()).u[end]
- for u0 in [u0_1, u0_2, u0_3], pV in [pV_1, pV_2, pV_3], pE in [pE_1, pE_2, pE_3]
- ss = solve(ODEProblem(lrs, u0, (0.0, 1.0), (pV, pE)), Tsit5()).u[end]
- @test all(isequal.(ss, ss_1_1))
+ lrs = LatticeReactionSystem(SIR_system, SIR_srs_2, very_small_2d_cartesian_grid)
+ u0 = [:S => 990.0, :I => rand_v_vals(lrs), :R => 0.0]
+ ps_1 = [:α => 0.1, :β => 0.01, :dS => 0.01, :dI => 0.01, :dR => 0.01]
+ ps_2 = [:α => 1//10, :β => 1//100, :dS => 1//100, :dI => 1//100, :dR => 1//100]
+ ps_3 = [:α => 1//10, :β => 0.01, :dS => 0.01, :dI => 1//100, :dR => 0.01]
+ sol_base = solve(ODEProblem(lrs, u0, (0.0, 100.0), ps_1), Rosenbrock23(); saveat = 0.1)
+ for ps in [ps_1, ps_2, ps_3]
+ for jac in [true, false], sparse in [true, false]
+ oprob = ODEProblem(lrs, u0, (0.0, 100.0), ps; jac, sparse)
+ @test sol_base ≈ solve(oprob, Rosenbrock23(); saveat = 0.1)
+ end
end
end
-# Confirms parameters can be provided in [pV; pE] and (pV, pE) form.
-let
- lrs = LatticeReactionSystem(SIR_system, SIR_srs_2, small_2d_grid)
- u0 = [:S => 990.0, :I => 20.0 * rand_v_vals(lrs.lattice), :R => 0.0]
- p1 = ([:α => 0.1 / 1000, :β => 0.01], [:dS => 0.01, :dI => 0.01, :dR => 0.01])
- p2 = [:α => 0.1 / 1000, :β => 0.01, :dS => 0.01, :dI => 0.01, :dR => 0.01]
- oprob1 = ODEProblem(lrs, u0, (0.0, 500.0), p1; jac = false)
- oprob2 = ODEProblem(lrs, u0, (0.0, 500.0), p2; jac = false)
-
- @test all(isapprox.(solve(oprob1, Tsit5()).u[end], solve(oprob2, Tsit5()).u[end]))
-end
-
-### Compare to Hand-written Functions ###
-
-# Compares the brusselator for a line of cells.
+# Tests various types of numbers for initial conditions/parameters (e.g. Real numbers, Float32, etc.).
let
- function spatial_brusselator_f(du, u, p, t)
- # Non-spatial
- for i in 1:2:(length(u) - 1)
- du[i] = p[1] + 0.5 * (u[i]^2) * u[i + 1] - u[i] - p[2] * u[i]
- du[i + 1] = p[2] * u[i] - 0.5 * (u[i]^2) * u[i + 1]
- end
-
- # Spatial
- du[1] += p[3] * (u[3] - u[1])
- du[end - 1] += p[3] * (u[end - 3] - u[end - 1])
- for i in 3:2:(length(u) - 3)
- du[i] += p[3] * (u[i - 2] + u[i + 2] - 2u[i])
- end
- end
- function spatial_brusselator_jac(J, u, p, t)
- J .= 0
- # Non-spatial
- for i in 1:2:(length(u) - 1)
- J[i, i] = u[i] * u[i + 1] - 1 - p[2]
- J[i, i + 1] = 0.5 * (u[i]^2)
- J[i + 1, i] = p[2] - u[i] * u[i + 1]
- J[i + 1, i + 1] = -0.5 * (u[i]^2)
- end
-
- # Spatial
- J[1, 1] -= p[3]
- J[1, 3] += p[3]
- J[end - 1, end - 1] -= p[3]
- J[end - 1, end - 3] += p[3]
- for i in 3:2:(length(u) - 3)
- J[i, i] -= 2 * p[3]
- J[i, i - 2] += p[3]
- J[i, i + 2] += p[3]
- end
- end
- function spatial_brusselator_jac_sparse(J, u, p, t)
- # Spatial
- J.nzval .= 0.0
- J.nzval[7:6:(end - 9)] .= -2p[3]
- J.nzval[1] = -p[3]
- J.nzval[end - 3] = -p[3]
- J.nzval[3:3:(end - 4)] .= p[3]
-
- # Non-spatial
- for i in 1:1:Int64(lenth(u) / 2 - 1)
- j = 6(i - 1) + 1
- J.nzval[j] = u[i] * u[i + 1] - 1 - p[2]
- J.nzval[j + 1] = 0.5 * (u[i]^2)
- J.nzval[j + 3] = p[2] - u[i] * u[i + 1]
- J.nzval[j + 4] = -0.5 * (u[i]^2)
+ # Declare u0 versions.
+ u0_Int64 = [:X => 2, :Y => [1, 1, 1, 2]]
+ u0_Float64 = [:X => 2.0, :Y => [1.0, 1.0, 1.0, 2.0]]
+ u0_Int32 = [:X => Int32(2), :Y => Int32.([1, 1, 1, 2])]
+ u0_Any = Pair{Symbol,Any}[:X => 2.0, :Y => [1.0, 1.0, 1.0, 2.0]]
+ u0s = (u0_Int64, u0_Float64, u0_Int32, u0_Any)
+
+ # Declare parameter versions.
+ dY_vals = spzeros(4,4)
+ dY_vals[1,2] = 1; dY_vals[2,1] = 1;
+ dY_vals[1,3] = 1; dY_vals[3,1] = 1;
+ dY_vals[2,4] = 1; dY_vals[4,2] = 1;
+ dY_vals[3,4] = 2; dY_vals[4,3] = 2;
+ p_Int64 = (:A => [1, 1, 1, 2], :B => 4, :dX => 1, :dY => Int64.(dY_vals))
+ p_Float64 = (:A => [1.0, 1.0, 1.0, 2.0], :B => 4.0, :dX => 1.0, :dY => Float64.(dY_vals))
+ p_Int32 = (:A => Int32.([1, 1, 1, 2]), :B => Int32(4), :dX => Int32(1), :dY => Int32.(dY_vals))
+ p_Any = Pair{Symbol,Any}[:A => [1.0, 1.0, 1.0, 2.0], :B => 4.0, :dX => 1.0, :dY => dY_vals]
+ ps = (p_Int64, p_Float64, p_Int32, p_Any)
+
+ # Creates a base solution to compare all solution to.
+ lrs_base = LatticeReactionSystem(brusselator_system, brusselator_srs_2, very_small_2d_graph_grid)
+ oprob_base = ODEProblem(lrs_base, u0s[1], (0.0, 1.0), ps[1])
+ sol_base = solve(oprob_base, QNDF(); saveat = 0.01)
+
+ # Checks all combinations of input types.
+ lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_2, very_small_2d_cartesian_grid)
+ for u0_base in u0s, p_base in ps
+ for u0 in [u0_base, Tuple(u0_base), Dict(u0_base)], p in [p_base, Dict(p_base)]
+ oprob = ODEProblem(lrs, u0, (0.0, 1.0), p; sparse = true, jac = true)
+ sol = solve(oprob, QNDF(); saveat = 0.01)
+ @test sol.u ≈ sol_base.u atol = 1e-6 rtol = 1e-6
end
- J.nzval[end - 3] = u[end - 1] * u[end] - 1 - p[end - 1]
- J.nzval[end - 2] = 0.5 * (u[end - 1]^2)
- J.nzval[end - 1] = p[2] - u[end - 1] * u[end]
- J.nzval[end] = -0.5 * (u[end - 1]^2)
end
- function make_jac_prototype(u0)
- jac_prototype_pre = zeros(length(u0), length(u0))
- for i in 1:2:(length(u0) - 1)
- jac_prototype_pre[i, i] = 1
- jac_prototype_pre[i + 1, i] = 1
- jac_prototype_pre[i, i + 1] = 1
- jac_prototype_pre[i + 1, i + 1] = 1
- end
- for i in 3:2:(length(u0) - 1)
- jac_prototype_pre[i - 2, i] = 1
- jac_prototype_pre[i, i - 2] = 1
- end
- return sparse(jac_prototype_pre)
- end
-
- u0 = 2 * rand(rng, 10000)
- p = [1.0, 4.0, 0.1]
- tspan = (0.0, 100.0)
-
- ofun_hw_dense = ODEFunction(spatial_brusselator_f; jac = spatial_brusselator_jac)
- ofun_hw_sparse = ODEFunction(spatial_brusselator_f; jac = spatial_brusselator_jac,
- jac_prototype = make_jac_prototype(u0))
-
- lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1,
- path_graph(Int64(length(u0) / 2)))
- u0V = [:X => u0[1:2:(end - 1)], :Y => u0[2:2:end]]
- pV = [:A => p[1], :B => p[2]]
- pE = [:dX => p[3]]
- ofun_aut_dense = ODEProblem(lrs, u0V, tspan, (pV, pE); jac = true, sparse = false).f
- ofun_aut_sparse = ODEProblem(lrs, u0V, tspan, (pV, pE); jac = true, sparse = true).f
-
- du_hw_dense = deepcopy(u0)
- du_hw_sparse = deepcopy(u0)
- du_aut_dense = deepcopy(u0)
- du_aut_sparse = deepcopy(u0)
-
- ofun_hw_dense(du_hw_dense, u0, p, 0.0)
- ofun_hw_sparse(du_hw_sparse, u0, p, 0.0)
- ofun_aut_dense(du_aut_dense, u0, p, 0.0)
- ofun_aut_sparse(du_aut_sparse, u0, p, 0.0)
-
- @test isapprox(du_hw_dense, du_aut_dense)
- @test isapprox(du_hw_sparse, du_aut_sparse)
+end
- J_hw_dense = deepcopy(zeros(length(u0), length(u0)))
- J_hw_sparse = deepcopy(make_jac_prototype(u0))
- J_aut_dense = deepcopy(zeros(length(u0), length(u0)))
- J_aut_sparse = deepcopy(make_jac_prototype(u0))
- ofun_hw_dense.jac(J_hw_dense, u0, p, 0.0)
- ofun_hw_sparse.jac(J_hw_sparse, u0, p, 0.0)
- ofun_aut_dense.jac(J_aut_dense, u0, p, 0.0)
- ofun_aut_sparse.jac(J_aut_sparse, u0, p, 0.0)
+### Error Tests ###
- @test isapprox(J_hw_dense, J_aut_dense)
- @test isapprox(J_hw_sparse, J_aut_sparse)
+# Checks that attempting to remove conserved quantities yields an error.
+let
+ lrs = LatticeReactionSystem(binding_system, binding_srs, very_small_2d_masked_grid)
+ @test_throws ArgumentError ODEProblem(lrs, binding_u0, (0.0, 10.0), binding_p; remove_conserved = true)
end
+
+# Checks that various erroneous inputs to `ODEProblem` yields errors.
+let
+ # Create `LatticeReactionSystem`.
+ @parameters d1 d2 D [edgeparameter=true]
+ @species X1(t) X2(t)
+ rxs = [Reaction(d1, [X1], [])]
+ @named rs = ReactionSystem(rxs, t)
+ rs = complete(rs)
+ lrs = LatticeReactionSystem(rs, [TransportReaction(D, X1)], CartesianGrid((4,)))
+
+ # Attempts to create `ODEProblem` using various faulty inputs.
+ u0 = [X1 => 1.0]
+ tspan = (0.0, 1.0)
+ ps = [d1 => 1.0, D => 0.1]
+ @test_throws ArgumentError ODEProblem(lrs, [1.0], tspan, ps)
+ @test_throws ArgumentError ODEProblem(lrs, u0, tspan, [1.0, 0.1])
+ @test_throws ArgumentError ODEProblem(lrs, [X1 => 1.0, X2 => 2.0], tspan, ps)
+ @test_throws ArgumentError ODEProblem(lrs, u0, tspan, [d1 => 1.0, d2 => 0.2, D => 0.1])
+ @test_throws ArgumentError ODEProblem(lrs, [X1 => [1.0, 2.0, 3.0]], tspan, ps)
+ @test_throws ArgumentError ODEProblem(lrs, u0, tspan, [d1 => [1.0, 2.0, 3.0], D => 0.1])
+ @test_throws ArgumentError ODEProblem(lrs, [X1 => [1.0 2.0; 3.0 4.0]], tspan, ps)
+ @test_throws ArgumentError ODEProblem(lrs, u0, tspan, [d1 => [1.0 2.0; 3.0 4.0], D => 0.1])
+ bad_D_vals_1 = sparse([0.0 1.0 0.0 1.0; 1.0 0.0 1.0 0.0; 0.0 1.0 0.0 1.0; 1.0 0.0 1.0 0.0])
+ @test_throws ArgumentError ODEProblem(lrs, u0, tspan, [d1 => 1.0, D => bad_D_vals_1])
+ bad_D_vals_2 = sparse([0.0 0.0 0.0 1.0; 1.0 0.0 1.0 0.0; 0.0 1.0 0.0 1.0; 1.0 0.0 0.0 0.0])
+ @test_throws ArgumentError ODEProblem(lrs, u0, tspan, [d1 => 1.0, D => bad_D_vals_2])
+end
\ No newline at end of file
diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl b/test/spatial_modelling/lattice_reaction_systems_jumps.jl
similarity index 57%
rename from test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl
rename to test/spatial_modelling/lattice_reaction_systems_jumps.jl
index 8fc019d8bf..576e543f40 100644
--- a/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl
+++ b/test/spatial_modelling/lattice_reaction_systems_jumps.jl
@@ -1,8 +1,7 @@
### Preparations ###
# Fetch packages.
-using JumpProcesses
-using Random, Statistics, SparseArrays, Test
+using JumpProcesses, Statistics, SparseArrays, Test
# Fetch test networks.
include("../spatial_test_networks.jl")
@@ -12,29 +11,29 @@ include("../spatial_test_networks.jl")
# Tests that there are no errors during runs for a variety of input forms.
let
- for grid in [small_2d_grid, short_path, small_directed_cycle]
+ for grid in [small_2d_graph_grid, small_2d_cartesian_grid, small_2d_masked_grid]
for srs in [Vector{TransportReaction}(), SIR_srs_1, SIR_srs_2]
lrs = LatticeReactionSystem(SIR_system, srs, grid)
u0_1 = [:S => 999, :I => 1, :R => 0]
- u0_2 = [:S => round.(Int64, 500.0 .+ 500.0 * rand_v_vals(lrs.lattice)), :I => 1, :R => 0, ]
- u0_3 = [:S => 950, :I => round.(Int64, 50 * rand_v_vals(lrs.lattice)), :R => round.(Int64, 50 * rand_v_vals(lrs.lattice))]
- u0_4 = [:S => round.(500.0 .+ 500.0 * rand_v_vals(lrs.lattice)), :I => round.(50 * rand_v_vals(lrs.lattice)), :R => round.(50 * rand_v_vals(lrs.lattice))]
- u0_5 = make_u0_matrix(u0_3, vertices(lrs.lattice), map(s -> Symbol(s.f), species(lrs.rs)))
- for u0 in [u0_1, u0_2, u0_3, u0_4, u0_5]
- p1 = [:α => 0.1 / 1000, :β => 0.01]
- p2 = [:α => 0.1 / 1000, :β => 0.02 * rand_v_vals(lrs.lattice)]
- p3 = [
- :α => 0.1 / 2000 * rand_v_vals(lrs.lattice),
- :β => 0.02 * rand_v_vals(lrs.lattice),
+ u0_2 = [:S => round.(Int64, 500 .+ 500 * rand_v_vals(lrs)), :I => 1, :R => 0]
+ u0_3 = [
+ :S => round.(Int64, 500 .+ 500 * rand_v_vals(lrs)),
+ :I => round.(Int64, 50 * rand_v_vals(lrs)),
+ :R => round.(Int64, 50 * rand_v_vals(lrs)),
+ ]
+ for u0 in [u0_1, u0_2, u0_3]
+ pV_1 = [:α => 0.1 / 1000, :β => 0.01]
+ pV_2 = [:α => 0.1 / 1000, :β => 0.02 * rand_v_vals(lrs)]
+ pV_3 = [
+ :α => 0.1 / 2000 * rand_v_vals(lrs),
+ :β => 0.02 * rand_v_vals(lrs),
]
- p4 = make_u0_matrix(p1, vertices(lrs.lattice), Symbol.(parameters(lrs.rs)))
- for pV in [p1] #, p2, p3, p4] # Removed until spatial non-diffusion parameters are supported.
- pE_1 = map(sp -> sp => 0.01, ModelingToolkit.getname.(edge_parameters(lrs)))
- pE_2 = map(sp -> sp => 0.01, ModelingToolkit.getname.(edge_parameters(lrs)))
- pE_3 = map(sp -> sp => rand_e_vals(lrs.lattice, 0.01), ModelingToolkit.getname.(edge_parameters(lrs)))
- pE_4 = make_u0_matrix(pE_3, edges(lrs.lattice), ModelingToolkit.getname.(edge_parameters(lrs)))
- for pE in [pE_1, pE_2, pE_3, pE_4]
- dprob = DiscreteProblem(lrs, u0, (0.0, 100.0), (pV, pE))
+ for pV in [pV_1, pV_2, pV_3]
+ pE_1 = [sp => 0.01 for sp in spatial_param_syms(lrs)]
+ pE_2 = [sp => rand_e_vals(lrs)/50.0 for sp in spatial_param_syms(lrs)]
+ for pE in [pE_1, pE_2]
+ isempty(spatial_param_syms(lrs)) && (pE = Vector{Pair{Symbol, Float64}}())
+ dprob = DiscreteProblem(lrs, u0, (0.0, 1.0), [pV; pE])
jprob = JumpProblem(lrs, dprob, NSM())
@test SciMLBase.successful_retcode(solve(jprob, SSAStepper()))
end
@@ -51,47 +50,31 @@ end
# In this base case, hopping rates should be on the form D_{s,i,j}.
let
# Prepares the system.
- lrs = LatticeReactionSystem(SIR_system, SIR_srs_2, small_2d_grid)
+ grid = small_2d_graph_grid
+ lrs = LatticeReactionSystem(SIR_system, SIR_srs_2, grid)
# Prepares various u0 input types.
u0_1 = [:I => 2.0, :S => 1.0, :R => 3.0]
- u0_2 = [:I => fill(2., nv(small_2d_grid)), :S => 1.0, :R => 3.0]
- u0_3 = [1.0, 2.0, 3.0]
- u0_4 = [1.0, fill(2., nv(small_2d_grid)), 3.0]
- u0_5 = permutedims(hcat(fill(1., nv(small_2d_grid)), fill(2., nv(small_2d_grid)), fill(3., nv(small_2d_grid))))
+ u0_2 = [:I => fill(2., nv(grid)), :S => 1.0, :R => 3.0]
# Prepare various (compartment) parameter input types.
pV_1 = [:β => 0.2, :α => 0.1]
- pV_2 = [:β => fill(0.2, nv(small_2d_grid)), :α => 1.0]
- pV_3 = [0.1, 0.2]
- pV_4 = [0.1, fill(0.2, nv(small_2d_grid))]
- pV_5 = permutedims(hcat(fill(0.1, nv(small_2d_grid)), fill(0.2, nv(small_2d_grid))))
+ pV_2 = [:β => fill(0.2, nv(grid)), :α => 1.0]
# Prepare various (diffusion) parameter input types.
pE_1 = [:dI => 0.02, :dS => 0.01, :dR => 0.03]
- pE_2 = [:dI => 0.02, :dS => fill(0.01, ne(small_2d_grid)), :dR => 0.03]
- pE_3 = [0.01, 0.02, 0.03]
- pE_4 = [fill(0.01, ne(small_2d_grid)), 0.02, 0.03]
- pE_5 = permutedims(hcat(fill(0.01, ne(small_2d_grid)), fill(0.02, ne(small_2d_grid)), fill(0.03, ne(small_2d_grid))))
+ dS_vals = spzeros(num_verts(lrs), num_verts(lrs))
+ foreach(e -> (dS_vals[e[1], e[2]] = 0.01), edge_iterator(lrs))
+ pE_2 = [:dI => 0.02, :dS => dS_vals, :dR => 0.03]
# Checks hopping rates and u0 are correct.
true_u0 = [fill(1.0, 1, 25); fill(2.0, 1, 25); fill(3.0, 1, 25)]
- true_hopping_rates = cumsum.([fill(dval, length(v)) for dval in [0.01,0.02,0.03], v in small_2d_grid.fadjlist])
+ true_hopping_rates = cumsum.([fill(dval, length(v)) for dval in [0.01,0.02,0.03], v in grid.fadjlist])
true_maj_scaled_rates = [0.1, 0.2]
true_maj_reactant_stoch = [[1 => 1, 2 => 1], [2 => 1]]
true_maj_net_stoch = [[1 => -1, 2 => 1], [2 => -1, 3 => 1]]
- for u0 in [u0_1, u0_2, u0_3, u0_4, u0_5]
- # Provides parameters as a tupple.
- for pV in [pV_1, pV_3], pE in [pE_1, pE_2, pE_3, pE_4, pE_5]
- dprob = DiscreteProblem(lrs, u0, (0.0, 100.0), (pV,pE))
- jprob = JumpProblem(lrs, dprob, NSM())
- @test jprob.prob.u0 == true_u0
- @test jprob.discrete_jump_aggregation.hop_rates.hop_const_cumulative_sums == true_hopping_rates
- @test jprob.massaction_jump.reactant_stoch == true_maj_reactant_stoch
- @test all(issetequal(ns1, ns2) for (ns1, ns2) in zip(jprob.massaction_jump.net_stoch, true_maj_net_stoch))
- end
- # Provides parameters as a combined vector.
- for pV in [pV_1], pE in [pE_1, pE_2]
+ for u0 in [u0_1, u0_2]
+ for pV in [pV_1, pV_2], pE in [pE_1, pE_2]
dprob = DiscreteProblem(lrs, u0, (0.0, 100.0), [pE; pV])
jprob = JumpProblem(lrs, dprob, NSM())
@test jprob.prob.u0 == true_u0
@@ -105,7 +88,7 @@ end
### SpatialMassActionJump Testing ###
-# Checks that the correct structure is produced.
+# Checks that the correct structures are produced.
let
# Network for reference:
# A, ∅ → X
@@ -114,12 +97,12 @@ let
# 1, X → ∅
# srs = [@transport_reaction dX X]
# Create LatticeReactionSystem
- lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_3d_grid)
+ lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_3d_graph_grid)
# Create JumpProblem
- u0 = [:X => 1, :Y => rand(1:10, lrs.num_verts)]
+ u0 = [:X => 1, :Y => rand(1:10, num_verts(lrs))]
tspan = (0.0, 100.0)
- ps = [:A => 1.0, :B => 5.0 .+ rand(lrs.num_verts), :dX => rand(lrs.num_edges)]
+ ps = [:A => 1.0, :B => 5.0 .+ rand_v_vals(lrs), :dX => rand_e_vals(lrs)]
dprob = DiscreteProblem(lrs, u0, tspan, ps)
jprob = JumpProblem(lrs, dprob, NSM())
@@ -127,21 +110,22 @@ let
jprob.massaction_jump.uniform_rates == [1.0, 0.5 ,10.] # 0.5 is due to combinatoric /2! in (2X + Y).
jprob.massaction_jump.spatial_rates[1,:] == ps[2][2]
# Test when new SII functions are ready, or we implement them in Catalyst.
- # @test isequal(to_int(getfield.(reactions(lrs.rs), :netstoich)), jprob.massaction_jump.net_stoch)
- # @test isequal(to_int(Pair.(getfield.(reactions(lrs.rs), :substrates),getfield.(reactions(lrs.rs), :substoich))), jprob.massaction_jump.net_stoch)
+ # @test isequal(to_int(getfield.(reactions(reactionsystem(lrs)), :netstoich)), jprob.massaction_jump.net_stoch)
+ # @test isequal(to_int(Pair.(getfield.(reactions(reactionsystem(lrs)), :substrates),getfield.(reactions(reactionsystem(lrs)), :substoich))), jprob.massaction_jump.net_stoch)
- # Checks that problem can be simulated.
+ # Checks that problems can be simulated.
@test SciMLBase.successful_retcode(solve(jprob, SSAStepper()))
end
-# Checks that simulations gives a correctly heterogeneous solution.
+# Checks that heterogeneous vertex parameters work. Checks that birth-death system with different
+# birth rates produce different means.
let
# Create model.
birth_death_network = @reaction_network begin
(p,d), 0 <--> X
end
srs = [(@transport_reaction D X)]
- lrs = LatticeReactionSystem(birth_death_network, srs, very_small_2d_grid)
+ lrs = LatticeReactionSystem(birth_death_network, srs, very_small_2d_graph_grid)
# Create JumpProblem.
u0 = [:X => 1]
@@ -153,7 +137,7 @@ let
# Simulate model (a few repeats to ensure things don't succeed by change for uniform rates).
# Check that higher p gives higher mean.
for i = 1:5
- sol = solve(jprob, SSAStepper(); saveat = 1., seed = i*1234)
+ sol = solve(jprob, SSAStepper(); saveat = 1.)
@test mean(getindex.(sol.u, 1)) < mean(getindex.(sol.u, 2)) < mean(getindex.(sol.u, 3)) < mean(getindex.(sol.u, 4))
end
end
@@ -192,7 +176,7 @@ let
tspan = (0.0, 10.0)
pV = [:kB => rates[1], :kD => rates[2]]
pE = [:D => diffusivity]
- dprob = DiscreteProblem(lrs, u0, tspan, (pV, pE))
+ dprob = DiscreteProblem(lrs, u0, tspan, [pV; pE])
# NRM could be added, but doesn't work. Might need Cartesian grid.
jump_problems = [JumpProblem(lrs, dprob, alg(); save_positions = (false, false)) for alg in [NSM, DirectCRDirect]]
@@ -214,4 +198,38 @@ let
@test abs(d) < reltol * non_spatial_mean[i]
end
end
+end
+
+
+### JumpProblem & Integrator Interfacing ###
+
+# Currently not supported, check that corresponding functions yield errors.
+let
+ # Prepare `LatticeReactionSystem`.
+ rs = @reaction_network begin
+ (k1,k2), X1 <--> X2
+ end
+ tr = @transport_reaction D X1
+ grid = CartesianGrid((2,2))
+ lrs = LatticeReactionSystem(rs, [tr], grid)
+
+ # Create problems.
+ u0 = [:X1 => 2, :X2 => [5 6; 7 8]]
+ tspan = (0.0, 10.0)
+ ps = [:k1 => 1.5, :k2 => [1.0 1.5; 2.0 3.5], :D => 0.1]
+ dprob = DiscreteProblem(lrs, u0, tspan, ps)
+ jprob = JumpProblem(lrs, dprob, NSM())
+
+ # Checks that rebuilding errors.
+ @test_throws Exception rebuild_lat_internals!(dprob)
+ @test_throws Exception rebuild_lat_internals!(jprob)
+end
+
+### Other Tests ###
+
+# Checks that providing a non-spatial `DiscreteProblem` to a `JumpProblem` gives an error.
+let
+ lrs = LatticeReactionSystem(binding_system, binding_srs, very_small_2d_masked_grid)
+ dprob = DiscreteProblem(binding_system, binding_u0, (0.0, 10.0), binding_p[1:2])
+ @test_throws ArgumentError JumpProblem(lrs, dprob, NSM())
end
\ No newline at end of file
diff --git a/test/spatial_modelling/lattice_reaction_systems_lattice_types.jl b/test/spatial_modelling/lattice_reaction_systems_lattice_types.jl
new file mode 100644
index 0000000000..dbdc233b89
--- /dev/null
+++ b/test/spatial_modelling/lattice_reaction_systems_lattice_types.jl
@@ -0,0 +1,260 @@
+### Preparations ###
+
+# Fetch packages.
+using Catalyst, Graphs, OrdinaryDiffEq, Test
+
+# Fetch test networks.
+include("../spatial_test_networks.jl")
+
+
+### Run Tests ###
+
+# Test errors when attempting to create networks with dimensions > 3.
+let
+ @test_throws Exception LatticeReactionSystem(brusselator_system, brusselator_srs_1, CartesianGrid((5, 5, 5, 5)))
+ @test_throws Exception LatticeReactionSystem(brusselator_system, brusselator_srs_1, fill(true, 5, 5, 5, 5))
+end
+
+# Checks that getter functions give the correct output.
+let
+ # Create LatticeReactionsSystems.
+ cartesian_1d_lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_1d_cartesian_grid)
+ cartesian_2d_lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_2d_cartesian_grid)
+ cartesian_3d_lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_3d_cartesian_grid)
+ masked_1d_lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_1d_masked_grid)
+ masked_2d_lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_2d_masked_grid)
+ masked_3d_lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_3d_masked_grid)
+ graph_lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, medium_2d_graph_grid)
+
+ # Test lattice type getters.
+ @test has_cartesian_lattice(cartesian_2d_lrs)
+ @test !has_cartesian_lattice(masked_2d_lrs)
+ @test !has_cartesian_lattice(graph_lrs)
+
+ @test !has_masked_lattice(cartesian_2d_lrs)
+ @test has_masked_lattice(masked_2d_lrs)
+ @test !has_masked_lattice(graph_lrs)
+
+ @test has_grid_lattice(cartesian_2d_lrs)
+ @test has_grid_lattice(masked_2d_lrs)
+ @test !has_grid_lattice(graph_lrs)
+
+ @test !has_graph_lattice(cartesian_2d_lrs)
+ @test !has_graph_lattice(masked_2d_lrs)
+ @test has_graph_lattice(graph_lrs)
+
+ # Checks grid dimensions.
+ @test grid_dims(cartesian_1d_lrs) == 1
+ @test grid_dims(cartesian_2d_lrs) == 2
+ @test grid_dims(cartesian_3d_lrs) == 3
+ @test grid_dims(masked_1d_lrs) == 1
+ @test grid_dims(masked_2d_lrs) == 2
+ @test grid_dims(masked_3d_lrs) == 3
+ @test_throws ArgumentError grid_dims(graph_lrs)
+
+ # Checks grid sizes.
+ @test grid_size(cartesian_1d_lrs) == (5,)
+ @test grid_size(cartesian_2d_lrs) == (5,5)
+ @test grid_size(cartesian_3d_lrs) == (5,5,5)
+ @test grid_size(masked_1d_lrs) == (5,)
+ @test grid_size(masked_2d_lrs) == (5,5)
+ @test grid_size(masked_3d_lrs) == (5,5,5)
+ @test_throws ArgumentError grid_size(graph_lrs)
+end
+
+# Checks grid dimensions for 2d and 3d grids where some dimension is equal to 1.
+let
+ # Creates LatticeReactionSystems
+ cartesian_2d_lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, CartesianGrid((1,5)))
+ cartesian_3d_lrs_1 = LatticeReactionSystem(brusselator_system, brusselator_srs_1, CartesianGrid((1,5,5)))
+ cartesian_3d_lrs_2 = LatticeReactionSystem(brusselator_system, brusselator_srs_1, CartesianGrid((1,1,5)))
+ masked_2d_lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, fill(true, 1, 5))
+ masked_3d_lrs_1 = LatticeReactionSystem(brusselator_system, brusselator_srs_1, fill(true, 1, 5,5))
+ masked_3d_lrs_2 = LatticeReactionSystem(brusselator_system, brusselator_srs_1, fill(true, 1, 1,5))
+
+ # Check grid dimensions.
+ @test grid_dims(cartesian_2d_lrs) == 2
+ @test grid_dims(cartesian_3d_lrs_1) == 3
+ @test grid_dims(cartesian_3d_lrs_2) == 3
+ @test grid_dims(masked_2d_lrs) == 2
+ @test grid_dims(masked_3d_lrs_1) == 3
+ @test grid_dims(masked_3d_lrs_2) == 3
+end
+
+# Checks that some grids, created using different approaches, generates the same spatial structures.
+# Checks that some grids, created using different approaches, generates the same simulation output.
+let
+ # Create LatticeReactionsSystems.
+ cartesian_grid = CartesianGrid((5, 5))
+ masked_grid = fill(true, 5, 5)
+ graph_grid = Graphs.grid([5, 5])
+
+ cartesian_lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, cartesian_grid)
+ masked_lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, masked_grid)
+ graph_lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, graph_grid)
+
+ # Check internal structures.
+ @test reactionsystem(cartesian_lrs) == reactionsystem(masked_lrs) == reactionsystem(graph_lrs)
+ @test spatial_reactions(cartesian_lrs) == spatial_reactions(masked_lrs) == spatial_reactions(graph_lrs)
+ @test num_verts(cartesian_lrs) == num_verts(masked_lrs) == num_verts(graph_lrs)
+ @test num_edges(cartesian_lrs) == num_edges(masked_lrs) == num_edges(graph_lrs)
+ @test num_species(cartesian_lrs) == num_species(masked_lrs) == num_species(graph_lrs)
+ @test isequal(spatial_species(cartesian_lrs), spatial_species(masked_lrs))
+ @test isequal(spatial_species(masked_lrs), spatial_species(graph_lrs))
+ @test isequal(parameters(cartesian_lrs), parameters(masked_lrs))
+ @test isequal(parameters(masked_lrs), parameters(graph_lrs))
+ @test isequal(vertex_parameters(cartesian_lrs), vertex_parameters(masked_lrs))
+ @test isequal(edge_parameters(masked_lrs), edge_parameters(graph_lrs))
+ @test issetequal(edge_iterator(cartesian_lrs), edge_iterator(masked_lrs))
+ @test issetequal(edge_iterator(masked_lrs), edge_iterator(graph_lrs))
+
+ # Checks that simulations yields the same output.
+ X_vals = rand(num_verts(cartesian_lrs))
+ u0_cartesian = [:X => reshape(X_vals, 5, 5), :Y => 2.0]
+ u0_masked = [:X => reshape(X_vals, 5, 5), :Y => 2.0]
+ u0_graph = [:X => X_vals, :Y => 2.0]
+ B_vals = rand(num_verts(cartesian_lrs))
+ pV_cartesian = [:A => 0.5 .+ reshape(B_vals, 5, 5), :B => 4.0]
+ pV_masked = [:A => 0.5 .+ reshape(B_vals, 5, 5), :B => 4.0]
+ pV_graph = [:A => 0.5 .+ B_vals, :B => 4.0]
+ pE = [:dX => 0.2]
+
+ cartesian_oprob = ODEProblem(cartesian_lrs, u0_cartesian, (0.0, 100.0), [pV_cartesian; pE])
+ masked_oprob = ODEProblem(masked_lrs, u0_masked, (0.0, 100.0), [pV_masked; pE])
+ graph_oprob = ODEProblem(graph_lrs, u0_graph, (0.0, 100.0), [pV_graph; pE])
+
+ cartesian_sol = solve(cartesian_oprob, QNDF(); saveat=0.1)
+ masked_sol = solve(masked_oprob, QNDF(); saveat=0.1)
+ graph_sol = solve(graph_oprob, QNDF(); saveat=0.1)
+
+ @test cartesian_sol.u == masked_sol.u == graph_sol.u
+end
+
+# Checks that a regular grid with absent vertices generate the same output as corresponding graph.
+let
+ # Create LatticeReactionsSystems.
+ masked_grid = [true true true; true false true; true true true]
+ graph_grid = SimpleGraph(8)
+ add_edge!(graph_grid, 1, 2); add_edge!(graph_grid, 2, 1);
+ add_edge!(graph_grid, 2, 3); add_edge!(graph_grid, 3, 2);
+ add_edge!(graph_grid, 3, 5); add_edge!(graph_grid, 5, 3);
+ add_edge!(graph_grid, 5, 8); add_edge!(graph_grid, 8, 5);
+ add_edge!(graph_grid, 8, 7); add_edge!(graph_grid, 7, 8);
+ add_edge!(graph_grid, 7, 6); add_edge!(graph_grid, 6, 7);
+ add_edge!(graph_grid, 6, 4); add_edge!(graph_grid, 4, 6);
+ add_edge!(graph_grid, 4, 1); add_edge!(graph_grid, 1, 4);
+
+ masked_lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, masked_grid)
+ graph_lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, graph_grid)
+
+ # Check internal structures.
+ @test num_verts(masked_lrs) == num_verts(graph_lrs)
+ @test num_edges(masked_lrs) == num_edges(graph_lrs)
+ @test issetequal(edge_iterator(masked_lrs), edge_iterator(graph_lrs))
+
+ # Checks that simulations yields the same output.
+ u0_masked_grid = [:X => [1. 4. 6.; 2. 0. 7.; 3. 5. 8.], :Y => 2.0]
+ u0_graph_grid = [:X => [1., 2., 3., 4., 5., 6., 7., 8.], :Y => 2.0]
+ pV_masked_grid = [:A => 0.5 .+ [1. 4. 6.; 2. 0. 7.; 3. 5. 8.], :B => 4.0]
+ pV_graph_grid = [:A => 0.5 .+ [1., 2., 3., 4., 5., 6., 7., 8.], :B => 4.0]
+ pE = [:dX => 0.2]
+
+ base_oprob = ODEProblem(masked_lrs, u0_masked_grid, (0.0, 100.0), [pV_masked_grid; pE])
+ base_osol = solve(base_oprob, QNDF(); saveat=0.1, abstol=1e-9, reltol=1e-9)
+
+ for jac in [false, true], sparse in [false, true]
+ masked_oprob = ODEProblem(masked_lrs, u0_masked_grid, (0.0, 100.0), [pV_masked_grid; pE]; jac, sparse)
+ graph_oprob = ODEProblem(graph_lrs, u0_graph_grid, (0.0, 100.0), [pV_graph_grid; pE]; jac, sparse)
+ masked_sol = solve(masked_oprob, QNDF(); saveat=0.1, abstol=1e-9, reltol=1e-9)
+ graph_sol = solve(graph_oprob, QNDF(); saveat=0.1, abstol=1e-9, reltol=1e-9)
+ @test base_osol ≈ masked_sol ≈ graph_sol
+ end
+end
+
+# For a system which is a single ine of vertices: (O-O-O-O-X-O-O-O), ensures that different simulations
+# approach yield the same result. Checks for both masked and Cartesian grid. For both, simulates where
+# initial conditions/vertex parameters are either a vector of the same length as the number of vertices (7),
+# Or as the grid. Here, we try grid sizes (n), (1,n), and (1,n,1) (so the same grid, but in 1d, 2d, and 3d).
+# For the Cartesian grid, we cannot represent the gap, so we make simulations both for length-4 and
+# length-3 grids.
+let
+ # Declares the initial condition/parameter values.
+ S_vals = [500.0, 600.0, 700.0, 800.0, 0.0, 900.0, 1000.0, 1100.0]
+ I_val = 1.0
+ R_val = 1.0
+ α_vals = [0.1, 0.11, 0.12, 0.13, 0.0, 0.14, 0.15, 0.16]
+ β_val = 0.01
+ dS_val = 0.05
+ SIR_p = [:α => 0.1 / 1000, :β => 0.01]
+
+ # Declares the grids (1d, 2d, and 3d). For each dimension, there are a 2 Cartesian grids (length 4 and 3).
+ cart_grid_1d_1 = CartesianGrid(4)
+ cart_grid_1d_2 = CartesianGrid(3)
+ cart_grid_2d_1 = CartesianGrid((4,1))
+ cart_grid_2d_2 = CartesianGrid((3,1))
+ cart_grid_3d_1 = CartesianGrid((1,4,1))
+ cart_grid_3d_2 = CartesianGrid((1,3,1))
+
+ masked_grid_1d = [true, true, true, true, false, true, true, true]
+ masked_grid_2d = reshape(masked_grid_1d,8,1)
+ masked_grid_3d = reshape(masked_grid_1d,1,8,1)
+
+ # Creaets a base solution to which we will compare all simulations.
+ lrs_base = LatticeReactionSystem(SIR_system, SIR_srs_1, masked_grid_1d)
+ oprob_base = ODEProblem(lrs_base, [:S => S_vals, :I => I_val, :R => R_val], (0.0, 100.0), [:α => α_vals, :β => β_val, :dS => dS_val])
+ sol_base = solve(oprob_base, Tsit5(); saveat = 1.0, abstol = 1e-9, reltol = 1e-9)
+
+ # Checks simulations for the masked grid (covering all 7 vertices, with a gap in the middle).
+ for grid in [masked_grid_1d, masked_grid_2d, masked_grid_3d]
+ # Checks where the values are vectors of length equal to the number of vertices.
+ lrs = LatticeReactionSystem(SIR_system, SIR_srs_1, grid)
+ u0 = [:S => [S_vals[1:4]; S_vals[6:8]], :I => I_val, :R => R_val]
+ ps = [:α => [α_vals[1:4]; α_vals[6:8]], :β => β_val, :dS => dS_val]
+ oprob = ODEProblem(lrs, u0, (0.0, 100.0), ps)
+ sol = solve(oprob, Tsit5(); saveat = 1.0, abstol = 1e-9, reltol = 1e-9)
+ @test sol ≈ sol_base
+
+ # Checks where the values are arrays of size equal to the grid.
+ u0 = [:S => reshape(S_vals, grid_size(lrs)), :I => I_val, :R => R_val]
+ ps = [:α => reshape(α_vals, grid_size(lrs)), :β => β_val, :dS => dS_val]
+ oprob = ODEProblem(lrs, u0, (0.0, 100.0), ps)
+ sol = solve(oprob, Tsit5(); saveat = 1.0, abstol = 1e-9, reltol = 1e-9)
+ @test sol ≈ sol_base
+ end
+
+ # Checks simulations for the first Cartesian grids (covering vertices 1 to 4).
+ for grid in [cart_grid_1d_1, cart_grid_2d_1, cart_grid_3d_1]
+ # Checks where the values are vectors of length equal to the number of vertices.
+ lrs = LatticeReactionSystem(SIR_system, SIR_srs_1, grid)
+ u0 = [:S => S_vals[1:4], :I => I_val, :R => R_val]
+ ps = [:α => α_vals[1:4], :β => β_val, :dS => dS_val]
+ oprob = ODEProblem(lrs, u0, (0.0, 100.0), ps)
+ sol = solve(oprob, Tsit5(); saveat = 1.0, abstol = 1e-9, reltol = 1e-9)
+ @test hcat(sol.u...) ≈ sol_base[1:12,:]
+
+ # Checks where the values are arrays of size equal to the grid.
+ u0 = [:S => reshape(S_vals[1:4], grid_size(lrs)), :I => I_val, :R => R_val]
+ ps = [:α => reshape(α_vals[1:4], grid_size(lrs)), :β => β_val, :dS => dS_val]
+ oprob = ODEProblem(lrs, u0, (0.0, 100.0), ps)
+ sol = solve(oprob, Tsit5(); saveat = 1.0, abstol = 1e-9, reltol = 1e-9)
+ @test hcat(sol.u...) ≈ sol_base[1:12,:]
+ end
+
+ # Checks simulations for the second Cartesian grids (covering vertices 6 to 8).
+ for grid in [cart_grid_1d_2, cart_grid_2d_2, cart_grid_3d_2]
+ # Checks where the values are vectors of length equal to the number of vertices.
+ lrs = LatticeReactionSystem(SIR_system, SIR_srs_1, grid)
+ u0 = [:S => S_vals[6:8], :I => I_val, :R => R_val]
+ ps = [:α => α_vals[6:8], :β => β_val, :dS => dS_val]
+ oprob = ODEProblem(lrs, u0, (0.0, 100.0), ps)
+ sol = solve(oprob, Tsit5(); saveat = 1.0, abstol = 1e-9, reltol = 1e-9)
+ @test hcat(sol.u...) ≈ sol_base[13:end,:]
+
+ # Checks where the values are arrays of size equal to the grid.
+ u0 = [:S => reshape(S_vals[6:8], grid_size(lrs)), :I => I_val, :R => R_val]
+ ps = [:α => reshape(α_vals[6:8], grid_size(lrs)), :β => β_val, :dS => dS_val]
+ oprob = ODEProblem(lrs, u0, (0.0, 100.0), ps)
+ sol = solve(oprob, Tsit5(); saveat = 1.0, abstol = 1e-9, reltol = 1e-9)
+ @test hcat(sol.u...) ≈ sol_base[13:end,:]
+ end
+end
\ No newline at end of file
diff --git a/test/spatial_modelling/lattice_solution_interfacing.jl b/test/spatial_modelling/lattice_solution_interfacing.jl
new file mode 100644
index 0000000000..238700a51f
--- /dev/null
+++ b/test/spatial_modelling/lattice_solution_interfacing.jl
@@ -0,0 +1,196 @@
+### Preparations ###
+
+# Fetch packages.
+using Catalyst, Graphs, JumpProcesses, OrdinaryDiffEq, SparseArrays, Test
+
+### `get_lrs_vals` Tests ###
+
+# Basic test. For simulations without change in system, check that the solution corresponds to known
+# initial condition throughout the solution.
+# Checks using both `t` sampling` and normal time step sampling.
+# Checks for both ODE and jump simulations.
+# Checks for all lattice types.
+let
+ # Prepare `LatticeReactionSystem`s.
+ rs = @reaction_network begin
+ (k1,k2), X1 <--> X2
+ end
+ tr = @transport_reaction D X1
+ lrs1 = LatticeReactionSystem(rs, [tr], CartesianGrid((2,)))
+ lrs2 = LatticeReactionSystem(rs, [tr], CartesianGrid((2,3)))
+ lrs3 = LatticeReactionSystem(rs, [tr], CartesianGrid((2,3,2)))
+ lrs4 = LatticeReactionSystem(rs, [tr], [true, true, false, true])
+ lrs5 = LatticeReactionSystem(rs, [tr], [true false; true true])
+ lrs6 = LatticeReactionSystem(rs, [tr], cycle_graph(4))
+
+ # Create problem inputs.
+ u0_1 = Dict([:X1 => 0, :X2 => [1, 2]])
+ u0_2 = Dict([:X1 => 0, :X2 => [1 2 3; 4 5 6]])
+ u0_3 = Dict([:X1 => 0, :X2 => fill(1, 2, 3, 2)])
+ u0_4 = Dict([:X1 => 0, :X2 => sparse([1, 2, 0, 3])])
+ u0_5 = Dict([:X1 => 0, :X2 => sparse([1 0; 2 3])])
+ u0_6 = Dict([:X1 => 0, :X2 => [1, 2, 3, 4]])
+ tspan = (0.0, 1.0)
+ ps = [:k1 => 0.0, :k2 => 0.0, :D => 0.0]
+
+ # Loops through all lattice cases and check that they are correct.
+ for (u0,lrs) in zip([u0_1, u0_2, u0_3, u0_4, u0_5, u0_6], [lrs1, lrs2, lrs3, lrs4, lrs5, lrs6])
+ # Simulates ODE version and checks `get_lrs_vals` on its solution.
+ oprob = ODEProblem(lrs, u0, tspan, ps)
+ osol = solve(oprob, Tsit5(), saveat = 0.5)
+ @test get_lrs_vals(osol, :X1, lrs) == get_lrs_vals(osol, :X1, lrs; t = 0.0:0.5:1.0)
+ @test all(all(val == Float64(u0[:X1]) for val in vals) for vals in get_lrs_vals(osol, :X1, lrs))
+ @test get_lrs_vals(osol, :X2, lrs) == get_lrs_vals(osol, :X2, lrs; t = 0.0:0.5:1.0) == fill(u0[:X2], 3)
+
+ # Simulates jump version and checks `get_lrs_vals` on its solution.
+ dprob = DiscreteProblem(lrs, u0, tspan, ps)
+ jprob = JumpProblem(lrs, dprob, NSM())
+ jsol = solve(jprob, SSAStepper(), saveat = 0.5)
+ @test get_lrs_vals(jsol, :X1, lrs) == get_lrs_vals(jsol, :X1, lrs; t = 0.0:0.5:1.0)
+ @test all(all(val == Float64(u0[:X1]) for val in vals) for vals in get_lrs_vals(jsol, :X1, lrs))
+ @test get_lrs_vals(jsol, :X2, lrs) == get_lrs_vals(jsol, :X2, lrs; t = 0.0:0.5:1.0) == fill(u0[:X2], 3)
+ end
+end
+
+# Checks on simulations where the system changes in time.
+# Checks that a solution has correct initial condition and end point (steady state).
+# Checks that solution is monotonously increasing/decreasing (it should be for this problem).
+let
+ # Prepare `LatticeReactionSystem`s.
+ rs = @reaction_network begin
+ (p,d), 0 <--> X
+ end
+ tr = @transport_reaction D X
+ lrs = LatticeReactionSystem(rs, [tr], CartesianGrid((2,)))
+
+ # Prepares a corresponding ODEProblem.
+ u0 = [:X => [1.0, 3.0]]
+ tspan = (0.0, 50.0)
+ ps = [:p => 2.0, :d => 1.0, :D => 0.01]
+ oprob = ODEProblem(lrs, u0, tspan, ps)
+
+ # Simulates the ODE. Checks that the start/end points are correct.
+ # Check that the first vertex is monotonously increasing in values, and that the second one is
+ # monotonously decreasing. The non evenly spaced `saveat` is so that non-monotonicity is
+ # not produced due to numeric errors.
+ saveat = [0.0, 1.0, 5.0, 10.0, 50.0]
+ sol = solve(oprob, Vern7(); abstol = 1e-8, reltol = 1e-8)
+ vals = get_lrs_vals(sol, :X, lrs)
+ @test vals[1] == [1.0, 3.0]
+ @test vals[end] ≈ [2.0, 2.0]
+ for i = 1:(length(saveat) - 1)
+ @test vals[i][1] < vals[i + 1][1]
+ @test vals[i][2] > vals[i + 1][2]
+ end
+end
+
+# Checks interpolation when sampling at time point. Check that values at `t` is in between the
+# sample points. Does so by checking that in simulation which is monotonously decreasing/increasing.
+let
+ # Prepare `LatticeReactionSystem`s.
+ rs = @reaction_network begin
+ (p,d), 0 <--> X
+ end
+ tr = @transport_reaction D X
+ lrs = LatticeReactionSystem(rs, [tr], CartesianGrid((2,)))
+
+ # Solved a corresponding ODEProblem.
+ u0 = [:X => [1.0, 3.0]]
+ tspan = (0.0, 1.0)
+ ps = [:p => 2.0, :d => 1.0, :D => 0.0]
+ oprob = ODEProblem(lrs, u0, tspan, ps)
+
+ # Solves and check the interpolation of t.
+ sol = solve(oprob, Tsit5(); saveat = 1.0)
+ t5_vals = get_lrs_vals(sol, :X, lrs; t = [0.5])[1]
+ @test sol.u[1][1] < t5_vals[1] < sol.u[2][1]
+ @test sol.u[1][2] > t5_vals[2] > sol.u[2][2]
+end
+
+### Error Tests ###
+
+# Checks that attempting to sample `t` outside tspan range yields an error.
+let
+ # Prepare `LatticeReactionSystem`s.
+ rs = @reaction_network begin
+ (p,d), 0 <--> X
+ end
+ tr = @transport_reaction D X
+ lrs = LatticeReactionSystem(rs, [tr], CartesianGrid((2,)))
+
+ # Solved a corresponding ODEProblem.
+ u0 = [:X => 1.0]
+ tspan = (1.0, 2.0)
+ ps = [:p => 2.0, :d => 1.0, :D => 1.0]
+ oprob = ODEProblem(lrs, u0, tspan, ps)
+
+ # Solves and check the interpolation of t.
+ sol = solve(oprob, Tsit5(); saveat = 1.0)
+ @test_throws Exception get_lrs_vals(sol, :X, lrs; t = [0.0])
+ @test_throws Exception get_lrs_vals(sol, :X, lrs; t = [3.0])
+end
+
+# Checks that attempting to sample `t` outside tspan range yields an error.
+let
+ # Prepare `LatticeReactionSystem`s.
+ rs = @reaction_network begin
+ (p,d), 0 <--> X
+ end
+ tr = @transport_reaction D X
+ lrs = LatticeReactionSystem(rs, [tr], CartesianGrid((2,)))
+
+ # Solved a corresponding ODEProblem.
+ u0 = [:X => 1.0]
+ tspan = (1.0, 2.0)
+ ps = [:p => 2.0, :d => 1.0, :D => 1.0]
+ oprob = ODEProblem(lrs, u0, tspan, ps)
+
+ # Solves and check the interpolation of t.
+ sol = solve(oprob, Tsit5(); saveat = 1.0)
+ @test_throws Exception get_lrs_vals(sol, :X, lrs; t = [0.0])
+ @test_throws Exception get_lrs_vals(sol, :X, lrs; t = [3.0])
+end
+
+# Checks that applying `get_lrs_vals` to a 3d masked lattice yields an error.
+let
+ # Prepare `LatticeReactionSystem`s.
+ rs = @reaction_network begin
+ (p,d), 0 <--> X
+ end
+ tr = @transport_reaction D X
+ lrs = LatticeReactionSystem(rs, [tr], rand([false, true], 2, 3, 4))
+
+ # Solved a corresponding ODEProblem.
+ u0 = [:X => 1.0]
+ tspan = (1.0, 2.0)
+ ps = [:p => 2.0, :d => 1.0, :D => 1.0]
+ oprob = ODEProblem(lrs, u0, tspan, ps)
+
+ # Solves and check the interpolation of t.
+ sol = solve(oprob, Tsit5(); saveat = 1.0)
+ @test_throws Exception get_lrs_vals(sol, :X, lrs)
+end
+
+### Other Tests ###
+
+# Checks that `get_lrs_vals` works for all types of symbols.
+let
+ t = default_t()
+ @species X(t)
+ @parameters d
+ @named rs = ReactionSystem([Reaction(d, [X], [])], t)
+ rs = complete(rs)
+ tr = @transport_reaction D X
+ lrs = LatticeReactionSystem(rs, [tr], CartesianGrid(2,))
+
+ # Solved a corresponding ODEProblem.
+ u0 = [:X => 1.0]
+ tspan = (0.0, 1.0)
+ ps = [:d => 1.0, :D => 0.1]
+ oprob = ODEProblem(lrs, u0, tspan, ps)
+
+ # Solves and check the interpolation of t.
+ sol = solve(oprob, Tsit5(); saveat = 1.0)
+ @test get_lrs_vals(sol, X, lrs) == get_lrs_vals(sol, rs.X, lrs) == get_lrs_vals(sol, :X, lrs)
+ @test get_lrs_vals(sol, X, lrs; t = 0.0:0.5:1.0) == get_lrs_vals(sol, rs.X, lrs; t = 0.0:0.5:1.0) == get_lrs_vals(sol, :X, lrs; t = 0.0:0.5:1.0)
+end
\ No newline at end of file
diff --git a/test/spatial_modelling/spatial_reactions.jl b/test/spatial_modelling/spatial_reactions.jl
new file mode 100644
index 0000000000..e764c619a3
--- /dev/null
+++ b/test/spatial_modelling/spatial_reactions.jl
@@ -0,0 +1,130 @@
+### Preparations ###
+
+# Fetch packages.
+using Catalyst, Test
+
+
+### TransportReaction Creation Tests ###
+
+# Tests TransportReaction with non-trivial rate.
+let
+ rs = @reaction_network begin
+ @parameters dV dE [edgeparameter=true]
+ (p,1), 0 <--> X
+ end
+ @unpack dV, dE, X = rs
+
+ tr = TransportReaction(dV*dE, X)
+ @test isequal(tr.rate, dV*dE)
+end
+
+# Tests transport_reactions function for creating TransportReactions.
+let
+ rs = @reaction_network begin
+ @parameters d
+ (p,1), 0 <--> X
+ end
+ @unpack d, X = rs
+ trs = TransportReactions([(d, X), (d, X)])
+ @test isequal(trs[1], trs[2])
+end
+
+# Test reactions with constants in rate.
+let
+ @variables t
+ @species X(t) Y(t)
+
+ tr_1 = TransportReaction(1.5, X)
+ tr_1_macro = @transport_reaction 1.5 X
+ @test isequal(tr_1.rate, tr_1_macro.rate)
+ @test isequal(tr_1.species, tr_1_macro.species)
+
+ tr_2 = TransportReaction(π, Y)
+ tr_2_macro = @transport_reaction π Y
+ @test isequal(tr_2.rate, tr_2_macro.rate)
+ @test isequal(tr_2.species, tr_2_macro.species)
+end
+
+### Spatial Reactions Getters Correctness ###
+
+# Test case 1.
+let
+ tr_1 = @transport_reaction dX X
+ tr_2 = @transport_reaction dY1*dY2 Y
+
+ # @test ModelingToolkit.getname.(species(tr_1)) == ModelingToolkit.getname.(spatial_species(tr_1)) == [:X] # species(::TransportReaction) currently not supported.
+ # @test ModelingToolkit.getname.(species(tr_2)) == ModelingToolkit.getname.(spatial_species(tr_2)) == [:Y]
+ @test ModelingToolkit.getname.(spatial_species(tr_1)) == [:X]
+ @test ModelingToolkit.getname.(spatial_species(tr_2)) == [:Y]
+ @test ModelingToolkit.getname.(parameters(tr_1)) == [:dX]
+ @test ModelingToolkit.getname.(parameters(tr_2)) == [:dY1, :dY2]
+
+ # @test issetequal(species(tr_1), [tr_1.species])
+ # @test issetequal(species(tr_2), [tr_2.species])
+ @test issetequal(spatial_species(tr_1), [tr_1.species])
+ @test issetequal(spatial_species(tr_2), [tr_2.species])
+end
+
+# Test case 2.
+let
+ rs = @reaction_network begin
+ @species X(t) Y(t)
+ @parameters dX dY1 dY2
+ end
+ @unpack X, Y, dX, dY1, dY2 = rs
+ tr_1 = TransportReaction(dX, X)
+ tr_2 = TransportReaction(dY1*dY2, Y)
+ # @test isequal(species(tr_1), [X])
+ # @test isequal(species(tr_1), [X])
+ @test issetequal(spatial_species(tr_2), [Y])
+ @test issetequal(spatial_species(tr_2), [Y])
+ @test issetequal(parameters(tr_1), [dX])
+ @test issetequal(parameters(tr_2), [dY1, dY2])
+end
+
+### Error Tests ###
+
+# Tests that creation of TransportReaction with non-parameters in rate yield errors.
+# Tests that errors are throw even when the rate is highly nested.
+let
+ @variables t
+ @species X(t) Y(t)
+ @parameters D1 D2 D3
+ @test_throws ErrorException TransportReaction(D1 + D2*(D3 + Y), X)
+ @test_throws ErrorException TransportReaction(Y, X)
+end
+
+### Other Tests ###
+
+# Test Interpolation
+# Does not currently work. The 3 tr_macro_ lines generate errors.
+let
+ rs = @reaction_network begin
+ @species X(t) Y(t) Z(t)
+ @parameters dX dY1 dY2 dZ
+ end
+ @unpack X, Y, Z, dX, dY1, dY2, dZ = rs
+ rate1 = dX
+ rate2 = dY1*dY2
+ species3 = Z
+ tr_1 = TransportReaction(dX, X)
+ tr_2 = TransportReaction(dY1*dY2, Y)
+ tr_3 = TransportReaction(dZ, Z)
+ tr_macro_1 = @transport_reaction $dX X
+ tr_macro_2 = @transport_reaction $(rate2) Y
+ @test_broken false
+ # tr_macro_3 = @transport_reaction dZ $species3 # Currently does not work, something with meta programming.
+
+ @test isequal(tr_1, tr_macro_1)
+ @test isequal(tr_2, tr_macro_2)
+ # @test isequal(tr_3, tr_macro_3)
+end
+
+# Checks that the `hash` functions work for `TransportReaction`s.
+let
+ tr1 = @transport_reaction D1 X
+ tr2 = @transport_reaction D1 X
+ tr3 = @transport_reaction D2 X
+ hash(tr1, 0x0000000000000001) == hash(tr2, 0x0000000000000001)
+ hash(tr2, 0x0000000000000001) != hash(tr3, 0x0000000000000001)
+end
\ No newline at end of file
diff --git a/test/spatial_test_networks.jl b/test/spatial_test_networks.jl
index f2f9472e87..e9322290c1 100644
--- a/test/spatial_test_networks.jl
+++ b/test/spatial_test_networks.jl
@@ -1,5 +1,7 @@
### Fetch packages ###
using Catalyst, Graphs
+using Catalyst: reactionsystem, spatial_reactions, lattice, num_verts, num_edges, num_species,
+ spatial_species, vertex_parameters, edge_parameters, edge_iterator
# Sets rnd number.
using StableRNGs
@@ -8,14 +10,34 @@ rng = StableRNG(12345)
### Helper Functions ###
# Generates randomised initial condition or parameter values.
-rand_v_vals(grid) = rand(rng, nv(grid))
rand_v_vals(grid, x::Number) = rand_v_vals(grid) * x
-rand_e_vals(grid) = rand(rng, ne(grid))
+rand_v_vals(lrs::LatticeReactionSystem) = rand_v_vals(lattice(lrs))
+function rand_v_vals(grid::DiGraph)
+ return rand(rng, nv(grid))
+end
+function rand_v_vals(grid::Catalyst.CartesianGridRej{N,T}) where {N,T}
+ return rand(rng, grid.dims)
+end
+function rand_v_vals(grid::Array{Bool, N}) where {N}
+ return rand(rng, size(grid))
+end
+
rand_e_vals(grid, x::Number) = rand_e_vals(grid) * x
-function make_u0_matrix(value_map, vals, symbols)
- (length(symbols) == 0) && (return zeros(0, length(vals)))
- d = Dict(value_map)
- return [(d[s] isa Vector) ? d[s][v] : d[s] for s in symbols, v in 1:length(vals)]
+function rand_e_vals(lrs::LatticeReactionSystem)
+ e_vals = spzeros(num_verts(lrs), num_verts(lrs))
+ for e in edge_iterator(lrs)
+ e_vals[e[1], e[2]] = rand(rng)
+ end
+ return e_vals
+end
+
+# Generates edge values, where each edge have the same value.
+function uniform_e_vals(lrs::LatticeReactionSystem, val)
+ e_vals = spzeros(num_verts(lrs), num_verts(lrs))
+ for e in edge_iterator(lrs)
+ e_vals[e[1], e[2]] = val
+ end
+ return e_vals
end
# Gets a symbol list of spatial parameters.
@@ -168,26 +190,63 @@ sigmaB_srs_2 = [sigmaB_tr_σB, sigmaB_tr_w, sigmaB_tr_v]
### Declares Lattices ###
-# Grids.
-very_small_2d_grid = Graphs.grid([2, 2])
-small_2d_grid = Graphs.grid([5, 5])
-medium_2d_grid = Graphs.grid([20, 20])
-large_2d_grid = Graphs.grid([100, 100])
+# Cartesian grids.
+very_small_1d_cartesian_grid = CartesianGrid(2)
+very_small_2d_cartesian_grid = CartesianGrid((2,2))
+very_small_3d_cartesian_grid = CartesianGrid((2,2,2))
+
+small_1d_cartesian_grid = CartesianGrid(5)
+small_2d_cartesian_grid = CartesianGrid((5,5))
+small_3d_cartesian_grid = CartesianGrid((5,5,5))
+
+large_1d_cartesian_grid = CartesianGrid(100)
+large_2d_cartesian_grid = CartesianGrid((100,100))
+large_3d_cartesian_grid = CartesianGrid((100,100,100))
+
+# Masked grids.
+very_small_1d_masked_grid = fill(true, 2)
+very_small_2d_masked_grid = fill(true, 2, 2)
+very_small_3d_masked_grid = fill(true, 2, 2, 2)
+
+small_1d_masked_grid = fill(true, 5)
+small_2d_masked_grid = fill(true, 5, 5)
+small_3d_masked_grid = fill(true, 5, 5, 5)
+
+large_1d_masked_grid = fill(true, 5)
+large_2d_masked_grid = fill(true, 5, 5)
+large_3d_masked_grid = fill(true, 5, 5, 5)
+
+random_1d_masked_grid = rand(rng, [true, true, true, false], 10)
+random_2d_masked_grid = rand(rng, [true, true, true, false], 10, 10)
+random_3d_masked_grid = rand(rng, [true, true, true, false], 10, 10, 10)
+
+# Graph - grids.
+very_small_1d_graph_grid = Graphs.grid([2])
+very_small_2d_graph_grid = Graphs.grid([2, 2])
+very_small_3d_graph_grid = Graphs.grid([2, 2, 2])
+
+small_1d_graph_grid = path_graph(5)
+small_2d_graph_grid = Graphs.grid([5,5])
+small_3d_graph_grid = Graphs.grid([5,5,5])
+
+medium_1d_graph_grid = path_graph(20)
+medium_2d_graph_grid = Graphs.grid([20,20])
+medium_3d_graph_grid = Graphs.grid([20,20,20])
-small_3d_grid = Graphs.grid([5, 5, 5])
-medium_3d_grid = Graphs.grid([20, 20, 20])
-large_3d_grid = Graphs.grid([100, 100, 100])
+large_1d_graph_grid = path_graph(100)
+large_2d_graph_grid = Graphs.grid([100,100])
+large_3d_graph_grid = Graphs.grid([100,100,100])
-# Paths.
+# Graph - paths.
short_path = path_graph(100)
long_path = path_graph(1000)
-# Unconnected graphs.
+# Graph - unconnected graphs.
unconnected_graph = SimpleGraph(10)
-# Undirected cycle.
+# Graph - undirected cycle.
undirected_cycle = cycle_graph(49)
-# Directed cycle.
+# Graph - directed cycle.
small_directed_cycle = cycle_graph(100)
large_directed_cycle = cycle_graph(1000)
\ No newline at end of file
diff --git a/test/test_networks.jl b/test/test_networks.jl
index ab2afad1ad..d38c56daec 100644
--- a/test/test_networks.jl
+++ b/test/test_networks.jl
@@ -347,7 +347,7 @@ reaction_networks_weird[10] = @reaction_network rnw10 begin
d, 5X1 → 4X1
end
-### Gathers all netowkrs in a simgle array ###
+### Gathers all networks in a single array ###
reaction_networks_all = [reaction_networks_standard...,
reaction_networks_hill...,
reaction_networks_conserved...,
diff --git a/test/upstream/mtk_problem_inputs.jl b/test/upstream/mtk_problem_inputs.jl
index 0e30f05631..ffe2037519 100644
--- a/test/upstream/mtk_problem_inputs.jl
+++ b/test/upstream/mtk_problem_inputs.jl
@@ -3,7 +3,8 @@
### Prepares Tests ###
# Fetch packages
-using Catalyst, JumpProcesses, NonlinearSolve, OrdinaryDiffEq, SteadyStateDiffEq, StochasticDiffEq, Test
+using Catalyst, JumpProcesses, NonlinearSolve, OrdinaryDiffEq, StaticArrays, SteadyStateDiffEq,
+ StochasticDiffEq, Test
# Sets rnd number.
using StableRNGs
@@ -33,6 +34,14 @@ begin
[X => 4, Y => 5, Z => 10],
[model.X => 4, model.Y => 5, model.Z => 10],
[:X => 4, :Y => 5, :Z => 10],
+ # Static vectors not providing default values.
+ SA[X => 4, Y => 5],
+ SA[model.X => 4, model.Y => 5],
+ SA[:X => 4, :Y => 5],
+ # Static vectors providing default values.
+ SA[X => 4, Y => 5, Z => 10],
+ SA[model.X => 4, model.Y => 5, model.Z => 10],
+ SA[:X => 4, :Y => 5, :Z => 10],
# Dicts not providing default values.
Dict([X => 4, Y => 5]),
Dict([model.X => 4, model.Y => 5]),
@@ -60,6 +69,14 @@ begin
[kp => 1.0, kd => 0.1, k1 => 0.25, k2 => 0.5, Z0 => 10],
[model.kp => 1.0, model.kd => 0.1, model.k1 => 0.25, model.k2 => 0.5, model.Z0 => 10],
[:kp => 1.0, :kd => 0.1, :k1 => 0.25, :k2 => 0.5, :Z0 => 10],
+ # Static vectors not providing default values.
+ SA[kp => 1.0, kd => 0.1, k1 => 0.25, Z0 => 10],
+ SA[model.kp => 1.0, model.kd => 0.1, model.k1 => 0.25, model.Z0 => 10],
+ SA[:kp => 1.0, :kd => 0.1, :k1 => 0.25, :Z0 => 10],
+ # Static vectors providing default values.
+ SA[kp => 1.0, kd => 0.1, k1 => 0.25, k2 => 0.5, Z0 => 10],
+ SA[model.kp => 1.0, model.kd => 0.1, model.k1 => 0.25, model.k2 => 0.5, model.Z0 => 10],
+ SA[:kp => 1.0, :kd => 0.1, :k1 => 0.25, :k2 => 0.5, :Z0 => 10],
# Dicts not providing default values.
Dict([kp => 1.0, kd => 0.1, k1 => 0.25, Z0 => 10]),
Dict([model.kp => 1.0, model.kd => 0.1, model.k1 => 0.25, model.Z0 => 10]),
@@ -158,6 +175,202 @@ let
end
end
+### Vector Species/Parameters Tests ###
+
+begin
+ # Declares the model (with vector species/parameters, with/without default values, and observables).
+ t = default_t()
+ @species X(t)[1:2] Y(t)[1:2] = [10.0, 20.0] XY(t)[1:2]
+ @parameters p[1:2] d[1:2] = [0.2, 0.5]
+ rxs = [
+ Reaction(p[1], [], [X[1]]),
+ Reaction(p[2], [], [X[2]]),
+ Reaction(d[1], [X[1]], []),
+ Reaction(d[2], [X[2]], []),
+ Reaction(p[1], [], [Y[1]]),
+ Reaction(p[2], [], [Y[2]]),
+ Reaction(d[1], [Y[1]], []),
+ Reaction(d[2], [Y[2]], [])
+ ]
+ observed = [XY[1] ~ X[1] + Y[1], XY[2] ~ X[2] + Y[2]]
+ @named model_vec = ReactionSystem(rxs, t; observed)
+ model_vec = complete(model_vec)
+
+ # Declares various u0 versions (scalarised and vector forms).
+ u0_alts_vec = [
+ # Vectors not providing default values.
+ [X => [1.0, 2.0]],
+ [X[1] => 1.0, X[2] => 2.0],
+ [model_vec.X => [1.0, 2.0]],
+ [model_vec.X[1] => 1.0, model_vec.X[2] => 2.0],
+ [:X => [1.0, 2.0]],
+ # Vectors providing default values.
+ [X => [1.0, 2.0], Y => [10.0, 20.0]],
+ [X[1] => 1.0, X[2] => 2.0, Y[1] => 10.0, Y[2] => 20.0],
+ [model_vec.X => [1.0, 2.0], model_vec.Y => [10.0, 20.0]],
+ [model_vec.X[1] => 1.0, model_vec.X[2] => 2.0, model_vec.Y[1] => 10.0, model_vec.Y[2] => 20.0],
+ [:X => [1.0, 2.0], :Y => [10.0, 20.0]],
+ # Static vectors not providing default values.
+ SA[X => [1.0, 2.0]],
+ SA[X[1] => 1.0, X[2] => 2.0],
+ SA[model_vec.X => [1.0, 2.0]],
+ SA[model_vec.X[1] => 1.0, model_vec.X[2] => 2.0],
+ SA[:X => [1.0, 2.0]],
+ # Static vectors providing default values.
+ SA[X => [1.0, 2.0], Y => [10.0, 20.0]],
+ SA[X[1] => 1.0, X[2] => 2.0, Y[1] => 10.0, Y[2] => 20.0],
+ SA[model_vec.X => [1.0, 2.0], model_vec.Y => [10.0, 20.0]],
+ SA[model_vec.X[1] => 1.0, model_vec.X[2] => 2.0, model_vec.Y[1] => 10.0, model_vec.Y[2] => 20.0],
+ SA[:X => [1.0, 2.0], :Y => [10.0, 20.0]],
+ # Dicts not providing default values.
+ Dict([X => [1.0, 2.0]]),
+ Dict([X[1] => 1.0, X[2] => 2.0]),
+ Dict([model_vec.X => [1.0, 2.0]]),
+ Dict([model_vec.X[1] => 1.0, model_vec.X[2] => 2.0]),
+ Dict([:X => [1.0, 2.0]]),
+ # Dicts providing default values.
+ Dict([X => [1.0, 2.0], Y => [10.0, 20.0]]),
+ Dict([X[1] => 1.0, X[2] => 2.0, Y[1] => 10.0, Y[2] => 20.0]),
+ Dict([model_vec.X => [1.0, 2.0], model_vec.Y => [10.0, 20.0]]),
+ Dict([model_vec.X[1] => 1.0, model_vec.X[2] => 2.0, model_vec.Y[1] => 10.0, model_vec.Y[2] => 20.0]),
+ Dict([:X => [1.0, 2.0], :Y => [10.0, 20.0]]),
+ # Tuples not providing default values.
+ (X => [1.0, 2.0]),
+ (X[1] => 1.0, X[2] => 2.0),
+ (model_vec.X => [1.0, 2.0]),
+ (model_vec.X[1] => 1.0, model_vec.X[2] => 2.0),
+ (:X => [1.0, 2.0]),
+ # Tuples providing default values.
+ (X => [1.0, 2.0], Y => [10.0, 20.0]),
+ (X[1] => 1.0, X[2] => 2.0, Y[1] => 10.0, Y[2] => 20.0),
+ (model_vec.X => [1.0, 2.0], model_vec.Y => [10.0, 20.0]),
+ (model_vec.X[1] => 1.0, model_vec.X[2] => 2.0, model_vec.Y[1] => 10.0, model_vec.Y[2] => 20.0),
+ (:X => [1.0, 2.0], :Y => [10.0, 20.0]),
+ ]
+
+ # Declares various ps versions (vector forms only).
+ p_alts_vec = [
+ # Vectors not providing default values.
+ [p => [1.0, 2.0]],
+ [model_vec.p => [1.0, 2.0]],
+ [:p => [1.0, 2.0]],
+ # Vectors providing default values.
+ [p => [4.0, 5.0], d => [0.2, 0.5]],
+ [model_vec.p => [4.0, 5.0], model_vec.d => [0.2, 0.5]],
+ [:p => [4.0, 5.0], :d => [0.2, 0.5]],
+ # Static vectors not providing default values.
+ SA[p => [1.0, 2.0]],
+ SA[model_vec.p => [1.0, 2.0]],
+ SA[:p => [1.0, 2.0]],
+ # Static vectors providing default values.
+ SA[p => [4.0, 5.0], d => [0.2, 0.5]],
+ SA[model_vec.p => [4.0, 5.0], model_vec.d => [0.2, 0.5]],
+ SA[:p => [4.0, 5.0], :d => [0.2, 0.5]],
+ # Dicts not providing default values.
+ Dict([p => [1.0, 2.0]]),
+ Dict([model_vec.p => [1.0, 2.0]]),
+ Dict([:p => [1.0, 2.0]]),
+ # Dicts providing default values.
+ Dict([p => [4.0, 5.0], d => [0.2, 0.5]]),
+ Dict([model_vec.p => [4.0, 5.0], model_vec.d => [0.2, 0.5]]),
+ Dict([:p => [4.0, 5.0], :d => [0.2, 0.5]]),
+ # Tuples not providing default values.
+ (p => [1.0, 2.0]),
+ (model_vec.p => [1.0, 2.0]),
+ (:p => [1.0, 2.0]),
+ # Tuples providing default values.
+ (p => [4.0, 5.0], d => [0.2, 0.5]),
+ (model_vec.p => [4.0, 5.0], model_vec.d => [0.2, 0.5]),
+ (:p => [4.0, 5.0], :d => [0.2, 0.5]),
+ ]
+
+ # Declares a timespan.
+ tspan = (0.0, 10.0)
+end
+
+# Perform ODE simulations (singular and ensemble).
+let
+ # Creates normal and ensemble problems.
+ base_oprob = ODEProblem(model_vec, u0_alts_vec[1], tspan, p_alts_vec[1])
+ base_sol = solve(base_oprob, Tsit5(); saveat = 1.0)
+ base_eprob = EnsembleProblem(base_oprob)
+ base_esol = solve(base_eprob, Tsit5(); trajectories = 2, saveat = 1.0)
+
+ # Simulates problems for all input types, checking that identical solutions are found.
+ @test_broken false # Cannot remake problem (https://github.com/SciML/ModelingToolkit.jl/issues/2804).
+ # for u0 in u0_alts_vec, p in p_alts_vec
+ # oprob = remake(base_oprob; u0, p)
+ # @test base_sol == solve(oprob, Tsit5(); saveat = 1.0)
+ # eprob = remake(base_eprob; u0, p)
+ # @test base_esol == solve(eprob, Tsit5(); trajectories = 2, saveat = 1.0)
+ # end
+end
+
+# Perform SDE simulations (singular and ensemble).
+let
+ # Creates normal and ensemble problems.
+ base_sprob = SDEProblem(model_vec, u0_alts_vec[1], tspan, p_alts_vec[1])
+ base_sol = solve(base_sprob, ImplicitEM(); seed, saveat = 1.0)
+ base_eprob = EnsembleProblem(base_sprob)
+ base_esol = solve(base_eprob, ImplicitEM(); seed, trajectories = 2, saveat = 1.0)
+
+ # Simulates problems for all input types, checking that identical solutions are found.
+ @test_broken false # Cannot remake problem (https://github.com/SciML/ModelingToolkit.jl/issues/2804).
+ # for u0 in u0_alts_vec, p in p_alts_vec
+ # sprob = remake(base_sprob; u0, p)
+ # @test base_sol == solve(sprob, ImplicitEM(); seed, saveat = 1.0)
+ # eprob = remake(base_eprob; u0, p)
+ # @test base_esol == solve(eprob, ImplicitEM(); seed, trajectories = 2, saveat = 1.0)
+ # end
+end
+
+# Perform jump simulations (singular and ensemble).
+let
+ # Creates normal and ensemble problems.
+ base_dprob = DiscreteProblem(model_vec, u0_alts_vec[1], tspan, p_alts_vec[1])
+ base_jprob = JumpProblem(model_vec, base_dprob, Direct(); rng)
+ base_sol = solve(base_jprob, SSAStepper(); seed, saveat = 1.0)
+ base_eprob = EnsembleProblem(base_jprob)
+ base_esol = solve(base_eprob, SSAStepper(); seed, trajectories = 2, saveat = 1.0)
+
+ # Simulates problems for all input types, checking that identical solutions are found.
+ @test_broken false # Cannot remake problem (https://github.com/SciML/ModelingToolkit.jl/issues/2804).
+ # for u0 in u0_alts_vec, p in p_alts_vec
+ # jprob = remake(base_jprob; u0, p)
+ # @test base_sol == solve(base_jprob, SSAStepper(); seed, saveat = 1.0)
+ # eprob = remake(base_eprob; u0, p)
+ # @test base_esol == solve(eprob, SSAStepper(); seed, trajectories = 2, saveat = 1.0)
+ # end
+end
+
+# Solves a nonlinear problem (EnsembleProblems are not possible for these).
+let
+ base_nlprob = NonlinearProblem(model_vec, u0_alts_vec[1], p_alts_vec[1])
+ base_sol = solve(base_nlprob, NewtonRaphson())
+ @test_broken false # Cannot remake problem (https://github.com/SciML/ModelingToolkit.jl/issues/2804).
+ # for u0 in u0_alts_vec, p in p_alts_vec
+ # nlprob = remake(base_nlprob; u0, p)
+ # @test base_sol == solve(nlprob, NewtonRaphson())
+ # end
+end
+
+# Perform steady state simulations (singular and ensemble).
+let
+ # Creates normal and ensemble problems.
+ base_ssprob = SteadyStateProblem(model_vec, u0_alts_vec[1], p_alts_vec[1])
+ base_sol = solve(base_ssprob, DynamicSS(Tsit5()))
+ base_eprob = EnsembleProblem(base_ssprob)
+ base_esol = solve(base_eprob, DynamicSS(Tsit5()); trajectories = 2)
+
+ # Simulates problems for all input types, checking that identical solutions are found.
+ @test_broken false # Cannot remake problem (https://github.com/SciML/ModelingToolkit.jl/issues/2804).
+ # for u0 in u0_alts_vec, p in p_alts_vec
+ # ssprob = remake(base_ssprob; u0, p)
+ # @test base_sol == solve(ssprob, DynamicSS(Tsit5()))
+ # eprob = remake(base_eprob; u0, p)
+ # @test base_esol == solve(eprob, DynamicSS(Tsit5()); trajectories = 2)
+ # end
+end
### Checks Errors On Faulty Inputs ###
@@ -172,11 +385,6 @@ let
@species X3(t)
@parameters k3
- # Creates systems (so these are not recreated in each problem call).
- osys = convert(ODESystem, rn)
- ssys = convert(SDESystem, rn)
- nsys = convert(NonlinearSystem, rn)
-
# Declares valid initial conditions and parameter values
u0_valid = [X1 => 1, X2 => 2]
ps_valid = [k1 => 0.5, k2 => 0.1]
@@ -188,6 +396,9 @@ let
[X1 => 1],
[rn.X1 => 1],
[:X1 => 1],
+ SA[X1 => 1],
+ SA[rn.X1 => 1],
+ SA[:X1 => 1],
Dict([X1 => 1]),
Dict([rn.X1 => 1]),
Dict([:X1 => 1]),
@@ -197,6 +408,8 @@ let
# Contain an additional value.
[X1 => 1, X2 => 2, X3 => 3],
[:X1 => 1, :X2 => 2, :X3 => 3],
+ SA[X1 => 1, X2 => 2, X3 => 3],
+ SA[:X1 => 1, :X2 => 2, :X3 => 3],
Dict([X1 => 1, X2 => 2, X3 => 3]),
Dict([:X1 => 1, :X2 => 2, :X3 => 3]),
(X1 => 1, X2 => 2, X3 => 3),
@@ -207,6 +420,9 @@ let
[k1 => 1.0],
[rn.k1 => 1.0],
[:k1 => 1.0],
+ SA[k1 => 1.0],
+ SA[rn.k1 => 1.0],
+ SA[:k1 => 1.0],
Dict([k1 => 1.0]),
Dict([rn.k1 => 1.0]),
Dict([:k1 => 1.0]),
@@ -216,6 +432,8 @@ let
# Contain an additional value.
[k1 => 1.0, k2 => 2.0, k3 => 3.0],
[:k1 => 1.0, :k2 => 2.0, :k3 => 3.0],
+ SA[k1 => 1.0, k2 => 2.0, k3 => 3.0],
+ SA[:k1 => 1.0, :k2 => 2.0, :k3 => 3.0],
Dict([k1 => 1.0, k2 => 2.0, k3 => 3.0]),
Dict([:k1 => 1.0, :k2 => 2.0, :k3 => 3.0]),
(k1 => 1.0, k2 => 2.0, k3 => 3.0),
@@ -223,26 +441,26 @@ let
]
# Loops through all potential parameter sets, checking their inputs yield errors.
- # Broken tests are due to this issue: https://github.com/SciML/ModelingToolkit.jl/issues/2779
- for ps in [ps_valid; ps_invalid], u0 in [u0_valid; u0s_invalid]
+ for ps in [[ps_valid]; ps_invalid], u0 in [[u0_valid]; u0s_invalid]
# Handles problems with/without tspan separately. Special check ensuring that valid inputs passes.
- for (xsys, XProblem) in zip([osys, ssys, rn], [ODEProblem, SDEProblem, DiscreteProblem])
- if (ps == ps_valid) && (u0 == u0_valid)
- XProblem(xsys, u0, (0.0, 1.0), ps); @test true;
+ for XProblem in [ODEProblem, SDEProblem, DiscreteProblem]
+ if isequal(ps, ps_valid) && isequal(u0, u0_valid)
+ XProblem(rn, u0, (0.0, 1.0), ps); @test true;
else
+ # Several of these cases do not throw errors (https://github.com/SciML/ModelingToolkit.jl/issues/2624).
@test_broken false
continue
- @test_throws Exception XProblem(xsys, u0, (0.0, 1.0), ps)
+ @test_throws Exception XProblem(rn, u0, (0.0, 1.0), ps)
end
end
- for (xsys, XProblem) in zip([nsys, osys], [NonlinearProblem, SteadyStateProblem])
- if (ps == ps_valid) && (u0 == u0_valid)
- XProblem(xsys, u0, ps); @test true;
+ for XProblem in [NonlinearProblem, SteadyStateProblem]
+ if isequal(ps, ps_valid) && isequal(u0, u0_valid)
+ XProblem(rn, u0, ps); @test true;
else
@test_broken false
continue
- @test_throws Exception XProblem(xsys, u0, ps)
+ @test_throws Exception XProblem(rn, u0, ps)
end
end
end
-end
\ No newline at end of file
+end
diff --git a/test/upstream/mtk_structure_indexing.jl b/test/upstream/mtk_structure_indexing.jl
index 29e7a01755..f8768b8ed0 100644
--- a/test/upstream/mtk_structure_indexing.jl
+++ b/test/upstream/mtk_structure_indexing.jl
@@ -37,20 +37,20 @@ begin
problems = [oprob, sprob, dprob, jprob, nprob, ssprob]
# Creates an `EnsembleProblem` for each problem.
- eoprob = EnsembleProblem(oprob)
- esprob = EnsembleProblem(sprob)
- edprob = EnsembleProblem(dprob)
- ejprob = EnsembleProblem(jprob)
- enprob = EnsembleProblem(nprob)
- essprob = EnsembleProblem(ssprob)
+ eoprob = EnsembleProblem(deepcopy(oprob))
+ esprob = EnsembleProblem(deepcopy(sprob))
+ edprob = EnsembleProblem(deepcopy(dprob))
+ ejprob = EnsembleProblem(deepcopy(jprob))
+ enprob = EnsembleProblem(deepcopy(nprob))
+ essprob = EnsembleProblem(deepcopy(ssprob))
eproblems = [eoprob, esprob, edprob, ejprob, enprob, essprob]
# Creates integrators.
- oint = init(oprob, Tsit5(); save_everystep=false)
- sint = init(sprob, ImplicitEM(); save_everystep=false)
+ oint = init(oprob, Tsit5(); save_everystep = false)
+ sint = init(sprob, ImplicitEM(); save_everystep = false)
jint = init(jprob, SSAStepper())
- nint = init(nprob, NewtonRaphson(); save_everystep=false)
- @test_broken ssint = init(ssprob, DynamicSS(Tsit5()); save_everystep=false) # https://github.com/SciML/SteadyStateDiffEq.jl/issues/79
+ nint = init(nprob, NewtonRaphson(); save_everystep = false)
+ @test_broken ssint = init(ssprob, DynamicSS(Tsit5()); save_everystep = false) # https://github.com/SciML/SciMLBase.jl/issues/660
integrators = [oint, sint, jint, nint]
# Creates solutions.
@@ -64,12 +64,13 @@ end
# Tests problem indexing and updating.
let
- for prob in [deepcopy(problems); deepcopy(eproblems)]
+ @test_broken false # A few cases fails for JumpProblem: https://github.com/SciML/ModelingToolkit.jl/issues/2838
+ for prob in deepcopy([oprob, sprob, dprob, nprob, ssprob, eoprob, esprob, edprob, enprob, essprob])
# Get u values (including observables).
@test prob[X] == prob[model.X] == prob[:X] == 4
@test prob[XY] == prob[model.XY] == prob[:XY] == 9
@test prob[[XY,Y]] == prob[[model.XY,model.Y]] == prob[[:XY,:Y]] == [9, 5]
- @test_broken prob[(XY,Y)] == prob[(model.XY,model.Y)] == prob[(:XY,:Y)] == (9, 5) # https://github.com/SciML/SciMLBase.jl/issues/709
+ @test prob[(XY,Y)] == prob[(model.XY,model.Y)] == prob[(:XY,:Y)] == (9, 5)
@test getu(prob, X)(prob) == getu(prob, model.X)(prob) == getu(prob, :X)(prob) == 4
@test getu(prob, XY)(prob) == getu(prob, model.XY)(prob) == getu(prob, :XY)(prob) == 9
@test getu(prob, [XY,Y])(prob) == getu(prob, [model.XY,model.Y])(prob) == getu(prob, [:XY,:Y])(prob) == [9, 5]
@@ -115,7 +116,8 @@ end
# Test remake function.
let
- for prob in [deepcopy(problems); deepcopy(eproblems)]
+ @test_broken false # Cannot check result for JumpProblem: https://github.com/SciML/ModelingToolkit.jl/issues/2838
+ for prob in deepcopy([oprob, sprob, dprob, nprob, ssprob, eoprob, esprob, edprob, enprob, essprob])
# Remake for all u0s.
rp = remake(prob; u0 = [X => 1, Y => 2])
@test rp[[X, Y]] == [1, 2]
@@ -152,8 +154,8 @@ end
# Test integrator indexing.
let
- @test_broken false # NOTE: Multiple problems for `nint` (https://github.com/SciML/SciMLBase.jl/issues/662).
- for int in deepcopy([oint, sint, jint])
+ @test_broken false # NOTE: Cannot even create a `ssint` (https://github.com/SciML/SciMLBase.jl/issues/660).
+ for int in deepcopy([oint, sint, jint, nint])
# Get u values.
@test int[X] == int[model.X] == int[:X] == 4
@test int[XY] == int[model.XY] == int[:XY] == 9
@@ -237,10 +239,10 @@ let
@test getu(sol, (XY,Y))(sol)[1] == getu(sol, (model.XY,model.Y))(sol)[1] == getu(sol, (:XY,:Y))(sol)[1] == (9, 5)
# Get u values via idxs and functional call.
- @test osol(0.0; idxs=X) == osol(0.0; idxs=model.X) == osol(0.0; idxs=:X) == 4
- @test osol(0.0; idxs=XY) == osol(0.0; idxs=model.XY) == osol(0.0; idxs=:XY) == 9
- @test osol(0.0; idxs = [XY,Y]) == osol(0.0; idxs = [model.XY,model.Y]) == osol(0.0; idxs = [:XY,:Y]) == [9, 5]
- @test_broken osol(0.0; idxs = (XY,Y)) == osol(0.0; idxs = (model.XY,model.Y)) == osol(0.0; idxs = (:XY,:Y)) == (9, 5) # https://github.com/SciML/SciMLBase.jl/issues/711
+ @test sol(0.0; idxs=X) == sol(0.0; idxs=model.X) == sol(0.0; idxs=:X) == 4
+ @test sol(0.0; idxs=XY) == sol(0.0; idxs=model.XY) == sol(0.0; idxs=:XY) == 9
+ @test sol(0.0; idxs = [XY,Y]) == sol(0.0; idxs = [model.XY,model.Y]) == sol(0.0; idxs = [:XY,:Y]) == [9, 5]
+ @test_broken sol(0.0; idxs = (XY,Y)) == sol(0.0; idxs = (model.XY,model.Y)) == sol(0.0; idxs = (:XY,:Y)) == (9, 5) # https://github.com/SciML/SciMLBase.jl/issues/711
# Get p values.
@test sol.ps[kp] == sol.ps[model.kp] == sol.ps[:kp] == 1.0
@@ -258,7 +260,7 @@ let
@test sol[X] == sol[model.X] == sol[:X]
@test sol[XY] == sol[model.XY][1] == sol[:XY]
@test sol[[XY,Y]] == sol[[model.XY,model.Y]] == sol[[:XY,:Y]]
- @test_broken sol[(XY,Y)] == sol[(model.XY,model.Y)] == sol[(:XY,:Y)] # https://github.com/SciML/SciMLBase.jl/issues/710
+ @test sol[(XY,Y)] == sol[(model.XY,model.Y)] == sol[(:XY,:Y)]
@test getu(sol, X)(sol) == getu(sol, model.X)(sol)[1] == getu(sol, :X)(sol)
@test getu(sol, XY)(sol) == getu(sol, model.XY)(sol)[1] == getu(sol, :XY)(sol)
@test getu(sol, [XY,Y])(sol) == getu(sol, [model.XY,model.Y])(sol) == getu(sol, [:XY,:Y])(sol)
@@ -277,7 +279,7 @@ end
# Tests plotting.
let
- for sol in deepcopy([osol, ssol, jsol])
+ for sol in deepcopy([osol, jsol, ssol])
# Single variable.
@test length(plot(sol; idxs = X).series_list) == 1
@test length(plot(sol; idxs = XY).series_list) == 1
@@ -382,4 +384,3 @@ let
reset_aggregated_jumps!(jint)
@test jint.cb.condition.ma_jumps.scaled_rates[1] == 6.0
end
-