From 8d2d6be784ed9e1b94e3ab2f699191df3a9b24e9 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 27 Dec 2023 17:21:35 +0100 Subject: [PATCH 01/47] init --- .../catalyst_for_new_julia_users.md | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md b/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md index 82c297b33d..f56fc260fc 100644 --- a/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md +++ b/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md @@ -54,15 +54,24 @@ To import a Julia package into a session, you can use the `using PackageName` co ```julia using Pkg Pkg.add("Catalyst") +using Catalyst ``` Here, the Julia package manager package (`Pkg`) is by default installed on your computer when Julia is installed, and can be activated directly. Next, we also wish to install the `DifferentialEquations` and `Plots` packages (for numeric simulation of models, and plotting, respectively). ```julia -Pkg.add("DifferentialEquations") -Pkg.add("Plots") +using Pkg +Pkg.activate(".") ``` Once a package has been installed through the `Pkg.add` command, this command does not have to be repeated if we restart our Julia session. We can now import all three packages into our current session with: ```@example ex2 using Catalyst +``` +This will only make Catalyst available for the current Julia session. If you exit Julia, you will have to run `using Catalyst` again to use its features (however, `Pkg.add("Catalyst")` does not need to be rerun). Next, we wish to install the `DifferentialEquations` and `Plots` packages (for numeric simulation of models, and plotting, respectively): +```julia +Pkg.add("DifferentialEquations") +Pkg.add("Plots") +``` +and also to import them into our current session: +```@example ex2 using DifferentialEquations using Plots ``` @@ -70,6 +79,30 @@ Here, if we restart Julia, these `using` commands *must be rerun*. A more comprehensive (but still short) introduction to package management in Julia is provided at [the end of this documentation page](@ref catalyst_for_new_julia_users_packages). It contains some useful information and is hence highly recommended reading. For a more detailed introduction to Julia package management, please read [the Pkg documentation](https://docs.julialang.org/en/v1/stdlib/Pkg/). +Here, while Catalyst has previously been installed on your computer, it has not been added to the new environment you created. To do so, simply run +```julia +using Pkg +Pkg.add("Catalyst") +``` +after which Catalyst can be imported through `using Catalyst`. You can get a list of all packages available in your current environment using: +```julia +Pkg.status() +``` + +So, why is this required, and why cannot we simply import any package installed on our computer? The reason is that most packages depend on other packages, and these dependencies may be restricted to only specific versions of these packages. This creates complicated dependency graphs that restrict what versions of what packages are compatible with each other. When you use `Pkg.add("PackageName")`, only a specific version of that package is actually added (the latest possible version as permitted by the dependency graph). Here, Julia environments both define what packages are available *and* their respective versions (these versions are also displayed by the `Pkg.status()` command). By doing this, Julia can guarantee that the packages (and their versions) specified in an environment are compatible with each other. + +The reason why all this is important is that it is *highly recommended* to, for each project, define a separate environment. To these, only add the required packages. General-purpose environments with a large number of packages often produce package-incompatibility issues. While these might not prevent you from installing all desired package, they often mean that you are unable to use the latest version of some packages. + +!!! note + A not-infrequent cause for reported errors with Catalyst (typically the inability to replicate code in tutorials) is package incompatibilities in large environments preventing the latest version of Catalyst from being installed. Hence, whenever an issue is encountered, it is useful to run `Pkg.status()` to check whenever the latest version of Catalyst is being used. + +Some additional useful Pkg commands are: +- `Pk.rm("PackageName")` removes a package from the current environment. +- `Pkg.update(PackageName")`: updates the designated package. + +!!! note + A useful feature of Julia's environment system is that enables the exact definition of what packages and versions were used to execute a script. This supports e.g. reproducibility in academic research. Here, by providing the corresponding Project.toml and Manifest.toml files, you can enable someone to reproduce the exact program just to perform some set of analyses. + ## Simulating a basic Catalyst model Now that we have some basic familiarity with Julia, and have installed and imported the required packages, we will create and simulate a basic chemical reaction network model using Catalyst. From b239bcf38d35102361a74c8b3471f2acd803c4c0 Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 29 Dec 2023 12:06:53 +0100 Subject: [PATCH 02/47] up --- .../catalyst_for_new_julia_users.md | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md b/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md index f56fc260fc..3c3a529dfd 100644 --- a/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md +++ b/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md @@ -70,8 +70,9 @@ This will only make Catalyst available for the current Julia session. If you exi Pkg.add("DifferentialEquations") Pkg.add("Plots") ``` -and also to import them into our current session: +Once a package has been installed through the `Pkg.add` command, this command does not have to be repeated if we restart our Julia session. We can now import all three packages into our current session with: ```@example ex2 +using Catalyst using DifferentialEquations using Plots ``` @@ -79,30 +80,6 @@ Here, if we restart Julia, these `using` commands *must be rerun*. A more comprehensive (but still short) introduction to package management in Julia is provided at [the end of this documentation page](@ref catalyst_for_new_julia_users_packages). It contains some useful information and is hence highly recommended reading. For a more detailed introduction to Julia package management, please read [the Pkg documentation](https://docs.julialang.org/en/v1/stdlib/Pkg/). -Here, while Catalyst has previously been installed on your computer, it has not been added to the new environment you created. To do so, simply run -```julia -using Pkg -Pkg.add("Catalyst") -``` -after which Catalyst can be imported through `using Catalyst`. You can get a list of all packages available in your current environment using: -```julia -Pkg.status() -``` - -So, why is this required, and why cannot we simply import any package installed on our computer? The reason is that most packages depend on other packages, and these dependencies may be restricted to only specific versions of these packages. This creates complicated dependency graphs that restrict what versions of what packages are compatible with each other. When you use `Pkg.add("PackageName")`, only a specific version of that package is actually added (the latest possible version as permitted by the dependency graph). Here, Julia environments both define what packages are available *and* their respective versions (these versions are also displayed by the `Pkg.status()` command). By doing this, Julia can guarantee that the packages (and their versions) specified in an environment are compatible with each other. - -The reason why all this is important is that it is *highly recommended* to, for each project, define a separate environment. To these, only add the required packages. General-purpose environments with a large number of packages often produce package-incompatibility issues. While these might not prevent you from installing all desired package, they often mean that you are unable to use the latest version of some packages. - -!!! note - A not-infrequent cause for reported errors with Catalyst (typically the inability to replicate code in tutorials) is package incompatibilities in large environments preventing the latest version of Catalyst from being installed. Hence, whenever an issue is encountered, it is useful to run `Pkg.status()` to check whenever the latest version of Catalyst is being used. - -Some additional useful Pkg commands are: -- `Pk.rm("PackageName")` removes a package from the current environment. -- `Pkg.update(PackageName")`: updates the designated package. - -!!! note - A useful feature of Julia's environment system is that enables the exact definition of what packages and versions were used to execute a script. This supports e.g. reproducibility in academic research. Here, by providing the corresponding Project.toml and Manifest.toml files, you can enable someone to reproduce the exact program just to perform some set of analyses. - ## Simulating a basic Catalyst model Now that we have some basic familiarity with Julia, and have installed and imported the required packages, we will create and simulate a basic chemical reaction network model using Catalyst. From 61ff1f644c3a001a12710f937cc8bb44c26d7dd3 Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 29 Dec 2023 15:17:15 +0100 Subject: [PATCH 03/47] init --- .../lattice_reaction_systems.jl | 76 +++++++++++++++---- .../lattice_reaction_systems_lattice_types.jl | 8 ++ 2 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index a153a0b2a6..faac2d70f5 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -1,14 +1,14 @@ ### 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 +struct LatticeReactionSystem{Q,R,S,T} # <: MT.AbstractTimeDependentSystem # Input values. """The reaction system within each compartment.""" - rs::ReactionSystem{S} + rs::ReactionSystem{Q} """The spatial reactions defined between individual nodes.""" - spatial_reactions::Vector{T} + spatial_reactions::Vector{R} """The graph on which the lattice is defined.""" - lattice::SimpleDiGraph{Int64} + lattice::S # Derived values. """The number of compartments.""" @@ -17,8 +17,7 @@ struct LatticeReactionSystem{S,T} # <: MT.AbstractTimeDependentSystem 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}} """ @@ -37,19 +36,29 @@ struct LatticeReactionSystem{S,T} # <: MT.AbstractTimeDependentSystem """ edge_parameters::Vector{BasicSymbolic{Real}} - 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) + """Whenever the initial input was a digraph.""" + init_digraph::Bool + """ + A list of all the edges on the lattice. + The format depends on the type of lattice (Cartesian grid, grid, or graph). + """ + edge_list::T + + function LatticeReactionSystem(rs::ReactionSystem{Q}, spatial_reactions::Vector{R}, + lattice::S, num_verts, edge_list::T; init_digraph = true) where {Q, R, S, T} + # Error checks. + if !(R <: AbstractSpatialReaction) | error("The second argument must be a vector of AbstractSpatialReaction subtypes.") end + # Computes derived values spatial species. Counts the total number of species. if isempty(spatial_reactions) spat_species = Vector{BasicSymbolic{Real}}[] else spat_species = unique(reduce(vcat, [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}}[] @@ -58,16 +67,46 @@ struct LatticeReactionSystem{S,T} # <: MT.AbstractTimeDependentSystem end edge_parameters = unique([rs_edge_parameters; srs_edge_parameters]) vertex_parameters = filter(!isedgeparameter, parameters(rs)) + + # Counts the number of edges and species types (number of vertexes already counted). + num_edges = false + num_species = length(unique([species(rs); spat_species])) + # Ensures the parameter order begins similarly to in the non-spatial ReactionSystem. ps = [parameters(rs); setdiff([edge_parameters; vertex_parameters], parameters(rs))] + # Checks that all spatial reactions are valid for this reactions system. foreach(sr -> check_spatial_reaction_validity(rs, sr; edge_parameters=edge_parameters), spatial_reactions) - return new{S,T}(rs, spatial_reactions, lattice, nv(lattice), ne(lattice), num_species, - init_digraph, spat_species, ps, vertex_parameters, edge_parameters) + + return new{Q,R,S,T}(rs, spatial_reactions, lattice, num_verts, ne(lattice), num_species, + spat_species, ps, vertex_parameters, edge_parameters, init_digraph, edge_list) end end -function LatticeReactionSystem(rs, srs, lat::SimpleGraph) - return LatticeReactionSystem(rs, srs, DiGraph(lat); init_digraph = false) + +# Creates a LatticeReactionSystem from a CartesianGrid lattice. +function LatticeReactionSystem(rs, srs, lattice::CartesianGridRej{S,T}; diagonal_connections=false) where {S,T} + num_verts = prod(lattice.dims) + edge_list = false + return LatticeReactionSystem(rs, srs, lattice, num_verts, edge_list; init_digraph = false, edge_list) +end + +# Creates a LatticeReactionSystem from a Boolean Array lattice. +function LatticeReactionSystem(rs, srs, lattice::Array{Bool, T}; diagonal_connections=false) where {T} + num_verts = count(lattice) + edge_list = false + return LatticeReactionSystem(rs, srs, lattice, num_verts, edge_list; init_digraph = false, edge_list) +end + +# Creates a LatticeReactionSystem from a DiGraph lattice. +function LatticeReactionSystem(rs, srs, lattice::SimpleGraph) + num_verts = nv(lattice) + edge_list = false + return LatticeReactionSystem(rs, srs, lattice, num_verts, edge_list; init_digraph = false, edge_list) +end + +# Creates a LatticeReactionSystem from a Graph lattice. +function LatticeReactionSystem(rs, srs, lattice::SimpleGraph) + return LatticeReactionSystem(rs, srs, DiGraph(lattice); init_digraph = false) end ### Lattice ReactionSystem Getters ### @@ -89,3 +128,10 @@ ModelingToolkit.nameof(lrs::LatticeReactionSystem) = nameof(lrs.rs) # Checks if a lattice reaction system is a pure (linear) transport reaction system. is_transport_system(lrs::LatticeReactionSystem) = all(sr -> sr isa TransportReaction, lrs.spatial_reactions) + +# Checks if a LatticeReactionSystem have a Cartesian grid lattice. +has_cartesian_lattice(lrs::LatticeReactionsSystem) = lrs.lattice isa CartesianGridRej{S,T} where {S,T} +# Checks if a LatticeReactionSystem have a regular grid lattice. +has_cartesian_lattice(lrs::LatticeReactionsSystem) = lrs.lattice isa Array{Bool, T} where T +# Checks if a LatticeReactionSystem have a graph lattice. +has_cartesian_lattice(lrs::LatticeReactionsSystem) = lrs.lattice isa SimpleDiGraph \ No newline at end of file diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl new file mode 100644 index 0000000000..4c13fa6779 --- /dev/null +++ b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl @@ -0,0 +1,8 @@ +### Preparations ### + +# Fetch packages. +using Catalyst, Graphs, OrdinaryDiffEq, Test + +### Run Tests ### + +# Checks that some grids, created using different approaches, generates the same spatial structures. \ No newline at end of file From 341424343f24cbafdbf004c68044582956ddc862 Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 29 Dec 2023 15:17:28 +0100 Subject: [PATCH 04/47] add test run --- test/runtests.jl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 9196ca91c8..079c8a33e7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -56,13 +56,11 @@ using SafeTestsets, Test @time @safetestset "MTK Problem Inputs" begin include("upstream/mtk_problem_inputs.jl") end #end - #if GROUP == "All" || GROUP == "Spatial" - # 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 "Lattice Reaction Systems Lattice Types" begin include("spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl") end + @time @safetestset "ODE Lattice Systems Simulations" begin include("spatial_reaction_systems/lattice_reaction_systems_ODEs.jl") end @time @safetestset "Jump Lattice Systems Simulations" begin include("spatial_reaction_systems/lattice_reaction_systems_jumps.jl") end - #end #if GROUP == "All" || GROUP == "Visualisation-Extensions" # Tests network visualisation. From 64cc18683cdba747e2b5cc6916593ff46dd46995 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 31 Dec 2023 14:23:46 +0100 Subject: [PATCH 05/47] init --- .../lattice_reaction_systems.jl | 72 ++++++++++++++----- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index faac2d70f5..cb6b4afbd4 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -1,14 +1,14 @@ ### 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{Q,R,S,T} # <: MT.AbstractTimeDependentSystem +struct LatticeReactionSystem{R,S,T} # <: MT.AbstractTimeDependentSystem # Input values. """The reaction system within each compartment.""" - rs::ReactionSystem{Q} + rs::ReactionSystem{R} """The spatial reactions defined between individual nodes.""" - spatial_reactions::Vector{R} + spatial_reactions::Vector{S} """The graph on which the lattice is defined.""" - lattice::S + lattice::T # Derived values. """The number of compartments.""" @@ -36,18 +36,23 @@ struct LatticeReactionSystem{Q,R,S,T} # <: MT.AbstractTimeDependentSystem """ edge_parameters::Vector{BasicSymbolic{Real}} - """Whenever the initial input was a digraph.""" - init_digraph::Bool + """ + Whenever the initial input was directed. True for Digraph input, false for other graphs and all grids. + Used later to know whenever duplication of edge parameter should be duplicated + (i.e. allow parameter for edge i => j to be used for j => i). + Even if false, giving separate parameters for both directions is still permitted. + """ + directed_edges::Bool """ A list of all the edges on the lattice. The format depends on the type of lattice (Cartesian grid, grid, or graph). """ - edge_list::T + edge_list::Vector{Vector{Pair{Int64,Int64}}} - function LatticeReactionSystem(rs::ReactionSystem{Q}, spatial_reactions::Vector{R}, - lattice::S, num_verts, edge_list::T; init_digraph = true) where {Q, R, S, T} + function LatticeReactionSystem(rs::ReactionSystem{R}, spatial_reactions::Vector{S}, + lattice::S, num_verts, edge_list::T; directed_edges = false) where {R, S, T} # Error checks. - if !(R <: AbstractSpatialReaction) | + if !(S <: AbstractSpatialReaction) | error("The second argument must be a vector of AbstractSpatialReaction subtypes.") end @@ -85,28 +90,61 @@ end # Creates a LatticeReactionSystem from a CartesianGrid lattice. function LatticeReactionSystem(rs, srs, lattice::CartesianGridRej{S,T}; diagonal_connections=false) where {S,T} + (length(lattice.dims) > 3) && error("Grids of higher dimension than 3 is currently not supported.") + diagonal_connections && error("Diagonal connections is currently unsupported for Cartesian grid lattices.") num_verts = prod(lattice.dims) + + # Prepares the list of edges. + edge_list = [Vector{Pair{Int64,Int64}}() for i in 1:num_verts] + + # Ensures that the matrix is on a 3d form + lattice = reshape(vals,dims..., fill(1,3- length(dims))...) + + edge_list = false - return LatticeReactionSystem(rs, srs, lattice, num_verts, edge_list; init_digraph = false, edge_list) + return LatticeReactionSystem(rs, srs, lattice, num_verts, edge_list; edge_list) end # Creates a LatticeReactionSystem from a Boolean Array lattice. -function LatticeReactionSystem(rs, srs, lattice::Array{Bool, T}; diagonal_connections=false) where {T} +function LatticeReactionSystem(rs, srs, lattice::Array{Bool, T}; diagonal_connections=false) where {T} + dims = size(vals) + (length(dims) > 3) && error("Grids of higher dimension than 3 is currently not supported.") + diagonal_connections && error("Diagonal connections is currently unsupported for grid lattices.") num_verts = count(lattice) - edge_list = false - return LatticeReactionSystem(rs, srs, lattice, num_verts, edge_list; init_digraph = false, edge_list) + + # Prepares the list of edges. + edge_list = [Vector{Pair{Int64,Int64}}() for i in 1:num_verts] + + # Ensures that the matrix is on a 3d form + lattice = CartesianGrid((lattice.dims..., fill(1,3- length(lattice.dims))...)) + + # Loops through, simultaneously, the coordinates of each position in the grid, as well as that + # coordinate's (scalar) flat index. + indices = [(i, j, k) for i in 1:lattice.dims[1], j in 1:lattice.dims[2], k in 1:lattice.dims[3]] + flat_indices = 1:prod(lattice.dims) + for ((i,j,k), idx) in zip(indices, flat_indices) + for ii in i-1:i+1, jj in j-1:j+1, kk in k-1:k+1 + # Finds the (scalar) flat index of this neighbour. If it is a valid neighbour, add it to edge_list. + n_idx = (k - 1) * (l * m) + (j - 1) * l + i + (1 <= n_idx <= flat_indices[end]) || continue + (n_idx == idx) && continue + push!(edge_list[cur_vert], n_idx) + end + end + + return LatticeReactionSystem(rs, srs, lattice, num_verts, edge_list; edge_list) end # Creates a LatticeReactionSystem from a DiGraph lattice. -function LatticeReactionSystem(rs, srs, lattice::SimpleGraph) +function LatticeReactionSystem(rs, srs, lattice::SimpleGraph; directed_edges = true) num_verts = nv(lattice) edge_list = false - return LatticeReactionSystem(rs, srs, lattice, num_verts, edge_list; init_digraph = false, edge_list) + return LatticeReactionSystem(rs, srs, lattice, num_verts, edge_list; directed_edges, edge_list) end # Creates a LatticeReactionSystem from a Graph lattice. function LatticeReactionSystem(rs, srs, lattice::SimpleGraph) - return LatticeReactionSystem(rs, srs, DiGraph(lattice); init_digraph = false) + return LatticeReactionSystem(rs, srs, DiGraph(lattice); directed_edges) end ### Lattice ReactionSystem Getters ### From da0b3f10a2f920942fa794b4f1ab279027ddd840 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 31 Dec 2023 15:22:25 +0100 Subject: [PATCH 06/47] add tests for lattice types --- .../lattice_reaction_systems.jl | 6 +- .../lattice_reaction_systems_ODEs.jl | 5 +- .../lattice_reaction_systems_lattice_types.jl | 69 ++++++++++++++++++- test/spatial_test_networks.jl | 32 +++++++-- 4 files changed, 102 insertions(+), 10 deletions(-) diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index cb6b4afbd4..e8e4d2cae0 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -47,7 +47,7 @@ struct LatticeReactionSystem{R,S,T} # <: MT.AbstractTimeDependentSystem A list of all the edges on the lattice. The format depends on the type of lattice (Cartesian grid, grid, or graph). """ - edge_list::Vector{Vector{Pair{Int64,Int64}}} + edge_list::Vector{Vector{Int64}} function LatticeReactionSystem(rs::ReactionSystem{R}, spatial_reactions::Vector{S}, lattice::S, num_verts, edge_list::T; directed_edges = false) where {R, S, T} @@ -95,7 +95,7 @@ function LatticeReactionSystem(rs, srs, lattice::CartesianGridRej{S,T}; diagonal num_verts = prod(lattice.dims) # Prepares the list of edges. - edge_list = [Vector{Pair{Int64,Int64}}() for i in 1:num_verts] + edge_list = [Vector{Int64}() for i in 1:num_verts] # Ensures that the matrix is on a 3d form lattice = reshape(vals,dims..., fill(1,3- length(dims))...) @@ -113,7 +113,7 @@ function LatticeReactionSystem(rs, srs, lattice::Array{Bool, T}; diagonal_connec num_verts = count(lattice) # Prepares the list of edges. - edge_list = [Vector{Pair{Int64,Int64}}() for i in 1:num_verts] + edge_list = [Vector{Int64}() for i in 1:num_verts] # Ensures that the matrix is on a 3d form lattice = CartesianGrid((lattice.dims..., fill(1,3- length(lattice.dims))...)) diff --git a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl index cdefc6ebc9..ec9f7d79d2 100644 --- a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl +++ b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl @@ -15,7 +15,10 @@ rng = StableRNG(12345) t = default_t() ### Tests Simulations Don't Error ### -for grid in [small_2d_grid, short_path, small_directed_cycle] +for grid in [small_2d_grid, short_path, small_directed_cycle, + small_1d_cartesian_grid, small_2d_cartesian_grid, small_3d_cartesian_grid, + small_1d_regular_grid, small_2d_regular_grid, small_3d_regular_grid, + random_2d_regular_grid] # Non-stiff case for srs in [Vector{TransportReaction}(), SIR_srs_1, SIR_srs_2] lrs = LatticeReactionSystem(SIR_system, srs, grid) diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl index 4c13fa6779..d4687a16c7 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl @@ -5,4 +5,71 @@ using Catalyst, Graphs, OrdinaryDiffEq, Test ### Run Tests ### -# Checks that some grids, created using different approaches, generates the same spatial structures. \ No newline at end of file +# 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 = Graphs.grid([5, 5]) + regular_grid = fill(true, 5, 5) + graph_grid = Graphs.grid([5, 5]) + + cartesian_lrs = LatticeReactionSystem(brusselator_system, srs, cartesian_grid) + regular_lrs = LatticeReactionSystem(brusselator_system, srs, regular_grid) + graph_lrs = LatticeReactionSystem(brusselator_system, srs, graph_grid) + + # Check internal structures. + @test cartesian_lrs.rs == regular_lrs.rs == graph_lrs.rs + @test cartesian_lrs.spatial_reactions == regular_lrs.spatial_reactions == graph_lrs.spatial_reactions + @test cartesian_lrs.num_verts == regular_lrs.num_verts == graph_lrs.num_verts + @test cartesian_lrs.num_edges == regular_lrs.num_edges == graph_lrs.num_edges + @test cartesian_lrs.num_species == regular_lrs.num_species == graph_lrs.num_species + @test cartesian_lrs.spat_species == regular_lrs.spat_species == graph_lrs.spat_species + @test cartesian_lrs.parameters == regular_lrs.parameters == graph_lrs.parameters + @test cartesian_lrs.vertex_parameters == regular_lrs.vertex_parameters == graph_lrs.vertex_parameters + @test cartesian_lrs.edge_parameters == regular_lrs.edge_parameters == graph_lrs.edge_parameters + @test cartesian_lrs.directed_edges == regular_lrs.directed_edges == graph_lrs.directed_edges + @test cartesian_lrs.edge_list == regular_lrs.edge_list == graph_lrs.edge_list + + # Checks that simulations yields the same output. + u0 = [:X => rand_v_vals(graph_lrs.lattice, 10.0), :Y => 2.0] + pV = [:A => 0.5 .+ rand_v_vals(graph_lrs.lattice, 0.5), :B => 4.0] + pE= map(sp -> sp => 0.2, spatial_param_syms(lrs)) + + cartesian_oprob = ODEProblem(cartesian_lrs, u0, (0.0, 100.0), (pV, pE)) + regular_oprob = ODEProblem(regular_lrs, u0, (0.0, 100.0), (pV, pE)) + graph_oprob = ODEProblem(graph_lrs, u0, (0.0, 100.0), (pV, pE)) + + cartesian_sol = solve(cartesian_oprob, QNDF(); saveat=0.1) + regular_sol = solve(regular_oprob, QNDF(); saveat=0.1) + graph_sol = solvegraph(_oprob, QNDF(); saveat=0.1) + + @test cartesian_sol.u ≈ regular_sol.u ≈ graph_sol +end + +# Checks that a regular grid with absent vertices generate the same output as corresponding graph. +let + # Create LatticeReactionsSystems + regular_grid = [true true true; true false true; true true true] + graph_grid = cycle_graph(8) + + regular_lrs = LatticeReactionSystem(brusselator_system, srs, regular_grid) + graph_lrs = LatticeReactionSystem(brusselator_system, srs, graph_grid) + + # Check internal structures. + @test regular_lrs.num_verts == graph_lrs.num_verts + @test regular_lrs.num_edges == graph_lrs.num_edges + @test regular_lrs.edge_list == graph_lrs.edge_list + + # Checks that simulations yields the same output. + u0 = [:X => rand_v_vals(graph_lrs.lattice, 10.0), :Y => 2.0] + pV = [:A => 0.5 .+ rand_v_vals(graph_lrs.lattice, 0.5), :B => 4.0] + pE= map(sp -> sp => 0.2, spatial_param_syms(lrs)) + + regular_oprob = ODEProblem(regular_lrs, u0, (0.0, 100.0), (pV, pE)) + graph_oprob = ODEProblem(graph_lrs, u0, (0.0, 100.0), (pV, pE)) + + regular_sol = solve(regular_oprob, QNDF(); saveat=0.1) + graph_sol = solvegraph(_oprob, QNDF(); saveat=0.1) + + @test regular_sol.u ≈ graph_sol +end \ No newline at end of file diff --git a/test/spatial_test_networks.jl b/test/spatial_test_networks.jl index f2f9472e87..813466754c 100644 --- a/test/spatial_test_networks.jl +++ b/test/spatial_test_networks.jl @@ -168,7 +168,29 @@ sigmaB_srs_2 = [sigmaB_tr_σB, sigmaB_tr_w, sigmaB_tr_v] ### Declares Lattices ### -# Grids. +# Cartesian grids. +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)) + +# Regular grids. +small_1d_regular_grid = fill(true, 5) +small_2d_regular_grid = fill(true, 5, 5) +small_3d_regular_grid = fill(true, 5, 5, 5) + +large_1d_regular_grid = fill(true, 5) +large_2d_regular_grid = fill(true, 5, 5) +large_3d_regular_grid = fill(true, 5, 5, 5) + +random_1d_regular_grid = rand([true, true, true, false], 10) +random_2d_regular_grid = rand([true, true, true, false], 10, 10) +random_3d_regular_grid = rand([true, true, true, false], 10, 10, 10) + +# Graph - grids. very_small_2d_grid = Graphs.grid([2, 2]) small_2d_grid = Graphs.grid([5, 5]) medium_2d_grid = Graphs.grid([20, 20]) @@ -178,16 +200,16 @@ small_3d_grid = Graphs.grid([5, 5, 5]) medium_3d_grid = Graphs.grid([20, 20, 20]) large_3d_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 From 170b9f63a9ff2e0c41537fb5281c260cedea5684 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 2 Jan 2024 17:04:06 +0100 Subject: [PATCH 07/47] up --- src/Catalyst.jl | 4 +++- src/spatial_reaction_systems/lattice_reaction_systems.jl | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Catalyst.jl b/src/Catalyst.jl index 2aab60fb54..a69883aaa1 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -8,7 +8,8 @@ using SparseArrays, DiffEqBase, Reexport, Setfield using LaTeXStrings, Latexify, Requires using JumpProcesses: JumpProcesses, JumpProblem, MassActionJump, ConstantRateJump, VariableRateJump, - SpatialMassActionJump + SpatialMassActionJump, + CartesianGrid, CartesianGridRej # ModelingToolkit imports and convenience functions we use using ModelingToolkit @@ -175,6 +176,7 @@ export isedgeparameter include("spatial_reaction_systems/lattice_reaction_systems.jl") export LatticeReactionSystem export spatial_species, vertex_parameters, edge_parameters +export CartesianGrid, CartesianGridReJ # (Implemented in JumpProcesses) # Various utility functions include("spatial_reaction_systems/utility.jl") diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index e8e4d2cae0..dded97c22e 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -83,7 +83,7 @@ struct LatticeReactionSystem{R,S,T} # <: MT.AbstractTimeDependentSystem # Checks that all spatial reactions are valid for this reactions 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, ne(lattice), num_species, + return new{R,S,T}(rs, spatial_reactions, lattice, num_verts, ne(lattice), num_species, spat_species, ps, vertex_parameters, edge_parameters, init_digraph, edge_list) end end @@ -168,8 +168,8 @@ ModelingToolkit.nameof(lrs::LatticeReactionSystem) = nameof(lrs.rs) is_transport_system(lrs::LatticeReactionSystem) = all(sr -> sr isa TransportReaction, lrs.spatial_reactions) # Checks if a LatticeReactionSystem have a Cartesian grid lattice. -has_cartesian_lattice(lrs::LatticeReactionsSystem) = lrs.lattice isa CartesianGridRej{S,T} where {S,T} +has_cartesian_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa CartesianGridRej{S,T} where {S,T} # Checks if a LatticeReactionSystem have a regular grid lattice. -has_cartesian_lattice(lrs::LatticeReactionsSystem) = lrs.lattice isa Array{Bool, T} where T +has_cartesian_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa Array{Bool, T} where T # Checks if a LatticeReactionSystem have a graph lattice. -has_cartesian_lattice(lrs::LatticeReactionsSystem) = lrs.lattice isa SimpleDiGraph \ No newline at end of file +has_cartesian_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa SimpleDiGraph \ No newline at end of file From 4c4762ff99040a19bd64ebdde8e9c96fd10d83fe Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 3 Jan 2024 10:52:57 +0100 Subject: [PATCH 08/47] finish alternative grids --- .../lattice_reaction_systems.jl | 13 ++++- .../lattice_reaction_systems_lattice_types.jl | 48 ++++++++++++++++++- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index dded97c22e..9cb81eda4c 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -170,6 +170,15 @@ is_transport_system(lrs::LatticeReactionSystem) = all(sr -> sr isa TransportReac # Checks if a LatticeReactionSystem have a Cartesian grid lattice. has_cartesian_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa CartesianGridRej{S,T} where {S,T} # Checks if a LatticeReactionSystem have a regular grid lattice. -has_cartesian_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa Array{Bool, T} where T +has_regular_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa Array{Bool, T} where T +# Checks if a LatticeReactionSystem have a grid lattice (cartesian or regular). +has_grid_lattice(lrs::LatticeReactionSystem) = (has_cartesian_lattice(lrs) || has_regular_lattice(lrs)) # Checks if a LatticeReactionSystem have a graph lattice. -has_cartesian_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa SimpleDiGraph \ No newline at end of file +has_graph_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa SimpleDiGraph + +# Returns the dimensions of a LatticeReactionNetwork with a grid lattice. +function grid_dims(lrs::LatticeReactionSystem) + has_cartesian_lattice(lrs) && (return length(lrs.lattice.dims)) + has_regular_lattice(lrs) && (return length(size(lrs.lattice))) + error("Dimensions are only defined for LatticeReactionSystems with grid-based lattices (not graph-based).") +end \ No newline at end of file diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl index d4687a16c7..6869ac9f1f 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl @@ -5,10 +5,54 @@ using Catalyst, Graphs, OrdinaryDiffEq, Test ### Run Tests ### +# Test errors when attempting to create networks with dimension > 3. +let + @test_throws Exception LatticeReactionSystem(brusselator_system, srs, CartesianGrid((5, 5, 5, 5))) + @test_throws Exception LatticeReactionSystem(brusselator_system, srs, fill(true, 5, 5, 5, 5)) +end + +# Checks that getter functions give the correct output. +let + # Create LatticeReactionsSystems. + cartesian_1d_lrs = LatticeReactionSystem(brusselator_system, srs, small_1d_cartesian_grid) + cartesian_2d_lrs = LatticeReactionSystem(brusselator_system, srs, small_2d_cartesian_grid) + cartesian_3d_lrs = LatticeReactionSystem(brusselator_system, srs, small_3d_cartesian_grid) + regular_1d_lrs = LatticeReactionSystem(brusselator_system, srs, small_1d_regular_grid) + regular_2d_lrs = LatticeReactionSystem(brusselator_system, srs, small_2d_regular_grid) + regular_3d_lrs = LatticeReactionSystem(brusselator_system, srs, small_3d_regular_grid) + graph_lrs = LatticeReactionSystem(brusselator_system, srs, small_2d_grid) + + # Test lattice type getters. + @test has_cartesian_grid_lattice(cartesian_2d_lrs) + @test !has_cartesian_grid_lattice(regular_2d_lrs) + @test !has_cartesian_grid_lattice(graph_lrs) + + @test !has_regular_grid_lattice(cartesian_2d_lrs) + @test has_regular_grid_lattice(regular_2d_lrs) + @test !has_regular_grid_lattice(graph_lrs) + + @test has_grid_lattice(cartesian_2d_lrs) + @test has_grid_lattice(regular_2d_lrs) + @test !has_grid_lattice(graph_lrs) + + @test !has_graph_lattice(cartesian_2d_lrs) + @test !has_graph_lattice(regular_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(regular_1d_lrs) == 1 + @test grid_dims(regular_2d_lrs) == 2 + @test grid_dims(regular_3d_lrs) == 3 + @test_throws Exception grid_dims(graph_lrs) +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 + # Create LatticeReactionsSystems. cartesian_grid = Graphs.grid([5, 5]) regular_grid = fill(true, 5, 5) graph_grid = Graphs.grid([5, 5]) @@ -48,7 +92,7 @@ end # Checks that a regular grid with absent vertices generate the same output as corresponding graph. let - # Create LatticeReactionsSystems + # Create LatticeReactionsSystems. regular_grid = [true true true; true false true; true true true] graph_grid = cycle_graph(8) From c6bd6af6b778fc3f21009bbfaeab6772a5ac9aeb Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 3 Jan 2024 10:53:59 +0100 Subject: [PATCH 09/47] add tests --- .../lattice_reaction_systems_lattice_types.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl index 6869ac9f1f..168711c9ff 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl @@ -23,13 +23,13 @@ let graph_lrs = LatticeReactionSystem(brusselator_system, srs, small_2d_grid) # Test lattice type getters. - @test has_cartesian_grid_lattice(cartesian_2d_lrs) - @test !has_cartesian_grid_lattice(regular_2d_lrs) - @test !has_cartesian_grid_lattice(graph_lrs) + @test has_cartesian_lattice(cartesian_2d_lrs) + @test has_cartesian_lattice(regular_2d_lrs) + @test has_cartesian_lattice(graph_lrs) - @test !has_regular_grid_lattice(cartesian_2d_lrs) - @test has_regular_grid_lattice(regular_2d_lrs) - @test !has_regular_grid_lattice(graph_lrs) + @test !has_regular_lattice(cartesian_2d_lrs) + @test !has_regular_lattice(regular_2d_lrs) + @test !has_regular_lattice(graph_lrs) @test has_grid_lattice(cartesian_2d_lrs) @test has_grid_lattice(regular_2d_lrs) From ed88b141caaed87a7d4c5a14d7373fe7e7c5e281 Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 26 Jan 2024 12:40:13 -0500 Subject: [PATCH 10/47] finalise grid alternatives. --- .../lattice_reaction_systems.jl | 151 +++++---- .../lattice_reaction_systems.jl | 304 ++++++++++++++++++ .../lattice_reaction_systems_lattice_types.jl | 72 ++--- test/spatial_test_networks.jl | 24 +- 4 files changed, 448 insertions(+), 103 deletions(-) create mode 100644 test/spatial_reaction_systems/lattice_reaction_systems.jl diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index 9cb81eda4c..6b48175feb 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -1,14 +1,15 @@ ### 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{R,S,T} # <: MT.AbstractTimeDependentSystem + +# Describes a spatial reaction network over a lattice. +# Adding the "<: MT.AbstractTimeDependentSystem" part messes up show, disabling me from creating LRSs. Should be fixed some time. +struct LatticeReactionSystem{Q,R,S,T} # <: MT.AbstractTimeDependentSystem # Input values. """The reaction system within each compartment.""" - rs::ReactionSystem{R} + rs::ReactionSystem{Q} """The spatial reactions defined between individual nodes.""" - spatial_reactions::Vector{S} + spatial_reactions::Vector{R} """The graph on which the lattice is defined.""" - lattice::T + lattice::S # Derived values. """The number of compartments.""" @@ -44,15 +45,15 @@ struct LatticeReactionSystem{R,S,T} # <: MT.AbstractTimeDependentSystem """ directed_edges::Bool """ - A list of all the edges on the lattice. + An iterator over all the edges on the lattice. The format depends on the type of lattice (Cartesian grid, grid, or graph). """ - edge_list::Vector{Vector{Int64}} + edge_iterator::T - function LatticeReactionSystem(rs::ReactionSystem{R}, spatial_reactions::Vector{S}, - lattice::S, num_verts, edge_list::T; directed_edges = false) where {R, S, T} + function LatticeReactionSystem(rs::ReactionSystem{Q}, spatial_reactions::Vector{R}, lattice::S, + num_verts, num_edges, edge_iterator::T; directed_edges = false) where {Q,R, S, T} # Error checks. - if !(S <: AbstractSpatialReaction) | + if !(R <: AbstractSpatialReaction) error("The second argument must be a vector of AbstractSpatialReaction subtypes.") end @@ -73,8 +74,8 @@ struct LatticeReactionSystem{R,S,T} # <: MT.AbstractTimeDependentSystem edge_parameters = unique([rs_edge_parameters; srs_edge_parameters]) vertex_parameters = filter(!isedgeparameter, parameters(rs)) - # Counts the number of edges and species types (number of vertexes already counted). - num_edges = false + # Counts the number of species types. The number of vertexes and compartments is given in input. + # `num_edges` cannot be computed here, because how it is computed depends on the lattice and `typeof(edge_iterator)`. num_species = length(unique([species(rs); spat_species])) # Ensures the parameter order begins similarly to in the non-spatial ReactionSystem. @@ -83,68 +84,108 @@ struct LatticeReactionSystem{R,S,T} # <: MT.AbstractTimeDependentSystem # Checks that all spatial reactions are valid for this reactions system. foreach(sr -> check_spatial_reaction_validity(rs, sr; edge_parameters=edge_parameters), spatial_reactions) - return new{R,S,T}(rs, spatial_reactions, lattice, num_verts, ne(lattice), num_species, - spat_species, ps, vertex_parameters, edge_parameters, init_digraph, edge_list) + return new{Q,R,S,T}(rs, spatial_reactions, lattice, num_verts, num_edges, num_species, + spat_species, ps, vertex_parameters, edge_parameters, directed_edges, edge_iterator) end end -# Creates a LatticeReactionSystem from a CartesianGrid lattice. +# Creates a LatticeReactionSystem from a CartesianGrid lattice (cartesian grid). function LatticeReactionSystem(rs, srs, lattice::CartesianGridRej{S,T}; diagonal_connections=false) where {S,T} + # Error checks. (length(lattice.dims) > 3) && error("Grids of higher dimension than 3 is currently not supported.") - diagonal_connections && error("Diagonal connections is currently unsupported for Cartesian grid lattices.") - num_verts = prod(lattice.dims) - - # Prepares the list of edges. - edge_list = [Vector{Int64}() for i in 1:num_verts] # Ensures that the matrix is on a 3d form - lattice = reshape(vals,dims..., fill(1,3- length(dims))...) - + lattice = CartesianGrid((lattice.dims..., fill(1, 3-length(lattice.dims))...)) + + # Counts vertexes and edges. The `num_edges` count formula counts the number of internal, side, + # edge, and corner vertexes (on the grid). The number of edges from each depend on whether diagonal + # connections are allowed. The formula holds even if l, m, and/or n are 1. + l,m,n = 2,3,4 + num_verts = l * m * n + (ni, ns, ne, nc) = diagonal_connections ? (26,17,11,7) : (6,5,4,3) + num_edges = ni*(l-2)*(m-2)*(n-2) + # Internal vertexes. + ns*(2(l-2)*(m-2) + 2(l-2)*(n-2) + 2(m-2)*(n-2)) + # Side vertexes. + ne*(4(l-2) + 4(m-2) + 4(n-2)) + # Edge vertexes. + nc*8 # Corner vertexes. + + # Creates an iterator over all edges. + # Currently creates a full vector. Future version might be for efficient. + edge_iterator = Vector{Pair{Int64,Int64}}(undef, num_edges) + # Loops through, simultaneously, the coordinates of each position in the grid, as well as that + # coordinate's (scalar) flat index. For each grid point, loops through all potential neighbours. + indices = [(L, M, N) for L in 1:l, M in 1:m, N in 1:n] + flat_indices = 1:num_verts + next_vert = 0 + for ((L, M, N), idx) in zip(indices, flat_indices) + for LL in max(L - 1, 1):min(L + 1, l), + MM in max(M - 1, 1):min(M + 1, m), + NN in max(N - 1, 1):min(N + 1, n) + + # Which (LL,MM,NN) indexes are valid neighbours depends on whether diagonal connects are permitted. + !diagonal_connections && (count([L==LL, M==MM, N==NN]) == 2) || continue + diagonal_connections && (L==LL) && (M==MM) && (N==NN) && continue + + # Computes the neighbours scalar index. Add that connection to `edge_iterator`. + neighbour_idx = LL + (MM - 1) * l + (NN - 1) * m * l + edge_iterator[next_vert += 1] = (idx => neighbour_idx) + end + end - edge_list = false - return LatticeReactionSystem(rs, srs, lattice, num_verts, edge_list; edge_list) + return LatticeReactionSystem(rs, srs, lattice, num_verts, num_edges, edge_iterator) end -# Creates a LatticeReactionSystem from a Boolean Array lattice. +# Creates a LatticeReactionSystem from a Boolean Array lattice (masked grid). function LatticeReactionSystem(rs, srs, lattice::Array{Bool, T}; diagonal_connections=false) where {T} - dims = size(vals) + # Error checks. + dims = size(lattice) (length(dims) > 3) && error("Grids of higher dimension than 3 is currently not supported.") - diagonal_connections && error("Diagonal connections is currently unsupported for grid lattices.") - num_verts = count(lattice) - - # Prepares the list of edges. - edge_list = [Vector{Int64}() for i in 1:num_verts] # Ensures that the matrix is on a 3d form - lattice = CartesianGrid((lattice.dims..., fill(1,3- length(lattice.dims))...)) + lattice = reshape(lattice, [dims...; fill(1, 3-length(dims))]...) + + # Counts vertexes (edges have to be counted after the iterator have been created). + num_verts = count(lattice) + # Creates an iterator over all edges.Currently a full vector of all edges (as pairs). + edge_iterator = Vector{Pair{Int64,Int64}}() # Loops through, simultaneously, the coordinates of each position in the grid, as well as that - # coordinate's (scalar) flat index. - indices = [(i, j, k) for i in 1:lattice.dims[1], j in 1:lattice.dims[2], k in 1:lattice.dims[3]] - flat_indices = 1:prod(lattice.dims) - for ((i,j,k), idx) in zip(indices, flat_indices) - for ii in i-1:i+1, jj in j-1:j+1, kk in k-1:k+1 - # Finds the (scalar) flat index of this neighbour. If it is a valid neighbour, add it to edge_list. - n_idx = (k - 1) * (l * m) + (j - 1) * l + i - (1 <= n_idx <= flat_indices[end]) || continue - (n_idx == idx) && continue - push!(edge_list[cur_vert], n_idx) + # coordinate's (scalar) flat index. For each grid point, loops through all potential neighbours. + l,m,n = size(lattice) + indices = [(L, M, N) for L in 1:l, M in 1:m, N in 1:n] + flat_indices = 1:num_verts + for ((L, M, N), idx) in zip(indices, flat_indices) + for LL in max(L - 1, 1):min(L + 1, l), + MM in max(M - 1, 1):min(M + 1, m), + NN in max(N - 1, 1):min(N + 1, n) + + # Ensures that the neighbour is a valid lattice point. + lattice[LL,MM,NN] || continue + + # Which (LL,MM,NN) indexes are valid neighbours depends on whether diagonal connects are permitted. + !diagonal_connections && (count([L==LL, M==MM, N==NN]) == 2) || continue + diagonal_connections && (L==LL) && (M==MM) && (N==NN) && continue + + # Computes the neighbours scalar index. Add that connection to `edge_iterator`. + neighbour_idx = LL + (MM - 1) * l + (NN - 1) * m * l + push!(edge_iterator, idx => neighbour_idx) end end + num_edges = length(edge_iterator) - return LatticeReactionSystem(rs, srs, lattice, num_verts, edge_list; edge_list) + return LatticeReactionSystem(rs, srs, lattice, num_verts, num_edges, edge_iterator) end -# Creates a LatticeReactionSystem from a DiGraph lattice. -function LatticeReactionSystem(rs, srs, lattice::SimpleGraph; directed_edges = true) +# Creates a LatticeReactionSystem from a DiGraph lattice (graph grid). +function LatticeReactionSystem(rs, srs, lattice::DiGraph; directed_edges = true) num_verts = nv(lattice) - edge_list = false - return LatticeReactionSystem(rs, srs, lattice, num_verts, edge_list; directed_edges, edge_list) + num_edges = ne(lattice) + edge_iterator = edges(lattice) + return LatticeReactionSystem(rs, srs, lattice, num_verts, num_edges, edge_iterator; directed_edges) end -# Creates a LatticeReactionSystem from a Graph lattice. +# Creates a LatticeReactionSystem from a Graph lattice (graph grid). function LatticeReactionSystem(rs, srs, lattice::SimpleGraph) - return LatticeReactionSystem(rs, srs, DiGraph(lattice); directed_edges) + return LatticeReactionSystem(rs, srs, DiGraph(lattice); directed_edges = false) end ### Lattice ReactionSystem Getters ### @@ -169,16 +210,16 @@ is_transport_system(lrs::LatticeReactionSystem) = all(sr -> sr isa TransportReac # Checks if a LatticeReactionSystem have a Cartesian grid lattice. has_cartesian_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa CartesianGridRej{S,T} where {S,T} -# Checks if a LatticeReactionSystem have a regular grid lattice. -has_regular_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa Array{Bool, T} where T -# Checks if a LatticeReactionSystem have a grid lattice (cartesian or regular). -has_grid_lattice(lrs::LatticeReactionSystem) = (has_cartesian_lattice(lrs) || has_regular_lattice(lrs)) +# Checks if a LatticeReactionSystem have a masked grid lattice. +has_masked_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa Array{Bool, T} where T +# Checks if a LatticeReactionSystem have a grid lattice (cartesian or masked). +has_grid_lattice(lrs::LatticeReactionSystem) = (has_cartesian_lattice(lrs) || has_masked_lattice(lrs)) # Checks if a LatticeReactionSystem have a graph lattice. has_graph_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa SimpleDiGraph # Returns the dimensions of a LatticeReactionNetwork with a grid lattice. function grid_dims(lrs::LatticeReactionSystem) has_cartesian_lattice(lrs) && (return length(lrs.lattice.dims)) - has_regular_lattice(lrs) && (return length(size(lrs.lattice))) + has_masked_lattice(lrs) && (return length(size(lrs.lattice))) error("Dimensions are only defined for LatticeReactionSystems with grid-based lattices (not graph-based).") end \ No newline at end of file diff --git a/test/spatial_reaction_systems/lattice_reaction_systems.jl b/test/spatial_reaction_systems/lattice_reaction_systems.jl new file mode 100644 index 0000000000..17ff9b3940 --- /dev/null +++ b/test/spatial_reaction_systems/lattice_reaction_systems.jl @@ -0,0 +1,304 @@ +### Preparations ### + +# Fetch packages. +using Catalyst, Graphs, Test + +# Pre declares a grid. +grid = Graphs.grid([2, 2]) + +### Tests LatticeReactionSystem Getters Correctness ### +# Test case 1. +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]) +end + +# Test case 2. +let + rs = @reaction_network begin + @parameters p1 p2 [edgeparameter=true] + end + @unpack p1, p2 = rs + + @test !isedgeparameter(p1) + @test isedgeparameter(p2) +end + +# Test case 3. +let + rs = @reaction_network begin + @parameters pX pY dX [edgeparameter=true] dY + (pX, 1), 0 <--> X + (pY, 1), 0 <--> Y + 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]) +end + +# Test case 4. +let + rs = @reaction_network begin + @parameters dX p + (pX, 1), 0 <--> X + (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), []) +end + +# Test case 5. +let + rs = @reaction_network begin + @species W(t) + @parameters pX pY dX [edgeparameter=true] dY + (pX, 1), 0 <--> X + (pY, 1), 0 <--> Y + (pZ, 1), 0 <--> Z + (pV, 1), 0 <--> V + 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]) +end + +# Test case 6. +let + rs = @reaction_network customname begin + (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 + +# 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 + 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 + +### Test Interpolation ### + +# Does not currently work. The 3 tr_macro_ lines generate errors. +# Test case 1. +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) +end + +### Tests Error generation ### + +# Test creation of TransportReaction with non-parameters in rate. +# Tests that it works even when 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 + +# 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) +end + +# Network where the rate depend on a species +let + rs = @reaction_network begin + @species Y(t) + (p, d), 0 <--> X + end + tr = @transport_reaction D*Y X + @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid) +end + +# Network with edge parameter in non-spatial reaction rate. +let + rs = @reaction_network begin + @parameters p [edgeparameter=true] + (p, d), 0 <--> X + end + tr = @transport_reaction D X + @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid) +end + +# Network where metadata has been added in rs (which is not seen in transport reaction). +let + rs = @reaction_network begin + @species X(t) [description="Species with added metadata"] + (p, d), 0 <--> X + end + tr = @transport_reaction D X + @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid) + + 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 + + +### Tests Grid Vertex and Edge Number Computation ### + +# 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 + + # 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 lrs1.num_edges == iterator_count(lrs1.edge_iterator) + @test lrs2.num_edges == iterator_count(lrs2.edge_iterator) + end + + # Graph grids (cannot test diagonal connections). + for lattice in [small_2d_grid, small_3d_grid, undirected_cycle, small_directed_cycle, unconnected_graph] + lrs1 = LatticeReactionSystem(SIR_system, SIR_srs_1, lattice) + @test lrs1.num_edges == iterator_count(lrs1.edge_iterator) + end +end + diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl index 168711c9ff..7771898c2f 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl @@ -17,35 +17,35 @@ let cartesian_1d_lrs = LatticeReactionSystem(brusselator_system, srs, small_1d_cartesian_grid) cartesian_2d_lrs = LatticeReactionSystem(brusselator_system, srs, small_2d_cartesian_grid) cartesian_3d_lrs = LatticeReactionSystem(brusselator_system, srs, small_3d_cartesian_grid) - regular_1d_lrs = LatticeReactionSystem(brusselator_system, srs, small_1d_regular_grid) - regular_2d_lrs = LatticeReactionSystem(brusselator_system, srs, small_2d_regular_grid) - regular_3d_lrs = LatticeReactionSystem(brusselator_system, srs, small_3d_regular_grid) + masked_1d_lrs = LatticeReactionSystem(brusselator_system, srs, small_1d_masked_grid) + masked_2d_lrs = LatticeReactionSystem(brusselator_system, srs, small_2d_masked_grid) + masked_3d_lrs = LatticeReactionSystem(brusselator_system, srs, small_3d_masked_grid) graph_lrs = LatticeReactionSystem(brusselator_system, srs, small_2d_grid) # Test lattice type getters. @test has_cartesian_lattice(cartesian_2d_lrs) - @test has_cartesian_lattice(regular_2d_lrs) + @test has_cartesian_lattice(masked_2d_lrs) @test has_cartesian_lattice(graph_lrs) - @test !has_regular_lattice(cartesian_2d_lrs) - @test !has_regular_lattice(regular_2d_lrs) - @test !has_regular_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(regular_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(regular_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(regular_1d_lrs) == 1 - @test grid_dims(regular_2d_lrs) == 2 - @test grid_dims(regular_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 Exception grid_dims(graph_lrs) end @@ -54,25 +54,25 @@ end let # Create LatticeReactionsSystems. cartesian_grid = Graphs.grid([5, 5]) - regular_grid = fill(true, 5, 5) + masked_grid = fill(true, 5, 5) graph_grid = Graphs.grid([5, 5]) cartesian_lrs = LatticeReactionSystem(brusselator_system, srs, cartesian_grid) - regular_lrs = LatticeReactionSystem(brusselator_system, srs, regular_grid) + masked_lrs = LatticeReactionSystem(brusselator_system, srs, masked_grid) graph_lrs = LatticeReactionSystem(brusselator_system, srs, graph_grid) # Check internal structures. - @test cartesian_lrs.rs == regular_lrs.rs == graph_lrs.rs - @test cartesian_lrs.spatial_reactions == regular_lrs.spatial_reactions == graph_lrs.spatial_reactions - @test cartesian_lrs.num_verts == regular_lrs.num_verts == graph_lrs.num_verts - @test cartesian_lrs.num_edges == regular_lrs.num_edges == graph_lrs.num_edges - @test cartesian_lrs.num_species == regular_lrs.num_species == graph_lrs.num_species - @test cartesian_lrs.spat_species == regular_lrs.spat_species == graph_lrs.spat_species - @test cartesian_lrs.parameters == regular_lrs.parameters == graph_lrs.parameters - @test cartesian_lrs.vertex_parameters == regular_lrs.vertex_parameters == graph_lrs.vertex_parameters - @test cartesian_lrs.edge_parameters == regular_lrs.edge_parameters == graph_lrs.edge_parameters - @test cartesian_lrs.directed_edges == regular_lrs.directed_edges == graph_lrs.directed_edges - @test cartesian_lrs.edge_list == regular_lrs.edge_list == graph_lrs.edge_list + @test cartesian_lrs.rs == masked_lrs.rs == graph_lrs.rs + @test cartesian_lrs.spatial_reactions == masked_lrs.spatial_reactions == graph_lrs.spatial_reactions + @test cartesian_lrs.num_verts == masked_lrs.num_verts == graph_lrs.num_verts + @test cartesian_lrs.num_edges == masked_lrs.num_edges == graph_lrs.num_edges + @test cartesian_lrs.num_species == masked_lrs.num_species == graph_lrs.num_species + @test cartesian_lrs.spat_species == masked_lrs.spat_species == graph_lrs.spat_species + @test cartesian_lrs.parameters == masked_lrs.parameters == graph_lrs.parameters + @test cartesian_lrs.vertex_parameters == masked_lrs.vertex_parameters == graph_lrs.vertex_parameters + @test cartesian_lrs.edge_parameters == masked_lrs.edge_parameters == graph_lrs.edge_parameters + @test cartesian_lrs.directed_edges == masked_lrs.directed_edges == graph_lrs.directed_edges + @test cartesian_lrs.edge_list == masked_lrs.edge_list == graph_lrs.edge_list # Checks that simulations yields the same output. u0 = [:X => rand_v_vals(graph_lrs.lattice, 10.0), :Y => 2.0] @@ -80,40 +80,40 @@ let pE= map(sp -> sp => 0.2, spatial_param_syms(lrs)) cartesian_oprob = ODEProblem(cartesian_lrs, u0, (0.0, 100.0), (pV, pE)) - regular_oprob = ODEProblem(regular_lrs, u0, (0.0, 100.0), (pV, pE)) + masked_oprob = ODEProblem(masked_lrs, u0, (0.0, 100.0), (pV, pE)) graph_oprob = ODEProblem(graph_lrs, u0, (0.0, 100.0), (pV, pE)) cartesian_sol = solve(cartesian_oprob, QNDF(); saveat=0.1) - regular_sol = solve(regular_oprob, QNDF(); saveat=0.1) + masked_sol = solve(masked_oprob, QNDF(); saveat=0.1) graph_sol = solvegraph(_oprob, QNDF(); saveat=0.1) - @test cartesian_sol.u ≈ regular_sol.u ≈ graph_sol + @test cartesian_sol.u ≈ masked_sol.u ≈ graph_sol end # Checks that a regular grid with absent vertices generate the same output as corresponding graph. let # Create LatticeReactionsSystems. - regular_grid = [true true true; true false true; true true true] + masked_grid = [true true true; true false true; true true true] graph_grid = cycle_graph(8) - regular_lrs = LatticeReactionSystem(brusselator_system, srs, regular_grid) + masked_lrs = LatticeReactionSystem(brusselator_system, srs, masked_grid) graph_lrs = LatticeReactionSystem(brusselator_system, srs, graph_grid) # Check internal structures. - @test regular_lrs.num_verts == graph_lrs.num_verts - @test regular_lrs.num_edges == graph_lrs.num_edges - @test regular_lrs.edge_list == graph_lrs.edge_list + @test masked_lrs.num_verts == graph_lrs.num_verts + @test masked_lrs.num_edges == graph_lrs.num_edges + @test masked_lrs.edge_list == graph_lrs.edge_list # Checks that simulations yields the same output. u0 = [:X => rand_v_vals(graph_lrs.lattice, 10.0), :Y => 2.0] pV = [:A => 0.5 .+ rand_v_vals(graph_lrs.lattice, 0.5), :B => 4.0] pE= map(sp -> sp => 0.2, spatial_param_syms(lrs)) - regular_oprob = ODEProblem(regular_lrs, u0, (0.0, 100.0), (pV, pE)) + masked_oprob = ODEProblem(masked_lrs, u0, (0.0, 100.0), (pV, pE)) graph_oprob = ODEProblem(graph_lrs, u0, (0.0, 100.0), (pV, pE)) - regular_sol = solve(regular_oprob, QNDF(); saveat=0.1) + masked_sol = solve(masked_oprob, QNDF(); saveat=0.1) graph_sol = solvegraph(_oprob, QNDF(); saveat=0.1) - @test regular_sol.u ≈ graph_sol + @test masked_sol.u ≈ graph_sol end \ No newline at end of file diff --git a/test/spatial_test_networks.jl b/test/spatial_test_networks.jl index 813466754c..b365b26fa2 100644 --- a/test/spatial_test_networks.jl +++ b/test/spatial_test_networks.jl @@ -177,18 +177,18 @@ large_1d_cartesian_grid = CartesianGrid(100) large_2d_cartesian_grid = CartesianGrid((100,100)) large_3d_cartesian_grid = CartesianGrid((100,100,100)) -# Regular grids. -small_1d_regular_grid = fill(true, 5) -small_2d_regular_grid = fill(true, 5, 5) -small_3d_regular_grid = fill(true, 5, 5, 5) - -large_1d_regular_grid = fill(true, 5) -large_2d_regular_grid = fill(true, 5, 5) -large_3d_regular_grid = fill(true, 5, 5, 5) - -random_1d_regular_grid = rand([true, true, true, false], 10) -random_2d_regular_grid = rand([true, true, true, false], 10, 10) -random_3d_regular_grid = rand([true, true, true, false], 10, 10, 10) +# Masked grids. +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([true, true, true, false], 10) +random_2d_masked_grid = rand([true, true, true, false], 10, 10) +random_3d_masked_grid = rand([true, true, true, false], 10, 10, 10) # Graph - grids. very_small_2d_grid = Graphs.grid([2, 2]) From 3405236932613abc937f7d8721ff2707e1617c4e Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 26 Jan 2024 17:00:49 -0500 Subject: [PATCH 11/47] updates --- .../spatial_ODE_systems.jl | 37 +++---- src/spatial_reaction_systems/utility.jl | 4 + ...attice_reaction_systems_ODE_performance.jl | 8 +- .../lattice_reaction_systems_ODEs.jl | 104 ++++++++++++++++-- test/spatial_test_networks.jl | 26 ++--- 5 files changed, 133 insertions(+), 46 deletions(-) diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index b967dd29fb..ddfdf0b171 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -24,13 +24,13 @@ struct LatticeTransportODEf{Q,R,S,T} """ v_ps_idx_types::Vector{Bool} """ - A vector of pairs, with a value for each species with transportation. + A vector of sparse, 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. """ - transport_rates::Vector{Pair{Int64, Vector{R}}} + transport_rates::Vector{SparseMatrixCSC{R, Int64}} """ 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 @@ -59,7 +59,7 @@ struct LatticeTransportODEf{Q,R,S,T} 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) + eds = lrs.edge_iterator 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 @@ -125,16 +125,15 @@ function DiffEqBase.ODEProblem(lrs::LatticeReactionSystem, u0_in, tspan, # Converts potential symmaps to varmaps # Vertex and edge parameters may be given in a tuple, or in a common vector, making parameter case complicated. 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 [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). + # Each parameter value in vert_ps is a vector (with one value for each parameter). + # edge_ps becomes sparse matrix. Here, index (i,j) is its value in the edge from vertex i to vertex j. + # Uniform vertex/edge parameters stores only a single value (in a length 1 vector, or size 1x1 sparse matrix). + # This is the parameters single value. vert_ps, edge_ps = lattice_process_p(p_in, vertex_parameters(lrs), edge_parameters(lrs), lrs) # Creates ODEProblem. @@ -261,25 +260,25 @@ end 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 + # Gets the indices of species at vertex i. idxs = get_indexes(vert_i, f_func.num_species) - # 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)) + # Vector of vertex ps at vert_i. + vert_i_ps = view_vert_ps_vector!(f_func, p, vert_i) - # evaluate reaction contributions to du at vert_i + # Evaluate reaction contributions to du at vert_i. f_func.ofunc((@view du[idxs]), (@view u[idxs]), vert_i_ps, t) end - # s_idx is species index among transport species, s is index among all species - # rates are the species' transport rates + # 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 + # 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) @@ -292,13 +291,13 @@ end function (jac_func::LatticeTransportODEjac)(J, u, p, t) J .= 0.0 - # Update the Jacobian from reaction terms + # 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)) + vert_ps = view_vert_ps_vector!(jac_func, p, vert_i) jac_func.ofunc.jac((@view J[idxs, idxs]), (@view u[idxs]), vert_ps, t) end # Updates for the spatial reactions (adds the Jacobian values from the diffusion reactions). J .+= jac_func.jac_transport -end \ No newline at end of file +end diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index 691ba79a3c..1f498630d2 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -289,6 +289,10 @@ function view_vert_ps_vector!(work_vert_ps, vert_ps, comp, enumerated_vert_ps_id end return work_vert_ps end +# Input is always either a LatticeTransportODEf or LatticeTransportODEjac function (which fields we then pass on). +function view_vert_ps_vector!(lt_ode_func, vert_ps, comp) + return view_vert_ps_vector!(lt_ode_func.work_vert_ps, vert_ps, comp, enumerate(lt_ode_func.v_ps_idx_types)) +end # Expands a u0/p information stored in Vector{Vector{}} for to Matrix form # (currently only used in Spatial Jump systems). diff --git a/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl b/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl index c3c801c603..992d864b20 100644 --- a/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl +++ b/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl @@ -19,7 +19,7 @@ include("../spatial_test_networks.jl") # Small grid, small, non-stiff, system. let - lrs = LatticeReactionSystem(SIR_system, SIR_srs_2, small_2d_grid) + lrs = LatticeReactionSystem(SIR_system, SIR_srs_2, small_2d_graph_grid) u0 = [:S => 990.0, :I => 20.0 * rand_v_vals(lrs.lattice), :R => 0.0] pV = SIR_p pE = [:dS => 0.01, :dI => 0.01, :dR => 0.01] @@ -49,7 +49,7 @@ end # Small grid, small, stiff, system. let - lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_2d_grid) + lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_2d_graph_grid) u0 = [:X => rand_v_vals(lrs.lattice, 10), :Y => rand_v_vals(lrs.lattice, 10)] pV = brusselator_p pE = [:dX => 0.2] @@ -80,7 +80,7 @@ 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), @@ -141,7 +141,7 @@ 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), diff --git a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl index ec9f7d79d2..20476eeee9 100644 --- a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl +++ b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl @@ -17,8 +17,8 @@ t = default_t() ### Tests Simulations Don't Error ### for grid in [small_2d_grid, short_path, small_directed_cycle, small_1d_cartesian_grid, small_2d_cartesian_grid, small_3d_cartesian_grid, - small_1d_regular_grid, small_2d_regular_grid, small_3d_regular_grid, - random_2d_regular_grid] + small_1d_masked_grid, small_2d_masked_grid, small_3d_masked_grid, + random_2d_masked_grid] # Non-stiff case for srs in [Vector{TransportReaction}(), SIR_srs_1, SIR_srs_2] lrs = LatticeReactionSystem(SIR_system, srs, grid) @@ -34,8 +34,6 @@ for grid in [small_2d_grid, short_path, small_directed_cycle, :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)] @@ -43,13 +41,11 @@ for grid in [small_2d_grid, short_path, small_directed_cycle, :α => 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())) @@ -67,8 +63,6 @@ for grid in [small_2d_grid, short_path, small_directed_cycle, 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] @@ -76,13 +70,11 @@ for grid in [small_2d_grid, short_path, small_directed_cycle, :A => 0.5 .+ rand_v_vals(lrs.lattice, 0.5), :B => 4.0 .+ rand_v_vals(lrs.lattice, 1.0), ] - 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())) @@ -272,6 +264,98 @@ let @test all(isapprox.(ss_1, ss_2)) end +### Test Grid Types ### + +# Tests that identical lattices (using different types of lattices) give identical results. +let + # 1d lattices. + lrs1_cartesian = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_1d_cartesian_grid) + lrs1_masked = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_1d_masked_grid) + lrs1_graph = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_1d_graph_grid) + + oprob1_cartesian = ODEProblem(lrs1_cartesian, sigmaB_u0, (0.0,10.0), sigmaB_p) + oprob1_masked = ODEProblem(lrs1_masked, sigmaB_u0, (0.0,10.0), sigmaB_p) + oprob1_graph = ODEProblem(lrs1_graph, sigmaB_u0, (0.0,10.0), sigmaB_p) + @test solve(oprob1_cartesian, QNDF()) == solve(oprob1_masked, QNDF()) == solve(oprob1_graph, QNDF()) + + # 2d lattices. + lrs2_cartesian = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_2d_cartesian_grid) + lrs2_masked = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_2d_masked_grid) + lrs2_graph = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_2d_graph_grid) + + oprob2_cartesian = ODEProblem(lrs2_cartesian, sigmaB_u0, (0.0,10.0), sigmaB_p) + oprob2_masked = ODEProblem(lrs2_masked, sigmaB_u0, (0.0,10.0), sigmaB_p) + oprob2_graph = ODEProblem(lrs2_graph, sigmaB_u0, (0.0,10.0), sigmaB_p) + @test solve(oprob2_cartesian, QNDF()) == solve(oprob2_masked, QNDF()) == solve(oprob2_graph, QNDF()) + + # 3d lattices. + lrs3_cartesian = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_3d_cartesian_grid) + lrs3_masked = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_3d_masked_grid) + lrs3_graph = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_3d_graph_grid) + + oprob3_cartesian = ODEProblem(lrs3_cartesian, sigmaB_u0, (0.0,10.0), sigmaB_p) + oprob3_masked = ODEProblem(lrs3_masked, sigmaB_u0, (0.0,10.0), sigmaB_p) + oprob3_graph = ODEProblem(lrs3_graph, sigmaB_u0, (0.0,10.0), sigmaB_p) + @test solve(oprob3_cartesian, QNDF()) == solve(oprob3_masked, QNDF()) == solve(oprob3_graph, QNDF()) +end + +# 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 vertexes are `true`). +let + for lattice in [CartesianGrid(3,4), fill(true, 3, 4)] + lrs = LatticeReactionSystem(SIR_system, SIR_srs_2, lattice) + + # Initial condition values. + S_vals_vec = [100, 200, 300, 100, 100, 100, 200, 200, 200, 300, 300, 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.02, 0.03, 0.01, 0.01, 0.02, 0.02, 0.02, 0.02, 0.03, 0.03, 0.03] + β_vals_mat = [0.01 0.02 0.03; 0.01 0.01 0.02; 0.02 0.02 0.02; 0.03 0.03 0.03] + SIR_p_vec = [:α => 0.1 / 1000, :β => β_vals_vec] + SIR_p_mat = [:α => 0.1 / 1000, :β => β_vals_mat] + + sol1 = solve(ODEProblem(lrs, SIR_u0_vec, (0.0, 10.0), SIR_p_vec)) + sol2 = solve(ODEProblem(lrs, SIR_u0_mat, (0.0, 10.0), SIR_p_vec)) + sol3 = solve(ODEProblem(lrs, SIR_u0_vec, (0.0, 10.0), SIR_p_mat)) + sol4 = solve(ODEProblem(lrs, SIR_u0_mat, (0.0, 10.0), SIR_p_mat)) + + @test sol1 == sol2 == sol3 = sol4 + 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_2, lattice) + + # Initial condition values. 999 is used for empty points. + S_vals_vec = [100, 200, 100, 200, 200, 200, 300, 300] + S_vals_mat = [100, 200, 999; 100, 999, 999; 200, 200, 200; 999, 300, 300] + 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.02, 0.03, 0.01, 0.01, 0.02, 0.02, 0.02, 0.02, 0.03, 0.03, 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] + SIR_p_mat = [:α => 0.1 / 1000, :β => β_vals_mat] + SIR_p_sparse_mat = [:α => 0.1 / 1000, :β => β_vals_sparse_mat] + + sol = solve(ODEProblem(lrs, SIR_u0_vec, (0.0, 10.0), SIR_p_vec)) + 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)) + end + end +end + ### Test Transport Reaction Types ### # Compares where spatial reactions are created with/without the macro. diff --git a/test/spatial_test_networks.jl b/test/spatial_test_networks.jl index b365b26fa2..f4fcb17f50 100644 --- a/test/spatial_test_networks.jl +++ b/test/spatial_test_networks.jl @@ -12,11 +12,6 @@ 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_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)] -end # Gets a symbol list of spatial parameters. function spatial_param_syms(lrs::LatticeReactionSystem) @@ -191,14 +186,19 @@ random_2d_masked_grid = rand([true, true, true, false], 10, 10) random_3d_masked_grid = rand([true, true, true, false], 10, 10, 10) # Graph - 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]) - -small_3d_grid = Graphs.grid([5, 5, 5]) -medium_3d_grid = Graphs.grid([20, 20, 20]) -large_3d_grid = Graphs.grid([100, 100, 100]) +very_small_2d_graph_grid = Graphs.grid([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]) + +large_1d_graph_grid = path_graph(100) +large_2d_graph_grid = Graphs.grid([100,100]) +large_3d_graph_grid = Graphs.grid([100,100,100]) # Graph - paths. short_path = path_graph(100) From 1ce1766960368de63e543b895b332dc3e2b639d5 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 6 Feb 2024 15:02:51 -0500 Subject: [PATCH 12/47] update --- src/Catalyst.jl | 1 + .../lattice_reaction_systems.jl | 83 ++-- .../spatial_ODE_systems.jl | 173 ++++---- src/spatial_reaction_systems/utility.jl | 389 ++++++++---------- .../lattice_reaction_systems_ODEs.jl | 313 +++++--------- .../lattice_reaction_systems.jl | 122 +++--- .../lattice_reaction_systems_lattice_types.jl | 139 ++++--- test/spatial_test_networks.jl | 20 +- 8 files changed, 612 insertions(+), 628 deletions(-) diff --git a/src/Catalyst.jl b/src/Catalyst.jl index a69883aaa1..a5f51efb89 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -177,6 +177,7 @@ include("spatial_reaction_systems/lattice_reaction_systems.jl") export LatticeReactionSystem export spatial_species, vertex_parameters, edge_parameters export CartesianGrid, CartesianGridReJ # (Implemented in JumpProcesses) +export has_cartesian_lattice, has_masked_lattice, has_grid_lattice, has_graph_lattice, grid_dims # Various utility functions include("spatial_reaction_systems/utility.jl") diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index 6b48175feb..be75d75d73 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -36,14 +36,6 @@ struct LatticeReactionSystem{Q,R,S,T} # <: MT.AbstractTimeDependentSystem e.g. (possibly) have an unique value at each edge of the system. """ edge_parameters::Vector{BasicSymbolic{Real}} - - """ - Whenever the initial input was directed. True for Digraph input, false for other graphs and all grids. - Used later to know whenever duplication of edge parameter should be duplicated - (i.e. allow parameter for edge i => j to be used for j => i). - Even if false, giving separate parameters for both directions is still permitted. - """ - directed_edges::Bool """ An iterator over all the edges on the lattice. The format depends on the type of lattice (Cartesian grid, grid, or graph). @@ -51,7 +43,7 @@ struct LatticeReactionSystem{Q,R,S,T} # <: MT.AbstractTimeDependentSystem edge_iterator::T function LatticeReactionSystem(rs::ReactionSystem{Q}, spatial_reactions::Vector{R}, lattice::S, - num_verts, num_edges, edge_iterator::T; directed_edges = false) where {Q,R, S, T} + num_verts, num_edges, edge_iterator::T) where {Q,R, S, T} # Error checks. if !(R <: AbstractSpatialReaction) error("The second argument must be a vector of AbstractSpatialReaction subtypes.") @@ -85,22 +77,22 @@ struct LatticeReactionSystem{Q,R,S,T} # <: MT.AbstractTimeDependentSystem 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, directed_edges, edge_iterator) + spat_species, ps, vertex_parameters, edge_parameters, edge_iterator) end end # Creates a LatticeReactionSystem from a CartesianGrid lattice (cartesian grid). -function LatticeReactionSystem(rs, srs, lattice::CartesianGridRej{S,T}; diagonal_connections=false) where {S,T} +function LatticeReactionSystem(rs, srs, lattice_in::CartesianGridRej{S,T}; diagonal_connections=false) where {S,T} # Error checks. - (length(lattice.dims) > 3) && error("Grids of higher dimension than 3 is currently not supported.") + (length(lattice_in.dims) > 3) && error("Grids of higher dimension than 3 is currently not supported.") - # Ensures that the matrix is on a 3d form - lattice = CartesianGrid((lattice.dims..., fill(1, 3-length(lattice.dims))...)) + # Ensures that the matrix has a 3d form (for intermediary computations, original is passed to constructor). + lattice = CartesianGrid((lattice_in.dims..., fill(1, 3-length(lattice_in.dims))...)) # Counts vertexes and edges. The `num_edges` count formula counts the number of internal, side, # edge, and corner vertexes (on the grid). The number of edges from each depend on whether diagonal # connections are allowed. The formula holds even if l, m, and/or n are 1. - l,m,n = 2,3,4 + l,m,n = lattice.dims num_verts = l * m * n (ni, ns, ne, nc) = diagonal_connections ? (26,17,11,7) : (6,5,4,3) num_edges = ni*(l-2)*(m-2)*(n-2) + # Internal vertexes. @@ -131,29 +123,41 @@ function LatticeReactionSystem(rs, srs, lattice::CartesianGridRej{S,T}; diagonal end end - return LatticeReactionSystem(rs, srs, lattice, num_verts, num_edges, edge_iterator) + return LatticeReactionSystem(rs, srs, lattice_in, num_verts, num_edges, edge_iterator) end # Creates a LatticeReactionSystem from a Boolean Array lattice (masked grid). -function LatticeReactionSystem(rs, srs, lattice::Array{Bool, T}; diagonal_connections=false) where {T} +function LatticeReactionSystem(rs, srs, lattice_in::Array{Bool, T}; diagonal_connections=false) where {T} # Error checks. - dims = size(lattice) + dims = size(lattice_in) (length(dims) > 3) && error("Grids of higher dimension than 3 is currently not supported.") - # Ensures that the matrix is on a 3d form - lattice = reshape(lattice, [dims...; fill(1, 3-length(dims))]...) + # Ensures that the matrix has a 3d form (for intermediary computations, original is passed to constructor). + # Also gets some basic lattice information. + lattice = reshape(lattice_in, [dims...; fill(1, 3-length(dims))]...) # Counts vertexes (edges have to be counted after the iterator have been created). num_verts = count(lattice) # Creates an iterator over all edges.Currently a full vector of all edges (as pairs). edge_iterator = Vector{Pair{Int64,Int64}}() + + # Makes a template matrix to store each vertex's index. The matrix is 0 where there are no vertex. + idx_matrix = fill(0, size(lattice_in)) + cur_vertex_idx = 0 + for flat_idx in 1:length(lattice) + if lattice[flat_idx] + idx_matrix[flat_idx] = (cur_vertex_idx += 1) + end + end + # Loops through, simultaneously, the coordinates of each position in the grid, as well as that # coordinate's (scalar) flat index. For each grid point, loops through all potential neighbours. - l,m,n = size(lattice) + l, m, n = size(lattice) indices = [(L, M, N) for L in 1:l, M in 1:m, N in 1:n] - flat_indices = 1:num_verts - for ((L, M, N), idx) in zip(indices, flat_indices) + for (L, M, N) in indices + # Ensures that we are in a valid lattice point. + lattice[L,M,N] || continue for LL in max(L - 1, 1):min(L + 1, l), MM in max(M - 1, 1):min(M + 1, m), NN in max(N - 1, 1):min(N + 1, n) @@ -166,27 +170,23 @@ function LatticeReactionSystem(rs, srs, lattice::Array{Bool, T}; diagonal_connec diagonal_connections && (L==LL) && (M==MM) && (N==NN) && continue # Computes the neighbours scalar index. Add that connection to `edge_iterator`. - neighbour_idx = LL + (MM - 1) * l + (NN - 1) * m * l - push!(edge_iterator, idx => neighbour_idx) + push!(edge_iterator, idx_matrix[L,M,N] => idx_matrix[LL,MM,NN]) end end num_edges = length(edge_iterator) - return LatticeReactionSystem(rs, srs, lattice, num_verts, num_edges, edge_iterator) + return LatticeReactionSystem(rs, srs, lattice_in, num_verts, num_edges, edge_iterator) end -# Creates a LatticeReactionSystem from a DiGraph lattice (graph grid). -function LatticeReactionSystem(rs, srs, lattice::DiGraph; directed_edges = true) +# 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 = edges(lattice) - return LatticeReactionSystem(rs, srs, lattice, num_verts, num_edges, edge_iterator; directed_edges) -end - -# Creates a LatticeReactionSystem from a Graph lattice (graph grid). -function LatticeReactionSystem(rs, srs, lattice::SimpleGraph) - return LatticeReactionSystem(rs, srs, DiGraph(lattice); directed_edges = false) + 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). +LatticeReactionSystem(rs, srs, lattice::SimpleGraph) = LatticeReactionSystem(rs, srs, DiGraph(lattice)) ### Lattice ReactionSystem Getters ### @@ -217,9 +217,12 @@ has_grid_lattice(lrs::LatticeReactionSystem) = (has_cartesian_lattice(lrs) || ha # Checks if a LatticeReactionSystem have a graph lattice. has_graph_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa SimpleDiGraph +# Returns the size of the lattice of a LatticeReactionNetwork with a grid lattice. +function grid_size(lrs::LatticeReactionSystem) + has_cartesian_lattice(lrs) && (return lrs.lattice.dims) + has_masked_lattice(lrs) && (return size(lrs.lattice)) + error("Grid size is only defined for LatticeReactionSystems with grid-based lattices (not graph-based).") +end + # Returns the dimensions of a LatticeReactionNetwork with a grid lattice. -function grid_dims(lrs::LatticeReactionSystem) - has_cartesian_lattice(lrs) && (return length(lrs.lattice.dims)) - has_masked_lattice(lrs) && (return length(size(lrs.lattice))) - error("Dimensions are only defined for LatticeReactionSystems with grid-based lattices (not graph-based).") -end \ No newline at end of file +grid_dims(lrs::LatticeReactionSystem) = length(grid_size(lrs)) \ No newline at end of file diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index ddfdf0b171..ecf5b6dd86 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -1,26 +1,25 @@ ### Spatial ODE Functor Structures ### # Functor with information for the forcing function of a spatial ODE with spatial movement on a lattice. -struct LatticeTransportODEf{Q,R,S,T} +struct LatticeTransportODEf{S,T} """The ODEFunction of the (non-spatial) reaction system which generated this function.""" - ofunc::Q + ofunc::S """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}} + vert_ps::Vector{Vector{T}} """ 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. """ - work_vert_ps::Vector{R} + work_vert_ps::Vector{T} """ 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. + This check is done once, and the value stored to this array. True means a uniform value. """ v_ps_idx_types::Vector{Bool} """ @@ -30,57 +29,67 @@ struct LatticeTransportODEf{Q,R,S,T} 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. """ - transport_rates::Vector{SparseMatrixCSC{R, Int64}} + transport_rates::Vector{Pair{Int64,SparseMatrixCSC{T, Int64}}} + """ + For each transport rate in transport_rates, its value is a (sparse) matrix with size either + (num_verts,num_verts) or (1,1). In the second case, that transportation rate is uniform across all edges. + To know how to access transport rate's value (without checking sizes), we can use this vector directly. + True means a uniform value. + """ + t_rate_idx_types::Vector{Bool} """ 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). """ - leaving_rates::Matrix{R} - """An (enumerate'ed) iterator over all the edges of the lattice.""" - edges::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}} + leaving_rates::Matrix{T} + """An iterator over all the edges of the lattice.""" + edge_iterator::Vector{Pair{Int64, Int64}} - 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} + function LatticeTransportODEf(ofunc::S, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{T}}}, transport_rates::Vector{Pair{Int64, SparseMatrixCSC{T, Int64}}}, + lrs::LatticeReactionSystem) where {S,T} + # Records. which parameters and rates are uniform and not. + v_ps_idx_types = map(vp -> length(vp[2]) == 1, vert_ps) + t_rate_idx_types = map(tr -> size(tr[2]) == (1,1), transport_rates) + + # Input `vert_ps` is a vector map taking each parameter symbolic to its value (potentially a vector). + # This vector is sorted according to the parameters order. Here, we simply extract its values only. + vert_ps = [vp[2] for vp in vert_ps] + + # Computes the leaving rates. 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)) + t_rate = trpair[2] + for e in lrs.edge_iterator # Updates the exit rate for species s_idx from vertex e.src - leaving_rates[s_idx, e.src] += get_component_value(rates, e_idx) + leaving_rates[s_idx, e[1]] += get_transport_rate(t_rate, e, t_rate_idx_types[s_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 = lrs.edge_iterator - 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) + + # Declares `work_vert_ps` (used as storage during computation) and an iterator over the edges. + work_vert_ps = zeros(length(vert_ps)) + edge_iterator = lrs.edge_iterator + new{S,T}(ofunc, lrs.num_verts, lrs.num_species, vert_ps, work_vert_ps, + v_ps_idx_types, transport_rates, t_rate_idx_types, leaving_rates, edge_iterator) 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} +struct LatticeTransportODEjac{R,S,T} """The ODEFunction of the (non-spatial) reaction system which generated this function.""" - ofunc::Q + ofunc::R """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}} + vert_ps::Vector{Vector{S}} """ 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. """ - work_vert_ps::Vector{R} + work_vert_ps::Vector{S} """ 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, @@ -90,22 +99,20 @@ struct LatticeTransportODEjac{Q,R,S,T} v_ps_idx_types::Vector{Bool} """Whether the Jacobian is sparse or not.""" 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}} + """The transport rates. This is a dense or sparse matrix (depending on what type of Jacobian is used).""" + jac_transport::T - function LatticeTransportODEjac(ofunc::R, vert_ps::Vector{Vector{S}}, lrs::LatticeReactionSystem, + function LatticeTransportODEjac(ofunc::R, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{S}}}, lrs::LatticeReactionSystem, jac_transport::Union{Nothing, SparseMatrixCSC{Float64, Int64}}, - edge_ps::Vector{Vector{T}}, sparse::Bool) where {R,S,T} + sparse::Bool) where {R,S} + # Input `vert_ps` is a vector map taking each parameter symbolic to its value (potentially a vector). + # This vector is sorted according to the parameters order. Here, we simply extract its values only. + vert_ps = [vp[2] for vp in vert_ps] + 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) + new{R,S,typeof(jac_transport)}(ofunc, lrs.num_verts, lrs.num_species, vert_ps, + work_vert_ps, v_ps_idx_types, sparse, jac_transport) end end @@ -128,31 +135,37 @@ function DiffEqBase.ODEProblem(lrs::LatticeReactionSystem, u0_in, tspan, 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 vertexes. # 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) - # Each parameter value in vert_ps is a vector (with one value for each parameter). - # edge_ps becomes sparse matrix. Here, index (i,j) is its value in the edge from vertex i to vertex j. + u0 = lattice_process_u0(u0_in, species(lrs), lrs) + # vert_ps and `edge_ps` are vector maps, taking each parameter's Symbolics to its value(s). + # vert_ps values are vectors. Here, index (i) is a parameters value in vertex i. + # edge_ps becomes sparse matrix. Here, index (i,j) is a parameters value in the edge from vertex i to vertex j. # Uniform vertex/edge parameters stores only a single value (in a length 1 vector, or size 1x1 sparse matrix). # This is the parameters single value. - vert_ps, edge_ps = lattice_process_p(p_in, vertex_parameters(lrs), edge_parameters(lrs), lrs) + # 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 ODEProblem. 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. + ps = [p[2] for p in [vert_ps; edge_ps]] + 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{A,Vector{T}}}, + edge_ps::Vector{Pair{B,SparseMatrixCSC{T, Int64}}}, jac::Bool, sparse::Bool, + name, include_zero_odes, combinatoric_ratelaws, remove_conserved, checks) where {A,B,T} if remove_conserved error("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). - transport_rates = make_sidxs_to_transrate_map(vert_ps, edge_ps, lrs) + 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)) @@ -164,23 +177,23 @@ function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Vector{T} 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) + f = LatticeTransportODEf(ofunc_sparse, vert_ps, transport_rates, 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) + J = LatticeTransportODEjac(ofunc_dense, vert_ps, lrs, jac_vals, 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) + f = LatticeTransportODEf(ofunc_dense, vert_ps, transport_rates, lrs) + J = LatticeTransportODEjac(ofunc_dense, vert_ps, lrs, jac_vals, false) jac_prototype = nothing end else if sparse ofunc_sparse = ODEFunction(osys; jac = false, sparse = true) - f = LatticeTransportODEf(ofunc_sparse, vert_ps, transport_rates, edge_ps, lrs) + f = LatticeTransportODEf(ofunc_sparse, vert_ps, transport_rates, 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) + f = LatticeTransportODEf(ofunc_dense, vert_ps, transport_rates, lrs) jac_prototype = nothing end J = nothing @@ -190,10 +203,10 @@ function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Vector{T} 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) +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 indexes of the transport species, and the species with transport only (and no non-spatial dynamics). - trans_species = first.(trans_rates) + 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. @@ -218,19 +231,19 @@ function build_jac_prototype(ns_jac_prototype::SparseMatrixCSC{Float64, Int64}, end # Indexes of elements due to spatial dynamics. - for e in edges(lrs.lattice) + for e in lrs.edge_iterator # Indexes due to terms for a species leaves its current 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, lrs.num_species) j_idxs[idx] = i_idxs[idx] idx += 1 end # Indexes due to terms for species arriving into a new 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, lrs.num_species) + j_idxs[idx] = get_index(e[2], s_idx, lrs.num_species) idx += 1 end end @@ -240,10 +253,10 @@ function build_jac_prototype(ns_jac_prototype::SparseMatrixCSC{Float64, Int64}, # 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) + for (s, rates) in transport_rates, e in lrs.edge_iterator + idx_src = get_index(e[1], s, lrs.num_species) + idx_dst = get_index(e[2], s, lrs.num_species) + val = get_transport_rate(rates, e, size(rates)==(1,1)) # Term due to species leaving source vertex. jac_prototype[idx_src, idx_src] -= val @@ -263,11 +276,11 @@ function (f_func::LatticeTransportODEf)(du, u, p, t) # Gets the indices of species at vertex i. idxs = get_indexes(vert_i, f_func.num_species) - # Vector of vertex ps at vert_i. - vert_i_ps = view_vert_ps_vector!(f_func, p, vert_i) + # Updates the vector which contains the vertex parameter values for vertex vert_i. + update_work_vert_ps!(f_func, p, vert_i) # Evaluate reaction contributions to du at vert_i. - f_func.ofunc((@view du[idxs]), (@view u[idxs]), vert_i_ps, t) + f_func.ofunc((@view du[idxs]), (@view u[idxs]), f_func.work_vert_ps, t) end # s_idx is species index among transport species, s is index among all species. @@ -275,14 +288,14 @@ function (f_func::LatticeTransportODEf)(du, u, p, t) 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] + idx_src = get_index(vert_i, s, f_func.num_species) + du[idx_src] -= f_func.leaving_rates[s_idx, vert_i] * u[idx_src] 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] + for e in f_func.edge_iterator + idx_src = get_index(e[1], s, f_func.num_species) + idx_dst = get_index(e[2], s, f_func.num_species) + du[idx_dst] += get_transport_rate(s_idx, f_func, e) * u[idx_src] end end end @@ -294,8 +307,8 @@ function (jac_func::LatticeTransportODEjac)(J, u, p, t) # 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, p, vert_i) - jac_func.ofunc.jac((@view J[idxs, idxs]), (@view u[idxs]), vert_ps, t) + update_work_vert_ps!(jac_func, p, vert_i) + jac_func.ofunc.jac((@view J[idxs, idxs]), (@view u[idxs]), jac_func.work_vert_ps, t) end # Updates for the spatial reactions (adds the Jacobian values from the diffusion reactions). diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index 1f498630d2..3b18fd31cf 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -15,283 +15,260 @@ 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) +function lattice_process_u0(u0_in, u0_syms, 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. For species with uniform initial conditions, + # The value vector holds that value only. For spatially heterogeneous initial conditions, + # the vector have teh same length as the number of vertexes (with one value for each). + u0 = vertex_value_map(u0, lrs) + + # Converts the initial condition to a single Vector (with one values for each species and vertex). + return expand_component_values([entry[2] for entry in u0], lrs.num_verts) end # From p input, splits it into diffusion parameters and compartment 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) - - # 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) - - # 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) - - # 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) +function lattice_process_p(ps_in, ps_vertex_syms, ps_edge_syms, 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/1x1. Else, they have size num_verts/num_vertsxnum_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, converts the 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) return vert_ps, edge_ps end -# Splits parameters into those for the vertexes and those for the edges. +# 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{BasicSymbolic{Real}, <:Any}, syms::Vector{BasicSymbolic{Real}}) + # Error checks + if !isempty(setdiff(keys(input), syms)) + error("You have provided values for the following unrecognised parameters/initial conditions: $(setdiff(keys(input), syms)).") + end + if !isempty(setdiff(syms, keys(input))) + error("You have not provided values for the following parameters/initial conditions: $(setdiff(syms, keys(input))).") + end -# 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.") + return [sym => input[sym] for sym in syms] 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]))))") - end - - return vert_ps_in, edge_ps_in +function lattice_process_input(input, syms::Vector{BasicSymbolic{Real}}) + lattice_process_input(Dict(input), syms) 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)).") - end - sorted_input = sort(input; by = p -> findfirst(isequal(p[1]), syms)) - return lattice_process_input(last.(sorted_input), syms, args...) +# Splits parameters into vertex and edge parameters. +#function split_parameters(ps::Vector{<: Pair}, p_vertex_syms::Vector, p_edge_syms::Vector) +function split_parameters(ps, p_vertex_syms, p_edge_syms) + 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 -# 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...) + +# Converts the values for initial condition/vertex parameters to the correct form: +# Map from symbolics to to vectors of length either 1 (for uniform values) or num_verts. +function vertex_value_map(values, lrs) + isempty(values) && (return Pair{BasicSymbolic{Real}, Vector{Float64}}[]) + return [entry[1] => vertex_value_form(entry[2], lrs, entry[1]) for entry in values] 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.") +# Converts the values for a specific component (species/parameter) to the correct vector form. +function vertex_value_form(values, lrs::LatticeReactionSystem, sym) + (values isa AbstractArray) || (return [values]) + if values isa Vector + if has_grid_lattice(lrs) && (size(values) == grid_size(lrs)) + vertex_value_form(values, lrs.num_verts, lrs.lattice, sym) + end + if (length(values) != lrs.num_verts) + error("You have provided ($(length(values))) values for $sym. This is not equal to the number of vertexes ($(lrs.num_verts)).") + end + return values + end + return vertex_value_form(values, lrs.num_verts, lrs.lattice, sym) 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...) +# Converts values to correct vector form for a Cartesian grid lattice. +function vertex_value_form(values::AbstractArray, num_verts::Int64, lattice::CartesianGridRej{S,T}, sym) where {S,T} + if size(values) != lattice.dims + error("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 + if (length(values) != num_verts) + error("You have provided ($(length(values))) values for $sym. This is not equal to the number of vertexes ($(num_verts)).") + end + return [values[flat_idx] for flat_idx in 1:num_verts] end -# If the input is of the correct form already, return it. -lattice_process_input(input::Vector{<:Vector}, syms::Vector{BasicSymbolic{Real}}, n::Int64) = input - -# 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 correct vector form for a masked grid lattice. +function vertex_value_form(values::AbstractArray, num_verts::Int64, lattice::Array{Bool, T}, sym) where {T} + if size(values) != size(lattice) + error("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.") + return_values = Vector{typeof(values[1])}(undef, num_verts) + cur_idx = 0 + for i = 1:length(lattice) + lattice[i] || continue + return_values[cur_idx += 1] = values[i] end + if (length(return_values) != num_verts) + error("You have provided ($(length(return_values))) values for $sym. This is not equal to the number of vertexes ($(num_verts)).") + end + return return_values 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 - end +# Converts the values for initial condition/vertex parameters to the correct form: +# Map from symbolics to to vectors of length either 1 (for uniform values) or num_verts. +function edge_value_map(values, lrs) + 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 a specific component (species/parameter) to the correct vector form. +function edge_value_form(values, lrs::LatticeReactionSystem, sym) + # If a scalar have been given, converts it to a size (1,1) sparse matrix. + (values isa SparseMatrixCSC) || (return sparse([1], [1], [values])) + + # Error checks. + if nnz(values) != lrs.num_edges + error("You have provided ($(nnz(values))) values for $sym. This is not equal to the number of edges ($(lrs.num_edges)).") + end + if !all(Base.isstored(values, e[1], e[2]) for e in lrs.edge_iterator) + error("Values was not provided for some edges for edge parameter $sym.") + 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)) + 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. +# In the first step, computes a map from species symbolics form to value(s). +# Second step converts to map from species index to value(s). +function make_sidxs_to_transrate_map(vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{T}}}, + edge_ps::Vector{Pair{BasicSymbolic{Real},SparseMatrixCSC{T, Int64}}}, + lrs::LatticeReactionSystem) where T + p_val_dict = Dict(vcat(vert_ps, edge_ps)) + transport_rates_speciesmap = compute_all_transport_rates(p_val_dict, lrs) + return Pair{Int64,SparseMatrixCSC{T, Int64}}[ + speciesmap(lrs.rs)[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(get_transport_rate_law(s, lrs), p_val_dict, lrs) for s in spatial_species(lrs)] # Sorts all the species => rate pairs according to their species index in species(::ReactionSystem). - return sort(unsorted_rates; by=rate -> findfirst(isequal(rate[1]), 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) +# If there are several transportation reactions for the species, their sum is used. +#function get_transport_rate_law(s::BasicSymbolic{Real}, lrs::LatticeReactionSystem) +function get_transport_rate_law(s, lrs) 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 + return sum(getfield.(rates, :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{SymbolicUtils.BasicSymbolic{Real}, Vector{Float64}}, num_edges::Int64) +#function compute_transport_rates(rate_law::Num, p_val_dict, lrs::LatticeReactionSystem) +function compute_transport_rates(rate_law, p_val_dict, lrs) # Finds parameters involved in rate and create a function evaluating the rate law. 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 becomes a size (1,1) sparse matrix. + # Else, the rates becomes 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(lrs.num_verts, lrs.num_verts) + for e in lrs.edge_iterator + 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 - ] +# Produces a dictionary with all parameters' values. Vertex parameters have their values converted to +# a sparse matrix (with one value for each edge, always using the source vertex's value) +function param_dict(vert_ps, edge_ps, lrs) + return merge(Dict(zip(vertex_parameters(lrs), vert_ps)), Dict(zip(edge_parameters(lrs), edge_ps))) end ### Accessing Unknown & Parameter Array Values ### + +# 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, num_verts) + 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 their 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). get_indexes(vert::Int64, num_species::Int64) = ((vert - 1) * num_species + 1):(vert * num_species) -# 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) - 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}) - get_component_value(values[component_idx], location_idx, location_types[component_idx]) +# Returns the value of a parameter in an edge. For vertex parameters, uses their values in the source. +function get_edge_value(values::Vector{T}, edge) where {T} + return (length(values) == 1) ? values[1] : values[edge[1]] 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) - get_component_value(values, location_idx, length(values) == 1) +function get_edge_value(values::SparseMatrixCSC{T, Int64}, edge) where {T} + return (size(values) == (1,1)) ? values[1,1] : values[edge[1],edge[2]] 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) - location_type ? values[1] : values[location_idx] + +# Returns the value of an initial condition of parameter in a vertex. +function get_vertex_value(values::Vector{T}, vert_idx) where {T} + return (length(values) == 1) ? values[1] : values[vert_idx] 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) - vcat([get_component_value.(values, comp) for comp in 1:n]...) + + + + + + + + +# Finds the transport rate of a parameter going from a source vertex to a destination vertex. +function get_transport_rate(transport_rate, edge::Pair{Int64,Int64}, t_rate_idx_types::Bool) + return t_rate_idx_types ? transport_rate[1,1] : transport_rate[edge[1],edge[2]] end -function expand_component_values(values::Vector{<:Vector}, n, location_types::Vector{Bool}) - vcat([get_component_value.(values, comp, location_types) for comp in 1:n]...) +# Finds the transportation rate for a specific species and a `LatticeTransportODEf` struct. +function get_transport_rate(trans_s_idx::Int64, f_func::LatticeTransportODEf, edge::Pair{Int64,Int64}) + get_transport_rate(f_func.transport_rates[trans_s_idx][2], edge, f_func.t_rate_idx_types[trans_s_idx]) 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) + + +# Updates the internal work_vert_ps vector for a given location. +# To this vector, we write the systems parameter values at a specific vertex. +function update_work_vert_ps!(work_vert_ps, vert_ps, comp, vert_ps_idx_types) # Loops through all parameters. - for (idx,loc_type) in enumerated_vert_ps_idx_types + for (idx,loc_type) in enumerate(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 end # Input is always either a LatticeTransportODEf or LatticeTransportODEjac function (which fields we then pass on). -function view_vert_ps_vector!(lt_ode_func, vert_ps, comp) - return view_vert_ps_vector!(lt_ode_func.work_vert_ps, vert_ps, comp, enumerate(lt_ode_func.v_ps_idx_types)) +function update_work_vert_ps!(lt_ode_func, vert_ps, comp) + return update_work_vert_ps!(lt_ode_func.work_vert_ps, vert_ps, comp, lt_ode_func.v_ps_idx_types) end # Expands a u0/p information stored in Vector{Vector{}} for to Matrix form diff --git a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl index 20476eeee9..52c13dbc68 100644 --- a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl +++ b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl @@ -15,7 +15,7 @@ rng = StableRNG(12345) t = default_t() ### Tests Simulations Don't Error ### -for grid in [small_2d_grid, short_path, small_directed_cycle, +for grid in [small_2d_graph_grid, short_path, small_directed_cycle, 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_2d_masked_grid] @@ -25,32 +25,28 @@ for grid in [small_2d_grid, short_path, small_directed_cycle, 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), ] - for u0 in [u0_1, u0_2, u0_3, u0_4, u0_5] + for u0 in [u0_1, u0_2, u0_3] 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), ] - for pV in [p1, p2, p3, p4] + for pV in [p1, p2, p3] 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), + pE_3 = map(sp -> sp => rand_e_vals(lrs, 0.01), 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)) + 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]) @test SciMLBase.successful_retcode(solve(oprob, Tsit5())) - 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())) end end @@ -63,23 +59,24 @@ for grid in [small_2d_grid, short_path, small_directed_cycle, 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)] - for u0 in [u0_1, u0_2, u0_3, u0_4] + for u0 in [u0_1, u0_2, u0_3] 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), ] - for pV in [p1, p2, p3, p4] + for pV in [p1, p2, p3] 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), + pE_3 = map(sp -> sp => rand_e_vals(lrs, 0.2), 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)) + 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, 10.0), [pV; pE]) @test SciMLBase.successful_retcode(solve(oprob, QNDF())) - oprob = ODEProblem(lrs, u0, (0.0, 10.0), (pV, pE); sparse = false) + oprob = ODEProblem(lrs, u0, (0.0, 10.0), [pV; pE]; sparse = false) @test SciMLBase.successful_retcode(solve(oprob, QNDF())) end end @@ -96,7 +93,7 @@ let 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)) + 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) @@ -119,7 +116,9 @@ 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] oprob = ODEProblem(lrs, u0, (0.0, 0.0), ps; jac=true, sparse=true) @@ -131,7 +130,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 @@ -145,7 +145,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 @@ -195,7 +196,7 @@ let u0 = [ :X => 1.0 .+ rand_v_vals(lrs.lattice), :Y => 2.0 * rand_v_vals(lrs.lattice), - :XY => 0.5, + :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] @@ -207,14 +208,14 @@ end # Checks that various combinations of jac and sparse gives the same result. let - lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_2d_grid) + lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_2d_graph_grid) u0 = [:X => rand_v_vals(lrs.lattice, 10), :Y => rand_v_vals(lrs.lattice, 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, 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) ss = solve(oprob, Rosenbrock23(); abstol = 1e-10, reltol = 1e-10).u[end] @test all(isapprox.(ss, @@ -228,42 +229,6 @@ let rtol = 0.0001)) end -# Checks that, when non directed graphs are provided, the parameters are re-ordered correctly. -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] - - 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)) -end - ### Test Grid Types ### # Tests that identical lattices (using different types of lattices) give identical results. @@ -273,9 +238,9 @@ let lrs1_masked = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_1d_masked_grid) lrs1_graph = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_1d_graph_grid) - oprob1_cartesian = ODEProblem(lrs1_cartesian, sigmaB_u0, (0.0,10.0), sigmaB_p) - oprob1_masked = ODEProblem(lrs1_masked, sigmaB_u0, (0.0,10.0), sigmaB_p) - oprob1_graph = ODEProblem(lrs1_graph, sigmaB_u0, (0.0,10.0), sigmaB_p) + oprob1_cartesian = ODEProblem(lrs1_cartesian, sigmaB_u0, (0.0,10.0), [sigmaB_p; sigmaB_p_spat]) + oprob1_masked = ODEProblem(lrs1_masked, sigmaB_u0, (0.0,10.0), [sigmaB_p; sigmaB_p_spat]) + oprob1_graph = ODEProblem(lrs1_graph, sigmaB_u0, (0.0,10.0), [sigmaB_p; sigmaB_p_spat]) @test solve(oprob1_cartesian, QNDF()) == solve(oprob1_masked, QNDF()) == solve(oprob1_graph, QNDF()) # 2d lattices. @@ -283,9 +248,9 @@ let lrs2_masked = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_2d_masked_grid) lrs2_graph = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_2d_graph_grid) - oprob2_cartesian = ODEProblem(lrs2_cartesian, sigmaB_u0, (0.0,10.0), sigmaB_p) - oprob2_masked = ODEProblem(lrs2_masked, sigmaB_u0, (0.0,10.0), sigmaB_p) - oprob2_graph = ODEProblem(lrs2_graph, sigmaB_u0, (0.0,10.0), sigmaB_p) + oprob2_cartesian = ODEProblem(lrs2_cartesian, sigmaB_u0, (0.0,10.0), [sigmaB_p; sigmaB_p_spat]) + oprob2_masked = ODEProblem(lrs2_masked, sigmaB_u0, (0.0,10.0), [sigmaB_p; sigmaB_p_spat]) + oprob2_graph = ODEProblem(lrs2_graph, sigmaB_u0, (0.0,10.0), [sigmaB_p; sigmaB_p_spat]) @test solve(oprob2_cartesian, QNDF()) == solve(oprob2_masked, QNDF()) == solve(oprob2_graph, QNDF()) # 3d lattices. @@ -293,36 +258,39 @@ let lrs3_masked = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_3d_masked_grid) lrs3_graph = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_3d_graph_grid) - oprob3_cartesian = ODEProblem(lrs3_cartesian, sigmaB_u0, (0.0,10.0), sigmaB_p) - oprob3_masked = ODEProblem(lrs3_masked, sigmaB_u0, (0.0,10.0), sigmaB_p) - oprob3_graph = ODEProblem(lrs3_graph, sigmaB_u0, (0.0,10.0), sigmaB_p) + oprob3_cartesian = ODEProblem(lrs3_cartesian, sigmaB_u0, (0.0,10.0), [sigmaB_p; sigmaB_p_spat]) + oprob3_masked = ODEProblem(lrs3_masked, sigmaB_u0, (0.0,10.0), [sigmaB_p; sigmaB_p_spat]) + oprob3_graph = ODEProblem(lrs3_graph, sigmaB_u0, (0.0,10.0), [sigmaB_p; sigmaB_p_spat]) @test solve(oprob3_cartesian, QNDF()) == solve(oprob3_masked, QNDF()) == solve(oprob3_graph, QNDF()) end # 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 vertexes are `true`). +# Tries for Vector, Tuple, and Dictionary inputs. let - for lattice in [CartesianGrid(3,4), fill(true, 3, 4)] - lrs = LatticeReactionSystem(SIR_system, SIR_srs_2, lattice) + 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, 200, 300, 100, 100, 100, 200, 200, 200, 300, 300, 300] - S_vals_mat = [100, 200, 300; 100, 100, 100; 200, 200, 200; 300, 300, 300] + 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.02, 0.03, 0.01, 0.01, 0.02, 0.02, 0.02, 0.02, 0.03, 0.03, 0.03] - β_vals_mat = [0.01 0.02 0.03; 0.01 0.01 0.02; 0.02 0.02 0.02; 0.03 0.03 0.03] - SIR_p_vec = [:α => 0.1 / 1000, :β => β_vals_vec] - SIR_p_mat = [:α => 0.1 / 1000, :β => β_vals_mat] - - sol1 = solve(ODEProblem(lrs, SIR_u0_vec, (0.0, 10.0), SIR_p_vec)) - sol2 = solve(ODEProblem(lrs, SIR_u0_mat, (0.0, 10.0), SIR_p_vec)) - sol3 = solve(ODEProblem(lrs, SIR_u0_vec, (0.0, 10.0), SIR_p_mat)) - sol4 = solve(ODEProblem(lrs, SIR_u0_mat, (0.0, 10.0), SIR_p_mat)) - - @test sol1 == sol2 == sol3 = sol4 + β_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 @@ -330,28 +298,29 @@ end # 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_2, lattice) - + lrs = LatticeReactionSystem(SIR_system, SIR_srs_1, lattice) + # Initial condition values. 999 is used for empty points. - S_vals_vec = [100, 200, 100, 200, 200, 200, 300, 300] - S_vals_mat = [100, 200, 999; 100, 999, 999; 200, 200, 200; 999, 300, 300] + 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.02, 0.03, 0.01, 0.01, 0.02, 0.02, 0.02, 0.02, 0.03, 0.03, 0.03] + β_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] - SIR_p_mat = [:α => 0.1 / 1000, :β => β_vals_mat] - SIR_p_sparse_mat = [:α => 0.1 / 1000, :β => β_vals_sparse_mat] - - sol = solve(ODEProblem(lrs, SIR_u0_vec, (0.0, 10.0), SIR_p_vec)) + 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)) + @test sol == solve(ODEProblem(lrs, u0, (0.0, 10.0), p), Tsit5()) end end end @@ -367,13 +336,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) + 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.lattice), :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 @@ -383,17 +352,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) - + 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.lattice), :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. @@ -422,7 +391,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) @@ -448,53 +417,30 @@ 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 ### Tests Special Cases ### -# Create network with various combinations of graph/di-graph and parameters. +# Create network 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. @@ -511,80 +457,43 @@ 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 - end -end - -# Provides initial conditions and parameters in various different ways. -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)) + @test ss_alt[((i - 1) * 6 + 1):((i - 1) * 6 + 3)] ≈ ss[((i - 1) * 3 + 1):((i - 1) * 3 + 3)] 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. @@ -673,8 +582,8 @@ let 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 + 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) diff --git a/test/spatial_reaction_systems/lattice_reaction_systems.jl b/test/spatial_reaction_systems/lattice_reaction_systems.jl index 17ff9b3940..c8c8ae3968 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems.jl @@ -3,25 +3,31 @@ # Fetch packages. using Catalyst, Graphs, Test +# Fetch test networks. +include("../spatial_test_networks.jl") + # Pre declares a grid. -grid = Graphs.grid([2, 2]) +grids = [CartesianGrid((2,2)), fill(true, 2, 2), Graphs.grid([2, 2])] ### Tests LatticeReactionSystem Getters Correctness ### + # Test case 1. 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. @@ -44,14 +50,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. @@ -62,14 +70,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. @@ -90,16 +100,18 @@ let 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. @@ -108,9 +120,11 @@ let (p, 1), 0 <--> X end tr = @transport_reaction d X - lrs = LatticeReactionSystem(rs, [tr], grid) + for grid in grids + lrs = LatticeReactionSystem(rs, [tr], grid) - @test nameof(lrs) == :customname + @test nameof(lrs) == :customname + end end ### Tests Spatial Reactions Getters Correctness ### @@ -234,7 +248,9 @@ let (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 @@ -244,7 +260,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. @@ -254,7 +272,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). @@ -264,14 +284,16 @@ let (p, d), 0 <--> X end tr = @transport_reaction D X - @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid) - - rs = @reaction_network begin - @parameters D [description="Parameter with added metadata"] - (p, d), 0 <--> X + 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 + end + tr = @transport_reaction D X + @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid) end - tr = @transport_reaction D X - @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid) end @@ -296,7 +318,7 @@ let end # Graph grids (cannot test diagonal connections). - for lattice in [small_2d_grid, small_3d_grid, undirected_cycle, small_directed_cycle, unconnected_graph] + 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 lrs1.num_edges == iterator_count(lrs1.edge_iterator) end diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl index 7771898c2f..8b50805ae0 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl @@ -3,32 +3,35 @@ # 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 dimension > 3. let - @test_throws Exception LatticeReactionSystem(brusselator_system, srs, CartesianGrid((5, 5, 5, 5))) - @test_throws Exception LatticeReactionSystem(brusselator_system, srs, fill(true, 5, 5, 5, 5)) + @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, srs, small_1d_cartesian_grid) - cartesian_2d_lrs = LatticeReactionSystem(brusselator_system, srs, small_2d_cartesian_grid) - cartesian_3d_lrs = LatticeReactionSystem(brusselator_system, srs, small_3d_cartesian_grid) - masked_1d_lrs = LatticeReactionSystem(brusselator_system, srs, small_1d_masked_grid) - masked_2d_lrs = LatticeReactionSystem(brusselator_system, srs, small_2d_masked_grid) - masked_3d_lrs = LatticeReactionSystem(brusselator_system, srs, small_3d_masked_grid) - graph_lrs = LatticeReactionSystem(brusselator_system, srs, small_2d_grid) + 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_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(masked_2d_lrs) @test !has_masked_lattice(graph_lrs) @test has_grid_lattice(cartesian_2d_lrs) @@ -49,17 +52,36 @@ let @test_throws Exception grid_dims(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 = Graphs.grid([5, 5]) + cartesian_grid = CartesianGrid((5, 5)) masked_grid = fill(true, 5, 5) graph_grid = Graphs.grid([5, 5]) - cartesian_lrs = LatticeReactionSystem(brusselator_system, srs, cartesian_grid) - masked_lrs = LatticeReactionSystem(brusselator_system, srs, masked_grid) - graph_lrs = LatticeReactionSystem(brusselator_system, srs, graph_grid) + 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 cartesian_lrs.rs == masked_lrs.rs == graph_lrs.rs @@ -67,53 +89,74 @@ let @test cartesian_lrs.num_verts == masked_lrs.num_verts == graph_lrs.num_verts @test cartesian_lrs.num_edges == masked_lrs.num_edges == graph_lrs.num_edges @test cartesian_lrs.num_species == masked_lrs.num_species == graph_lrs.num_species - @test cartesian_lrs.spat_species == masked_lrs.spat_species == graph_lrs.spat_species - @test cartesian_lrs.parameters == masked_lrs.parameters == graph_lrs.parameters - @test cartesian_lrs.vertex_parameters == masked_lrs.vertex_parameters == graph_lrs.vertex_parameters - @test cartesian_lrs.edge_parameters == masked_lrs.edge_parameters == graph_lrs.edge_parameters - @test cartesian_lrs.directed_edges == masked_lrs.directed_edges == graph_lrs.directed_edges - @test cartesian_lrs.edge_list == masked_lrs.edge_list == graph_lrs.edge_list + @test isequal(cartesian_lrs.spat_species, masked_lrs.spat_species) + @test isequal(masked_lrs.spat_species, graph_lrs.spat_species) + @test isequal(cartesian_lrs.parameters, masked_lrs.parameters) + @test isequal(masked_lrs.parameters, graph_lrs.parameters) + @test isequal(cartesian_lrs.vertex_parameters, masked_lrs.vertex_parameters) + @test isequal(masked_lrs.edge_parameters, graph_lrs.edge_parameters) + @test issetequal(cartesian_lrs.edge_iterator, masked_lrs.edge_iterator) + @test issetequal(masked_lrs.edge_iterator, graph_lrs.edge_iterator) # Checks that simulations yields the same output. - u0 = [:X => rand_v_vals(graph_lrs.lattice, 10.0), :Y => 2.0] - pV = [:A => 0.5 .+ rand_v_vals(graph_lrs.lattice, 0.5), :B => 4.0] - pE= map(sp -> sp => 0.2, spatial_param_syms(lrs)) - - cartesian_oprob = ODEProblem(cartesian_lrs, u0, (0.0, 100.0), (pV, pE)) - masked_oprob = ODEProblem(masked_lrs, u0, (0.0, 100.0), (pV, pE)) - graph_oprob = ODEProblem(graph_lrs, u0, (0.0, 100.0), (pV, pE)) + X_vals = rand(cartesian_lrs.num_verts) + u0_cartesian = [:X => reshape(u0_vals, 5, 5), :Y => 2.0] + u0_masked = [:X => reshape(u0_vals, 5, 5), :Y => 2.0] + u0_graph = [:X => u0_vals, :Y => 2.0] + B_vals = rand(cartesian_lrs.num_verts) + 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 = solvegraph(_oprob, QNDF(); saveat=0.1) + graph_sol = solve(graph_oprob, QNDF(); saveat=0.1) - @test cartesian_sol.u ≈ masked_sol.u ≈ graph_sol + @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 = cycle_graph(8) + 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) - masked_lrs = LatticeReactionSystem(brusselator_system, srs, masked_grid) - graph_lrs = LatticeReactionSystem(brusselator_system, srs, graph_grid) - # Check internal structures. @test masked_lrs.num_verts == graph_lrs.num_verts @test masked_lrs.num_edges == graph_lrs.num_edges - @test masked_lrs.edge_list == graph_lrs.edge_list - + @test issetequal(masked_lrs.edge_iterator, graph_lrs.edge_iterator) + # Checks that simulations yields the same output. - u0 = [:X => rand_v_vals(graph_lrs.lattice, 10.0), :Y => 2.0] - pV = [:A => 0.5 .+ rand_v_vals(graph_lrs.lattice, 0.5), :B => 4.0] - pE= map(sp -> sp => 0.2, spatial_param_syms(lrs)) - - masked_oprob = ODEProblem(masked_lrs, u0, (0.0, 100.0), (pV, pE)) - graph_oprob = ODEProblem(graph_lrs, u0, (0.0, 100.0), (pV, pE)) - - masked_sol = solve(masked_oprob, QNDF(); saveat=0.1) - graph_sol = solvegraph(_oprob, QNDF(); saveat=0.1) - - @test masked_sol.u ≈ graph_sol + 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 \ No newline at end of file diff --git a/test/spatial_test_networks.jl b/test/spatial_test_networks.jl index f4fcb17f50..4e68d5785f 100644 --- a/test/spatial_test_networks.jl +++ b/test/spatial_test_networks.jl @@ -8,10 +8,26 @@ 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(lrs.lattice) +function rand_v_vals(grid::DiGraph) + return rand(rng, nv(grid)) +end +function rand_v_vals(grid::Catalyst.CartesianGridRej{S,T}) where {S,T} + return rand(rng, grid.dims) +end +function rand_v_vals(grid::Array{Bool, T}) where {T} + return rand(rng, size(grid)) +end + rand_e_vals(grid, x::Number) = rand_e_vals(grid) * x +function rand_e_vals(lrs::LatticeReactionSystem) + e_vals = spzeros(lrs.num_verts, lrs.num_verts) + for e in lrs.edge_iterator + e_vals[e[1], e[2]] = rand() + end + return e_vals +end # Gets a symbol list of spatial parameters. function spatial_param_syms(lrs::LatticeReactionSystem) From 64fa97baa224020ea8cffcd0592bde171dcc0809 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 6 Feb 2024 16:18:54 -0500 Subject: [PATCH 13/47] up --- src/spatial_reaction_systems/utility.jl | 2 +- .../lattice_reaction_systems_ODEs.jl | 41 +++++++++++++++++++ test/spatial_test_networks.jl | 4 ++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index 3b18fd31cf..4c4d95043c 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -44,7 +44,7 @@ function lattice_process_p(ps_in, ps_vertex_syms, ps_edge_syms, lrs::LatticeReac 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) - + return vert_ps, edge_ps end diff --git a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl index 52c13dbc68..090481d3e8 100644 --- a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl +++ b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl @@ -494,6 +494,47 @@ let end end +# Tests various types of numbers for initial conditions/parameters (e.g. Real numbers, Float32, etc.). +let + # 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, 20.0), ps[1]) + sol_base = solve(oprob_base, QNDF(); abstol=1e-8, reltol=1e-8, saveat=0.1) + + # Checks all combinations of input types. + for grid in [very_small_2d_cartesian_grid, very_small_2d_masked_grid, very_small_2d_graph_grid] + lrs_base = LatticeReactionSystem(brusselator_system, brusselator_srs_2, 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, Tuple(p_base), Dict(p_base)] + for sparse in [false, true], jac in [false, true] + oprob = ODEProblem(lrs, u0, (0.0, 20.0), p; sparse, jac) + sol = solve(oprob, QNDF(); abstol=1e-8, reltol=1e-8, saveat=0.1) + @test sol == sol_base + end + end + end + end +end + ### Compare to Hand-written Functions ### # Compares the brusselator for a line of cells. diff --git a/test/spatial_test_networks.jl b/test/spatial_test_networks.jl index 4e68d5785f..ef1836958e 100644 --- a/test/spatial_test_networks.jl +++ b/test/spatial_test_networks.jl @@ -180,6 +180,8 @@ sigmaB_srs_2 = [sigmaB_tr_σB, sigmaB_tr_w, sigmaB_tr_v] ### Declares Lattices ### # Cartesian grids. +very_small_2d_cartesian_grid = CartesianGrid((2,2)) + small_1d_cartesian_grid = CartesianGrid(5) small_2d_cartesian_grid = CartesianGrid((5,5)) small_3d_cartesian_grid = CartesianGrid((5,5,5)) @@ -189,6 +191,8 @@ large_2d_cartesian_grid = CartesianGrid((100,100)) large_3d_cartesian_grid = CartesianGrid((100,100,100)) # Masked grids. +very_small_2d_masked_grid = fill(true, 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) From ad5c71ef9320afbb87950e4c1302e7951ec6a66f Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 6 Feb 2024 16:40:34 -0500 Subject: [PATCH 14/47] up --- src/Catalyst.jl | 2 +- .../lattice_reaction_systems.jl | 39 ++- .../lattice_reaction_systems_ODEs.jl | 242 +++++++++--------- .../lattice_reaction_systems.jl | 2 +- .../lattice_reaction_systems_lattice_types.jl | 6 +- 5 files changed, 160 insertions(+), 131 deletions(-) diff --git a/src/Catalyst.jl b/src/Catalyst.jl index a5f51efb89..9f6da7d1d0 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -177,7 +177,7 @@ include("spatial_reaction_systems/lattice_reaction_systems.jl") export LatticeReactionSystem export spatial_species, vertex_parameters, edge_parameters export CartesianGrid, CartesianGridReJ # (Implemented in JumpProcesses) -export has_cartesian_lattice, has_masked_lattice, has_grid_lattice, has_graph_lattice, grid_dims +export has_cartesian_lattice, has_masked_lattice, has_grid_lattice, has_graph_lattice, grid_dims, grid_size # Various utility functions include("spatial_reaction_systems/utility.jl") diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index be75d75d73..47a3d75e70 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -208,21 +208,48 @@ ModelingToolkit.nameof(lrs::LatticeReactionSystem) = nameof(lrs.rs) # Checks if a lattice reaction system is a pure (linear) transport reaction system. is_transport_system(lrs::LatticeReactionSystem) = all(sr -> sr isa TransportReaction, lrs.spatial_reactions) -# Checks if a LatticeReactionSystem have a Cartesian grid lattice. +""" + has_cartesian_lattice(lrs::LatticeReactionSystem) + +Returns `true` if `lrs` was created using a cartesian grid lattice (e.g. created via `CartesianGrid(5,5)`). Else, returns `false`. +""" has_cartesian_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa CartesianGridRej{S,T} where {S,T} -# Checks if a LatticeReactionSystem have a masked grid lattice. + +""" + has_masked_lattice(lrs::LatticeReactionSystem) + +Returns `true` if `lrs` was created using a masked grid lattice (e.g. created via `[true true; true false]`). Else, returns `false`. +""" has_masked_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa Array{Bool, T} where T -# Checks if a LatticeReactionSystem have a grid lattice (cartesian or masked). + +""" + has_grid_lattice(lrs::LatticeReactionSystem) + +Returns `true` if `lrs` was created using a cartesian or masked grid lattice. Else, returns `false`. +""" has_grid_lattice(lrs::LatticeReactionSystem) = (has_cartesian_lattice(lrs) || has_masked_lattice(lrs)) -# Checks if a LatticeReactionSystem have a graph lattice. + +""" + has_graph_lattice(lrs::LatticeReactionSystem) + +Returns `true` if `lrs` was created using a graph grid lattice (e.g. created via `path_graph(5)`). Else, returns `false`. +""" has_graph_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa SimpleDiGraph -# Returns the size of the lattice of a LatticeReactionNetwork with a grid lattice. +""" + 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. +""" function grid_size(lrs::LatticeReactionSystem) has_cartesian_lattice(lrs) && (return lrs.lattice.dims) has_masked_lattice(lrs) && (return size(lrs.lattice)) error("Grid size is only defined for LatticeReactionSystems with grid-based lattices (not graph-based).") end -# Returns the dimensions of a LatticeReactionNetwork with a grid lattice. +""" + 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) = length(grid_size(lrs)) \ 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 090481d3e8..42cef58c28 100644 --- a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl +++ b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl @@ -229,10 +229,130 @@ let rtol = 0.0001)) end +# 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 + + 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) + + 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) + + @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 + # 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, small_1d_cartesian_grid) lrs1_masked = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_1d_masked_grid) @@ -522,7 +642,7 @@ let # Checks all combinations of input types. for grid in [very_small_2d_cartesian_grid, very_small_2d_masked_grid, very_small_2d_graph_grid] - lrs_base = LatticeReactionSystem(brusselator_system, brusselator_srs_2, grid) + lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_2, 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, Tuple(p_base), Dict(p_base)] for sparse in [false, true], jac in [false, true] @@ -533,122 +653,4 @@ let end end end -end - -### Compare to Hand-written Functions ### - -# Compares 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 - - 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) - - 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) - - @test isapprox(J_hw_dense, J_aut_dense) - @test isapprox(J_hw_sparse, J_aut_sparse) -end +end \ No newline at end of file diff --git a/test/spatial_reaction_systems/lattice_reaction_systems.jl b/test/spatial_reaction_systems/lattice_reaction_systems.jl index c8c8ae3968..ed29142640 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems.jl @@ -7,7 +7,7 @@ using Catalyst, Graphs, Test include("../spatial_test_networks.jl") # Pre declares a grid. -grids = [CartesianGrid((2,2)), fill(true, 2, 2), Graphs.grid([2, 2])] +grids = [very_small_2d_cartesian_grid, very_small_2d_masked_grid, very_small_2d_graph_grid] ### Tests LatticeReactionSystem Getters Correctness ### diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl index 8b50805ae0..c75d0d1c96 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl @@ -100,9 +100,9 @@ let # Checks that simulations yields the same output. X_vals = rand(cartesian_lrs.num_verts) - u0_cartesian = [:X => reshape(u0_vals, 5, 5), :Y => 2.0] - u0_masked = [:X => reshape(u0_vals, 5, 5), :Y => 2.0] - u0_graph = [:X => u0_vals, :Y => 2.0] + 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(cartesian_lrs.num_verts) 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] From a265597c16bdef83a992b85f872e5b7c3f2c9475 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 6 Feb 2024 19:49:28 -0500 Subject: [PATCH 15/47] "finish" remake. --- .../lattice_reaction_systems.jl | 91 ++++---- .../spatial_ODE_systems.jl | 168 ++++++++------- .../spatial_reactions.jl | 21 +- src/spatial_reaction_systems/utility.jl | 202 ++++++++++-------- ...attice_reaction_systems_ODE_performance.jl | 16 +- .../lattice_reaction_systems_ODEs.jl | 11 +- 6 files changed, 271 insertions(+), 238 deletions(-) diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index 47a3d75e70..a525757753 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -4,57 +4,60 @@ # Adding the "<: MT.AbstractTimeDependentSystem" part messes up show, disabling me from creating LRSs. Should be fixed some time. struct LatticeReactionSystem{Q,R,S,T} # <: MT.AbstractTimeDependentSystem # Input values. - """The reaction system within each compartment.""" + """The (non-spatial) reaction system within each vertexes.""" rs::ReactionSystem{Q} - """The spatial reactions defined between individual nodes.""" + """The spatial reactions defined between individual vertexes.""" spatial_reactions::Vector{R} - """The graph on which the lattice is defined.""" + """The lattice on which the (discrete) spatial system is defined.""" lattice::S # Derived values. - """The number of compartments.""" + """The number of vertexes (compartments).""" num_verts::Int64 """The number of edges.""" num_edges::Int64 """The number of species.""" num_species::Int64 - """Species that may move spatially.""" + """List of species that may move spatially.""" spat_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 vertexes and edges). """ parameters::Vector{BasicSymbolic{Real}} """ - 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 vertexes, + e.g. that possibly could have unique values at each vertex of the system. """ vertex_parameters::Vector{BasicSymbolic{Real}} """ - 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}} """ - An iterator over all the edges on the lattice. - The format depends on the type of lattice (Cartesian grid, grid, or graph). + 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{Q}, spatial_reactions::Vector{R}, lattice::S, - num_verts, num_edges, edge_iterator::T) where {Q,R, S, T} + num_verts::Int64, num_edges::Int64, edge_iterator::T) where {Q,R,S,T} # Error checks. if !(R <: AbstractSpatialReaction) error("The second argument must be a vector of AbstractSpatialReaction subtypes.") end - # Computes derived values spatial species. Counts the total number of species. + # Computes the species which are parts of spatial reactions. Also counts total number of + # species types. if isempty(spatial_reactions) spat_species = Vector{BasicSymbolic{Real}}[] else spat_species = unique(reduce(vcat, [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)) @@ -66,10 +69,6 @@ struct LatticeReactionSystem{Q,R,S,T} # <: MT.AbstractTimeDependentSystem edge_parameters = unique([rs_edge_parameters; srs_edge_parameters]) vertex_parameters = filter(!isedgeparameter, parameters(rs)) - # Counts the number of species types. The number of vertexes and compartments is given in input. - # `num_edges` cannot be computed here, because how it is computed depends on the lattice and `typeof(edge_iterator)`. - num_species = length(unique([species(rs); spat_species])) - # Ensures the parameter order begins similarly to in the non-spatial ReactionSystem. ps = [parameters(rs); setdiff([edge_parameters; vertex_parameters], parameters(rs))] @@ -86,25 +85,26 @@ function LatticeReactionSystem(rs, srs, lattice_in::CartesianGridRej{S,T}; diago # Error checks. (length(lattice_in.dims) > 3) && error("Grids of higher dimension than 3 is currently not supported.") - # Ensures that the matrix has a 3d form (for intermediary computations, original is passed to constructor). + # Ensures that the matrix has a 3d form (used for intermediary computations only, + # the original is passed to the constructor). lattice = CartesianGrid((lattice_in.dims..., fill(1, 3-length(lattice_in.dims))...)) # Counts vertexes and edges. The `num_edges` count formula counts the number of internal, side, - # edge, and corner vertexes (on the grid). The number of edges from each depend on whether diagonal + # edge, and corner vertexes (on the grid). The number of edges from each depends on whether diagonal # connections are allowed. The formula holds even if l, m, and/or n are 1. l,m,n = lattice.dims num_verts = l * m * n (ni, ns, ne, nc) = diagonal_connections ? (26,17,11,7) : (6,5,4,3) - num_edges = ni*(l-2)*(m-2)*(n-2) + # Internal vertexes. - ns*(2(l-2)*(m-2) + 2(l-2)*(n-2) + 2(m-2)*(n-2)) + # Side vertexes. - ne*(4(l-2) + 4(m-2) + 4(n-2)) + # Edge vertexes. - nc*8 # Corner vertexes. + num_edges = ni*(l-2)*(m-2)*(n-2) + # Edges from internal vertexes. + ns*(2(l-2)*(m-2) + 2(l-2)*(n-2) + 2(m-2)*(n-2)) + # Edges from side vertexes. + ne*(4(l-2) + 4(m-2) + 4(n-2)) + # Edges from edge vertexes. + nc*8 # Edges from corner vertexes. # Creates an iterator over all edges. # Currently creates a full vector. Future version might be for efficient. edge_iterator = Vector{Pair{Int64,Int64}}(undef, num_edges) # Loops through, simultaneously, the coordinates of each position in the grid, as well as that - # coordinate's (scalar) flat index. For each grid point, loops through all potential neighbours. + # coordinate's (scalar) flat index. For each grid point, loops through all potential neighbours. indices = [(L, M, N) for L in 1:l, M in 1:m, N in 1:n] flat_indices = 1:num_verts next_vert = 0 @@ -117,7 +117,7 @@ function LatticeReactionSystem(rs, srs, lattice_in::CartesianGridRej{S,T}; diago !diagonal_connections && (count([L==LL, M==MM, N==NN]) == 2) || continue diagonal_connections && (L==LL) && (M==MM) && (N==NN) && continue - # Computes the neighbours scalar index. Add that connection to `edge_iterator`. + # Computes the neighbour's flat (scalar) index. Add the edge to edge_iterator. neighbour_idx = LL + (MM - 1) * l + (NN - 1) * m * l edge_iterator[next_vert += 1] = (idx => neighbour_idx) end @@ -132,17 +132,16 @@ function LatticeReactionSystem(rs, srs, lattice_in::Array{Bool, T}; diagonal_con dims = size(lattice_in) (length(dims) > 3) && error("Grids of higher dimension than 3 is currently not supported.") - # Ensures that the matrix has a 3d form (for intermediary computations, original is passed to constructor). - # Also gets some basic lattice information. + # Ensures that the matrix has a 3d form (used for intermediary computations only, + # the original is passed to the constructor). lattice = reshape(lattice_in, [dims...; fill(1, 3-length(dims))]...) # Counts vertexes (edges have to be counted after the iterator have been created). num_verts = count(lattice) - # Creates an iterator over all edges.Currently a full vector of all edges (as pairs). - edge_iterator = Vector{Pair{Int64,Int64}}() - # Makes a template matrix to store each vertex's index. The matrix is 0 where there are no vertex. + # Makes a template matrix to store each vertex's index. The matrix is 0 where there is no vertex. + # The template is used in the next step. idx_matrix = fill(0, size(lattice_in)) cur_vertex_idx = 0 for flat_idx in 1:length(lattice) @@ -151,8 +150,10 @@ function LatticeReactionSystem(rs, srs, lattice_in::Array{Bool, T}; diagonal_con end end - # Loops through, simultaneously, the coordinates of each position in the grid, as well as that - # coordinate's (scalar) flat index. For each grid point, loops through all potential neighbours. + # Creates an iterator over all edges. A vector with pairs of each edge's source to its destination. + edge_iterator = Vector{Pair{Int64,Int64}}() + # Loops through, the coordinates of each position in the grid. + # For each grid point, loops through all potential neighbours and adds edges to edge_iterator. l, m, n = size(lattice) indices = [(L, M, N) for L in 1:l, M in 1:m, N in 1:n] for (L, M, N) in indices @@ -169,7 +170,7 @@ function LatticeReactionSystem(rs, srs, lattice_in::Array{Bool, T}; diagonal_con !diagonal_connections && (count([L==LL, M==MM, N==NN]) == 2) || continue diagonal_connections && (L==LL) && (M==MM) && (N==NN) && continue - # Computes the neighbours scalar index. Add that connection to `edge_iterator`. + # Computes the neighbour's scalar index. Add that connection to `edge_iterator`. push!(edge_iterator, idx_matrix[L,M,N] => idx_matrix[LL,MM,NN]) end end @@ -188,6 +189,7 @@ end # Creates a LatticeReactionSystem from a (undirected) Graph lattice (graph grid). LatticeReactionSystem(rs, srs, lattice::SimpleGraph) = LatticeReactionSystem(rs, srs, DiGraph(lattice)) + ### Lattice ReactionSystem Getters ### # Get all species. @@ -197,9 +199,9 @@ spatial_species(lrs::LatticeReactionSystem) = lrs.spat_species # Get all parameters. ModelingToolkit.parameters(lrs::LatticeReactionSystem) = lrs.parameters -# Get all parameters which values are tied to vertexes (compartments). +# Get all parameters whose values are tied to vertexes (compartments). vertex_parameters(lrs::LatticeReactionSystem) = lrs.vertex_parameters -# Get all parameters which values are tied to edges (adjacencies). +# Get all parameters whose values are tied to edges (adjacencies). edge_parameters(lrs::LatticeReactionSystem) = lrs.edge_parameters # Gets the lrs name (same as rs name). @@ -211,35 +213,39 @@ is_transport_system(lrs::LatticeReactionSystem) = all(sr -> sr isa TransportReac """ has_cartesian_lattice(lrs::LatticeReactionSystem) -Returns `true` if `lrs` was created using a cartesian grid lattice (e.g. created via `CartesianGrid(5,5)`). Else, returns `false`. +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) = lrs.lattice isa CartesianGridRej{S,T} where {S,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]`). Else, returns `false`. +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) = lrs.lattice isa Array{Bool, T} where T """ has_grid_lattice(lrs::LatticeReactionSystem) -Returns `true` if `lrs` was created using a cartesian or masked grid lattice. Else, returns `false`. +Returns `true` if `lrs` was created using a cartesian or masked grid lattice. Otherwise, returns `false`. """ has_grid_lattice(lrs::LatticeReactionSystem) = (has_cartesian_lattice(lrs) || has_masked_lattice(lrs)) """ has_graph_lattice(lrs::LatticeReactionSystem) -Returns `true` if `lrs` was created using a graph grid lattice (e.g. created via `path_graph(5)`). Else, returns `false`. +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) = lrs.lattice 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. +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. """ function grid_size(lrs::LatticeReactionSystem) has_cartesian_lattice(lrs) && (return lrs.lattice.dims) @@ -250,6 +256,7 @@ 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`. +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) = length(grid_size(lrs)) \ No newline at end of file diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index ecf5b6dd86..19b7b7e05e 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -2,43 +2,46 @@ # Functor with information for the forcing function of a spatial ODE with spatial movement on a lattice. struct LatticeTransportODEf{S,T} - """The ODEFunction of the (non-spatial) reaction system which generated this function.""" + """The ODEFunction of the (non-spatial) ReactionSystem that generated this LatticeTransportODEf instance.""" ofunc::S """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.""" + """The values of the parameters that are tied to vertexes.""" vert_ps::Vector{Vector{T}} """ - 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. + Vector for storing temporary values. Repeatedly during simulations, we need to retrieve the + parameter values in a certain vertex. However, since most parameters (likely) are uniform + (and hence only have 1 value stored), we need to create a new vector each time we need to retrieve + the parameter values in a new vertex. To avoid relocating these values repeatedly, we write them + to this vector. """ work_vert_ps::Vector{T} """ - 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. True means a uniform value. + For each parameter in vert_ps, its value is a vector with a length of either num_verts or 1. + To know whenever a parameter's value needs expanding to the work_vert_ps array, its length needs checking. + This check is done once, and the value is stored in this array. True means a uniform value. """ v_ps_idx_types::Vector{Bool} """ - A vector of sparse, 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. + 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. """ transport_rates::Vector{Pair{Int64,SparseMatrixCSC{T, Int64}}} """ - For each transport rate in transport_rates, its value is a (sparse) matrix with size either - (num_verts,num_verts) or (1,1). In the second case, that transportation rate is uniform across all edges. - To know how to access transport rate's value (without checking sizes), we can use this vector directly. - True means a uniform value. + 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. """ t_rate_idx_types::Vector{Bool} """ - A matrix, NxM, where N is the number of species with transportation and M the number of vertexes. + A matrix, NxM, where N is the number of species with transportation and M is 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). """ @@ -46,27 +49,28 @@ struct LatticeTransportODEf{S,T} """An iterator over all the edges of the lattice.""" edge_iterator::Vector{Pair{Int64, Int64}} - function LatticeTransportODEf(ofunc::S, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{T}}}, transport_rates::Vector{Pair{Int64, SparseMatrixCSC{T, Int64}}}, + function LatticeTransportODEf(ofunc::S, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{T}}}, + transport_rates::Vector{Pair{Int64, SparseMatrixCSC{T, Int64}}}, lrs::LatticeReactionSystem) where {S,T} - # Records. which parameters and rates are uniform and not. + # Records which parameters and rates are uniform and which are not. v_ps_idx_types = map(vp -> length(vp[2]) == 1, vert_ps) t_rate_idx_types = map(tr -> size(tr[2]) == (1,1), transport_rates) - # Input `vert_ps` is a vector map taking each parameter symbolic to its value (potentially a vector). - # This vector is sorted according to the parameters order. Here, we simply extract its values only. + # Input `vert_ps` is a vector map taking each parameter symbolic to its value (potentially a + # vector). This vector is already sorted according to the order of the parameters. Here, we extract + # its values only and put them into `vert_ps`. vert_ps = [vp[2] for vp in vert_ps] - # Computes the leaving rates. + # Computes the leaving rate matrix. leaving_rates = zeros(length(transport_rates), lrs.num_verts) - for (s_idx, trpair) in enumerate(transport_rates) - t_rate = trpair[2] + for (s_idx, tr_pair) in enumerate(transport_rates) for e in lrs.edge_iterator - # Updates the exit rate for species s_idx from vertex e.src - leaving_rates[s_idx, e[1]] += get_transport_rate(t_rate, e, t_rate_idx_types[s_idx]) + # 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 - # Declares `work_vert_ps` (used as storage during computation) and an iterator over the edges. + # Declares `work_vert_ps` (used as storage during computation) and the edge iterator. work_vert_ps = zeros(length(vert_ps)) edge_iterator = lrs.edge_iterator new{S,T}(ofunc, lrs.num_verts, lrs.num_species, vert_ps, work_vert_ps, @@ -76,25 +80,26 @@ end # Functor with information for the Jacobian function of a spatial ODE with spatial movement on a lattice. struct LatticeTransportODEjac{R,S,T} - """The ODEFunction of the (non-spatial) reaction system which generated this function.""" + """The ODEFunction of the (non-spatial) ReactionSystem that generated this LatticeTransportODEf instance.""" ofunc::R """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.""" + """The values of the parameters that are tied to vertexes.""" vert_ps::Vector{Vector{S}} """ - 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. + Vector for storing temporary values. Repeatedly during simulations, we need to retrieve the + parameter values in a certain vertex. However, since most parameters (likely) are uniform + (and hence only have 1 value stored), we need to create a new vector each time we need to retrieve + the parameter values in a new vertex. To avoid relocating these values repeatedly, we write them + to this vector. """ - work_vert_ps::Vector{S} + work_vert_ps::Vector{S} """ - 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. + For each parameter in vert_ps, its value is a vector with a length of either num_verts or 1. + To know whenever a parameter's value needs expanding to the work_vert_ps array, its length needs checking. + This check is done once, and the value is stored in this array. True means a uniform value. """ v_ps_idx_types::Vector{Bool} """Whether the Jacobian is sparse or not.""" @@ -102,11 +107,12 @@ struct LatticeTransportODEjac{R,S,T} """The transport rates. This is a dense or sparse matrix (depending on what type of Jacobian is used).""" jac_transport::T - function LatticeTransportODEjac(ofunc::R, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{S}}}, lrs::LatticeReactionSystem, + function LatticeTransportODEjac(ofunc::R, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{S}}}, jac_transport::Union{Nothing, SparseMatrixCSC{Float64, Int64}}, - sparse::Bool) where {R,S} - # Input `vert_ps` is a vector map taking each parameter symbolic to its value (potentially a vector). - # This vector is sorted according to the parameters order. Here, we simply extract its values only. + lrs::LatticeReactionSystem, sparse::Bool) where {R,S} + # Input `vert_ps` is a vector map taking each parameter symbolic to its value (potentially a + # vector). This vector is already sorted according to the order of the parameters. Here, we extract + # its values only and put them into `vert_ps`. vert_ps = [vp[2] for vp in vert_ps] work_vert_ps = zeros(lrs.num_verts) @@ -129,26 +135,24 @@ function DiffEqBase.ODEProblem(lrs::LatticeReactionSystem, u0_in, tspan, 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 = 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 vertexes. + # u0 is simply a vector with all the species' initial condition values across all vertexes. # 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) - # vert_ps and `edge_ps` are vector maps, taking each parameter's Symbolics to its value(s). - # vert_ps values are vectors. Here, index (i) is a parameters value in vertex i. - # edge_ps becomes sparse matrix. Here, index (i,j) is a parameters value in the edge from vertex i to vertex j. - # Uniform vertex/edge parameters stores only a single value (in a length 1 vector, or size 1x1 sparse matrix). - # This is the parameters single value. + # 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 becomes a sparse matrix. 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 ODEProblem. + # Creates the ODEFunction. ofun = build_odefunction(lrs, vert_ps, edge_ps, jac, sparse, name, include_zero_odes, - combinatoric_ratelaws, remove_conserved, checks) + combinatoric_ratelaws, remove_conserved, checks) # Combines `vert_ps` and `edge_ps` to a single vector with values only (not a map). Creates ODEProblem. ps = [p[2] for p in [vert_ps; edge_ps]] @@ -156,15 +160,16 @@ function DiffEqBase.ODEProblem(lrs::LatticeReactionSystem, u0_in, tspan, end # Builds an ODEFunction for a spatial ODEProblem. -function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{A,Vector{T}}}, - edge_ps::Vector{Pair{B,SparseMatrixCSC{T, Int64}}}, jac::Bool, sparse::Bool, - name, include_zero_odes, combinatoric_ratelaws, remove_conserved, checks) where {A,B,T} +function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{T}}}, + edge_ps::Vector{Pair{BasicSymbolic{Real},SparseMatrixCSC{T, Int64}}}, + jac::Bool, sparse::Bool, name, include_zero_odes, combinatoric_ratelaws, + remove_conserved, checks) where {T} if remove_conserved error("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). + # Creates a map, taking (the index in species(lrs) of) each species (with transportation) + # to its transportation rate (uniform or one value for each edge). The rates are sparse matrices. transport_rates = make_sidxs_to_transrate_map(vert_ps, edge_ps, lrs) # Prepares the Jacobian and forcing functions (depending on jacobian and sparsity selection). @@ -172,18 +177,18 @@ function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{A,Ve 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. + # Long term we could write separate versions 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) + jac_transport = build_jac_prototype(ofunc_sparse.jac_prototype, transport_rates, lrs; set_nonzero = true) if sparse f = LatticeTransportODEf(ofunc_sparse, vert_ps, transport_rates, 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, true) - jac_prototype = jac_vals + jac_transport = build_jac_prototype(ofunc_sparse.jac_prototype, transport_rates, lrs; set_nonzero = true) + J = LatticeTransportODEjac(ofunc_dense, vert_ps, jac_transport, lrs, true) + jac_prototype = jac_transport else f = LatticeTransportODEf(ofunc_dense, vert_ps, transport_rates, lrs) - J = LatticeTransportODEjac(ofunc_dense, vert_ps, lrs, jac_vals, false) + J = LatticeTransportODEjac(ofunc_dense, vert_ps, jac_transport, lrs, false) jac_prototype = nothing end else @@ -202,10 +207,13 @@ function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{A,Ve return ODEFunction(f; jac = J, jac_prototype = jac_prototype) 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}, transport_rates::Vector{Pair{Int64,SparseMatrixCSC{T, Int64}}}, - lrs::LatticeReactionSystem; set_nonzero = false) where T - # Finds the indexes of the transport species, and the species with transport only (and no non-spatial dynamics). +# 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 indexes 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) @@ -221,7 +229,7 @@ function build_jac_prototype(ns_jac_prototype::SparseMatrixCSC{Float64, Int64}, i_idxs = Vector{Int}(undef, num_entries) j_idxs = Vector{Int}(undef, num_entries) - # Indexes of elements due to non-spatial dynamics. + # Indexes of elements caused by non-spatial dynamics. for vert in 1:lrs.num_verts for n in 1:length(ns_i_idxs) i_idxs[idx] = get_index(vert, ns_i_idxs[n], lrs.num_species) @@ -230,9 +238,9 @@ function build_jac_prototype(ns_jac_prototype::SparseMatrixCSC{Float64, Int64}, end end - # Indexes of elements due to spatial dynamics. + # Indexes of elements caused by spatial dynamics. for e in lrs.edge_iterator - # Indexes due to terms for a species leaves its current vertex (but does not have + # 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 @@ -240,7 +248,7 @@ function build_jac_prototype(ns_jac_prototype::SparseMatrixCSC{Float64, Int64}, 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[1], s_idx, lrs.num_species) j_idxs[idx] = get_index(e[2], s_idx, lrs.num_species) @@ -248,7 +256,7 @@ function build_jac_prototype(ns_jac_prototype::SparseMatrixCSC{Float64, Int64}, end end - # Create sparse jacobian prototype with 0-valued entries. + # Create a sparse Jacobian prototype with 0-valued entries. jac_prototype = sparse(i_idxs, j_idxs, zeros(num_entries)) # Set element values. @@ -273,25 +281,25 @@ end 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. + # Gets the indexes of all the species at vertex i. idxs = get_indexes(vert_i, f_func.num_species) - # Updates the vector which contains the vertex parameter values for vertex vert_i. + # Updates the work vector to contain the vertex parameter values for vertex vert_i. update_work_vert_ps!(f_func, p, vert_i) # Evaluate reaction contributions to du at vert_i. f_func.ofunc((@view du[idxs]), (@view u[idxs]), f_func.work_vert_ps, t) end - # s_idx is species index among transport species, s is index among all species. + # 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(f_func.transport_rates) - # Rate for leaving vert_i + # Rate for leaving source vertex vert_i. for vert_i in 1:(f_func.num_verts) idx_src = get_index(vert_i, s, f_func.num_species) du[idx_src] -= f_func.leaving_rates[s_idx, vert_i] * u[idx_src] end - # Add rates for entering a given vertex via an incoming edge. + # Add rates for entering a destination vertex via an incoming edge. for e in f_func.edge_iterator idx_src = get_index(e[1], s, f_func.num_species) idx_dst = get_index(e[2], s, f_func.num_species) @@ -300,17 +308,17 @@ function (f_func::LatticeTransportODEf)(du, u, p, t) end end -# Defines the jacobian functor's effect on the (spatial) ODE system. +# Defines the Jacobian functor's effect on the (spatial) ODE system. function (jac_func::LatticeTransportODEjac)(J, u, p, t) J .= 0.0 - # Update the Jacobian from reaction terms. + # Update the Jacobian from non-spatial reaction terms. for vert_i in 1:(jac_func.num_verts) idxs = get_indexes(vert_i, jac_func.num_species) update_work_vert_ps!(jac_func, p, vert_i) jac_func.ofunc.jac((@view J[idxs, idxs]), (@view u[idxs]), jac_func.work_vert_ps, t) end - # Updates for the spatial reactions (adds the Jacobian values from the diffusion reactions). + # Updates for the spatial reactions (adds the Jacobian values from the transportation reactions). J .+= jac_func.jac_transport end diff --git a/src/spatial_reaction_systems/spatial_reactions.jl b/src/spatial_reaction_systems/spatial_reactions.jl index 581bd7f735..b13afee14f 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 @@ -17,12 +17,13 @@ function isedgeparameter(x, default = false) Symbolics.getmetadata(x, EdgeParameter, default) end + ### Transport Reaction Structures ### # 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 +41,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 @@ -70,17 +71,17 @@ 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 @@ -135,8 +136,10 @@ function hash(tr::TransportReaction, h::UInt) Base.hash(tr.species, h) 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] @@ -145,7 +148,7 @@ 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 diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index 4c4d95043c..f1c2a6e4ed 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -13,45 +13,46 @@ 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, lrs::LatticeReactionSystem) +# 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{BasicSymbolic{Real}}, 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. For species with uniform initial conditions, - # The value vector holds that value only. For spatially heterogeneous initial conditions, - # the vector have teh same length as the number of vertexes (with one value for each). + # 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 vertexes (storing one value for each). u0 = vertex_value_map(u0, lrs) - # Converts the initial condition to a single Vector (with one values for each species and vertex). + # 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], lrs.num_verts) 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(ps_in, ps_vertex_syms, ps_edge_syms, lrs::LatticeReactionSystem) +function lattice_process_p(ps_in, ps_vertex_syms::Vector{BasicSymbolic{Real}}, + ps_edge_syms::Vector{BasicSymbolic{Real}}, 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/1x1. Else, they have size num_verts/num_vertsxnum_verts. + # 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, converts the values to the correct form (vectors for vert_ps and sparse matrices for edge_ps). + # 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) - + return vert_ps, edge_ps end # 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{BasicSymbolic{Real}, <:Any}, syms::Vector{BasicSymbolic{Real}}) +function lattice_process_input(input::Dict{BasicSymbolic{Real}, T}, syms::Vector{BasicSymbolic{Real}}) where {T} # Error checks if !isempty(setdiff(keys(input), syms)) error("You have provided values for the following unrecognised parameters/initial conditions: $(setdiff(keys(input), syms)).") @@ -62,40 +63,56 @@ function lattice_process_input(input::Dict{BasicSymbolic{Real}, <:Any}, syms::Ve return [sym => input[sym] for sym in syms] end -function lattice_process_input(input, syms::Vector{BasicSymbolic{Real}}) - lattice_process_input(Dict(input), syms) +function lattice_process_input(input, syms::Vector{BasicSymbolic{Real}}) + if ((input isa Vector) || (input isa Vector)) && all(entry isa Pair for entry in input) + return lattice_process_input(Dict(input), syms) + end + error("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::Vector{<: Pair}, p_vertex_syms::Vector, p_edge_syms::Vector) -function split_parameters(ps, p_vertex_syms, p_edge_syms) +# function split_parameters(ps::Vector{<: Pair}, p_vertex_syms::Vector, p_edge_syms::Vector) +function split_parameters(ps, p_vertex_syms::Vector{BasicSymbolic{Real}}, p_edge_syms::Vector{BasicSymbolic{Real}}) 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 -# Converts the values for initial condition/vertex parameters to the correct form: -# Map from symbolics to to vectors of length either 1 (for uniform values) or num_verts. -function vertex_value_map(values, lrs) +# 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 -# Converts the values for a specific component (species/parameter) to the correct vector form. -function vertex_value_form(values, lrs::LatticeReactionSystem, sym) + +# Converts the values for an individual species/vertex parameter to its correct vector form. +function vertex_value_form(values, lrs::LatticeReactionSystem, sym::BasicSymbolic{Real}) + # 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)) vertex_value_form(values, lrs.num_verts, lrs.lattice, 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) != lrs.num_verts) error("You have provided ($(length(values))) values for $sym. This is not equal to the number of vertexes ($(lrs.num_verts)).") end return values end + + # (2d and 3d) Cartesian and masked grids can take non-vector, non-scalar, values input. return vertex_value_form(values, lrs.num_verts, lrs.lattice, sym) end -# Converts values to correct vector form for a Cartesian grid lattice. -function vertex_value_form(values::AbstractArray, num_verts::Int64, lattice::CartesianGridRej{S,T}, sym) where {S,T} + +# Converts values to the correct vector form for a Cartesian grid lattice. +function vertex_value_form(values::AbstractArray, num_verts::Int64, lattice::CartesianGridRej{S,T}, + sym::BasicSymbolic{Real}) where {S,T} if size(values) != lattice.dims error("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 @@ -104,32 +121,40 @@ function vertex_value_form(values::AbstractArray, num_verts::Int64, lattice::Car end return [values[flat_idx] for flat_idx in 1:num_verts] end -# Converts values to correct vector form for a masked grid lattice. -function vertex_value_form(values::AbstractArray, num_verts::Int64, lattice::Array{Bool, T}, sym) where {T} + +# 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{Real}) where {T} if size(values) != size(lattice) error("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 + + # 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 i = 1:length(lattice) - lattice[i] || continue - return_values[cur_idx += 1] = values[i] + for (idx,val) = enumerate(values) + lattice[idx] || continue + return_values[cur_idx += 1] = val end + + # Checks that the correct number of values was provided, and returns the values. if (length(return_values) != num_verts) error("You have provided ($(length(return_values))) values for $sym. This is not equal to the number of vertexes ($(num_verts)).") end return return_values end -# Converts the values for initial condition/vertex parameters to the correct form: -# Map from symbolics to to vectors of length either 1 (for uniform values) or num_verts. -function edge_value_map(values, lrs) +# 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 a specific component (species/parameter) to the correct vector form. + +# Converts the values for an individual edge parameter to its correct sparse matrix form. function edge_value_form(values, lrs::LatticeReactionSystem, sym) - # If a scalar have been given, converts it to a size (1,1) sparse matrix. + # 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. @@ -140,6 +165,8 @@ function edge_value_form(values, lrs::LatticeReactionSystem, sym) error("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 @@ -147,12 +174,14 @@ end # 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. -# In the first step, computes a map from species symbolics form to value(s). -# Second step converts to map from species index to value(s). function make_sidxs_to_transrate_map(vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{T}}}, edge_ps::Vector{Pair{BasicSymbolic{Real},SparseMatrixCSC{T, Int64}}}, - lrs::LatticeReactionSystem) where T + lrs::LatticeReactionSystem) where {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(lrs.rs)[spat_rates[1]] => spat_rates[2] for spat_rates in transport_rates_speciesmap @@ -160,38 +189,30 @@ function make_sidxs_to_transrate_map(vert_ps::Vector{Pair{BasicSymbolic{Real},Ve 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. +# 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) - for s in spatial_species(lrs)] + 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). -# If there are several transportation reactions for the species, their sum is used. -#function get_transport_rate_law(s::BasicSymbolic{Real}, lrs::LatticeReactionSystem) -function get_transport_rate_law(s, lrs) - rates = filter(sr -> isequal(s, sr.species), lrs.spatial_reactions) - return sum(getfield.(rates, :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, lrs::LatticeReactionSystem) -function compute_transport_rates(rate_law, p_val_dict, lrs) - # 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{Real}, 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, the rates becomes a size (1,1) sparse matrix. - # Else, the rates becomes a size (num_verts,num_verts) sparse matrix. + # 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] + 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 transport_rates = spzeros(lrs.num_verts, lrs.num_verts) @@ -203,77 +224,70 @@ function compute_transport_rates(rate_law, p_val_dict, lrs) end end -# Produces a dictionary with all parameters' values. Vertex parameters have their values converted to -# a sparse matrix (with one value for each edge, always using the source vertex's value) -function param_dict(vert_ps, edge_ps, lrs) - return merge(Dict(zip(vertex_parameters(lrs), vert_ps)), Dict(zip(edge_parameters(lrs), edge_ps))) +# 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{Real}, lrs::LatticeReactionSystem) + rates = filter(sr -> isequal(s, sr.species), lrs.spatial_reactions) + return sum(getfield.(rates, :rate)) end ### Accessing Unknown & Parameter Array Values ### - -# Converts a vector of vectors to a long vector. +# 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, num_verts) +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 their are num_species species). +# 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 indexes in the u array of all species in vertex vert (when there are num_species species). get_indexes(vert::Int64, num_species::Int64) = ((vert - 1) * num_species + 1):(vert * num_species) -# Returns the value of a parameter in an edge. For vertex parameters, uses their values in the source. -function get_edge_value(values::Vector{T}, edge) where {T} +# 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 -function get_edge_value(values::SparseMatrixCSC{T, Int64}, edge) where {T} +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 -# Returns the value of an initial condition of parameter in a vertex. -function get_vertex_value(values::Vector{T}, vert_idx) where {T} +# 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 - - - - - - - - -# Finds the transport rate of a parameter going from a source vertex to a destination vertex. -function get_transport_rate(transport_rate, edge::Pair{Int64,Int64}, t_rate_idx_types::Bool) +# 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 -# Finds the transportation rate for a specific species and a `LatticeTransportODEf` struct. +# Finds the transportation rate for a specific species, LatticeTransportODEf struct, and edge. function get_transport_rate(trans_s_idx::Int64, f_func::LatticeTransportODEf, edge::Pair{Int64,Int64}) get_transport_rate(f_func.transport_rates[trans_s_idx][2], edge, f_func.t_rate_idx_types[trans_s_idx]) end - - -# Updates the internal work_vert_ps vector for a given location. -# To this vector, we write the systems parameter values at a specific vertex. -function update_work_vert_ps!(work_vert_ps, vert_ps, comp, vert_ps_idx_types) +# Updates the internal work_vert_ps vector for a given vertex. +# To this vector, we write the system's parameter values at the specific vertex. +function update_work_vert_ps!(work_vert_ps::Vector{S}, all_ps::Vector{T}, vert::Int64, + vert_ps_idx_types::Vector{Bool}) where {S,T} # Loops through all parameters. for (idx,loc_type) in enumerate(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]) + work_vert_ps[idx] = (loc_type ? all_ps[idx][1] : all_ps[idx][vert]) end end -# Input is always either a LatticeTransportODEf or LatticeTransportODEjac function (which fields we then pass on). -function update_work_vert_ps!(lt_ode_func, vert_ps, comp) - return update_work_vert_ps!(lt_ode_func.work_vert_ps, vert_ps, comp, lt_ode_func.v_ps_idx_types) +# Input is either a LatticeTransportODEf or LatticeTransportODEjac function (which fields we pass on). +function update_work_vert_ps!(lt_ode_func, all_ps::Vector{T}, vert::Int64) where {T} + return update_work_vert_ps!(lt_ode_func.work_vert_ps, all_ps, vert, lt_ode_func.v_ps_idx_types) 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) +function matrix_expand_component_values(values::Vector{<:Vector}, n::Int64) reshape(expand_component_values(values, n), length(values), n) end diff --git a/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl b/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl index 992d864b20..74328d6979 100644 --- a/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl +++ b/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl @@ -23,7 +23,7 @@ let u0 = [:S => 990.0, :I => 20.0 * rand_v_vals(lrs.lattice), :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 @@ -38,7 +38,7 @@ let u0 = [:S => 990.0, :I => 20.0 * rand_v_vals(lrs.lattice), :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 @@ -53,7 +53,7 @@ let u0 = [:X => rand_v_vals(lrs.lattice, 10), :Y => rand_v_vals(lrs.lattice, 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 @@ -68,7 +68,7 @@ let u0 = [:X => rand_v_vals(lrs.lattice, 10), :Y => rand_v_vals(lrs.lattice, 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. @@ -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 @@ -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 @@ -156,7 +156,7 @@ let ] 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 @@ -182,7 +182,7 @@ let ] 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/spatial_modelling/lattice_reaction_systems_ODEs.jl b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl index 42cef58c28..aae6295254 100644 --- a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl +++ b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl @@ -303,7 +303,8 @@ let end u0 = 2 * rand(rng, 10000) - p = [1.0, 4.0, 0.1] + p_hw = [1.0, 4.0, 0.1] + p_aut = [[1.0], [4.0], sparse([1], [1], [0.1])] tspan = (0.0, 100.0) ofun_hw_dense = ODEFunction(spatial_brusselator_f; jac = spatial_brusselator_jac) @@ -323,10 +324,10 @@ let 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) + ofun_hw_dense(du_hw_dense, u0, p_hw, 0.0) + ofun_hw_sparse(du_hw_sparse, u0, p_hw, 0.0) + ofun_aut_dense(du_aut_dense, u0, p_aut, 0.0) + ofun_aut_sparse(du_aut_sparse, u0, p_aut, 0.0) @test isapprox(du_hw_dense, du_aut_dense) @test isapprox(du_hw_sparse, du_aut_sparse) From 17cba9e9a7a9ec19f18211548d3dcfdb4614195b Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 7 Feb 2024 08:57:24 -0500 Subject: [PATCH 16/47] up --- src/spatial_reaction_systems/utility.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index f1c2a6e4ed..8577affa4d 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -18,7 +18,7 @@ end function lattice_process_u0(u0_in, u0_syms::Vector{BasicSymbolic{Real}}, 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) + 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, From e7437f7969b11837b453c7d6e0868163560ac18c Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 7 Feb 2024 10:13:22 -0500 Subject: [PATCH 17/47] up --- src/spatial_reaction_systems/utility.jl | 4 +- .../lattice_reaction_systems_ODEs.jl | 32 +++---- .../lattice_reaction_systems_lattice_types.jl | 89 +++++++++++++++++++ 3 files changed, 107 insertions(+), 18 deletions(-) diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index 8577affa4d..6cfdafe580 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -64,7 +64,7 @@ function lattice_process_input(input::Dict{BasicSymbolic{Real}, T}, syms::Vector return [sym => input[sym] for sym in syms] end function lattice_process_input(input, syms::Vector{BasicSymbolic{Real}}) - if ((input isa Vector) || (input isa Vector)) && all(entry isa Pair for entry in input) + if ((input isa Vector) || (input isa Tuple)) && all(entry isa Pair for entry in input) return lattice_process_input(Dict(input), syms) end error("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).") @@ -95,7 +95,7 @@ function vertex_value_form(values, lrs::LatticeReactionSystem, sym::BasicSymboli # 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)) - vertex_value_form(values, lrs.num_verts, lrs.lattice, sym) + return vertex_value_form(values, lrs.num_verts, lrs.lattice, sym) end # For the case where the i'th value of the vector corresponds to the value in the i'th vertex. diff --git a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl index aae6295254..2acdd00e24 100644 --- a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl +++ b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl @@ -302,32 +302,32 @@ let return sparse(jac_prototype_pre) end - u0 = 2 * rand(rng, 10000) - p_hw = [1.0, 4.0, 0.1] - p_aut = [[1.0], [4.0], sparse([1], [1], [0.1])] + num_verts = 5000 + 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(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 + 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_hw, 0.0) - ofun_hw_sparse(du_hw_sparse, u0, p_hw, 0.0) - ofun_aut_dense(du_aut_dense, u0, p_aut, 0.0) - ofun_aut_sparse(du_aut_sparse, u0, p_aut, 0.0) + 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) @@ -339,8 +339,8 @@ let 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) + 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) diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl index c75d0d1c96..b3523523ab 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl @@ -6,6 +6,7 @@ using Catalyst, Graphs, OrdinaryDiffEq, Test # Fetch test networks. include("../spatial_test_networks.jl") + ### Run Tests ### # Test errors when attempting to create networks with dimension > 3. @@ -159,4 +160,92 @@ let 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 vertexes: (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 vertexes (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 sol[:,:] ≈ 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 sol[:,:] ≈ 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 sol[:,:] ≈ 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 sol[:,:] ≈ sol_base[13:end,:] + end end \ No newline at end of file From 48f27b84ab46ccf4de772dc023ef2c8d70768d0d Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 7 Feb 2024 20:28:53 -0500 Subject: [PATCH 18/47] Additional inner remake, add new functions for generating edge parameter values --- src/Catalyst.jl | 1 + .../lattice_reaction_systems.jl | 396 +++++++++++++----- .../spatial_ODE_systems.jl | 30 +- src/spatial_reaction_systems/utility.jl | 14 +- .../lattice_reaction_systems_ODEs.jl | 2 +- .../lattice_reaction_systems.jl | 72 ++++ .../lattice_reaction_systems_lattice_types.jl | 13 +- test/spatial_test_networks.jl | 4 +- 8 files changed, 391 insertions(+), 141 deletions(-) diff --git a/src/Catalyst.jl b/src/Catalyst.jl index 9f6da7d1d0..e235f2b40c 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -178,6 +178,7 @@ export LatticeReactionSystem export spatial_species, vertex_parameters, edge_parameters 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 # Various utility functions include("spatial_reaction_systems/utility.jl") diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index a525757753..01d60cce5e 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -1,18 +1,23 @@ +### 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 lattice. -# Adding the "<: MT.AbstractTimeDependentSystem" part messes up show, disabling me from creating LRSs. Should be fixed some time. +# Adding the "<: MT.AbstractTimeDependentSystem" part messes up show, disabling me from creating LRSs. Should be fixed sometime. struct LatticeReactionSystem{Q,R,S,T} # <: MT.AbstractTimeDependentSystem # Input values. - """The (non-spatial) reaction system within each vertexes.""" + """The (non-spatial) reaction system within each vertex.""" rs::ReactionSystem{Q} - """The spatial reactions defined between individual vertexes.""" + """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 vertexes (compartments).""" + """The number of vertices (compartments).""" num_verts::Int64 """The number of edges.""" num_edges::Int64 @@ -23,11 +28,11 @@ struct LatticeReactionSystem{Q,R,S,T} # <: MT.AbstractTimeDependentSystem spat_species::Vector{BasicSymbolic{Real}} """ All parameters related to the lattice reaction system - (both those whose values are tied to vertexes and edges). + (both those whose values are tied to vertices and edges). """ parameters::Vector{BasicSymbolic{Real}} """ - Parameters which values are tied to vertexes, + 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}} @@ -50,7 +55,7 @@ struct LatticeReactionSystem{Q,R,S,T} # <: MT.AbstractTimeDependentSystem error("The second argument must be a vector of AbstractSpatialReaction subtypes.") end - # Computes the species which are parts of spatial reactions. Also counts total number of + # 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}}[] @@ -72,7 +77,7 @@ struct LatticeReactionSystem{Q,R,S,T} # <: MT.AbstractTimeDependentSystem # Ensures the parameter order begins similarly to in the non-spatial ReactionSystem. ps = [parameters(rs); setdiff([edge_parameters; vertex_parameters], parameters(rs))] - # Checks that all spatial reactions are valid for this reactions system. + # 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, @@ -80,117 +85,137 @@ struct LatticeReactionSystem{Q,R,S,T} # <: MT.AbstractTimeDependentSystem end end -# Creates a LatticeReactionSystem from a CartesianGrid lattice (cartesian grid). -function LatticeReactionSystem(rs, srs, lattice_in::CartesianGridRej{S,T}; diagonal_connections=false) where {S,T} +# 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). +LatticeReactionSystem(rs, srs, lattice::SimpleGraph) = LatticeReactionSystem(rs, srs, DiGraph(lattice)) + +# 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. - (length(lattice_in.dims) > 3) && error("Grids of higher dimension than 3 is currently not supported.") + (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 + end + + return LatticeReactionSystem(rs, srs, lattice, num_verts, num_edges, edge_iterator) +end + + +### LatticeReactionSystem Helper Functions ### +# Note, most of these are specifically for (Cartesian or masked) grids, we call them `grid`, not `lattice`. - # Ensures that the matrix has a 3d form (used for intermediary computations only, - # the original is passed to the constructor). - lattice = CartesianGrid((lattice_in.dims..., fill(1, 3-length(lattice_in.dims))...)) +# 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 vertexes and edges. The `num_edges` count formula counts the number of internal, side, - # edge, and corner vertexes (on the grid). The number of edges from each depends on whether diagonal - # connections are allowed. The formula holds even if l, m, and/or n are 1. - l,m,n = lattice.dims - num_verts = l * m * n +# 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 vertexes. - ns*(2(l-2)*(m-2) + 2(l-2)*(n-2) + 2(m-2)*(n-2)) + # Edges from side vertexes. - ne*(4(l-2) + 4(m-2) + 4(n-2)) + # Edges from edge vertexes. - nc*8 # Edges from corner vertexes. - - # Creates an iterator over all edges. - # Currently creates a full vector. Future version might be for efficient. - edge_iterator = Vector{Pair{Int64,Int64}}(undef, num_edges) - # Loops through, simultaneously, the coordinates of each position in the grid, as well as that - # coordinate's (scalar) flat index. For each grid point, loops through all potential neighbours. - indices = [(L, M, N) for L in 1:l, M in 1:m, N in 1:n] - flat_indices = 1:num_verts - next_vert = 0 - for ((L, M, N), idx) in zip(indices, flat_indices) - for LL in max(L - 1, 1):min(L + 1, l), - MM in max(M - 1, 1):min(M + 1, m), - NN in max(N - 1, 1):min(N + 1, n) - - # Which (LL,MM,NN) indexes are valid neighbours depends on whether diagonal connects are permitted. - !diagonal_connections && (count([L==LL, M==MM, N==NN]) == 2) || continue - diagonal_connections && (L==LL) && (M==MM) && (N==NN) && continue - - # Computes the neighbour's flat (scalar) index. Add the edge to edge_iterator. - neighbour_idx = LL + (MM - 1) * l + (NN - 1) * m * l - edge_iterator[next_vert += 1] = (idx => neighbour_idx) - end + 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 + +# 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 - return LatticeReactionSystem(rs, srs, lattice_in, num_verts, num_edges, edge_iterator) +# 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 -# Creates a LatticeReactionSystem from a Boolean Array lattice (masked grid). -function LatticeReactionSystem(rs, srs, lattice_in::Array{Bool, T}; diagonal_connections=false) where {T} - # Error checks. - dims = size(lattice_in) - (length(dims) > 3) && error("Grids of higher dimension than 3 is currently not supported.") - - # Ensures that the matrix has a 3d form (used for intermediary computations only, - # the original is passed to the constructor). - lattice = reshape(lattice_in, [dims...; fill(1, 3-length(dims))]...) - - # Counts vertexes (edges have to be counted after the iterator have been created). - num_verts = count(lattice) - - - # Makes a template matrix to store each vertex's index. The matrix is 0 where there is no vertex. - # The template is used in the next step. - idx_matrix = fill(0, size(lattice_in)) - cur_vertex_idx = 0 - for flat_idx in 1:length(lattice) - if lattice[flat_idx] - idx_matrix[flat_idx] = (cur_vertex_idx += 1) - 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 - # Creates an iterator over all edges. A vector with pairs of each edge's source to its destination. - edge_iterator = Vector{Pair{Int64,Int64}}() - # Loops through, the coordinates of each position in the grid. - # For each grid point, loops through all potential neighbours and adds edges to edge_iterator. - l, m, n = size(lattice) - indices = [(L, M, N) for L in 1:l, M in 1:m, N in 1:n] - for (L, M, N) in indices - # Ensures that we are in a valid lattice point. - lattice[L,M,N] || continue - for LL in max(L - 1, 1):min(L + 1, l), - MM in max(M - 1, 1):min(M + 1, m), - NN in max(N - 1, 1):min(N + 1, n) - - # Ensures that the neighbour is a valid lattice point. - lattice[LL,MM,NN] || continue - - # Which (LL,MM,NN) indexes are valid neighbours depends on whether diagonal connects are permitted. - !diagonal_connections && (count([L==LL, M==MM, N==NN]) == 2) || continue - diagonal_connections && (L==LL) && (M==MM) && (N==NN) && continue - - # Computes the neighbour's scalar index. Add that connection to `edge_iterator`. - push!(edge_iterator, idx_matrix[L,M,N] => idx_matrix[LL,MM,NN]) - 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 - num_edges = length(edge_iterator) - return LatticeReactionSystem(rs, srs, lattice_in, num_verts, num_edges, edge_iterator) + # 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 -# 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) +# 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} + all(0 < g_idx <= dim_leng for (g_idx, dim_leng) in zip(grid_idx, g_size)) || return false + return (grid isa Array{Bool}) ? grid[grid_idx...] : true end -# Creates a LatticeReactionSystem from a (undirected) Graph lattice (graph grid). -LatticeReactionSystem(rs, srs, lattice::SimpleGraph) = LatticeReactionSystem(rs, srs, DiGraph(lattice)) +# 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). +get_grid_indices(grid::CartesianGridRej{N,T}) where {N,T} = CartesianIndices(ones(grid_size(grid))) +get_grid_indices(grid::Array{Bool, N}) where {N} = CartesianIndices(grid) -### Lattice ReactionSystem Getters ### + +### LatticeReactionSystem Getters ### # Get all species. species(lrs::LatticeReactionSystem) = unique([species(lrs.rs); lrs.spat_species]) @@ -199,14 +224,11 @@ spatial_species(lrs::LatticeReactionSystem) = lrs.spat_species # Get all parameters. ModelingToolkit.parameters(lrs::LatticeReactionSystem) = lrs.parameters -# Get all parameters whose values are tied to vertexes (compartments). +# Get all parameters whose values are tied to vertices (compartments). vertex_parameters(lrs::LatticeReactionSystem) = lrs.vertex_parameters # Get all parameters whose values are tied to edges (adjacencies). edge_parameters(lrs::LatticeReactionSystem) = lrs.edge_parameters -# Gets the lrs name (same as rs name). -ModelingToolkit.nameof(lrs::LatticeReactionSystem) = nameof(lrs.rs) - # Checks if a lattice reaction system is a pure (linear) transport reaction system. is_transport_system(lrs::LatticeReactionSystem) = all(sr -> sr isa TransportReaction, lrs.spatial_reactions) @@ -216,7 +238,7 @@ is_transport_system(lrs::LatticeReactionSystem) = all(sr -> sr isa TransportReac 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) = lrs.lattice isa CartesianGridRej{S,T} where {S,T} +has_cartesian_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa CartesianGridRej{N,T} where {N,T} """ has_masked_lattice(lrs::LatticeReactionSystem) @@ -224,7 +246,7 @@ has_cartesian_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa CartesianGri 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) = lrs.lattice isa Array{Bool, T} where T +has_masked_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa Array{Bool, N} where N """ has_grid_lattice(lrs::LatticeReactionSystem) @@ -247,11 +269,10 @@ has_graph_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa SimpleDiGraph 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. """ -function grid_size(lrs::LatticeReactionSystem) - has_cartesian_lattice(lrs) && (return lrs.lattice.dims) - has_masked_lattice(lrs) && (return size(lrs.lattice)) - error("Grid size is only defined for LatticeReactionSystems with grid-based lattices (not graph-based).") -end +grid_size(lrs::LatticeReactionSystem) = grid_size(lrs.lattice) +grid_size(lattice::CartesianGridRej{N,T}) where {N,T} = lattice.dims +grid_size(lattice::Array{Bool, N}) where {N} = size(lattice) +grid_size(lattice::Graph) = error("Grid size is only defined for LatticeReactionSystems with grid-based lattices (not graph-based).") """ grid_dims(lrs::LatticeReactionSystem) @@ -259,4 +280,151 @@ end 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) = length(grid_size(lrs)) \ No newline at end of file +grid_dims(lrs::LatticeReactionSystem) = grid_dims(lrs.lattice) +grid_dims(lattice::GridLattice{N,T}) where {N,T} = return N +grid_dims(lattice::Graph) = error("Grid dimensions is only defined for LatticeReactionSystems with grid-based lattices (not graph-based).") + + +### Generic Getters ### + +# Gets the lrs name (same as rs name). +ModelingToolkit.nameof(lrs::LatticeReactionSystem) = nameof(lrs.rs) + + +### 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(lrs.lattice, lrs.num_verts)[1] + values = spzeros(lrs.num_verts,lrs.num_verts) + + # Loops through all edges, and applies the value function to these. + for e in lrs.edge_iterator + # 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/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index 19b7b7e05e..98256e5e78 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -8,7 +8,7 @@ struct LatticeTransportODEf{S,T} num_verts::Int64 """The number of species.""" num_species::Int64 - """The values of the parameters that are tied to vertexes.""" + """The values of the parameters that are tied to vertices.""" vert_ps::Vector{Vector{T}} """ Vector for storing temporary values. Repeatedly during simulations, we need to retrieve the @@ -41,7 +41,7 @@ struct LatticeTransportODEf{S,T} """ t_rate_idx_types::Vector{Bool} """ - A matrix, NxM, where N is the number of species with transportation and M is the number of vertexes. + 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). """ @@ -86,7 +86,7 @@ struct LatticeTransportODEjac{R,S,T} num_verts::Int64 """The number of species.""" num_species::Int64 - """The values of the parameters that are tied to vertexes.""" + """The values of the parameters that are tied to vertices.""" vert_ps::Vector{Vector{S}} """ Vector for storing temporary values. Repeatedly during simulations, we need to retrieve the @@ -140,7 +140,7 @@ function DiffEqBase.ODEProblem(lrs::LatticeReactionSystem, u0_in, tspan, 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 vertexes. + # 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) # vert_ps and `edge_ps` are vector maps, taking each parameter's Symbolics representation to its value(s). @@ -212,24 +212,24 @@ end 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 indexes of both the transport species, + # 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)) i_idxs = Vector{Int}(undef, num_entries) j_idxs = Vector{Int}(undef, num_entries) - # Indexes of elements caused by non-spatial dynamics. + # Indices of elements caused by non-spatial dynamics. for vert in 1:lrs.num_verts for n in 1:length(ns_i_idxs) i_idxs[idx] = get_index(vert, ns_i_idxs[n], lrs.num_species) @@ -238,7 +238,7 @@ function build_jac_prototype(ns_jac_prototype::SparseMatrixCSC{Float64, Int64}, end end - # Indexes of elements caused by spatial dynamics. + # Indices of elements caused by spatial dynamics. for e in lrs.edge_iterator # 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 @@ -281,7 +281,7 @@ end function (f_func::LatticeTransportODEf)(du, u, p, t) # Updates for non-spatial reactions. for vert_i in 1:(f_func.num_verts) - # Gets the indexes of all the species at vertex i. + # Gets the indices of all the species at vertex i. idxs = get_indexes(vert_i, f_func.num_species) # Updates the work vector to contain the vertex parameter values for vertex vert_i. @@ -294,15 +294,15 @@ function (f_func::LatticeTransportODEf)(du, u, p, t) # 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(f_func.transport_rates) - # Rate for leaving source vertex vert_i. - for vert_i in 1:(f_func.num_verts) - idx_src = get_index(vert_i, s, f_func.num_species) + # Rate for leaving source vertex vert_i. + for vert_i in 1:(f_func.num_verts) + idx_src = get_index(vert_i, s, f_func.num_species) du[idx_src] -= f_func.leaving_rates[s_idx, vert_i] * u[idx_src] end # Add rates for entering a destination vertex via an incoming edge. - for e in f_func.edge_iterator + for e in f_func.edge_iterator idx_src = get_index(e[1], s, f_func.num_species) - idx_dst = get_index(e[2], s, f_func.num_species) + idx_dst = get_index(e[2], s, f_func.num_species) du[idx_dst] += get_transport_rate(s_idx, f_func, e) * u[idx_src] end end diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index 6cfdafe580..f5b68091ad 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -23,7 +23,7 @@ function lattice_process_u0(u0_in, u0_syms::Vector{BasicSymbolic{Real}}, lrs::La # 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 vertexes (storing one value for each). + # 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). @@ -101,7 +101,7 @@ function vertex_value_form(values, lrs::LatticeReactionSystem, sym::BasicSymboli # 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) != lrs.num_verts) - error("You have provided ($(length(values))) values for $sym. This is not equal to the number of vertexes ($(lrs.num_verts)).") + error("You have provided ($(length(values))) values for $sym. This is not equal to the number of vertices ($(lrs.num_verts)).") end return values end @@ -111,13 +111,13 @@ function vertex_value_form(values, lrs::LatticeReactionSystem, sym::BasicSymboli end # Converts values to the correct vector form for a Cartesian grid lattice. -function vertex_value_form(values::AbstractArray, num_verts::Int64, lattice::CartesianGridRej{S,T}, - sym::BasicSymbolic{Real}) where {S,T} +function vertex_value_form(values::AbstractArray, num_verts::Int64, lattice::CartesianGridRej{N,T}, + sym::BasicSymbolic{Real}) where {N,T} if size(values) != lattice.dims error("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 if (length(values) != num_verts) - error("You have provided ($(length(values))) values for $sym. This is not equal to the number of vertexes ($(num_verts)).") + error("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 @@ -140,7 +140,7 @@ function vertex_value_form(values::AbstractArray, num_verts::Int64, lattice::Arr # Checks that the correct number of values was provided, and returns the values. if (length(return_values) != num_verts) - error("You have provided ($(length(return_values))) values for $sym. This is not equal to the number of vertexes ($(num_verts)).") + error("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 @@ -242,7 +242,7 @@ 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 there are num_species species). +# Gets the indices in the u array of all species in vertex vert (when there are num_species species). get_indexes(vert::Int64, num_species::Int64) = ((vert - 1) * num_species + 1):(vert * num_species) # Returns the value of a parameter in an edge. For vertex parameters, use their values in the source. diff --git a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl index 2acdd00e24..a2052ffb5f 100644 --- a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl +++ b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl @@ -386,7 +386,7 @@ let end # 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 vertexes are `true`). +# 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)] diff --git a/test/spatial_reaction_systems/lattice_reaction_systems.jl b/test/spatial_reaction_systems/lattice_reaction_systems.jl index ed29142640..1a729563c3 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems.jl @@ -324,3 +324,75 @@ let end end +### Tests Edge Value Computation Helper Functions ### + +# Checks that all species ends up in the correct place in 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, 10000.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())[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())[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())[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())[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())[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())[end][125], n^3, rtol=1e-6) +end + +# 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_reaction_systems/lattice_reaction_systems_lattice_types.jl b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl index b3523523ab..0f30979dfc 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl @@ -51,6 +51,15 @@ let @test grid_dims(masked_2d_lrs) == 2 @test grid_dims(masked_3d_lrs) == 3 @test_throws Exception 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 Exception grid_size(graph_lrs) end # Checks grid dimensions for 2d and 3d grids where some dimension is equal to 1. @@ -162,9 +171,9 @@ let end end -# For a system which is a single ine of vertexes: (O-O-O-O-X-O-O-O), ensures that different simulations +# 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 vertexes (7), +# 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. diff --git a/test/spatial_test_networks.jl b/test/spatial_test_networks.jl index ef1836958e..2dc1b6dea4 100644 --- a/test/spatial_test_networks.jl +++ b/test/spatial_test_networks.jl @@ -13,10 +13,10 @@ rand_v_vals(lrs::LatticeReactionSystem) = rand_v_vals(lrs.lattice) function rand_v_vals(grid::DiGraph) return rand(rng, nv(grid)) end -function rand_v_vals(grid::Catalyst.CartesianGridRej{S,T}) where {S,T} +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, T}) where {T} +function rand_v_vals(grid::Array{Bool, N}) where {N} return rand(rng, size(grid)) end From 944a41e0c97c80c5e432212e7fa7ab4ca6e9136f Mon Sep 17 00:00:00 2001 From: Torkel Date: Thu, 8 Feb 2024 10:25:37 -0500 Subject: [PATCH 19/47] up --- .../lattice_reaction_systems.jl | 27 ++++++++++++++++++- test/spatial_test_networks.jl | 8 +++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/test/spatial_reaction_systems/lattice_reaction_systems.jl b/test/spatial_reaction_systems/lattice_reaction_systems.jl index 1a729563c3..8d08bfdaa2 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems.jl @@ -1,7 +1,7 @@ ### Preparations ### # Fetch packages. -using Catalyst, Graphs, Test +using Catalyst, Graphs, OrdinaryDiffEq, Test # Fetch test networks. include("../spatial_test_networks.jl") @@ -326,6 +326,31 @@ end ### Tests Edge Value Computation Helper Functions ### +# Checks that computes 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 + 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(lrs.lattice, lrs.num_verts)[1] + edge_values = make_edge_p_values(lrs, make_edge_p_value) + + for e in lrs.edge_iterator + @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 ends up in the correct place in in a pure flow system (checking various dimensions). let # Prepares a system with a single species which is transported only. diff --git a/test/spatial_test_networks.jl b/test/spatial_test_networks.jl index 2dc1b6dea4..3b87b6de60 100644 --- a/test/spatial_test_networks.jl +++ b/test/spatial_test_networks.jl @@ -24,7 +24,7 @@ rand_e_vals(grid, x::Number) = rand_e_vals(grid) * x function rand_e_vals(lrs::LatticeReactionSystem) e_vals = spzeros(lrs.num_verts, lrs.num_verts) for e in lrs.edge_iterator - e_vals[e[1], e[2]] = rand() + e_vals[e[1], e[2]] = rand(rng) end return e_vals end @@ -201,9 +201,9 @@ 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([true, true, true, false], 10) -random_2d_masked_grid = rand([true, true, true, false], 10, 10) -random_3d_masked_grid = rand([true, true, true, false], 10, 10, 10) +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_2d_graph_grid = Graphs.grid([2, 2]) From 475a95e0a0066635f9e900b440e527f56b82e4d6 Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 19 Apr 2024 10:57:48 -0400 Subject: [PATCH 20/47] save progress --- .../lattice_reaction_systems.jl | 72 ++++++++++++++++--- 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index 01d60cce5e..ef9a9584dd 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -6,8 +6,7 @@ const GridLattice{N,T} = Union{Array{Bool, N}, CartesianGridRej{N,T}} ### Lattice Reaction Network Structure ### # Describes a spatial reaction network over a lattice. -# Adding the "<: MT.AbstractTimeDependentSystem" part messes up show, disabling me from creating LRSs. Should be fixed sometime. -struct LatticeReactionSystem{Q,R,S,T} # <: MT.AbstractTimeDependentSystem +struct LatticeReactionSystem{Q,R,S,T} <: MT.AbstractTimeDependentSystem # Input values. """The (non-spatial) reaction system within each vertex.""" rs::ReactionSystem{Q} @@ -54,6 +53,21 @@ struct LatticeReactionSystem{Q,R,S,T} # <: MT.AbstractTimeDependentSystem if !(R <: AbstractSpatialReaction) error("The second argument must be a vector of AbstractSpatialReaction subtypes.") end + if !isempty(MT.get_systems(rs)) + error("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(states(rs)) + error("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)) + error("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)) + @warn "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. @@ -215,15 +229,11 @@ get_grid_indices(grid::CartesianGridRej{N,T}) where {N,T} = CartesianIndices(one get_grid_indices(grid::Array{Bool, N}) where {N} = CartesianIndices(grid) -### LatticeReactionSystem Getters ### +### LatticeReactionSystem-specific Getters ### -# 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 -# Get all parameters. -ModelingToolkit.parameters(lrs::LatticeReactionSystem) = lrs.parameters # Get all parameters whose values are tied to vertices (compartments). vertex_parameters(lrs::LatticeReactionSystem) = lrs.vertex_parameters # Get all parameters whose values are tied to edges (adjacencies). @@ -285,11 +295,53 @@ grid_dims(lattice::GridLattice{N,T}) where {N,T} = return N grid_dims(lattice::Graph) = error("Grid dimensions is only defined for LatticeReactionSystems with grid-based lattices (not graph-based).") -### Generic Getters ### +### Catalyst-based Getters ### + +# Get all species. +species(lrs::LatticeReactionSystem) = unique([species(lrs.rs); lrs.spat_species]) + +# Generic ones (simply forwards call to the non-spatial system). +reactions(lrs::LatticeReactionSystem) = reactions(lrs.rs) + +### ModelingToolkit-based Getters ### + +# Get all parameters. +MT.parameters(lrs::LatticeReactionSystem) = lrs.parameters + +# Generic ones (simply forwards call to the non-spatial system). +MT.nameof(lrs::LatticeReactionSystem) = MT.nameof(lrs.rs) +MT.get_iv(lrs::LatticeReactionSystem) = MT.get_iv(lrs.rs) +MT.equations(lrs::LatticeReactionSystem) = MT.equations(lrs.rs) +MT.equations(lrs::LatticeReactionSystem) = MT.equations(lrs.rs) +MT.states(lrs::LatticeReactionSystem) = MT.states(lrs.rs) +#MT.unknowns(lrs::LatticeReactionSystem) = MT.unknowns(lrs.rs) +MT.parameters(lrs::LatticeReactionSystem) = MT.parameters(lrs.rs) +MT.get_metadata(lrs::LatticeReactionSystem) = MT.get_metadata(lrs.rs) + +# 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) + error("The `get_eqs` function is primarily relevant for composed models. LatticeReactionSystems cannot be composed, and hence this function should not be used. Please consider using `equations` instead.") + # MT.get_eqs(lrs.rs) +end +function MT.get_states(lrs::LatticeReactionSystem) + error("The `get_unknowns` is primarily relevant for composed models. LatticeReactionSystems cannot be composed, and hence this function should not be used. Please consider using `unknowns` instead.") + # MT.get_states(lrs.rs) +end +function MT.get_ps(lrs::LatticeReactionSystem) + error("The `get_ps` function is primarily relevant for composed models. LatticeReactionSystems cannot be composed, and hence this function should not be used. Please consider using `parameters` instead.") + # MT.get_ps(lrs.rs) +end -# Gets the lrs name (same as rs name). -ModelingToolkit.nameof(lrs::LatticeReactionSystem) = nameof(lrs.rs) +# 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) + error("LatticeReactionSystems are used to model a spatial systems on a discrete lattice. The `independent_variables` function is used to retrieve the independent variables of systems with time and space independent variables. LatticeReactionSystems can only have a single independent variable (the time one). If you want to retrieve this one, please consider using the `get_iv` function.)") +end ### Edge Parameter Value Generators ### From f4bcceebc7af6f779919f23ac0233d08cf03fbe1 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 29 Apr 2024 14:58:34 -0400 Subject: [PATCH 21/47] up --- .../lattice_reaction_systems.jl | 80 ++++++++++--------- .../spatial_ODE_systems.jl | 38 ++++----- src/spatial_reaction_systems/utility.jl | 24 +++--- .../lattice_reaction_systems_ODEs.jl | 30 +++---- .../lattice_reaction_systems.jl | 10 +-- .../lattice_reaction_systems_lattice_types.jl | 44 +++++----- test/spatial_test_networks.jl | 8 +- 7 files changed, 122 insertions(+), 112 deletions(-) diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index ef9a9584dd..c7f7974478 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -9,7 +9,7 @@ const GridLattice{N,T} = Union{Array{Bool, N}, CartesianGridRej{N,T}} struct LatticeReactionSystem{Q,R,S,T} <: MT.AbstractTimeDependentSystem # Input values. """The (non-spatial) reaction system within each vertex.""" - rs::ReactionSystem{Q} + reactionsystem::ReactionSystem{Q} """The spatial reactions defined between individual vertices.""" spatial_reactions::Vector{R} """The lattice on which the (discrete) spatial system is defined.""" @@ -24,7 +24,7 @@ struct LatticeReactionSystem{Q,R,S,T} <: MT.AbstractTimeDependentSystem num_species::Int64 """List of species that may move spatially.""" - spat_species::Vector{BasicSymbolic{Real}} + spatial_species::Vector{BasicSymbolic{Real}} """ All parameters related to the lattice reaction system (both those whose values are tied to vertices and edges). @@ -231,16 +231,27 @@ get_grid_indices(grid::Array{Bool, N}) where {N} = CartesianIndices(grid) ### LatticeReactionSystem-specific Getters ### -# Get all species that may be transported. -spatial_species(lrs::LatticeReactionSystem) = lrs.spat_species - -# Get all parameters whose values are tied to vertices (compartments). -vertex_parameters(lrs::LatticeReactionSystem) = lrs.vertex_parameters -# Get all parameters whose values are tied to edges (adjacencies). -edge_parameters(lrs::LatticeReactionSystem) = lrs.edge_parameters +# 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) + +# Non-trivial getters. +""" + is_transport_system(lrs::LatticeReactionSystem) -# Checks if a lattice reaction system is a pure (linear) transport reaction system. -is_transport_system(lrs::LatticeReactionSystem) = all(sr -> sr isa TransportReaction, lrs.spatial_reactions) +Returns `true` if all spatial reactions in `lrs` are `TransportReaction`s. +""" +is_transport_system(lrs::LatticeReactionSystem) = all(sr -> sr isa TransportReaction, spatial_reactions(lrs)) """ has_cartesian_lattice(lrs::LatticeReactionSystem) @@ -248,7 +259,7 @@ is_transport_system(lrs::LatticeReactionSystem) = all(sr -> sr isa TransportReac 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) = lrs.lattice isa CartesianGridRej{N,T} where {N,T} +has_cartesian_lattice(lrs::LatticeReactionSystem) = lattice(lrs) isa CartesianGridRej{N,T} where {N,T} """ has_masked_lattice(lrs::LatticeReactionSystem) @@ -256,7 +267,7 @@ has_cartesian_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa CartesianGri 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) = lrs.lattice isa Array{Bool, N} where N +has_masked_lattice(lrs::LatticeReactionSystem) = lattice(lrs) isa Array{Bool, N} where N """ has_grid_lattice(lrs::LatticeReactionSystem) @@ -271,7 +282,7 @@ has_grid_lattice(lrs::LatticeReactionSystem) = (has_cartesian_lattice(lrs) || ha 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) = lrs.lattice isa SimpleDiGraph +has_graph_lattice(lrs::LatticeReactionSystem) = lattice(lrs) isa SimpleDiGraph """ grid_size(lrs::LatticeReactionSystem) @@ -279,7 +290,7 @@ has_graph_lattice(lrs::LatticeReactionSystem) = lrs.lattice isa SimpleDiGraph 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(lrs.lattice) +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) grid_size(lattice::Graph) = error("Grid size is only defined for LatticeReactionSystems with grid-based lattices (not graph-based).") @@ -290,7 +301,7 @@ grid_size(lattice::Graph) = error("Grid size is only defined for LatticeReaction 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(lrs.lattice) +grid_dims(lrs::LatticeReactionSystem) = grid_dims(lattice(lrs)) grid_dims(lattice::GridLattice{N,T}) where {N,T} = return N grid_dims(lattice::Graph) = error("Grid dimensions is only defined for LatticeReactionSystems with grid-based lattices (not graph-based).") @@ -298,39 +309,36 @@ grid_dims(lattice::Graph) = error("Grid dimensions is only defined for LatticeRe ### Catalyst-based Getters ### # Get all species. -species(lrs::LatticeReactionSystem) = unique([species(lrs.rs); lrs.spat_species]) +species(lrs::LatticeReactionSystem) = unique([species(reactionsystem(lrs)); spatial_species(lrs)]) # Generic ones (simply forwards call to the non-spatial system). -reactions(lrs::LatticeReactionSystem) = reactions(lrs.rs) +reactions(lrs::LatticeReactionSystem) = reactions(reactionsystem(lrs)) ### ModelingToolkit-based Getters ### -# Get all parameters. -MT.parameters(lrs::LatticeReactionSystem) = lrs.parameters - -# Generic ones (simply forwards call to the non-spatial system). -MT.nameof(lrs::LatticeReactionSystem) = MT.nameof(lrs.rs) -MT.get_iv(lrs::LatticeReactionSystem) = MT.get_iv(lrs.rs) -MT.equations(lrs::LatticeReactionSystem) = MT.equations(lrs.rs) -MT.equations(lrs::LatticeReactionSystem) = MT.equations(lrs.rs) -MT.states(lrs::LatticeReactionSystem) = MT.states(lrs.rs) -#MT.unknowns(lrs::LatticeReactionSystem) = MT.unknowns(lrs.rs) -MT.parameters(lrs::LatticeReactionSystem) = MT.parameters(lrs.rs) -MT.get_metadata(lrs::LatticeReactionSystem) = MT.get_metadata(lrs.rs) +# 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.equations(lrs::LatticeReactionSystem) = MT.equations(reactionsystem(lrs)) +MT.states(lrs::LatticeReactionSystem) = MT.states(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) error("The `get_eqs` function is primarily relevant for composed models. LatticeReactionSystems cannot be composed, and hence this function should not be used. Please consider using `equations` instead.") - # MT.get_eqs(lrs.rs) + # MT.get_eqs(reactionsystem(lrs)) end function MT.get_states(lrs::LatticeReactionSystem) error("The `get_unknowns` is primarily relevant for composed models. LatticeReactionSystems cannot be composed, and hence this function should not be used. Please consider using `unknowns` instead.") - # MT.get_states(lrs.rs) + # MT.get_states(reactionsystem(lrs)) end function MT.get_ps(lrs::LatticeReactionSystem) error("The `get_ps` function is primarily relevant for composed models. LatticeReactionSystems cannot be composed, and hence this function should not be used. Please consider using `parameters` instead.") - # MT.get_ps(lrs.rs) + # MT.get_ps(reactionsystem(lrs)) end # Technically should not be used, but has to be declared for the `show` function to work. @@ -393,11 +401,11 @@ function make_edge_p_values(lrs::LatticeReactionSystem, make_edge_p_value::Funct end # Makes the flat to index grid converts. Predeclared the edge parameter value sparse matrix. - flat_to_grid_idx = get_index_converters(lrs.lattice, lrs.num_verts)[1] - values = spzeros(lrs.num_verts,lrs.num_verts) + 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 lrs.edge_iterator + 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() diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index 98256e5e78..6d048c93d2 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -62,9 +62,9 @@ struct LatticeTransportODEf{S,T} vert_ps = [vp[2] for vp in vert_ps] # Computes the leaving rate matrix. - leaving_rates = zeros(length(transport_rates), lrs.num_verts) + leaving_rates = zeros(length(transport_rates), num_verts(lrs)) for (s_idx, tr_pair) in enumerate(transport_rates) - for e in lrs.edge_iterator + 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 @@ -72,8 +72,8 @@ struct LatticeTransportODEf{S,T} # Declares `work_vert_ps` (used as storage during computation) and the edge iterator. work_vert_ps = zeros(length(vert_ps)) - edge_iterator = lrs.edge_iterator - new{S,T}(ofunc, lrs.num_verts, lrs.num_species, vert_ps, work_vert_ps, + edge_iterator = Catalyst.edge_iterator(lrs) + new{S,T}(ofunc, num_verts(lrs), num_species(lrs), vert_ps, work_vert_ps, v_ps_idx_types, transport_rates, t_rate_idx_types, leaving_rates, edge_iterator) end end @@ -115,9 +115,9 @@ struct LatticeTransportODEjac{R,S,T} # its values only and put them into `vert_ps`. vert_ps = [vp[2] for vp in vert_ps] - work_vert_ps = zeros(lrs.num_verts) + work_vert_ps = zeros(num_verts(lrs)) v_ps_idx_types = map(vp -> length(vp) == 1, vert_ps) - new{R,S,typeof(jac_transport)}(ofunc, lrs.num_verts, lrs.num_species, vert_ps, + new{R,S,typeof(jac_transport)}(ofunc, num_verts(lrs), num_species(lrs) , vert_ps, work_vert_ps, v_ps_idx_types, sparse, jac_transport) end end @@ -129,7 +129,7 @@ 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.") @@ -224,34 +224,34 @@ function build_jac_prototype(ns_jac_prototype::SparseMatrixCSC{Float64, Int64}, # 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) # Indices of elements caused by non-spatial dynamics. - for vert in 1:lrs.num_verts + 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 # Indices of elements caused by spatial dynamics. - for e in lrs.edge_iterator + 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[1], 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 destination vertex. for s_idx in trans_species - i_idxs[idx] = get_index(e[1], s_idx, lrs.num_species) - j_idxs[idx] = get_index(e[2], 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 @@ -261,9 +261,9 @@ function build_jac_prototype(ns_jac_prototype::SparseMatrixCSC{Float64, Int64}, # Set element values. if set_nonzero - for (s, rates) in transport_rates, e in lrs.edge_iterator - idx_src = get_index(e[1], s, lrs.num_species) - idx_dst = get_index(e[2], s, lrs.num_species) + 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 leaving source vertex. diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index f5b68091ad..71caf6f55a 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -27,7 +27,7 @@ function lattice_process_u0(u0_in, u0_syms::Vector{BasicSymbolic{Real}}, lrs::La 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], lrs.num_verts) + return expand_component_values([entry[2] for entry in u0], num_verts(lrs)) end # From a parameter input, split it into vertex parameters and edge parameters. @@ -95,19 +95,19 @@ function vertex_value_form(values, lrs::LatticeReactionSystem, sym::BasicSymboli # 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, lrs.num_verts, lrs.lattice, sym) + 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) != lrs.num_verts) - error("You have provided ($(length(values))) values for $sym. This is not equal to the number of vertices ($(lrs.num_verts)).") + if (length(values) != num_verts(lrs)) + error("You have provided ($(length(values))) values for $sym. This is not equal to the number of vertices ($(num_verts(lrs))).") end return values end # (2d and 3d) Cartesian and masked grids can take non-vector, non-scalar, values input. - return vertex_value_form(values, lrs.num_verts, lrs.lattice, sym) + return vertex_value_form(values, num_verts(lrs), lattice(lrs), sym) end # Converts values to the correct vector form for a Cartesian grid lattice. @@ -158,10 +158,10 @@ function edge_value_form(values, lrs::LatticeReactionSystem, sym) (values isa SparseMatrixCSC) || (return sparse([1], [1], [values])) # Error checks. - if nnz(values) != lrs.num_edges - error("You have provided ($(nnz(values))) values for $sym. This is not equal to the number of edges ($(lrs.num_edges)).") + if nnz(values) != num_edges(lrs) + error("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 lrs.edge_iterator) + if !all(Base.isstored(values, e[1], e[2]) for e in edge_iterator(lrs)) error("Values was not provided for some edges for edge parameter $sym.") end @@ -184,7 +184,7 @@ function make_sidxs_to_transrate_map(vert_ps::Vector{Pair{BasicSymbolic{Real},Ve # 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(lrs.rs)[spat_rates[1]] => spat_rates[2] for spat_rates in transport_rates_speciesmap + speciesmap(reactionsystem(lrs))[spat_rates[1]] => spat_rates[2] for spat_rates in transport_rates_speciesmap ] end @@ -215,8 +215,8 @@ function compute_transport_rates(s::BasicSymbolic{Real}, p_val_dict, lrs::Lattic 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 - transport_rates = spzeros(lrs.num_verts, lrs.num_verts) - for e in lrs.edge_iterator + 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 @@ -228,7 +228,7 @@ end # (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{Real}, lrs::LatticeReactionSystem) - rates = filter(sr -> isequal(s, sr.species), lrs.spatial_reactions) + rates = filter(sr -> isequal(s, sr.species), spatial_reactions(lrs)) return sum(getfield.(rates, :rate)) end diff --git a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl index a2052ffb5f..7c62bc7892 100644 --- a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl +++ b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl @@ -23,18 +23,18 @@ for grid in [small_2d_graph_grid, short_path, small_directed_cycle, 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_2 = [:S => 500.0 .+ 500.0 * rand_v_vals(lattice(lrs)), :I => 1.0, :R => 0.0] u0_3 = [ - :S => 500.0 .+ 500.0 * rand_v_vals(lrs.lattice), - :I => 50 * rand_v_vals(lrs.lattice), - :R => 50 * rand_v_vals(lrs.lattice), + :S => 500.0 .+ 500.0 * rand_v_vals(lattice(lrs)), + :I => 50 * rand_v_vals(lattice(lrs)), + :R => 50 * rand_v_vals(lattice(lrs)), ] for u0 in [u0_1, u0_2, u0_3] p1 = [:α => 0.1 / 1000, :β => 0.01] - p2 = [:α => 0.1 / 1000, :β => 0.02 * rand_v_vals(lrs.lattice)] + p2 = [:α => 0.1 / 1000, :β => 0.02 * rand_v_vals(lattice(lrs))] p3 = [ - :α => 0.1 / 2000 * rand_v_vals(lrs.lattice), - :β => 0.02 * rand_v_vals(lrs.lattice), + :α => 0.1 / 2000 * rand_v_vals(lattice(lrs)), + :β => 0.02 * rand_v_vals(lattice(lrs)), ] for pV in [p1, p2, p3] pE_1 = map(sp -> sp => 0.01, spatial_param_syms(lrs)) @@ -57,14 +57,14 @@ for grid in [small_2d_graph_grid, short_path, small_directed_cycle, 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_2 = [:X => rand_v_vals(lattice(lrs), 10.0), :Y => 2.0] + u0_3 = [:X => rand_v_vals(lattice(lrs), 20), :Y => rand_v_vals(lattice(lrs), 10)] for u0 in [u0_1, u0_2, u0_3] p1 = [:A => 1.0, :B => 4.0] - p2 = [:A => 0.5 .+ rand_v_vals(lrs.lattice, 0.5), :B => 4.0] + p2 = [:A => 0.5 .+ rand_v_vals(lattice(lrs), 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), + :A => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), + :B => 4.0 .+ rand_v_vals(lattice(lrs), 1.0), ] for pV in [p1, p2, p3] pE_1 = map(sp -> sp => 0.2, spatial_param_syms(lrs)) @@ -194,8 +194,8 @@ 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), + :X => 1.0 .+ rand_v_vals(lattice(lrs)), + :Y => 2.0 * rand_v_vals(lattice(lrs)), :XY => 0.5 ] oprob = ODEProblem(lrs, u0, (0.0, 1000.0), binding_p; tstops = 0.1:0.1:1000.0) @@ -209,7 +209,7 @@ end # Checks that various combinations of jac and sparse gives the same result. let lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_2d_graph_grid) - u0 = [:X => rand_v_vals(lrs.lattice, 10), :Y => rand_v_vals(lrs.lattice, 10)] + u0 = [:X => rand_v_vals(lattice(lrs), 10), :Y => rand_v_vals(lattice(lrs), 10)] pV = brusselator_p pE = [:dX => 0.2] oprob = ODEProblem(lrs, u0, (0.0, 50.0), [pV; pE]; jac = false, sparse = false) diff --git a/test/spatial_reaction_systems/lattice_reaction_systems.jl b/test/spatial_reaction_systems/lattice_reaction_systems.jl index 8d08bfdaa2..b2823b3111 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems.jl @@ -313,14 +313,14 @@ let 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 lrs1.num_edges == iterator_count(lrs1.edge_iterator) - @test lrs2.num_edges == iterator_count(lrs2.edge_iterator) + @test lrs1.num_edges == iterator_count(edge_iterator(lrs1)) + @test lrs2.num_edges == 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 lrs1.num_edges == iterator_count(lrs1.edge_iterator) + @test lrs1.num_edges == iterator_count(edge_iterator(lrs1)) end end @@ -342,10 +342,10 @@ let 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(lrs.lattice, lrs.num_verts)[1] + 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) - for e in lrs.edge_iterator + 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 diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl index 0f30979dfc..de77d54d5f 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl @@ -94,26 +94,26 @@ let graph_lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, graph_grid) # Check internal structures. - @test cartesian_lrs.rs == masked_lrs.rs == graph_lrs.rs - @test cartesian_lrs.spatial_reactions == masked_lrs.spatial_reactions == graph_lrs.spatial_reactions - @test cartesian_lrs.num_verts == masked_lrs.num_verts == graph_lrs.num_verts - @test cartesian_lrs.num_edges == masked_lrs.num_edges == graph_lrs.num_edges - @test cartesian_lrs.num_species == masked_lrs.num_species == graph_lrs.num_species - @test isequal(cartesian_lrs.spat_species, masked_lrs.spat_species) - @test isequal(masked_lrs.spat_species, graph_lrs.spat_species) - @test isequal(cartesian_lrs.parameters, masked_lrs.parameters) - @test isequal(masked_lrs.parameters, graph_lrs.parameters) - @test isequal(cartesian_lrs.vertex_parameters, masked_lrs.vertex_parameters) - @test isequal(masked_lrs.edge_parameters, graph_lrs.edge_parameters) - @test issetequal(cartesian_lrs.edge_iterator, masked_lrs.edge_iterator) - @test issetequal(masked_lrs.edge_iterator, graph_lrs.edge_iterator) + @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(cartesian_lrs.num_verts) + 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(cartesian_lrs.num_verts) + 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] @@ -148,9 +148,9 @@ let graph_lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, graph_grid) # Check internal structures. - @test masked_lrs.num_verts == graph_lrs.num_verts - @test masked_lrs.num_edges == graph_lrs.num_edges - @test issetequal(masked_lrs.edge_iterator, graph_lrs.edge_iterator) + @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] @@ -230,14 +230,14 @@ let 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 sol[:,:] ≈ sol_base[1:12,:] + @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 sol[:,:] ≈ sol_base[1:12,:] + @test hcat(sol.u...) ≈ sol_base[1:12,:] end # Checks simulations for the second Cartesian grids (covering vertices 6 to 8). @@ -248,13 +248,13 @@ let 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 sol[:,:] ≈ sol_base[13:end,:] + @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 sol[:,:] ≈ sol_base[13:end,:] + @test hcat(sol.u...) ≈ sol_base[13:end,:] end end \ No newline at end of file diff --git a/test/spatial_test_networks.jl b/test/spatial_test_networks.jl index 3b87b6de60..bb106c9ee6 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 @@ -9,7 +11,7 @@ rng = StableRNG(12345) # Generates randomised initial condition or parameter values. rand_v_vals(grid, x::Number) = rand_v_vals(grid) * x -rand_v_vals(lrs::LatticeReactionSystem) = rand_v_vals(lrs.lattice) +rand_v_vals(lrs::LatticeReactionSystem) = rand_v_vals(lattice(lrs)) function rand_v_vals(grid::DiGraph) return rand(rng, nv(grid)) end @@ -22,8 +24,8 @@ end rand_e_vals(grid, x::Number) = rand_e_vals(grid) * x function rand_e_vals(lrs::LatticeReactionSystem) - e_vals = spzeros(lrs.num_verts, lrs.num_verts) - for e in lrs.edge_iterator + 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 From 56df4cb17c208ceb5977662c5fa343da058f2b1d Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 31 May 2024 17:31:29 -0400 Subject: [PATCH 22/47] rebase fix --- .../catalyst_for_new_julia_users.md | 10 ---------- src/Catalyst.jl | 3 +-- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md b/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md index 3c3a529dfd..82c297b33d 100644 --- a/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md +++ b/docs/src/introduction_to_catalyst/catalyst_for_new_julia_users.md @@ -54,19 +54,9 @@ To import a Julia package into a session, you can use the `using PackageName` co ```julia using Pkg Pkg.add("Catalyst") -using Catalyst ``` Here, the Julia package manager package (`Pkg`) is by default installed on your computer when Julia is installed, and can be activated directly. Next, we also wish to install the `DifferentialEquations` and `Plots` packages (for numeric simulation of models, and plotting, respectively). ```julia -using Pkg -Pkg.activate(".") -``` -Once a package has been installed through the `Pkg.add` command, this command does not have to be repeated if we restart our Julia session. We can now import all three packages into our current session with: -```@example ex2 -using Catalyst -``` -This will only make Catalyst available for the current Julia session. If you exit Julia, you will have to run `using Catalyst` again to use its features (however, `Pkg.add("Catalyst")` does not need to be rerun). Next, we wish to install the `DifferentialEquations` and `Plots` packages (for numeric simulation of models, and plotting, respectively): -```julia Pkg.add("DifferentialEquations") Pkg.add("Plots") ``` diff --git a/src/Catalyst.jl b/src/Catalyst.jl index e235f2b40c..39752d6901 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -8,8 +8,7 @@ using SparseArrays, DiffEqBase, Reexport, Setfield using LaTeXStrings, Latexify, Requires using JumpProcesses: JumpProcesses, JumpProblem, MassActionJump, ConstantRateJump, VariableRateJump, - SpatialMassActionJump, - CartesianGrid, CartesianGridRej + SpatialMassActionJump, CartesianGrid, CartesianGridRej # ModelingToolkit imports and convenience functions we use using ModelingToolkit From d2e0527b07b25af51c43f9b0067229240bab3689 Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 31 May 2024 17:44:15 -0400 Subject: [PATCH 23/47] states -> unknowns --- src/spatial_reaction_systems/lattice_jump_systems.jl | 2 +- .../lattice_reaction_systems.jl | 11 +++++------ src/spatial_reaction_systems/utility.jl | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/spatial_reaction_systems/lattice_jump_systems.jl b/src/spatial_reaction_systems/lattice_jump_systems.jl index 818da05d1a..78db1600bd 100644 --- a/src/spatial_reaction_systems/lattice_jump_systems.jl +++ b/src/spatial_reaction_systems/lattice_jump_systems.jl @@ -125,7 +125,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]) # diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index c7f7974478..a4bc32e4e1 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -56,7 +56,7 @@ struct LatticeReactionSystem{Q,R,S,T} <: MT.AbstractTimeDependentSystem if !isempty(MT.get_systems(rs)) error("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(states(rs)) + if length(species(rs)) != length(unknowns(rs)) error("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)) @@ -322,8 +322,7 @@ 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.equations(lrs::LatticeReactionSystem) = MT.equations(reactionsystem(lrs)) -MT.states(lrs::LatticeReactionSystem) = MT.states(reactionsystem(lrs)) -#MT.unknowns(lrs::LatticeReactionSystem) = MT.unknowns(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. @@ -332,9 +331,9 @@ function MT.get_eqs(lrs::LatticeReactionSystem) error("The `get_eqs` function is primarily relevant for composed models. LatticeReactionSystems cannot be composed, and hence this function should not be used. Please consider using `equations` instead.") # MT.get_eqs(reactionsystem(lrs)) end -function MT.get_states(lrs::LatticeReactionSystem) - error("The `get_unknowns` is primarily relevant for composed models. LatticeReactionSystems cannot be composed, and hence this function should not be used. Please consider using `unknowns` instead.") - # MT.get_states(reactionsystem(lrs)) +function MT.get_unknowns(lrs::LatticeReactionSystem) + error("The `get_unknowns` function is primarily relevant for composed models. LatticeReactionSystems cannot be composed, and hence this function should not be used. Please consider using `unknowns` instead.") + # MT.get_unknowns(reactionsystem(lrs)) end function MT.get_ps(lrs::LatticeReactionSystem) error("The `get_ps` function is primarily relevant for composed models. LatticeReactionSystems cannot be composed, and hence this function should not be used. Please consider using `parameters` instead.") diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index 71caf6f55a..8aa5b14322 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -320,7 +320,7 @@ end # 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. + # 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.") From 25a69e45ab931cd1ba7c80f3abcbd413b2eea759 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 5 Jun 2024 12:22:09 -0400 Subject: [PATCH 24/47] up --- src/Catalyst.jl | 6 +- .../lattice_jump_systems.jl | 42 +- .../lattice_reaction_systems.jl | 11 +- .../spatial_ODE_systems.jl | 2 +- src/spatial_reaction_systems/utility.jl | 8 +- ...attice_reaction_systems_ODE_performance.jl | 52 +-- test/runtests.jl | 48 +-- .../lattice_reaction_systems.jl | 367 ------------------ .../lattice_reaction_systems_ODEs.jl | 0 .../lattice_reaction_systems_jumps.jl | 28 +- .../simulate_PDEs.jl | 0 11 files changed, 74 insertions(+), 490 deletions(-) delete mode 100644 test/spatial_modelling/lattice_reaction_systems.jl rename test/{spatial_modelling => spatial_reaction_systems}/lattice_reaction_systems_ODEs.jl (100%) rename test/{spatial_modelling => spatial_reaction_systems}/simulate_PDEs.jl (100%) diff --git a/src/Catalyst.jl b/src/Catalyst.jl index 39752d6901..cc4fd9c141 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -179,11 +179,11 @@ 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 -# Various utility functions -include("spatial_reaction_systems/utility.jl") - # Specific spatial problem types. include("spatial_reaction_systems/spatial_ODE_systems.jl") include("spatial_reaction_systems/lattice_jump_systems.jl") +# Various utility functions +include("spatial_reaction_systems/utility.jl") + end # module diff --git a/src/spatial_reaction_systems/lattice_jump_systems.jl b/src/spatial_reaction_systems/lattice_jump_systems.jl index 78db1600bd..5322b49b4e 100644 --- a/src/spatial_reaction_systems/lattice_jump_systems.jl +++ b/src/spatial_reaction_systems/lattice_jump_systems.jl @@ -15,7 +15,7 @@ function DiffEqBase.DiscreteProblem(lrs::LatticeReactionSystem, u0_in, tspan, p_ # Converts u0 and p to their internal forms. # 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) + u0 = lattice_process_u0(u0_in, species(lrs), num_verts(lrs)) # 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). @@ -27,8 +27,8 @@ function DiffEqBase.DiscreteProblem(lrs::LatticeReactionSystem, u0_in, tspan, p_ 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...) +function JumpProcesses.JumpProblem(lrs::LatticeReactionSystem, dprob, aggregator, args...; name = nameof(reactionsystem(lrs)), + combinatoric_ratelaws = get_combinatoric_ratelaws(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`?") @@ -42,10 +42,10 @@ function JumpProcesses.JumpProblem(lrs::LatticeReactionSystem, dprob, aggregator # 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])) return JumpProblem(non_spat_dprob, aggregator, sma_jumps; - hopping_constants, spatial_system = lrs.lattice, name, kwargs...) + hopping_constants, spatial_system = lattice(lrs), name, kwargs...) end # Creates the hopping constants from a discrete problem and a lattice reaction system. @@ -57,14 +57,14 @@ function make_hopping_constants(dprob::DiscreteProblem, lrs::LatticeReactionSyst # 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)] + hopping_constants = [Vector{Float64}(undef, length(lattice(lrs).fadjlist[j])) + for i in 1:(num_species(lrs)), j in 1:(num_verts(lrs))] # 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 (e_idx, e) in enumerate(edges(lattice(lrs))) + dst_idx = findfirst(isequal(e.dst), lattice(lrs).fadjlist[e.src]) # For each species, sets that hopping rate. - for s_idx in 1:(lrs.num_species) + for s_idx in 1:(num_species(lrs)) hopping_constants[s_idx, e.src][dst_idx] = get_component_value(all_diff_rates[s_idx], e_idx) end end @@ -77,33 +77,33 @@ end # Not sure if there is any form of performance improvement from that though. Possibly is 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 = [Catalyst.has_spatial_vertex_component(rx.rate, lrs; vert_ps = dprob.p[1]) 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))) + u_rates = Vector{Float64}(undef, length(reactions(reactionsystem(lrs))) - count(is_spatials)) + s_rates = Matrix{Float64}(undef, count(is_spatials), num_verts(lrs)) + reactant_stoich = Vector{Vector{Pair{Int64, Int64}}}(undef, length(reactions(reactionsystem(lrs)))) + net_stoich = Vector{Vector{Pair{Int64, Int64}}}(undef, length(reactions(reactionsystem(lrs)))) # 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] 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]) 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. diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index a4bc32e4e1..4e521d81ac 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -328,16 +328,13 @@ 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) - error("The `get_eqs` function is primarily relevant for composed models. LatticeReactionSystems cannot be composed, and hence this function should not be used. Please consider using `equations` instead.") - # MT.get_eqs(reactionsystem(lrs)) + MT.get_eqs(reactionsystem(lrs)) end function MT.get_unknowns(lrs::LatticeReactionSystem) - error("The `get_unknowns` function is primarily relevant for composed models. LatticeReactionSystems cannot be composed, and hence this function should not be used. Please consider using `unknowns` instead.") - # MT.get_unknowns(reactionsystem(lrs)) + MT.get_unknowns(reactionsystem(lrs)) end function MT.get_ps(lrs::LatticeReactionSystem) - error("The `get_ps` function is primarily relevant for composed models. LatticeReactionSystems cannot be composed, and hence this function should not be used. Please consider using `parameters` instead.") - # MT.get_ps(reactionsystem(lrs)) + MT.get_ps(reactionsystem(lrs)) end # Technically should not be used, but has to be declared for the `show` function to work. @@ -347,7 +344,7 @@ end # Other non-relevant getters. function MT.independent_variables(lrs::LatticeReactionSystem) - error("LatticeReactionSystems are used to model a spatial systems on a discrete lattice. The `independent_variables` function is used to retrieve the independent variables of systems with time and space independent variables. LatticeReactionSystems can only have a single independent variable (the time one). If you want to retrieve this one, please consider using the `get_iv` function.)") + MT.independent_variables(reactionsystem(lrs)) end ### Edge Parameter Value Generators ### diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index 6d048c93d2..9d0f169e49 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -173,7 +173,7 @@ function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{Basi 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)) + osys = complete(convert(ODESystem, reactionsystem(lrs); 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. diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index 8aa5b14322..857f651f87 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -312,7 +312,7 @@ function compute_edge_value(exp, lrs::LatticeReactionSystem, 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] + for idxE in 1:num_edges(lrs)] end # For an expression, computes its values using the provided state and parameter vectors. @@ -348,7 +348,7 @@ function compute_vertex_value(exp, lrs::LatticeReactionSystem; u=nothing, vert_p return [exp_func([sym_val_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] + for idxV in 1:num_verts(lrs)] end ### System Property Checks ### @@ -373,14 +373,14 @@ function has_spatial_vertex_component(exp, lrs::LatticeReactionSystem; u=nothing # 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] + p_idxs = [ModelingToolkit.parameter_index(reactionsystem(lrs), 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] + u_idxs = [ModelingToolkit.variable_index(reactionsystem(lrs), sym) for sym in exp_u] any(length(u[u_idx]) != 1 for u_idx in u_idxs) && return true end diff --git a/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl b/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl index 74328d6979..972ea1529c 100644 --- a/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl +++ b/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl @@ -20,7 +20,7 @@ include("../spatial_test_networks.jl") # Small grid, small, non-stiff, system. let lrs = LatticeReactionSystem(SIR_system, SIR_srs_2, small_2d_graph_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(lattice(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) @@ -35,7 +35,7 @@ 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(lattice(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) @@ -50,7 +50,7 @@ end # Small grid, small, stiff, system. let lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_2d_graph_grid) - u0 = [:X => rand_v_vals(lrs.lattice, 10), :Y => rand_v_vals(lrs.lattice, 10)] + u0 = [:X => rand_v_vals(lattice(lrs), 10), :Y => rand_v_vals(lattice(lrs), 10)] pV = brusselator_p pE = [:dX => 0.2] oprob = ODEProblem(lrs, u0, (0.0, 100.0), [pV; pE]) @@ -65,7 +65,7 @@ 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(lattice(lrs), 10), :Y => rand_v_vals(lattice(lrs), 10)] pV = brusselator_p pE = [:dX => 0.2] oprob = ODEProblem(lrs, u0, (0.0, 100.0), [pV; pE]) @@ -82,10 +82,10 @@ let lrs = LatticeReactionSystem(CuH_Amination_system, CuH_Amination_srs_2, 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(lattice(lrs), 0.005), + :Ligand => 0.005 .+ rand_v_vals(lattice(lrs), 0.005), :CuoAcLigand => 0.0, - :Silane => 0.5 .+ rand_v_vals(lrs.lattice, 0.5), + :Silane => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), :CuHLigand => 0.0, :SilaneOAc => 0.0, :Styrene => 0.16, @@ -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(lattice(lrs), 0.005), + :Ligand => 0.005 .+ rand_v_vals(lattice(lrs), 0.005), :CuoAcLigand => 0.0, - :Silane => 0.5 .+ rand_v_vals(lrs.lattice, 0.5), + :Silane => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), :CuHLigand => 0.0, :SilaneOAc => 0.0, :Styrene => 0.16, @@ -143,14 +143,14 @@ end let 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(lattice(lrs), 0.5), + :w2 => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), + :w2v => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), + :v => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), + :w2v2 => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), + :vP => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), + :σB => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), + :w2σB => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), :vPp => 0.0, :phos => 0.4, ] @@ -169,14 +169,14 @@ 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(lattice(lrs), 0.5), + :w2 => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), + :w2v => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), + :v => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), + :w2v2 => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), + :vP => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), + :σB => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), + :w2σB => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), :vPp => 0.0, :phos => 0.4, ] diff --git a/test/runtests.jl b/test/runtests.jl index 079c8a33e7..c2910e424b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,54 +10,8 @@ using SafeTestsets, Test ### Run Tests ### @time begin - #if GROUP == "All" || GROUP == "ModelCreation" - # Tests the `ReactionSystem` structure and its properties. - @time @safetestset "Reaction Structure" begin include("reactionsystem_core/reaction.jl") end - @time @safetestset "ReactionSystem Structure" begin include("reactionsystem_core/reactionsystem.jl") end - @time @safetestset "Higher Order Reactions" begin include("reactionsystem_core/higher_order_reactions.jl") end - @time @safetestset "Symbolic Stoichiometry" begin include("reactionsystem_core/symbolic_stoichiometry.jl") end - @time @safetestset "Parameter Type Designation" begin include("reactionsystem_core/parameter_type_designation.jl") end - @time @safetestset "Custom CRN Functions" begin include("reactionsystem_core/custom_crn_functions.jl") end - # @time @safetestset "Coupled CRN/Equation Systems" begin include("reactionsystem_core/coupled_equation_crn_systems.jl") end - @time @safetestset "Events" begin include("reactionsystem_core/events.jl") end - - # Tests model creation via the @reaction_network DSL. - @time @safetestset "DSL Basic Model Construction" begin include("dsl/dsl_basic_model_construction.jl") end - @time @safetestset "DSL Advanced Model Construction" begin include("dsl/dsl_advanced_model_construction.jl") end - @time @safetestset "DSL Options" begin include("dsl/dsl_options.jl") end - # Tests compositional and hierarchical modelling. - @time @safetestset "ReactionSystem Components Based Creation" begin include("compositional_modelling/component_based_model_creation.jl") end - #end - - #if GROUP == "All" || GROUP == "Miscellaneous-NetworkAnalysis" - # Tests various miscellaneous features. - @time @safetestset "API" begin include("miscellaneous_tests/api.jl") end - @time @safetestset "Units" begin include("miscellaneous_tests/units.jl") end - @time @safetestset "Steady State Stability Computations" begin include("miscellaneous_tests/stability_computation.jl") end - @time @safetestset "Compound Species" begin include("miscellaneous_tests/compound_macro.jl") end - @time @safetestset "Reaction Balancing" begin include("miscellaneous_tests/reaction_balancing.jl") end - - # Tests reaction network analysis features. - @time @safetestset "Conservation Laws" begin include("network_analysis/conservation_laws.jl") end - @time @safetestset "Network Properties" begin include("network_analysis/network_properties.jl") end - #end - - #if GROUP == "All" || GROUP == "Simulation" - # Tests ODE, SDE, jump simulations, nonlinear solving, and steady state simulations. - @time @safetestset "ODE System Simulations" begin include("simulation_and_solving/simulate_ODEs.jl") end - @time @safetestset "Automatic Jacobian Construction" begin include("simulation_and_solving/jacobian_construction.jl") end - @time @safetestset "SDE System Simulations" begin include("simulation_and_solving/simulate_SDEs.jl") end - @time @safetestset "Jump System Simulations" begin include("simulation_and_solving/simulate_jumps.jl") end - @time @safetestset "Nonlinear and SteadyState System Solving" begin include("simulation_and_solving/solve_nonlinear.jl") end - - # Tests upstream SciML and DiffEq stuff. - @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 - #end - - @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 "Lattice Reaction Systems" begin include("spatial_reaction_systems/lattice_reaction_systems.jl") end @time @safetestset "Lattice Reaction Systems Lattice Types" begin include("spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl") end @time @safetestset "ODE Lattice Systems Simulations" begin include("spatial_reaction_systems/lattice_reaction_systems_ODEs.jl") end @time @safetestset "Jump Lattice Systems Simulations" begin include("spatial_reaction_systems/lattice_reaction_systems_jumps.jl") end diff --git a/test/spatial_modelling/lattice_reaction_systems.jl b/test/spatial_modelling/lattice_reaction_systems.jl deleted file mode 100644 index a1fce40846..0000000000 --- a/test/spatial_modelling/lattice_reaction_systems.jl +++ /dev/null @@ -1,367 +0,0 @@ -### Preparations ### - -# Fetch packages. -using Catalyst, Graphs, Test -using Symbolics: unwrap -t = default_t() - -# Pre declares a grid. -grid = Graphs.grid([2, 2]) - - -### Tests LatticeReactionSystem Getters Correctness ### - -# Test case 1. -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]) -end - -# Test case 2. -let - rs = @reaction_network begin - @parameters p1 p2 [edgeparameter=true] - end - @unpack p1, p2 = rs - - @test !isedgeparameter(p1) - @test isedgeparameter(p2) -end - -# Test case 3. -let - rs = @reaction_network begin - @parameters pX pY dX [edgeparameter=true] dY - (pX, 1), 0 <--> X - (pY, 1), 0 <--> Y - 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]) -end - -# Test case 4. -let - rs = @reaction_network begin - @parameters dX p - (pX, 1), 0 <--> X - (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), []) -end - -# Test case 5. -let - rs = @reaction_network begin - @species W(t) - @parameters pX pY dX [edgeparameter=true] dY - (pX, 1), 0 <--> X - (pY, 1), 0 <--> Y - (pZ, 1), 0 <--> Z - (pV, 1), 0 <--> V - end - @unpack dX, X, V = rs - @parameters dV dW - @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]) -end - -# Test case 6. -let - rs = @reaction_network customname begin - (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 - -# 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 - 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. -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) -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) -end - -# Network where the rate depend on a species -let - rs = @reaction_network begin - @species Y(t) - (p, d), 0 <--> X - end - tr = @transport_reaction D*Y X - @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid) -end - -# Network with edge parameter in non-spatial reaction rate. -let - rs = @reaction_network begin - @parameters p [edgeparameter=true] - (p, d), 0 <--> X - end - tr = @transport_reaction D X - @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid) -end - -# Network where metadata has been added in rs (which is not seen in transport reaction). -let - rs = @reaction_network begin - @species X(t) [description="Species with added metadata"] - (p, d), 0 <--> X - end - tr = @transport_reaction D X - @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid) - - 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 - - -### Test Designation of Parameter Types ### -# Currently not supported. Won't be until the LatticeReactionSystem internal update is merged. - -# Checks that parameter types designated in the non-spatial `ReactionSystem` is handled correctly. -@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] == SymbolicUtils.BasicSymbolic{Real} - @test p_types[:l1] == SymbolicUtils.BasicSymbolic{Real} - @test p_types[:k2] == SymbolicUtils.BasicSymbolic{Float64} - @test p_types[:l2] == SymbolicUtils.BasicSymbolic{Float64} - @test p_types[:k3] == SymbolicUtils.BasicSymbolic{Int64} - @test p_types[:l3] == SymbolicUtils.BasicSymbolic{Int64} - @test p_types[:k4] == SymbolicUtils.BasicSymbolic{Float32} - @test p_types[:l4] == SymbolicUtils.BasicSymbolic{Float32} - @test p_types[:k5] == SymbolicUtils.BasicSymbolic{Rational{Int64}} - @test p_types[:l5] == SymbolicUtils.BasicSymbolic{Rational{Int64}} - @test p_types[:D1] == SymbolicUtils.BasicSymbolic{Float32} - @test p_types[:D2] == SymbolicUtils.BasicSymbolic{Real} - @test p_types[:D3] == SymbolicUtils.BasicSymbolic{Rational{Int64}} -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 - (p,d), 0 <--> X - end - @parameters D::Rational{Int64} - tr = TransportReaction(D, rs.X) - lrs = LatticeReactionSystem(rs, [tr], grid) - - p_types = Dict([ModelingToolkit.nameof(p) => typeof(unwrap(p)) for p in parameters(lrs)]) - @test p_types[:p] == SymbolicUtils.BasicSymbolic{Float32} - @test p_types[:d] == SymbolicUtils.BasicSymbolic{Real} - @test p_types[:D] == SymbolicUtils.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] -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 diff --git a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl b/test/spatial_reaction_systems/lattice_reaction_systems_ODEs.jl similarity index 100% rename from test/spatial_modelling/lattice_reaction_systems_ODEs.jl rename to test/spatial_reaction_systems/lattice_reaction_systems_ODEs.jl diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl b/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl index 8fc019d8bf..361b9d822e 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl @@ -16,23 +16,23 @@ let 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))) + u0_2 = [:S => round.(Int64, 500.0 .+ 500.0 * rand_v_vals(lattice(lrs))), :I => 1, :R => 0, ] + u0_3 = [:S => 950, :I => round.(Int64, 50 * rand_v_vals(lattice(lrs))), :R => round.(Int64, 50 * rand_v_vals(lattice(lrs)))] + u0_4 = [:S => round.(500.0 .+ 500.0 * rand_v_vals(lattice(lrs))), :I => round.(50 * rand_v_vals(lattice(lrs))), :R => round.(50 * rand_v_vals(lattice(lrs)))] + u0_5 = make_u0_matrix(u0_3, vertices(lattice(lrs)), map(s -> Symbol(s.f), species(reactionsystem(lrs)))) 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)] + p2 = [:α => 0.1 / 1000, :β => 0.02 * rand_v_vals(lattice(lrs))] p3 = [ - :α => 0.1 / 2000 * rand_v_vals(lrs.lattice), - :β => 0.02 * rand_v_vals(lrs.lattice), + :α => 0.1 / 2000 * rand_v_vals(lattice(lrs)), + :β => 0.02 * rand_v_vals(lattice(lrs)), ] - p4 = make_u0_matrix(p1, vertices(lrs.lattice), Symbol.(parameters(lrs.rs))) + p4 = make_u0_matrix(p1, vertices(lattice(lrs)), Symbol.(parameters(reactionsystem(lrs)))) 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))) + pE_3 = map(sp -> sp => rand_e_vals(lattice(lrs), 0.01), ModelingToolkit.getname.(edge_parameters(lrs))) + pE_4 = make_u0_matrix(pE_3, edges(lattice(lrs)), 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)) jprob = JumpProblem(lrs, dprob, NSM()) @@ -117,9 +117,9 @@ let lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_3d_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(num_verts(lrs)), :dX => rand(num_edges(lrs))] dprob = DiscreteProblem(lrs, u0, tspan, ps) jprob = JumpProblem(lrs, dprob, NSM()) @@ -127,8 +127,8 @@ 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. @test SciMLBase.successful_retcode(solve(jprob, SSAStepper())) diff --git a/test/spatial_modelling/simulate_PDEs.jl b/test/spatial_reaction_systems/simulate_PDEs.jl similarity index 100% rename from test/spatial_modelling/simulate_PDEs.jl rename to test/spatial_reaction_systems/simulate_PDEs.jl From 48847d4f206812ca90047c6d31b5808065c2b47a Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 5 Jun 2024 12:24:04 -0400 Subject: [PATCH 25/47] testup --- test/runtests.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index ff6a80a1e6..31456e6244 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -45,8 +45,9 @@ using SafeTestsets, Test #if GROUP == "All" || GROUP == "Spatial" # 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 "Lattice Reaction Systems" begin include("spatial_reaction_systems/lattice_reaction_systems.jl") end + @time @safetestset "Lattice Reaction Systems Lattice Types" begin include("spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl") end + @time @safetestset "ODE Lattice Systems Simulations" begin include("spatial_reaction_systems/lattice_reaction_systems_ODEs.jl") end @time @safetestset "Jump Lattice Systems Simulations" begin include("spatial_reaction_systems/lattice_reaction_systems_jumps.jl") end #if GROUP == "All" || GROUP == "Visualisation-Extensions" From 9e69861fc1385432b5ebea14a91ee7be047ba8c9 Mon Sep 17 00:00:00 2001 From: Torkel Date: Thu, 6 Jun 2024 08:59:40 -0400 Subject: [PATCH 26/47] up --- .../spatial_ODE_systems.jl | 72 +++++++++++++++---- src/spatial_reaction_systems/utility.jl | 17 +++-- .../lattice_reaction_systems_ODEs.jl | 2 +- 3 files changed, 70 insertions(+), 21 deletions(-) diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index 9d0f169e49..6822374bb0 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -8,6 +8,17 @@ struct LatticeTransportODEf{S,T} num_verts::Int64 """The number of species.""" num_species::Int64 + """The indexes of the vertex parameters in the parameter vector (`parameters(lrs)`).""" + vert_p_idxs::Vector{Int64} + """The indexes of the edge parameters in the parameter vector (`parameters(lrs)`).""" + edge_p_idxs::Vector{Int64} + """ + The non-spatial `ReactionSystem` which was used to create the `LatticeReactionSystem` contain + a set of parameters (either identical to, or a sub set of, `parameters(lrs)`). This vector + contain the indexes of the non-spatial system's parameters in `parameters(lrs)`. These are + required to manage the non-spatial ODEFunction in the spatial call. + """ + nonspatial_rs_p_idxs::Vector{Int64} """The values of the parameters that are tied to vertices.""" vert_ps::Vector{Vector{T}} """ @@ -17,10 +28,10 @@ struct LatticeTransportODEf{S,T} the parameter values in a new vertex. To avoid relocating these values repeatedly, we write them to this vector. """ - work_vert_ps::Vector{T} + work_ps::Vector{T} """ For each parameter in vert_ps, its value is a vector with a length of either num_verts or 1. - To know whenever a parameter's value needs expanding to the work_vert_ps array, its length needs checking. + To know whenever a parameter's value needs expanding to the work_ps array, its length needs checking. This check is done once, and the value is stored in this array. True means a uniform value. """ v_ps_idx_types::Vector{Bool} @@ -55,7 +66,13 @@ struct LatticeTransportODEf{S,T} # Records which parameters and rates are uniform and which are not. v_ps_idx_types = map(vp -> length(vp[2]) == 1, vert_ps) t_rate_idx_types = map(tr -> size(tr[2]) == (1,1), transport_rates) - + + # Computes the indexes of various parameters in in the `parameters(lrs)` vector. + vert_p_idxs = subset_indexes_of(vertex_parameters(lrs), parameters(lrs)) + edge_p_idxs = subset_indexes_of(edge_parameters(lrs), parameters(lrs)) + nonspatial_rs_p_idxs = subset_indexes_of(parameters(reactionsystem(lrs)), parameters(lrs)) + + # Computes the indexes of the vertex parameters in the vector of parameters. # Input `vert_ps` is a vector map taking each parameter symbolic to its value (potentially a # vector). This vector is already sorted according to the order of the parameters. Here, we extract # its values only and put them into `vert_ps`. @@ -70,11 +87,12 @@ struct LatticeTransportODEf{S,T} end end - # Declares `work_vert_ps` (used as storage during computation) and the edge iterator. - work_vert_ps = zeros(length(vert_ps)) + # Declares `work_ps` (used as storage during computation) and the edge iterator. + work_ps = zeros(length(parameters(lrs))) edge_iterator = Catalyst.edge_iterator(lrs) - new{S,T}(ofunc, num_verts(lrs), num_species(lrs), vert_ps, work_vert_ps, - v_ps_idx_types, transport_rates, t_rate_idx_types, leaving_rates, edge_iterator) + new{S,T}(ofunc, num_verts(lrs), num_species(lrs), vert_p_idxs, edge_p_idxs, + nonspatial_rs_p_idxs, vert_ps, work_ps, v_ps_idx_types, transport_rates, + t_rate_idx_types, leaving_rates, edge_iterator) end end @@ -86,6 +104,17 @@ struct LatticeTransportODEjac{R,S,T} num_verts::Int64 """The number of species.""" num_species::Int64 + """The indexes of the vertex parameters in the parameter vector (`parameters(lrs)`).""" + vert_p_idxs::Vector{Int64} + """The indexes of the edge parameters in the parameter vector (`parameters(lrs)`).""" + edge_p_idxs::Vector{Int64} + """ + The non-spatial `ReactionSystem` which was used to create the `LatticeReactionSystem` contain + a set of parameters (either identical to, or a sub set of, `parameters(lrs)`). This vector + contain the indexes of the non-spatial system's parameters in `parameters(lrs)`. These are + required to manage the non-spatial ODEFunction in the spatial call. + """ + nonspatial_rs_p_idxs::Vector{Int64} """The values of the parameters that are tied to vertices.""" vert_ps::Vector{Vector{S}} """ @@ -95,10 +124,10 @@ struct LatticeTransportODEjac{R,S,T} the parameter values in a new vertex. To avoid relocating these values repeatedly, we write them to this vector. """ - work_vert_ps::Vector{S} + work_ps::Vector{S} """ For each parameter in vert_ps, its value is a vector with a length of either num_verts or 1. - To know whenever a parameter's value needs expanding to the work_vert_ps array, its length needs checking. + To know whenever a parameter's value needs expanding to the work_ps array, its length needs checking. This check is done once, and the value is stored in this array. True means a uniform value. """ v_ps_idx_types::Vector{Bool} @@ -110,18 +139,30 @@ struct LatticeTransportODEjac{R,S,T} function LatticeTransportODEjac(ofunc::R, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{S}}}, jac_transport::Union{Nothing, SparseMatrixCSC{Float64, Int64}}, lrs::LatticeReactionSystem, sparse::Bool) where {R,S} + + # Computes the indexes of various parameters in in the `parameters(lrs)` vector. + vert_p_idxs = subset_indexes_of(vertex_parameters(lrs), parameters(lrs)) + edge_p_idxs = subset_indexes_of(edge_parameters(lrs), parameters(lrs)) + nonspatial_rs_p_idxs = subset_indexes_of(parameters(reactionsystem(lrs)), parameters(lrs)) + # Input `vert_ps` is a vector map taking each parameter symbolic to its value (potentially a # vector). This vector is already sorted according to the order of the parameters. Here, we extract # its values only and put them into `vert_ps`. vert_ps = [vp[2] for vp in vert_ps] - work_vert_ps = zeros(num_verts(lrs)) + work_ps = zeros(length(parameters(lrs))) v_ps_idx_types = map(vp -> length(vp) == 1, vert_ps) - new{R,S,typeof(jac_transport)}(ofunc, num_verts(lrs), num_species(lrs) , vert_ps, - work_vert_ps, v_ps_idx_types, sparse, jac_transport) + new{R,S,typeof(jac_transport)}(ofunc, num_verts(lrs), num_species(lrs) , vert_p_idxs, + edge_p_idxs, nonspatial_rs_p_idxs, vert_ps, + work_ps, v_ps_idx_types, sparse, jac_transport) end end +# For each symbolic in syms1, returns a vector with their indexes in syms2. +function subset_indexes_of(syms1, syms2) + [findfirst(isequal(sym1, sym2) for sym2 in syms2) for sym1 in syms1] +end + ### ODEProblem ### # Creates an ODEProblem from a LatticeReactionSystem. @@ -155,7 +196,8 @@ function DiffEqBase.ODEProblem(lrs::LatticeReactionSystem, u0_in, tspan, combinatoric_ratelaws, remove_conserved, checks) # Combines `vert_ps` and `edge_ps` to a single vector with values only (not a map). Creates ODEProblem. - ps = [p[2] for p in [vert_ps; edge_ps]] + 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 @@ -288,7 +330,7 @@ function (f_func::LatticeTransportODEf)(du, u, p, t) update_work_vert_ps!(f_func, p, vert_i) # Evaluate reaction contributions to du at vert_i. - f_func.ofunc((@view du[idxs]), (@view u[idxs]), f_func.work_vert_ps, t) + f_func.ofunc((@view du[idxs]), (@view u[idxs]), nonspatial_ps(f_func), t) end # s_idx is the species index among transport species, s is the index among all species. @@ -316,7 +358,7 @@ function (jac_func::LatticeTransportODEjac)(J, u, p, t) for vert_i in 1:(jac_func.num_verts) idxs = get_indexes(vert_i, jac_func.num_species) update_work_vert_ps!(jac_func, p, vert_i) - jac_func.ofunc.jac((@view J[idxs, idxs]), (@view u[idxs]), jac_func.work_vert_ps, t) + jac_func.ofunc.jac((@view J[idxs, idxs]), (@view u[idxs]), nonspatial_ps(jac_func), t) end # Updates for the spatial reactions (adds the Jacobian values from the transportation reactions). diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index 857f651f87..2a05303c1f 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -268,21 +268,28 @@ function get_transport_rate(trans_s_idx::Int64, f_func::LatticeTransportODEf, ed get_transport_rate(f_func.transport_rates[trans_s_idx][2], edge, f_func.t_rate_idx_types[trans_s_idx]) end -# Updates the internal work_vert_ps vector for a given vertex. +# Updates the internal work_ps vector for a given vertex. Updates only the parameters that +# actually are vertex parameters. # To this vector, we write the system's parameter values at the specific vertex. -function update_work_vert_ps!(work_vert_ps::Vector{S}, all_ps::Vector{T}, vert::Int64, - vert_ps_idx_types::Vector{Bool}) where {S,T} +function update_work_vert_ps!(work_ps::Vector{S}, vert_p_idxs::Vector{Int64}, all_ps::Vector{T}, + vert::Int64, vert_ps_idx_types::Vector{Bool}) where {S,T} # Loops through all parameters. for (idx,loc_type) in enumerate(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 ? all_ps[idx][1] : all_ps[idx][vert]) + work_ps[vert_p_idxs[idx]] = (loc_type ? all_ps[vert_p_idxs[idx]][1] : all_ps[vert_p_idxs[idx]][vert]) end end # Input is either a LatticeTransportODEf or LatticeTransportODEjac function (which fields we pass on). function update_work_vert_ps!(lt_ode_func, all_ps::Vector{T}, vert::Int64) where {T} - return update_work_vert_ps!(lt_ode_func.work_vert_ps, all_ps, vert, lt_ode_func.v_ps_idx_types) + return update_work_vert_ps!(lt_ode_func.work_ps, lt_ode_func.vert_p_idxs, all_ps, vert, lt_ode_func.v_ps_idx_types) +end + +# Fetches the parameter values that currently are in the work parameter vector and which +# corresponds to the parameters of the non-spatial `ReactionSystem` stored in the `ReactionSystem`. +function nonspatial_ps(lt_ode_func) + return @view lt_ode_func.work_ps[lt_ode_func.nonspatial_rs_p_idxs] end # Expands a u0/p information stored in Vector{Vector{}} for to Matrix form diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_ODEs.jl b/test/spatial_reaction_systems/lattice_reaction_systems_ODEs.jl index 7c62bc7892..66b8f41952 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems_ODEs.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems_ODEs.jl @@ -89,7 +89,7 @@ end # Checks that non-spatial brusselator simulation is identical to all on an unconnected lattice. 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)] + u0 = [:X => 2.0 + 2.0 * rand(rng), :Y => 10.0 * (1.0 * rand(rng))] pV = brusselator_p pE = [:dX => 0.2] oprob_nonspatial = ODEProblem(brusselator_system, u0, (0.0, 100.0), pV) From f0f4a09c7c3a5a45d1a10d044e47537ad5dcce17 Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 14 Jun 2024 08:55:12 -0400 Subject: [PATCH 27/47] update JumpProblem for latest version --- src/Catalyst.jl | 3 + .../lattice_jump_systems.jl | 79 ++-- .../lattice_reaction_systems.jl | 12 +- .../spatial_ODE_systems.jl | 5 +- src/spatial_reaction_systems/utility.jl | 59 +-- test/runtests.jl | 4 +- .../lattice_reaction_systems.jl | 321 +++++++------ .../lattice_reaction_systems_ODEs.jl | 11 +- .../lattice_reaction_systems_jumps.jl | 84 ++-- .../lattice_reaction_systems_lattice_types.jl | 0 .../simulate_PDEs.jl | 0 .../lattice_reaction_systems.jl | 423 ------------------ test/spatial_test_networks.jl | 9 + 13 files changed, 312 insertions(+), 698 deletions(-) rename test/{spatial_reaction_systems => spatial_modelling}/lattice_reaction_systems_ODEs.jl (98%) rename test/{spatial_reaction_systems => spatial_modelling}/lattice_reaction_systems_jumps.jl (63%) rename test/{spatial_reaction_systems => spatial_modelling}/lattice_reaction_systems_lattice_types.jl (100%) rename test/{spatial_reaction_systems => spatial_modelling}/simulate_PDEs.jl (100%) delete mode 100644 test/spatial_reaction_systems/lattice_reaction_systems.jl diff --git a/src/Catalyst.jl b/src/Catalyst.jl index d386e4f98c..edf896dd54 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -183,6 +183,9 @@ export make_edge_p_values, make_directed_edge_values include("spatial_reaction_systems/spatial_ODE_systems.jl") 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/spatial_reaction_systems/lattice_jump_systems.jl b/src/spatial_reaction_systems/lattice_jump_systems.jl index cbe8374859..44b26ecbbd 100644 --- a/src/spatial_reaction_systems/lattice_jump_systems.jl +++ b/src/spatial_reaction_systems/lattice_jump_systems.jl @@ -7,28 +7,25 @@ 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), num_verts(lrs)) - # 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) - - # 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...) + 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 (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. +# Builds a spatial JumpProblem from a DiscreteProblem containing a `LatticeReactionSystem`. function JumpProcesses.JumpProblem(lrs::LatticeReactionSystem, dprob, aggregator, args...; name = nameof(reactionsystem(lrs)), combinatoric_ratelaws = get_combinatoric_ratelaws(reactionsystem(lrs)), kwargs...) # Error checks. @@ -37,50 +34,53 @@ function JumpProcesses.JumpProblem(lrs::LatticeReactionSystem, dprob, aggregator 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 does 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, 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 = lattice(lrs), 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)) + 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(lattice(lrs).fadjlist[j])) - for i in 1:(num_species(lrs)), j in 1:(num_verts(lrs))] - - # For each edge, finds each position in `hopping_constants`. - for (e_idx, e) in enumerate(edges(lattice(lrs))) - dst_idx = findfirst(isequal(e.dst), lattice(lrs).fadjlist[e.src]) - # For each species, sets that hopping rate. - for s_idx in 1:(num_species(lrs)) - 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. +# 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(reactionsystem(lrs))] + 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. @@ -91,10 +91,10 @@ function make_spatial_majumps(dprob, lrs::LatticeReactionSystem) net_stoich = Vector{Vector{Pair{Int64, Int64}}}(undef, length(reactions(reactionsystem(lrs)))) # Loops through reactions with non-spatial rates, computes their rates and stoichiometries. - cur_rx = 1; + cur_rx = 1 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, reactionsystem(lrs)) net_stoich[cur_rx] = int_map(rx.netstoich, reactionsystem(lrs)) @@ -103,8 +103,7 @@ function make_spatial_majumps(dprob, lrs::LatticeReactionSystem) # Loops through reactions with spatial rates, computes their rates and stoichiometries. 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, reactionsystem(lrs)) net_stoich[cur_rx] = int_map(rx.netstoich, reactionsystem(lrs)) @@ -114,7 +113,7 @@ function make_spatial_majumps(dprob, lrs::LatticeReactionSystem) 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 ### diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index a543267137..43f410932e 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -307,6 +307,16 @@ grid_dims(lrs::LatticeReactionSystem) = grid_dims(lattice(lrs)) grid_dims(lattice::GridLattice{N,T}) where {N,T} = return N grid_dims(lattice::Graph) = error("Grid dimensions is only defined for LatticeReactionSystems with grid-based lattices (not graph-based).") +""" + 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 ### @@ -393,7 +403,7 @@ end D_vals = make_edge_p_values(lrs, make_edge_p_value) ``` """ -function make_edge_p_values(lrs::LatticeReactionSystem, make_edge_p_value::Function, ) +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 diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index 6ec501b635..bd9fdf8e4e 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -186,7 +186,7 @@ function DiffEqBase.ODEProblem(lrs::LatticeReactionSystem, u0_in, tspan, 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 becomes a sparse matrix. Here, index (i,j) is a parameter's value in the edge from vertex i to vertex j. + # 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) @@ -321,6 +321,9 @@ end # Defines the forcing functor's effect on the (spatial) ODE system. function (f_func::LatticeTransportODEf)(du, u, p, t) + println(du) + println(u) + println(p) # Updates for non-spatial reactions. for vert_i in 1:(f_func.num_verts) # Gets the indices of all the species at vertex i. diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index 2ec7e104b0..43c7932185 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -327,36 +327,25 @@ end # 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) +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.") + error("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 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. - 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:num_verts(lrs)] + 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 ### @@ -373,26 +362,10 @@ function has_spatial_edge_component(exp, lrs::LatticeReactionSystem, edge_ps) 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(reactionsystem(lrs), 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(reactionsystem(lrs), 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, checks 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/test/runtests.jl b/test/runtests.jl index bf6be2bbfd..23cb457f4b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -9,6 +9,7 @@ using SafeTestsets, Test ### Run Tests ### @time begin + @time @safetestset "Jump Lattice Systems Simulations" begin include("spatial_modelling/lattice_reaction_systems_jumps.jl") end # Tests the `ReactionSystem` structure and its properties. @time @safetestset "Reaction Structure" begin include("reactionsystem_core/reaction.jl") end @@ -53,8 +54,9 @@ using SafeTestsets, Test # 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 "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_reaction_systems/lattice_reaction_systems_jumps.jl") end + @time @safetestset "Jump Lattice Systems Simulations" begin include("spatial_modelling/lattice_reaction_systems_jumps.jl") end # Tests network visualisation. @time @safetestset "Latexify" begin include("visualisation/latexify.jl") end diff --git a/test/spatial_modelling/lattice_reaction_systems.jl b/test/spatial_modelling/lattice_reaction_systems.jl index d25c72694c..b2823b3111 100644 --- a/test/spatial_modelling/lattice_reaction_systems.jl +++ b/test/spatial_modelling/lattice_reaction_systems.jl @@ -1,13 +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 grid. +grids = [very_small_2d_cartesian_grid, very_small_2d_masked_grid, very_small_2d_graph_grid] ### Tests LatticeReactionSystem Getters Correctness ### @@ -16,16 +16,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 +50,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 +70,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 +94,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,9 +120,11 @@ let (p, 1), 0 <--> X end tr = @transport_reaction d X - lrs = LatticeReactionSystem(rs, [tr], grid) + for grid in grids + lrs = LatticeReactionSystem(rs, [tr], grid) - @test nameof(lrs) == :customname + @test nameof(lrs) == :customname + end end ### Tests Spatial Reactions Getters Correctness ### @@ -180,6 +191,7 @@ end # Test reactions with constants in rate. let + @variables t @species X(t) Y(t) tr_1 = TransportReaction(1.5, X) @@ -223,6 +235,7 @@ end # Test creation of TransportReaction with non-parameters in rate. # Tests that it works even when 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) @@ -235,7 +248,9 @@ let (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 +260,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 +272,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 +284,140 @@ 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 - tr = @transport_reaction D X - @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid) end -### Test Designation of Parameter Types ### -# Currently not supported. Won't be until the LatticeReactionSystem internal update is merged. +### Tests Grid Vertex and Edge Number Computation ### -# 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 +# 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 + + # 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 lrs1.num_edges == iterator_count(edge_iterator(lrs1)) + @test lrs2.num_edges == 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 lrs1.num_edges == iterator_count(edge_iterator(lrs1)) 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}} 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 computes 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 - 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] + # 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) + + 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 -# 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 all species ends up in the correct place in 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, 10000.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())[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())[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())[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())[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())[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())[end][125], n^3, rtol=1e-6) +end + +# 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_reaction_systems/lattice_reaction_systems_ODEs.jl b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl similarity index 98% rename from test/spatial_reaction_systems/lattice_reaction_systems_ODEs.jl rename to test/spatial_modelling/lattice_reaction_systems_ODEs.jl index 66b8f41952..b8114e4eee 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems_ODEs.jl +++ b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl @@ -30,17 +30,16 @@ for grid in [small_2d_graph_grid, short_path, small_directed_cycle, :R => 50 * rand_v_vals(lattice(lrs)), ] for u0 in [u0_1, u0_2, u0_3] - p1 = [:α => 0.1 / 1000, :β => 0.01] - p2 = [:α => 0.1 / 1000, :β => 0.02 * rand_v_vals(lattice(lrs))] - p3 = [ + pV_1 = [:α => 0.1 / 1000, :β => 0.01] + pV_2 = [:α => 0.1 / 1000, :β => 0.02 * rand_v_vals(lattice(lrs))] + pV_3 = [ :α => 0.1 / 2000 * rand_v_vals(lattice(lrs)), :β => 0.02 * rand_v_vals(lattice(lrs)), ] - for pV in [p1, p2, p3] + 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)) + 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]) diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl b/test/spatial_modelling/lattice_reaction_systems_jumps.jl similarity index 63% rename from test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl rename to test/spatial_modelling/lattice_reaction_systems_jumps.jl index 361b9d822e..9ad28f9ed4 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl +++ b/test/spatial_modelling/lattice_reaction_systems_jumps.jl @@ -12,29 +12,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(lattice(lrs))), :I => 1, :R => 0, ] - u0_3 = [:S => 950, :I => round.(Int64, 50 * rand_v_vals(lattice(lrs))), :R => round.(Int64, 50 * rand_v_vals(lattice(lrs)))] - u0_4 = [:S => round.(500.0 .+ 500.0 * rand_v_vals(lattice(lrs))), :I => round.(50 * rand_v_vals(lattice(lrs))), :R => round.(50 * rand_v_vals(lattice(lrs)))] - u0_5 = make_u0_matrix(u0_3, vertices(lattice(lrs)), map(s -> Symbol(s.f), species(reactionsystem(lrs)))) - 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(lattice(lrs))] - p3 = [ + u0_2 = [:S => round.(Int64, 500 .+ 500 * rand_v_vals(lattice(lrs))), :I => 1, :R => 0] + u0_3 = [ + :S => round.(Int64, 500 .+ 500 * rand_v_vals(lattice(lrs))), + :I => round.(Int64, 50 * rand_v_vals(lattice(lrs))), + :R => round.(Int64, 50 * rand_v_vals(lattice(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(lattice(lrs))] + pV_3 = [ :α => 0.1 / 2000 * rand_v_vals(lattice(lrs)), :β => 0.02 * rand_v_vals(lattice(lrs)), ] - p4 = make_u0_matrix(p1, vertices(lattice(lrs)), Symbol.(parameters(reactionsystem(lrs)))) - 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(lattice(lrs), 0.01), ModelingToolkit.getname.(edge_parameters(lrs))) - pE_4 = make_u0_matrix(pE_3, edges(lattice(lrs)), 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 +51,29 @@ 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)))) + pE_2 = [:dI => 0.02, :dS => uniform_e_vals(lrs, 0.01), :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 +87,7 @@ end ### SpatialMassActionJump Testing ### -# Checks that the correct structure is produced. +# Checks that the correct structure;s is produced. let # Network for reference: # A, ∅ → X @@ -114,12 +96,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, num_verts(lrs))] tspan = (0.0, 100.0) - ps = [:A => 1.0, :B => 5.0 .+ rand(num_verts(lrs)), :dX => rand(num_edges(lrs))] + ps = [:A => 1.0, :B => 5.0 .+ rand_v_vals(lattice(lrs)), :dX => rand_e_vals(lrs)] dprob = DiscreteProblem(lrs, u0, tspan, ps) jprob = JumpProblem(lrs, dprob, NSM()) @@ -134,14 +116,15 @@ let @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 +136,8 @@ 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.) + println() @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]] diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl b/test/spatial_modelling/lattice_reaction_systems_lattice_types.jl similarity index 100% rename from test/spatial_reaction_systems/lattice_reaction_systems_lattice_types.jl rename to test/spatial_modelling/lattice_reaction_systems_lattice_types.jl diff --git a/test/spatial_reaction_systems/simulate_PDEs.jl b/test/spatial_modelling/simulate_PDEs.jl similarity index 100% rename from test/spatial_reaction_systems/simulate_PDEs.jl rename to test/spatial_modelling/simulate_PDEs.jl diff --git a/test/spatial_reaction_systems/lattice_reaction_systems.jl b/test/spatial_reaction_systems/lattice_reaction_systems.jl deleted file mode 100644 index b2823b3111..0000000000 --- a/test/spatial_reaction_systems/lattice_reaction_systems.jl +++ /dev/null @@ -1,423 +0,0 @@ -### Preparations ### - -# Fetch packages. -using Catalyst, Graphs, OrdinaryDiffEq, Test - -# Fetch test networks. -include("../spatial_test_networks.jl") - -# Pre declares a grid. -grids = [very_small_2d_cartesian_grid, very_small_2d_masked_grid, very_small_2d_graph_grid] - -### Tests LatticeReactionSystem Getters Correctness ### - -# Test case 1. -let - rs = @reaction_network begin - (p, 1), 0 <--> X - end - 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. -let - rs = @reaction_network begin - @parameters p1 p2 [edgeparameter=true] - end - @unpack p1, p2 = rs - - @test !isedgeparameter(p1) - @test isedgeparameter(p2) -end - -# Test case 3. -let - rs = @reaction_network begin - @parameters pX pY dX [edgeparameter=true] dY - (pX, 1), 0 <--> X - (pY, 1), 0 <--> Y - end - tr_1 = @transport_reaction dX X - tr_2 = @transport_reaction dY Y - 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. -let - rs = @reaction_network begin - @parameters dX p - (pX, 1), 0 <--> X - (pY, 1), 0 <--> Y - end - tr_1 = @transport_reaction dX X - 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. -let - rs = @reaction_network begin - @species W(t) - @parameters pX pY dX [edgeparameter=true] dY - (pX, 1), 0 <--> X - (pY, 1), 0 <--> Y - (pZ, 1), 0 <--> Z - (pV, 1), 0 <--> V - 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) - 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. -let - rs = @reaction_network customname begin - (p, 1), 0 <--> X - end - tr = @transport_reaction d X - for grid in grids - lrs = LatticeReactionSystem(rs, [tr], grid) - - @test nameof(lrs) == :customname - end -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 - -# 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 - 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 - -### Test Interpolation ### - -# Does not currently work. The 3 tr_macro_ lines generate errors. -# Test case 1. -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) -end - -### Tests Error generation ### - -# Test creation of TransportReaction with non-parameters in rate. -# Tests that it works even when 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 - -# 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 - for grid in grids - @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid) - end -end - -# Network where the rate depend on a species -let - rs = @reaction_network begin - @species Y(t) - (p, d), 0 <--> X - end - tr = @transport_reaction D*Y X - for grid in grids - @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid) - end -end - -# Network with edge parameter in non-spatial reaction rate. -let - rs = @reaction_network begin - @parameters p [edgeparameter=true] - (p, d), 0 <--> X - end - tr = @transport_reaction D X - 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). -let - rs = @reaction_network begin - @species X(t) [description="Species with added metadata"] - (p, d), 0 <--> X - end - tr = @transport_reaction D X - 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 - end - tr = @transport_reaction D X - @test_throws ErrorException LatticeReactionSystem(rs, [tr], grid) - end -end - - -### Tests Grid Vertex and Edge Number Computation ### - -# 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 - - # 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 lrs1.num_edges == iterator_count(edge_iterator(lrs1)) - @test lrs2.num_edges == 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 lrs1.num_edges == iterator_count(edge_iterator(lrs1)) - end -end - -### Tests Edge Value Computation Helper Functions ### - -# Checks that computes 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 - 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) - - 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 ends up in the correct place in 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, 10000.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())[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())[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())[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())[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())[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())[end][125], n^3, rtol=1e-6) -end - -# 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_test_networks.jl b/test/spatial_test_networks.jl index bb106c9ee6..924fecb045 100644 --- a/test/spatial_test_networks.jl +++ b/test/spatial_test_networks.jl @@ -31,6 +31,15 @@ function rand_e_vals(lrs::LatticeReactionSystem) 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. function spatial_param_syms(lrs::LatticeReactionSystem) ModelingToolkit.getname.(edge_parameters(lrs)) From 115b966ea76a023e45c85907de3c953b30fb34f0 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 7 Jul 2024 09:24:31 -0400 Subject: [PATCH 28/47] save progress --- .../spatial_ODE_systems.jl | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index bd9fdf8e4e..e53160de0c 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -13,6 +13,10 @@ struct LatticeTransportODEf{S,T} """The indexes of the edge parameters in the parameter vector (`parameters(lrs)`).""" edge_p_idxs::Vector{Int64} """ + Work in progress. + """ + mtk_ps + """ The non-spatial `ReactionSystem` which was used to create the `LatticeReactionSystem` contain a set of parameters (either identical to, or a sub set of, `parameters(lrs)`). This vector contain the indexes of the non-spatial system's parameters in `parameters(lrs)`. These are @@ -72,6 +76,9 @@ struct LatticeTransportODEf{S,T} edge_p_idxs = subset_indexes_of(edge_parameters(lrs), parameters(lrs)) nonspatial_rs_p_idxs = subset_indexes_of(parameters(reactionsystem(lrs)), parameters(lrs)) + # WIP. + mtk_ps = ModelingToolkit.MTKParameters(complete(convert(ODESystem, reactionsystem(lrs))), [e[1] => e[2][1] for e in vert_ps]) + # Computes the indexes of the vertex parameters in the vector of parameters. # Input `vert_ps` is a vector map taking each parameter symbolic to its value (potentially a # vector). This vector is already sorted according to the order of the parameters. Here, we extract @@ -90,7 +97,7 @@ struct LatticeTransportODEf{S,T} # Declares `work_ps` (used as storage during computation) and the edge iterator. work_ps = zeros(length(parameters(lrs))) edge_iterator = Catalyst.edge_iterator(lrs) - new{S,T}(ofunc, num_verts(lrs), num_species(lrs), vert_p_idxs, edge_p_idxs, + new{S,T}(ofunc, num_verts(lrs), num_species(lrs), vert_p_idxs, edge_p_idxs, mtk_ps, nonspatial_rs_p_idxs, vert_ps, work_ps, v_ps_idx_types, transport_rates, t_rate_idx_types, leaving_rates, edge_iterator) end @@ -109,6 +116,10 @@ struct LatticeTransportODEjac{R,S,T} """The indexes of the edge parameters in the parameter vector (`parameters(lrs)`).""" edge_p_idxs::Vector{Int64} """ + Work in progress. + """ + mtk_ps + """ The non-spatial `ReactionSystem` which was used to create the `LatticeReactionSystem` contain a set of parameters (either identical to, or a sub set of, `parameters(lrs)`). This vector contain the indexes of the non-spatial system's parameters in `parameters(lrs)`. These are @@ -145,6 +156,9 @@ struct LatticeTransportODEjac{R,S,T} edge_p_idxs = subset_indexes_of(edge_parameters(lrs), parameters(lrs)) nonspatial_rs_p_idxs = subset_indexes_of(parameters(reactionsystem(lrs)), parameters(lrs)) + # WIP. + mtk_ps = ModelingToolkit.MTKParameters(complete(convert(ODESystem, reactionsystem(lrs))), [e[1] => e[2][1] for e in vert_ps]) + # Input `vert_ps` is a vector map taking each parameter symbolic to its value (potentially a # vector). This vector is already sorted according to the order of the parameters. Here, we extract # its values only and put them into `vert_ps`. @@ -153,7 +167,7 @@ struct LatticeTransportODEjac{R,S,T} work_ps = zeros(length(parameters(lrs))) v_ps_idx_types = map(vp -> length(vp) == 1, vert_ps) new{R,S,typeof(jac_transport)}(ofunc, num_verts(lrs), num_species(lrs) , vert_p_idxs, - edge_p_idxs, nonspatial_rs_p_idxs, vert_ps, + edge_p_idxs, mtk_ps, nonspatial_rs_p_idxs, vert_ps, work_ps, v_ps_idx_types, sparse, jac_transport) end end @@ -321,9 +335,6 @@ end # Defines the forcing functor's effect on the (spatial) ODE system. function (f_func::LatticeTransportODEf)(du, u, p, t) - println(du) - println(u) - println(p) # Updates for non-spatial reactions. for vert_i in 1:(f_func.num_verts) # Gets the indices of all the species at vertex i. From 56d5f86dea16a4e0457b4e0227d3ba348551a374 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 7 Jul 2024 09:45:49 -0400 Subject: [PATCH 29/47] save progress --- .../spatial_ODE_systems.jl | 24 ++++++++++++++----- src/spatial_reaction_systems/utility.jl | 8 +++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index e53160de0c..6e713cff23 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -17,6 +17,10 @@ struct LatticeTransportODEf{S,T} """ mtk_ps """ + Work in progress. + """ + p_setters + """ The non-spatial `ReactionSystem` which was used to create the `LatticeReactionSystem` contain a set of parameters (either identical to, or a sub set of, `parameters(lrs)`). This vector contain the indexes of the non-spatial system's parameters in `parameters(lrs)`. These are @@ -77,7 +81,9 @@ struct LatticeTransportODEf{S,T} nonspatial_rs_p_idxs = subset_indexes_of(parameters(reactionsystem(lrs)), parameters(lrs)) # WIP. - mtk_ps = ModelingToolkit.MTKParameters(complete(convert(ODESystem, reactionsystem(lrs))), [e[1] => e[2][1] for e in vert_ps]) + nonspatial_osys = complete(convert(ODESystem, reactionsystem(lrs))) + mtk_ps = MT.MTKParameters(nonspatial_osys, [e[1] => e[2][1] for e in vert_ps]) + p_setters = [MT.setp(nonspatial_osys, p) for p in parameters(nonspatial_osys)] # Computes the indexes of the vertex parameters in the vector of parameters. # Input `vert_ps` is a vector map taking each parameter symbolic to its value (potentially a @@ -97,7 +103,7 @@ struct LatticeTransportODEf{S,T} # Declares `work_ps` (used as storage during computation) and the edge iterator. work_ps = zeros(length(parameters(lrs))) edge_iterator = Catalyst.edge_iterator(lrs) - new{S,T}(ofunc, num_verts(lrs), num_species(lrs), vert_p_idxs, edge_p_idxs, mtk_ps, + new{S,T}(ofunc, num_verts(lrs), num_species(lrs), vert_p_idxs, edge_p_idxs, mtk_ps, p_setters, nonspatial_rs_p_idxs, vert_ps, work_ps, v_ps_idx_types, transport_rates, t_rate_idx_types, leaving_rates, edge_iterator) end @@ -120,6 +126,10 @@ struct LatticeTransportODEjac{R,S,T} """ mtk_ps """ + Work in progress. + """ + p_setters + """ The non-spatial `ReactionSystem` which was used to create the `LatticeReactionSystem` contain a set of parameters (either identical to, or a sub set of, `parameters(lrs)`). This vector contain the indexes of the non-spatial system's parameters in `parameters(lrs)`. These are @@ -157,7 +167,9 @@ struct LatticeTransportODEjac{R,S,T} nonspatial_rs_p_idxs = subset_indexes_of(parameters(reactionsystem(lrs)), parameters(lrs)) # WIP. - mtk_ps = ModelingToolkit.MTKParameters(complete(convert(ODESystem, reactionsystem(lrs))), [e[1] => e[2][1] for e in vert_ps]) + nonspatial_osys = complete(convert(ODESystem, reactionsystem(lrs))) + mtk_ps = MT.MTKParameters(nonspatial_osys, [e[1] => e[2][1] for e in vert_ps]) + p_setters = [MT.setp(nonspatial_osys, p) for p in parameters(nonspatial_osys)] # Input `vert_ps` is a vector map taking each parameter symbolic to its value (potentially a # vector). This vector is already sorted according to the order of the parameters. Here, we extract @@ -167,7 +179,7 @@ struct LatticeTransportODEjac{R,S,T} work_ps = zeros(length(parameters(lrs))) v_ps_idx_types = map(vp -> length(vp) == 1, vert_ps) new{R,S,typeof(jac_transport)}(ofunc, num_verts(lrs), num_species(lrs) , vert_p_idxs, - edge_p_idxs, mtk_ps, nonspatial_rs_p_idxs, vert_ps, + edge_p_idxs, mtk_ps, p_setters, nonspatial_rs_p_idxs, vert_ps, work_ps, v_ps_idx_types, sparse, jac_transport) end end @@ -344,7 +356,7 @@ function (f_func::LatticeTransportODEf)(du, u, p, t) update_work_vert_ps!(f_func, p, vert_i) # Evaluate reaction contributions to du at vert_i. - f_func.ofunc((@view du[idxs]), (@view u[idxs]), nonspatial_ps(f_func), t) + f_func.ofunc((@view du[idxs]), (@view u[idxs]), f_func.mtk_ps, t) end # s_idx is the species index among transport species, s is the index among all species. @@ -372,7 +384,7 @@ function (jac_func::LatticeTransportODEjac)(J, u, p, t) for vert_i in 1:(jac_func.num_verts) idxs = get_indexes(vert_i, jac_func.num_species) update_work_vert_ps!(jac_func, p, vert_i) - jac_func.ofunc.jac((@view J[idxs, idxs]), (@view u[idxs]), nonspatial_ps(jac_func), t) + jac_func.ofunc.jac((@view J[idxs, idxs]), (@view u[idxs]), jac_func.mtk_ps, t) end # Updates for the spatial reactions (adds the Jacobian values from the transportation reactions). diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index 43c7932185..92000302cc 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -287,6 +287,14 @@ function update_work_vert_ps!(lt_ode_func, all_ps::Vector{T}, vert::Int64) where return update_work_vert_ps!(lt_ode_func.work_ps, lt_ode_func.vert_p_idxs, all_ps, vert, lt_ode_func.v_ps_idx_types) end +# Input is either a LatticeTransportODEf or LatticeTransportODEjac function (which fields we pass on). +function update_work_vert_ps!(lt_ode_func, all_ps::Vector{T}, vert::Int64) where {T} + for (setp, (idx, loc_type)) in zip(lt_ode_func.p_setters, enumerate(lt_ode_func.v_ps_idx_types)) + p_val = (loc_type ? all_ps[lt_ode_func.vert_p_idxs[idx]][1] : all_ps[lt_ode_func.vert_p_idxs[idx]][vert]) + setp(lt_ode_func.mtk_ps, p_val) + end +end + # Fetches the parameter values that currently are in the work parameter vector and which # corresponds to the parameters of the non-spatial `ReactionSystem` stored in the `ReactionSystem`. function nonspatial_ps(lt_ode_func) From 2409dcca72d284057e3ced7ad49ae9c055d97b34 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 7 Jul 2024 11:00:20 -0400 Subject: [PATCH 30/47] save prog --- .../spatial_ODE_systems.jl | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index 6e713cff23..5dede5e1df 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -68,7 +68,7 @@ struct LatticeTransportODEf{S,T} """An iterator over all the edges of the lattice.""" edge_iterator::Vector{Pair{Int64, Int64}} - function LatticeTransportODEf(ofunc::S, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{T}}}, + function LatticeTransportODEf(ofunc::S, vert_ps, transport_rates::Vector{Pair{Int64, SparseMatrixCSC{T, Int64}}}, lrs::LatticeReactionSystem) where {S,T} # Records which parameters and rates are uniform and which are not. @@ -82,7 +82,8 @@ struct LatticeTransportODEf{S,T} # WIP. nonspatial_osys = complete(convert(ODESystem, reactionsystem(lrs))) - mtk_ps = MT.MTKParameters(nonspatial_osys, [e[1] => e[2][1] for e in vert_ps]) + p_init = [p => Dict(vert_ps)[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(nonspatial_osys)] # Computes the indexes of the vertex parameters in the vector of parameters. @@ -157,7 +158,7 @@ struct LatticeTransportODEjac{R,S,T} """The transport rates. This is a dense or sparse matrix (depending on what type of Jacobian is used).""" jac_transport::T - function LatticeTransportODEjac(ofunc::R, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{S}}}, + function LatticeTransportODEjac(ofunc::R, vert_ps, jac_transport::Union{Nothing, SparseMatrixCSC{Float64, Int64}}, lrs::LatticeReactionSystem, sparse::Bool) where {R,S} @@ -250,23 +251,23 @@ function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{Basi ofunc_sparse = ODEFunction(osys; jac = true, sparse = true) jac_transport = build_jac_prototype(ofunc_sparse.jac_prototype, transport_rates, lrs; set_nonzero = true) if sparse - f = LatticeTransportODEf(ofunc_sparse, vert_ps, transport_rates, lrs) + f = LatticeTransportODEf(ofunc_sparse, [vert_ps; edge_ps], transport_rates, lrs) jac_transport = build_jac_prototype(ofunc_sparse.jac_prototype, transport_rates, lrs; set_nonzero = true) - J = LatticeTransportODEjac(ofunc_dense, vert_ps, jac_transport, lrs, true) + J = LatticeTransportODEjac(ofunc_dense, [vert_ps; edge_ps], jac_transport, lrs, true) jac_prototype = jac_transport else - f = LatticeTransportODEf(ofunc_dense, vert_ps, transport_rates, lrs) - J = LatticeTransportODEjac(ofunc_dense, vert_ps, jac_transport, lrs, false) + f = LatticeTransportODEf(ofunc_dense, [vert_ps; edge_ps], transport_rates, lrs) + J = LatticeTransportODEjac(ofunc_dense, [vert_ps; edge_ps], jac_transport, lrs, false) jac_prototype = nothing end else if sparse ofunc_sparse = ODEFunction(osys; jac = false, sparse = true) - f = LatticeTransportODEf(ofunc_sparse, vert_ps, transport_rates, lrs) + f = LatticeTransportODEf(ofunc_sparse, [vert_ps; edge_ps], transport_rates, 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, lrs) + f = LatticeTransportODEf(ofunc_dense, [vert_ps; edge_ps], transport_rates, lrs) jac_prototype = nothing end J = nothing From 1caacd5add4e6f23c1abd30a4b030e6a7b5994e7 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 7 Jul 2024 15:33:33 -0400 Subject: [PATCH 31/47] save progress --- .../spatial_ODE_systems.jl | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index 5dede5e1df..35b2a3c2a4 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -68,7 +68,7 @@ struct LatticeTransportODEf{S,T} """An iterator over all the edges of the lattice.""" edge_iterator::Vector{Pair{Int64, Int64}} - function LatticeTransportODEf(ofunc::S, vert_ps, + function LatticeTransportODEf(ofunc::S, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{T}}}, edge_ps, transport_rates::Vector{Pair{Int64, SparseMatrixCSC{T, Int64}}}, lrs::LatticeReactionSystem) where {S,T} # Records which parameters and rates are uniform and which are not. @@ -82,7 +82,7 @@ struct LatticeTransportODEf{S,T} # WIP. nonspatial_osys = complete(convert(ODESystem, reactionsystem(lrs))) - p_init = [p => Dict(vert_ps)[p][1] for p in parameters(nonspatial_osys)] + p_init = [p => Dict([vert_ps; edge_ps])[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(nonspatial_osys)] @@ -158,7 +158,7 @@ struct LatticeTransportODEjac{R,S,T} """The transport rates. This is a dense or sparse matrix (depending on what type of Jacobian is used).""" jac_transport::T - function LatticeTransportODEjac(ofunc::R, vert_ps, + function LatticeTransportODEjac(ofunc::R, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{S}}}, edge_ps, jac_transport::Union{Nothing, SparseMatrixCSC{Float64, Int64}}, lrs::LatticeReactionSystem, sparse::Bool) where {R,S} @@ -169,7 +169,8 @@ struct LatticeTransportODEjac{R,S,T} # WIP. nonspatial_osys = complete(convert(ODESystem, reactionsystem(lrs))) - mtk_ps = MT.MTKParameters(nonspatial_osys, [e[1] => e[2][1] for e in vert_ps]) + p_init = [p => Dict([vert_ps; edge_ps])[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(nonspatial_osys)] # Input `vert_ps` is a vector map taking each parameter symbolic to its value (potentially a @@ -251,23 +252,23 @@ function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{Basi ofunc_sparse = ODEFunction(osys; jac = true, sparse = true) jac_transport = build_jac_prototype(ofunc_sparse.jac_prototype, transport_rates, lrs; set_nonzero = true) if sparse - f = LatticeTransportODEf(ofunc_sparse, [vert_ps; edge_ps], transport_rates, lrs) + f = LatticeTransportODEf(ofunc_sparse, vert_ps, edge_ps, transport_rates, lrs) jac_transport = build_jac_prototype(ofunc_sparse.jac_prototype, transport_rates, lrs; set_nonzero = true) - J = LatticeTransportODEjac(ofunc_dense, [vert_ps; edge_ps], jac_transport, lrs, true) + J = LatticeTransportODEjac(ofunc_dense, vert_ps, edge_ps, jac_transport, lrs, true) jac_prototype = jac_transport else - f = LatticeTransportODEf(ofunc_dense, [vert_ps; edge_ps], transport_rates, lrs) - J = LatticeTransportODEjac(ofunc_dense, [vert_ps; edge_ps], jac_transport, lrs, false) + f = LatticeTransportODEf(ofunc_dense, vert_ps, edge_ps, transport_rates, lrs) + J = LatticeTransportODEjac(ofunc_dense, vert_ps, edge_ps, jac_transport, lrs, false) jac_prototype = nothing end else if sparse ofunc_sparse = ODEFunction(osys; jac = false, sparse = true) - f = LatticeTransportODEf(ofunc_sparse, [vert_ps; edge_ps], transport_rates, lrs) + f = LatticeTransportODEf(ofunc_sparse, vert_ps, edge_ps, transport_rates, 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; edge_ps], transport_rates, lrs) + f = LatticeTransportODEf(ofunc_dense, vert_ps, edge_ps, transport_rates, lrs) jac_prototype = nothing end J = nothing @@ -347,7 +348,7 @@ function build_jac_prototype(ns_jac_prototype::SparseMatrixCSC{Float64, Int64}, end # Defines the forcing functor's effect on the (spatial) ODE system. -function (f_func::LatticeTransportODEf)(du, u, p, t) +function (f_func::LatticeTransportODEf)(du::AbstractVector, u, p, t) # Updates for non-spatial reactions. for vert_i in 1:(f_func.num_verts) # Gets the indices of all the species at vertex i. @@ -378,7 +379,7 @@ function (f_func::LatticeTransportODEf)(du, u, p, t) end # Defines the Jacobian functor's effect on the (spatial) ODE system. -function (jac_func::LatticeTransportODEjac)(J, u, p, t) +function (jac_func::LatticeTransportODEjac)(J::AbstractMatrix, u, p, t) J .= 0.0 # Update the Jacobian from non-spatial reaction terms. From ae077c20a0c0587bfb440dcade6aaa42d0f9cffb Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 7 Jul 2024 16:02:15 -0400 Subject: [PATCH 32/47] save progress --- .../spatial_ODE_systems.jl | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index 35b2a3c2a4..7d4893bea6 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -1,5 +1,130 @@ ### Spatial ODE Functor Structures ### +# Functor with information for the forcing function of a spatial ODE with spatial movement on a lattice. +struct LatticeTransportODEFunction{R,S,T} + """The ODEFunction of the (non-spatial) ReactionSystem that generated this LatticeTransportODEf instance.""" + ofunc::S + """The number of vertices.""" + num_verts::Int64 + """The number of species.""" + num_species::Int64 + """The indexes of the vertex parameters in the parameter vector (`parameters(lrs)`).""" + vert_p_idxs::Vector{Int64} + """The indexes of the edge parameters in the parameter vector (`parameters(lrs)`).""" + edge_p_idxs::Vector{Int64} + """ + Work in progress. + """ + mtk_ps + """ + Work in progress. + """ + p_setters + """ + The non-spatial `ReactionSystem` which was used to create the `LatticeReactionSystem` contain + a set of parameters (either identical to, or a sub set of, `parameters(lrs)`). This vector + contain the indexes of the non-spatial system's parameters in `parameters(lrs)`. These are + required to manage the non-spatial ODEFunction in the spatial call. + """ + nonspatial_rs_p_idxs::Vector{Int64} + """The values of the parameters that are tied to vertices.""" + vert_ps::Vector{Vector{T}} + """ + Vector for storing temporary values. Repeatedly during simulations, we need to retrieve the + parameter values in a certain vertex. However, since most parameters (likely) are uniform + (and hence only have 1 value stored), we need to create a new vector each time we need to retrieve + the parameter values in a new vertex. To avoid relocating these values repeatedly, we write them + to this vector. + """ + work_ps::Vector{T} + """ + For each parameter in vert_ps, its value is a vector with a length of either num_verts or 1. + To know whenever a parameter's value needs expanding to the work_ps array, its length needs checking. + This check is done once, and the value is stored in this array. True means a uniform value. + """ + v_ps_idx_types::Vector{Bool} + """ + 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. + """ + transport_rates::Vector{Pair{Int64,SparseMatrixCSC{T, Int64}}} + """ + 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. + """ + t_rate_idx_types::Vector{Bool} + """ + 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). + """ + leaving_rates::Matrix{T} + """An iterator over all the edges of the lattice.""" + edge_iterator::Vector{Pair{Int64, Int64}} + """Whether the Jacobian is sparse or not.""" + sparse::Bool + """The transport rates. This is a dense or sparse matrix (depending on what type of Jacobian is used).""" + jac_transport::R + + function LatticeTransportODEFunction(ofunc::S, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{T}}}, edge_ps, + transport_rates::Vector{Pair{Int64, SparseMatrixCSC{T, Int64}}}, + lrs::LatticeReactionSystem) where {S,T} + # Records which parameters and rates are uniform and which are not. + v_ps_idx_types = map(vp -> length(vp[2]) == 1, vert_ps) + t_rate_idx_types = map(tr -> size(tr[2]) == (1,1), transport_rates) + + # Computes the indexes of various parameters in in the `parameters(lrs)` vector. + vert_p_idxs = subset_indexes_of(vertex_parameters(lrs), parameters(lrs)) + edge_p_idxs = subset_indexes_of(edge_parameters(lrs), parameters(lrs)) + nonspatial_rs_p_idxs = subset_indexes_of(parameters(reactionsystem(lrs)), parameters(lrs)) + + # WIP. + nonspatial_osys = complete(convert(ODESystem, reactionsystem(lrs))) + p_init = [p => Dict([vert_ps; edge_ps])[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(nonspatial_osys)] + + # Computes the indexes of the vertex parameters in the vector of parameters. + # Input `vert_ps` is a vector map taking each parameter symbolic to its value (potentially a + # vector). This vector is already sorted according to the order of the parameters. Here, we extract + # its values only and put them into `vert_ps`. + vert_ps = [vp[2] for vp in vert_ps] + + # Computes the leaving rate matrix. + 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 + + # Declares `work_ps` (used as storage during computation) and the edge iterator. + work_ps = zeros(length(parameters(lrs))) + edge_iterator = Catalyst.edge_iterator(lrs) + new{S,T}(ofunc, num_verts(lrs), num_species(lrs), vert_p_idxs, edge_p_idxs, mtk_ps, p_setters, + nonspatial_rs_p_idxs, vert_ps, work_ps, v_ps_idx_types, transport_rates, + t_rate_idx_types, leaving_rates, edge_iterator) + end +end + + + + + + + + + + + + # Functor with information for the forcing function of a spatial ODE with spatial movement on a lattice. struct LatticeTransportODEf{S,T} """The ODEFunction of the (non-spatial) ReactionSystem that generated this LatticeTransportODEf instance.""" From bb6370d01e867b091e457fb265f78666bf364b2f Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 7 Jul 2024 16:47:19 -0400 Subject: [PATCH 33/47] save progress --- .../spatial_ODE_systems.jl | 63 ++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index 7d4893bea6..7b2751838e 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -74,7 +74,8 @@ struct LatticeTransportODEFunction{R,S,T} function LatticeTransportODEFunction(ofunc::S, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{T}}}, edge_ps, transport_rates::Vector{Pair{Int64, SparseMatrixCSC{T, Int64}}}, - lrs::LatticeReactionSystem) where {S,T} + lrs::LatticeReactionSystem, jac_transport::Union{Nothing, SparseMatrixCSC{Float64, Int64}}, + sparse::Bool) where {S,T} # Records which parameters and rates are uniform and which are not. v_ps_idx_types = map(vp -> length(vp[2]) == 1, vert_ps) t_rate_idx_types = map(tr -> size(tr[2]) == (1,1), transport_rates) @@ -108,12 +109,58 @@ struct LatticeTransportODEFunction{R,S,T} # Declares `work_ps` (used as storage during computation) and the edge iterator. work_ps = zeros(length(parameters(lrs))) edge_iterator = Catalyst.edge_iterator(lrs) - new{S,T}(ofunc, num_verts(lrs), num_species(lrs), vert_p_idxs, edge_p_idxs, mtk_ps, p_setters, + new{typeof(jac_transport),S,T}(ofunc, num_verts(lrs), num_species(lrs), vert_p_idxs, edge_p_idxs, mtk_ps, p_setters, nonspatial_rs_p_idxs, vert_ps, work_ps, v_ps_idx_types, transport_rates, - t_rate_idx_types, leaving_rates, edge_iterator) + t_rate_idx_types, leaving_rates, edge_iterator, jac_transport) + end +end + + +# Defines the forcing functor's effect on the (spatial) ODE system. +function (f_func::LatticeTransportODEFunction)(du::AbstractVector, u, p, t) + # Updates for non-spatial reactions. + for vert_i in 1:(f_func.num_verts) + # Gets the indices of all the species at vertex i. + idxs = get_indexes(vert_i, f_func.num_species) + + # Updates the work vector to contain the vertex parameter values for vertex vert_i. + update_work_vert_ps!(f_func, p, vert_i) + + # Evaluate reaction contributions to du at vert_i. + f_func.ofunc((@view du[idxs]), (@view u[idxs]), f_func.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(f_func.transport_rates) + # Rate for leaving source vertex vert_i. + for vert_i in 1:(f_func.num_verts) + idx_src = get_index(vert_i, s, f_func.num_species) + du[idx_src] -= f_func.leaving_rates[s_idx, vert_i] * u[idx_src] + end + # Add rates for entering a destination vertex via an incoming edge. + for e in f_func.edge_iterator + idx_src = get_index(e[1], s, f_func.num_species) + idx_dst = get_index(e[2], s, f_func.num_species) + du[idx_dst] += get_transport_rate(s_idx, f_func, e) * u[idx_src] + end end end +# Defines the Jacobian functor's effect on the (spatial) ODE system. +function (jac_func::LatticeTransportODEFunction)(J::AbstractMatrix, u, p, t) + J .= 0.0 + + # Update the Jacobian from non-spatial reaction terms. + for vert_i in 1:(jac_func.num_verts) + idxs = get_indexes(vert_i, jac_func.num_species) + update_work_vert_ps!(jac_func, p, vert_i) + jac_func.ofunc.jac((@view J[idxs, idxs]), (@view u[idxs]), jac_func.mtk_ps, t) + end + + # Updates for the spatial reactions (adds the Jacobian values from the transportation reactions). + J .+= jac_func.jac_transport +end @@ -399,6 +446,16 @@ function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{Basi J = nothing end + ofunc_dense = ODEFunction(osys; jac = true, sparse = false) + ofunc_sparse = ODEFunction(osys; jac = true, sparse = true) + jac_transport = build_jac_prototype(ofunc_sparse.jac_prototype, transport_rates, lrs; set_nonzero = true) + f = LatticeTransportODEFunction(ofunc_sparse, vert_ps, edge_ps, transport_rates, lrs, jac_transport, sparse) + if jac + J = f + else + J = nothing + end + return ODEFunction(f; jac = J, jac_prototype = jac_prototype) end From d7cd5c2f3962186cc78bfe70b3a6f07a930898cd Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 7 Jul 2024 21:48:54 -0400 Subject: [PATCH 34/47] save progress --- .../spatial_ODE_systems.jl | 489 ++++-------------- src/spatial_reaction_systems/utility.jl | 30 +- 2 files changed, 107 insertions(+), 412 deletions(-) diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index 7b2751838e..ef79a0752c 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -1,57 +1,44 @@ -### 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 LatticeTransportODEFunction{R,S,T} - """The ODEFunction of the (non-spatial) ReactionSystem that generated this LatticeTransportODEf instance.""" - ofunc::S - """The number of vertices.""" - num_verts::Int64 - """The number of species.""" - num_species::Int64 - """The indexes of the vertex parameters in the parameter vector (`parameters(lrs)`).""" - vert_p_idxs::Vector{Int64} - """The indexes of the edge parameters in the parameter vector (`parameters(lrs)`).""" - edge_p_idxs::Vector{Int64} - """ - Work in progress. +# Functor with information about a spatial Lattice Reaction ODE;s forcing and Jacobian functions. +# Also used as ODE Function input to corresponding `ODEProblem`. +struct LatticeTransportODEFunction{P,Q,R,S,T} """ - mtk_ps + The ODEFunction of the (non-spatial) ReactionSystem that generated this + LatticeTransportODEFunction instance. """ - Work in progress. - """ - p_setters + ofunc::P + """The lattice's number of vertices.""" + num_verts::Int64 + """The system's number of species.""" + num_species::Int64 """ - The non-spatial `ReactionSystem` which was used to create the `LatticeReactionSystem` contain - a set of parameters (either identical to, or a sub set of, `parameters(lrs)`). This vector - contain the indexes of the non-spatial system's parameters in `parameters(lrs)`. These are - required to manage the non-spatial ODEFunction in the spatial call. + 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)`). """ - nonspatial_rs_p_idxs::Vector{Int64} - """The values of the parameters that are tied to vertices.""" - vert_ps::Vector{Vector{T}} + heterogeneous_vert_p_idxs::Vector{Int64} """ - Vector for storing temporary values. Repeatedly during simulations, we need to retrieve the - parameter values in a certain vertex. However, since most parameters (likely) are uniform - (and hence only have 1 value stored), we need to create a new vector each time we need to retrieve - the parameter values in a new vertex. To avoid relocating these values repeatedly, we write them - to this vector. + 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. """ - work_ps::Vector{T} + mtk_ps::Q """ - For each parameter in vert_ps, its value is a vector with a length of either num_verts or 1. - To know whenever a parameter's value needs expanding to the work_ps array, its length needs checking. - This check is done once, and the value is stored in this array. True means a uniform value. + Stores a SymbolicIndexingInterface `setp` function for each heterogeneous vertex parameter (i.e. + vertex parameter which 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`. """ - v_ps_idx_types::Vector{Bool} + p_setters::R """ 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. + 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. """ - transport_rates::Vector{Pair{Int64,SparseMatrixCSC{T, Int64}}} + transport_rates::Vector{Pair{Int64,SparseMatrixCSC{S, Int64}}} """ 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 @@ -60,44 +47,37 @@ struct LatticeTransportODEFunction{R,S,T} """ t_rate_idx_types::Vector{Bool} """ - 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). + 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). """ - leaving_rates::Matrix{T} + leaving_rates::Matrix{S} """An iterator over all the edges of the lattice.""" edge_iterator::Vector{Pair{Int64, Int64}} - """Whether the Jacobian is sparse or not.""" - sparse::Bool - """The transport rates. This is a dense or sparse matrix (depending on what type of Jacobian is used).""" - jac_transport::R + """ + The transport rates. This is a dense or sparse matrix (depending on what type of Jacobian is + used). + """ + jac_transport::T - function LatticeTransportODEFunction(ofunc::S, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{T}}}, edge_ps, - transport_rates::Vector{Pair{Int64, SparseMatrixCSC{T, Int64}}}, - lrs::LatticeReactionSystem, jac_transport::Union{Nothing, SparseMatrixCSC{Float64, Int64}}, - sparse::Bool) where {S,T} - # Records which parameters and rates are uniform and which are not. - v_ps_idx_types = map(vp -> length(vp[2]) == 1, vert_ps) - t_rate_idx_types = map(tr -> size(tr[2]) == (1,1), transport_rates) - - # Computes the indexes of various parameters in in the `parameters(lrs)` vector. - vert_p_idxs = subset_indexes_of(vertex_parameters(lrs), parameters(lrs)) - edge_p_idxs = subset_indexes_of(edge_parameters(lrs), parameters(lrs)) - nonspatial_rs_p_idxs = subset_indexes_of(parameters(reactionsystem(lrs)), parameters(lrs)) + function LatticeTransportODEFunction(ofunc::P, ps::Vector{<:Pair}, + lrs::LatticeReactionSystem, transport_rates::Vector{Pair{Int64, SparseMatrixCSC{S, Int64}}}, + jac_transport::Union{Nothing, Matrix{S}, SparseMatrixCSC{S, Int64}}, sparse::Bool) where {P,S} + + # Creates a vector with the heterogeneous vertex parameters' indexes in the full parameter vector. + p_dict = Dict(ps) + heterogeneous_vert_p_idxs = findall((p_dict[p] isa Vector) && (length(p_dict[p]) > 1) + for p in parameters(lrs)) - # WIP. + # Creates the MTKParameters structure and `p_setters` vector (which are used to manage + # the vertex parameter values during the simulations). nonspatial_osys = complete(convert(ODESystem, reactionsystem(lrs))) - p_init = [p => Dict([vert_ps; edge_ps])[p][1] for p in parameters(nonspatial_osys)] + 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(nonspatial_osys)] + p_setters = [MT.setp(nonspatial_osys, p) for p in parameters(lrs)[heterogeneous_vert_p_idxs]] - # Computes the indexes of the vertex parameters in the vector of parameters. - # Input `vert_ps` is a vector map taking each parameter symbolic to its value (potentially a - # vector). This vector is already sorted according to the order of the parameters. Here, we extract - # its values only and put them into `vert_ps`. - vert_ps = [vp[2] for vp in vert_ps] - - # Computes the leaving rate matrix. + # Computes the transport rate type vector and leaving rate matrix. + 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) @@ -106,261 +86,61 @@ struct LatticeTransportODEFunction{R,S,T} end end - # Declares `work_ps` (used as storage during computation) and the edge iterator. - work_ps = zeros(length(parameters(lrs))) - edge_iterator = Catalyst.edge_iterator(lrs) - new{typeof(jac_transport),S,T}(ofunc, num_verts(lrs), num_species(lrs), vert_p_idxs, edge_p_idxs, mtk_ps, p_setters, - nonspatial_rs_p_idxs, vert_ps, work_ps, v_ps_idx_types, transport_rates, - t_rate_idx_types, leaving_rates, edge_iterator, jac_transport) + # 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) end end - -# Defines the forcing functor's effect on the (spatial) ODE system. -function (f_func::LatticeTransportODEFunction)(du::AbstractVector, u, p, t) +# 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:(f_func.num_verts) + for vert_i in 1:(lt_ofun.num_verts) # Gets the indices of all the species at vertex i. - idxs = get_indexes(vert_i, f_func.num_species) + idxs = get_indexes(vert_i, lt_ofun.num_species) - # Updates the work vector to contain the vertex parameter values for vertex vert_i. - update_work_vert_ps!(f_func, p, vert_i) - - # Evaluate reaction contributions to du at vert_i. - f_func.ofunc((@view du[idxs]), (@view u[idxs]), f_func.mtk_ps, t) + # 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(f_func.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:(f_func.num_verts) - idx_src = get_index(vert_i, s, f_func.num_species) - du[idx_src] -= f_func.leaving_rates[s_idx, vert_i] * u[idx_src] + 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 f_func.edge_iterator - idx_src = get_index(e[1], s, f_func.num_species) - idx_dst = get_index(e[2], s, f_func.num_species) - du[idx_dst] += get_transport_rate(s_idx, f_func, e) * u[idx_src] + 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(s_idx, lt_ofun, e) * u[idx_src] end end end -# Defines the Jacobian functor's effect on the (spatial) ODE system. -function (jac_func::LatticeTransportODEFunction)(J::AbstractMatrix, u, p, t) +# 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:(jac_func.num_verts) - idxs = get_indexes(vert_i, jac_func.num_species) - update_work_vert_ps!(jac_func, p, vert_i) - jac_func.ofunc.jac((@view J[idxs, idxs]), (@view u[idxs]), jac_func.mtk_ps, t) + 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 .+= jac_func.jac_transport -end - - - - - - - - - - - -# Functor with information for the forcing function of a spatial ODE with spatial movement on a lattice. -struct LatticeTransportODEf{S,T} - """The ODEFunction of the (non-spatial) ReactionSystem that generated this LatticeTransportODEf instance.""" - ofunc::S - """The number of vertices.""" - num_verts::Int64 - """The number of species.""" - num_species::Int64 - """The indexes of the vertex parameters in the parameter vector (`parameters(lrs)`).""" - vert_p_idxs::Vector{Int64} - """The indexes of the edge parameters in the parameter vector (`parameters(lrs)`).""" - edge_p_idxs::Vector{Int64} - """ - Work in progress. - """ - mtk_ps - """ - Work in progress. - """ - p_setters - """ - The non-spatial `ReactionSystem` which was used to create the `LatticeReactionSystem` contain - a set of parameters (either identical to, or a sub set of, `parameters(lrs)`). This vector - contain the indexes of the non-spatial system's parameters in `parameters(lrs)`. These are - required to manage the non-spatial ODEFunction in the spatial call. - """ - nonspatial_rs_p_idxs::Vector{Int64} - """The values of the parameters that are tied to vertices.""" - vert_ps::Vector{Vector{T}} - """ - Vector for storing temporary values. Repeatedly during simulations, we need to retrieve the - parameter values in a certain vertex. However, since most parameters (likely) are uniform - (and hence only have 1 value stored), we need to create a new vector each time we need to retrieve - the parameter values in a new vertex. To avoid relocating these values repeatedly, we write them - to this vector. - """ - work_ps::Vector{T} - """ - For each parameter in vert_ps, its value is a vector with a length of either num_verts or 1. - To know whenever a parameter's value needs expanding to the work_ps array, its length needs checking. - This check is done once, and the value is stored in this array. True means a uniform value. - """ - v_ps_idx_types::Vector{Bool} - """ - 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. - """ - transport_rates::Vector{Pair{Int64,SparseMatrixCSC{T, Int64}}} - """ - 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. - """ - t_rate_idx_types::Vector{Bool} - """ - 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). - """ - leaving_rates::Matrix{T} - """An iterator over all the edges of the lattice.""" - edge_iterator::Vector{Pair{Int64, Int64}} - - function LatticeTransportODEf(ofunc::S, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{T}}}, edge_ps, - transport_rates::Vector{Pair{Int64, SparseMatrixCSC{T, Int64}}}, - lrs::LatticeReactionSystem) where {S,T} - # Records which parameters and rates are uniform and which are not. - v_ps_idx_types = map(vp -> length(vp[2]) == 1, vert_ps) - t_rate_idx_types = map(tr -> size(tr[2]) == (1,1), transport_rates) - - # Computes the indexes of various parameters in in the `parameters(lrs)` vector. - vert_p_idxs = subset_indexes_of(vertex_parameters(lrs), parameters(lrs)) - edge_p_idxs = subset_indexes_of(edge_parameters(lrs), parameters(lrs)) - nonspatial_rs_p_idxs = subset_indexes_of(parameters(reactionsystem(lrs)), parameters(lrs)) - - # WIP. - nonspatial_osys = complete(convert(ODESystem, reactionsystem(lrs))) - p_init = [p => Dict([vert_ps; edge_ps])[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(nonspatial_osys)] - - # Computes the indexes of the vertex parameters in the vector of parameters. - # Input `vert_ps` is a vector map taking each parameter symbolic to its value (potentially a - # vector). This vector is already sorted according to the order of the parameters. Here, we extract - # its values only and put them into `vert_ps`. - vert_ps = [vp[2] for vp in vert_ps] - - # Computes the leaving rate matrix. - 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 - - # Declares `work_ps` (used as storage during computation) and the edge iterator. - work_ps = zeros(length(parameters(lrs))) - edge_iterator = Catalyst.edge_iterator(lrs) - new{S,T}(ofunc, num_verts(lrs), num_species(lrs), vert_p_idxs, edge_p_idxs, mtk_ps, p_setters, - nonspatial_rs_p_idxs, vert_ps, work_ps, v_ps_idx_types, transport_rates, - t_rate_idx_types, leaving_rates, edge_iterator) - end -end - -# Functor with information for the Jacobian function of a spatial ODE with spatial movement on a lattice. -struct LatticeTransportODEjac{R,S,T} - """The ODEFunction of the (non-spatial) ReactionSystem that generated this LatticeTransportODEf instance.""" - ofunc::R - """The number of vertices.""" - num_verts::Int64 - """The number of species.""" - num_species::Int64 - """The indexes of the vertex parameters in the parameter vector (`parameters(lrs)`).""" - vert_p_idxs::Vector{Int64} - """The indexes of the edge parameters in the parameter vector (`parameters(lrs)`).""" - edge_p_idxs::Vector{Int64} - """ - Work in progress. - """ - mtk_ps - """ - Work in progress. - """ - p_setters - """ - The non-spatial `ReactionSystem` which was used to create the `LatticeReactionSystem` contain - a set of parameters (either identical to, or a sub set of, `parameters(lrs)`). This vector - contain the indexes of the non-spatial system's parameters in `parameters(lrs)`. These are - required to manage the non-spatial ODEFunction in the spatial call. - """ - nonspatial_rs_p_idxs::Vector{Int64} - """The values of the parameters that are tied to vertices.""" - vert_ps::Vector{Vector{S}} - """ - Vector for storing temporary values. Repeatedly during simulations, we need to retrieve the - parameter values in a certain vertex. However, since most parameters (likely) are uniform - (and hence only have 1 value stored), we need to create a new vector each time we need to retrieve - the parameter values in a new vertex. To avoid relocating these values repeatedly, we write them - to this vector. - """ - work_ps::Vector{S} - """ - For each parameter in vert_ps, its value is a vector with a length of either num_verts or 1. - To know whenever a parameter's value needs expanding to the work_ps array, its length needs checking. - This check is done once, and the value is stored in this array. True means a uniform value. - """ - v_ps_idx_types::Vector{Bool} - """Whether the Jacobian is sparse or not.""" - sparse::Bool - """The transport rates. This is a dense or sparse matrix (depending on what type of Jacobian is used).""" - jac_transport::T - - function LatticeTransportODEjac(ofunc::R, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{S}}}, edge_ps, - jac_transport::Union{Nothing, SparseMatrixCSC{Float64, Int64}}, - lrs::LatticeReactionSystem, sparse::Bool) where {R,S} - - # Computes the indexes of various parameters in in the `parameters(lrs)` vector. - vert_p_idxs = subset_indexes_of(vertex_parameters(lrs), parameters(lrs)) - edge_p_idxs = subset_indexes_of(edge_parameters(lrs), parameters(lrs)) - nonspatial_rs_p_idxs = subset_indexes_of(parameters(reactionsystem(lrs)), parameters(lrs)) - - # WIP. - nonspatial_osys = complete(convert(ODESystem, reactionsystem(lrs))) - p_init = [p => Dict([vert_ps; edge_ps])[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(nonspatial_osys)] - - # Input `vert_ps` is a vector map taking each parameter symbolic to its value (potentially a - # vector). This vector is already sorted according to the order of the parameters. Here, we extract - # its values only and put them into `vert_ps`. - vert_ps = [vp[2] for vp in vert_ps] - - work_ps = zeros(length(parameters(lrs))) - v_ps_idx_types = map(vp -> length(vp) == 1, vert_ps) - new{R,S,typeof(jac_transport)}(ofunc, num_verts(lrs), num_species(lrs) , vert_p_idxs, - edge_p_idxs, mtk_ps, p_setters, nonspatial_rs_p_idxs, vert_ps, - work_ps, v_ps_idx_types, sparse, jac_transport) - end -end - -# For each symbolic in syms1, returns a vector with their indexes in syms2. -function subset_indexes_of(syms1, syms2) - [findfirst(isequal(sym1, sym2) for sym2 in syms2) for sym1 in syms1] + J .+= lt_ofun.jac_transport end ### ODEProblem ### @@ -406,57 +186,36 @@ function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{Basi edge_ps::Vector{Pair{BasicSymbolic{Real},SparseMatrixCSC{T, Int64}}}, jac::Bool, sparse::Bool, name, include_zero_odes, combinatoric_ratelaws, remove_conserved, checks) where {T} + # Error check. if remove_conserved error("Removal of conserved quantities is currently not supported for `LatticeReactionSystem`s") end - # Creates a map, taking (the index in species(lrs) of) each species (with transportation) - # to its transportation rate (uniform or one value for each edge). The rates are sparse matrices. - transport_rates = make_sidxs_to_transrate_map(vert_ps, edge_ps, lrs) - - # Prepares the Jacobian and forcing functions (depending on jacobian and sparsity selection). + # Prepares the inputs to the `LatticeTransportODEFunction` functor. osys = complete(convert(ODESystem, reactionsystem(lrs); 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 versions of these functions for generic input. - ofunc_dense = ODEFunction(osys; jac = true, sparse = false) - ofunc_sparse = ODEFunction(osys; jac = true, sparse = true) - jac_transport = build_jac_prototype(ofunc_sparse.jac_prototype, transport_rates, lrs; set_nonzero = true) - if sparse - f = LatticeTransportODEf(ofunc_sparse, vert_ps, edge_ps, transport_rates, lrs) - jac_transport = build_jac_prototype(ofunc_sparse.jac_prototype, transport_rates, lrs; set_nonzero = true) - J = LatticeTransportODEjac(ofunc_dense, vert_ps, edge_ps, jac_transport, lrs, true) - jac_prototype = jac_transport - else - f = LatticeTransportODEf(ofunc_dense, vert_ps, edge_ps, transport_rates, lrs) - J = LatticeTransportODEjac(ofunc_dense, vert_ps, edge_ps, jac_transport, lrs, false) - jac_prototype = nothing - end - else - if sparse - ofunc_sparse = ODEFunction(osys; jac = false, sparse = true) - f = LatticeTransportODEf(ofunc_sparse, vert_ps, edge_ps, transport_rates, 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, edge_ps, transport_rates, lrs) - jac_prototype = nothing - end - J = nothing - end - ofunc_dense = ODEFunction(osys; jac = true, sparse = false) ofunc_sparse = ODEFunction(osys; jac = true, sparse = true) - jac_transport = build_jac_prototype(ofunc_sparse.jac_prototype, transport_rates, lrs; set_nonzero = true) - f = LatticeTransportODEFunction(ofunc_sparse, vert_ps, edge_ps, transport_rates, lrs, jac_transport, sparse) - if jac - J = f + transport_rates = make_sidxs_to_transrate_map(vert_ps, edge_ps, lrs) + + # Depending on Jacobian and sparsity options, computes the Jacobian transport matrix and prototype. + if sparse && !jac + jac_transport = nothing + jac_prototype = nothing else - 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, transport_rates, jac_transport, sparse) + J = (jac ? f : nothing) + + # Extracts the `Symbol` form for species and parameters. Creates and returns the `ODEFunction`. + syms = MT.getname.(species(lrs)) + paramsyms = MT.getname.(parameters(lrs)) + return ODEFunction(f; jac = J, jac_prototype, syms, paramsyms) end # Builds a jacobian prototype. @@ -528,49 +287,3 @@ function build_jac_prototype(ns_jac_prototype::SparseMatrixCSC{Float64, Int64}, return jac_prototype end - -# Defines the forcing functor's effect on the (spatial) ODE system. -function (f_func::LatticeTransportODEf)(du::AbstractVector, u, p, t) - # Updates for non-spatial reactions. - for vert_i in 1:(f_func.num_verts) - # Gets the indices of all the species at vertex i. - idxs = get_indexes(vert_i, f_func.num_species) - - # Updates the work vector to contain the vertex parameter values for vertex vert_i. - update_work_vert_ps!(f_func, p, vert_i) - - # Evaluate reaction contributions to du at vert_i. - f_func.ofunc((@view du[idxs]), (@view u[idxs]), f_func.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(f_func.transport_rates) - # Rate for leaving source vertex vert_i. - for vert_i in 1:(f_func.num_verts) - idx_src = get_index(vert_i, s, f_func.num_species) - du[idx_src] -= f_func.leaving_rates[s_idx, vert_i] * u[idx_src] - end - # Add rates for entering a destination vertex via an incoming edge. - for e in f_func.edge_iterator - idx_src = get_index(e[1], s, f_func.num_species) - idx_dst = get_index(e[2], s, f_func.num_species) - du[idx_dst] += get_transport_rate(s_idx, f_func, e) * u[idx_src] - end - end -end - -# Defines the Jacobian functor's effect on the (spatial) ODE system. -function (jac_func::LatticeTransportODEjac)(J::AbstractMatrix, u, p, t) - J .= 0.0 - - # Update the Jacobian from non-spatial reaction terms. - for vert_i in 1:(jac_func.num_verts) - idxs = get_indexes(vert_i, jac_func.num_species) - update_work_vert_ps!(jac_func, p, vert_i) - jac_func.ofunc.jac((@view J[idxs, idxs]), (@view u[idxs]), jac_func.mtk_ps, t) - end - - # Updates for the spatial reactions (adds the Jacobian values from the transportation reactions). - J .+= jac_func.jac_transport -end diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index 92000302cc..3f7952f2ea 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -265,33 +265,15 @@ function get_transport_rate(transport_rate::SparseMatrixCSC{T, Int64}, edge::Pai return t_rate_idx_types ? transport_rate[1,1] : transport_rate[edge[1],edge[2]] end # Finds the transportation rate for a specific species, LatticeTransportODEf struct, and edge. -function get_transport_rate(trans_s_idx::Int64, f_func::LatticeTransportODEf, edge::Pair{Int64,Int64}) +function get_transport_rate(trans_s_idx::Int64, f_func, edge::Pair{Int64,Int64}) get_transport_rate(f_func.transport_rates[trans_s_idx][2], edge, f_func.t_rate_idx_types[trans_s_idx]) end -# Updates the internal work_ps vector for a given vertex. Updates only the parameters that -# actually are vertex parameters. -# To this vector, we write the system's parameter values at the specific vertex. -function update_work_vert_ps!(work_ps::Vector{S}, vert_p_idxs::Vector{Int64}, all_ps::Vector{T}, - vert::Int64, vert_ps_idx_types::Vector{Bool}) where {S,T} - # Loops through all parameters. - for (idx,loc_type) in enumerate(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_ps[vert_p_idxs[idx]] = (loc_type ? all_ps[vert_p_idxs[idx]][1] : all_ps[vert_p_idxs[idx]][vert]) - end -end -# Input is either a LatticeTransportODEf or LatticeTransportODEjac function (which fields we pass on). -function update_work_vert_ps!(lt_ode_func, all_ps::Vector{T}, vert::Int64) where {T} - return update_work_vert_ps!(lt_ode_func.work_ps, lt_ode_func.vert_p_idxs, all_ps, vert, lt_ode_func.v_ps_idx_types) -end - -# Input is either a LatticeTransportODEf or LatticeTransportODEjac function (which fields we pass on). -function update_work_vert_ps!(lt_ode_func, all_ps::Vector{T}, vert::Int64) where {T} - for (setp, (idx, loc_type)) in zip(lt_ode_func.p_setters, enumerate(lt_ode_func.v_ps_idx_types)) - p_val = (loc_type ? all_ps[lt_ode_func.vert_p_idxs[idx]][1] : all_ps[lt_ode_func.vert_p_idxs[idx]][vert]) - setp(lt_ode_func.mtk_ps, p_val) +# For a `LatticeTransportODEFunction`, updates 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 end From 102537e776c56b10e22605610ad9bc9229aaf038 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 8 Jul 2024 00:02:12 -0400 Subject: [PATCH 35/47] finish internal remake. Permit non-Float64 ints --- .../lattice_reaction_systems.jl | 6 +-- .../spatial_ODE_systems.jl | 14 +++--- src/spatial_reaction_systems/utility.jl | 29 ++++++------ .../lattice_reaction_systems.jl | 3 +- .../lattice_reaction_systems_ODEs.jl | 47 ++++++++++++------- .../lattice_reaction_systems_jumps.jl | 4 +- 6 files changed, 60 insertions(+), 43 deletions(-) diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index 43f410932e..7d985cb74c 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -29,17 +29,17 @@ struct LatticeReactionSystem{Q,R,S,T} <: MT.AbstractTimeDependentSystem All parameters related to the lattice reaction system (both those whose values are tied to vertices and edges). """ - parameters::Vector{BasicSymbolic{Real}} + parameters::Vector{Any} """ 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 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 diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index ef79a0752c..a037030d28 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -62,7 +62,7 @@ struct LatticeTransportODEFunction{P,Q,R,S,T} function LatticeTransportODEFunction(ofunc::P, ps::Vector{<:Pair}, lrs::LatticeReactionSystem, transport_rates::Vector{Pair{Int64, SparseMatrixCSC{S, Int64}}}, - jac_transport::Union{Nothing, Matrix{S}, SparseMatrixCSC{S, Int64}}, sparse::Bool) where {P,S} + jac_transport::Union{Nothing, Matrix{S}, SparseMatrixCSC{S, Int64}}) where {P,S} # Creates a vector with the heterogeneous vertex parameters' indexes in the full parameter vector. p_dict = Dict(ps) @@ -118,7 +118,7 @@ function (lt_ofun::LatticeTransportODEFunction)(du::AbstractVector, u, p, t) 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(s_idx, lt_ofun, e) * u[idx_src] + du[idx_dst] += get_transport_rate(rates, e, lt_ofun.t_rate_idx_types[s_idx]) * u[idx_src] end end end @@ -182,10 +182,10 @@ function DiffEqBase.ODEProblem(lrs::LatticeReactionSystem, u0_in, tspan, end # Builds an ODEFunction for a spatial ODEProblem. -function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{BasicSymbolic{Real},Vector{T}}}, - edge_ps::Vector{Pair{BasicSymbolic{Real},SparseMatrixCSC{T, Int64}}}, +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 {T} + remove_conserved, checks) where {R,S,T} # Error check. if remove_conserved error("Removal of conserved quantities is currently not supported for `LatticeReactionSystem`s") @@ -209,7 +209,7 @@ function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{Basi end # Creates the `LatticeTransportODEFunction` functor (if `jac`, sets it as the Jacobian as well). - f = LatticeTransportODEFunction(ofunc_dense, [vert_ps; edge_ps], lrs, transport_rates, jac_transport, sparse) + f = LatticeTransportODEFunction(ofunc_dense, [vert_ps; edge_ps], lrs, transport_rates, jac_transport) J = (jac ? f : nothing) # Extracts the `Symbol` form for species and parameters. Creates and returns the `ODEFunction`. @@ -268,7 +268,7 @@ function build_jac_prototype(ns_jac_prototype::SparseMatrixCSC{Float64, Int64}, end # Create a sparse Jacobian prototype with 0-valued entries. - jac_prototype = sparse(i_idxs, j_idxs, zeros(num_entries)) + jac_prototype = sparse(i_idxs, j_idxs, zeros(T, num_entries)) # Set element values. if set_nonzero diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index 3f7952f2ea..4a881118ab 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -15,7 +15,7 @@ end # 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{BasicSymbolic{Real}}, lrs::LatticeReactionSystem) +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) @@ -32,8 +32,8 @@ end # From a parameter input, split it into vertex parameters and edge parameters. # Store these in the desired internal format. -function lattice_process_p(ps_in, ps_vertex_syms::Vector{BasicSymbolic{Real}}, - ps_edge_syms::Vector{BasicSymbolic{Real}}, lrs::LatticeReactionSystem) +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). @@ -52,7 +52,7 @@ end # 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{BasicSymbolic{Real}, T}, syms::Vector{BasicSymbolic{Real}}) where {T} +function lattice_process_input(input::Dict{<:Any, T}, syms::Vector) where {T} # Error checks if !isempty(setdiff(keys(input), syms)) error("You have provided values for the following unrecognised parameters/initial conditions: $(setdiff(keys(input), syms)).") @@ -63,7 +63,7 @@ function lattice_process_input(input::Dict{BasicSymbolic{Real}, T}, syms::Vector return [sym => input[sym] for sym in syms] end -function lattice_process_input(input, syms::Vector{BasicSymbolic{Real}}) +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 @@ -71,8 +71,7 @@ function lattice_process_input(input, syms::Vector{BasicSymbolic{Real}}) end # Splits parameters into vertex and edge parameters. -# function split_parameters(ps::Vector{<: Pair}, p_vertex_syms::Vector, p_edge_syms::Vector) -function split_parameters(ps, p_vertex_syms::Vector{BasicSymbolic{Real}}, p_edge_syms::Vector{BasicSymbolic{Real}}) +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 @@ -86,7 +85,7 @@ function vertex_value_map(values, lrs::LatticeReactionSystem) end # Converts the values for an individual species/vertex parameter to its correct vector form. -function vertex_value_form(values, lrs::LatticeReactionSystem, sym::BasicSymbolic{Real}) +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]) @@ -112,7 +111,7 @@ end # 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{Real}) where {N,T} + sym::BasicSymbolic) where {N,T} if size(values) != lattice.dims error("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 @@ -124,7 +123,7 @@ end # 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{Real}) where {T} + sym::BasicSymbolic) where {T} if size(values) != size(lattice) error("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 @@ -174,9 +173,9 @@ end # 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{BasicSymbolic{Real},Vector{T}}}, - edge_ps::Vector{Pair{BasicSymbolic{Real},SparseMatrixCSC{T, Int64}}}, - lrs::LatticeReactionSystem) where {T} +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)) @@ -203,7 +202,7 @@ end # 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{Real}, p_val_dict, lrs::LatticeReactionSystem) +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) @@ -228,7 +227,7 @@ end # 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{Real}, lrs::LatticeReactionSystem) +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 diff --git a/test/spatial_modelling/lattice_reaction_systems.jl b/test/spatial_modelling/lattice_reaction_systems.jl index b2823b3111..ce8898d18c 100644 --- a/test/spatial_modelling/lattice_reaction_systems.jl +++ b/test/spatial_modelling/lattice_reaction_systems.jl @@ -223,10 +223,11 @@ let 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) # 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_2, tr_macro_2) # @test isequal(tr_3, tr_macro_3) end diff --git a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl index b8114e4eee..2805612869 100644 --- a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl +++ b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl @@ -85,24 +85,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 * (1.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 @@ -119,7 +118,7 @@ let 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. @@ -172,7 +171,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) @@ -614,6 +613,22 @@ let end end +# Tests with non-Int64 parameter values. +let + 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 = [:α => Float32(0.1), :β => Float32(0.01), :dS => Float32(0.01), :dI => Float32(0.01), :dR => Float32(0.01)] + ps_3 = [:α => 1//10, :β => 0.01, :dS => 0.01, :dI => 1//100, :dR => Float32(0.01)] + sol_base = solve(ODEProblem(lrs, u0, (0.0, 100.0), ps_1), Rosenbrock23(); savetat = 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(); savetat = 0.1) + end + end +end + # Tests various types of numbers for initial conditions/parameters (e.g. Real numbers, Float32, etc.). let # Declare u0 versions. diff --git a/test/spatial_modelling/lattice_reaction_systems_jumps.jl b/test/spatial_modelling/lattice_reaction_systems_jumps.jl index 9ad28f9ed4..51c0c8c996 100644 --- a/test/spatial_modelling/lattice_reaction_systems_jumps.jl +++ b/test/spatial_modelling/lattice_reaction_systems_jumps.jl @@ -64,7 +64,9 @@ let # Prepare various (diffusion) parameter input types. pE_1 = [:dI => 0.02, :dS => 0.01, :dR => 0.03] - pE_2 = [:dI => 0.02, :dS => uniform_e_vals(lrs, 0.01), :dR => 0.03] + 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)] From a13b812e3a986013004966fa28a98577bb72bfba Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 8 Jul 2024 00:19:13 -0400 Subject: [PATCH 36/47] test update --- .../lattice_reaction_systems_ODEs.jl | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl index 2805612869..e4b838f9ef 100644 --- a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl +++ b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl @@ -613,18 +613,20 @@ let end end -# Tests with non-Int64 parameter values. +# 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_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 = [:α => Float32(0.1), :β => Float32(0.01), :dS => Float32(0.01), :dI => Float32(0.01), :dR => Float32(0.01)] - ps_3 = [:α => 1//10, :β => 0.01, :dS => 0.01, :dI => 1//100, :dR => Float32(0.01)] - sol_base = solve(ODEProblem(lrs, u0, (0.0, 100.0), ps_1), Rosenbrock23(); savetat = 0.1) + 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(); savetat = 0.1) + @test sol_base ≈ solve(oprob, Rosenbrock23(); saveat = 0.1) end end end From 5b6d6ecbbce7bc7e10aeafdc5b31edd16f210fbd Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 8 Jul 2024 06:39:55 -0400 Subject: [PATCH 37/47] enable rebuildig oproblems/integrators --- src/Catalyst.jl | 1 + .../spatial_ODE_systems.jl | 172 ++++++++++++++---- .../spatial_reactions.jl | 5 + .../lattice_reaction_systems_ODEs.jl | 118 ++++++++++++ .../lattice_reaction_systems_jumps.jl | 5 +- 5 files changed, 260 insertions(+), 41 deletions(-) diff --git a/src/Catalyst.jl b/src/Catalyst.jl index edf896dd54..8b8895cdea 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -181,6 +181,7 @@ export make_edge_p_values, make_directed_edge_values # 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. diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index a037030d28..b186cc7199 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -1,6 +1,6 @@ ### Spatial ODE Functor Structure ### -# Functor with information about a spatial Lattice Reaction ODE;s forcing and Jacobian functions. +# 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} """ @@ -59,40 +59,60 @@ struct LatticeTransportODEFunction{P,Q,R,S,T} used). """ jac_transport::T + """ Whether sparse jacobian representation is used. """ + sparse::Bool + """Remove when we add this as problem metadata""" + lrs::LatticeReactionSystem function LatticeTransportODEFunction(ofunc::P, ps::Vector{<:Pair}, lrs::LatticeReactionSystem, transport_rates::Vector{Pair{Int64, SparseMatrixCSC{S, Int64}}}, - jac_transport::Union{Nothing, Matrix{S}, SparseMatrixCSC{S, Int64}}) where {P,S} - - # Creates a vector with the heterogeneous vertex parameters' indexes in the full parameter vector. - p_dict = Dict(ps) - heterogeneous_vert_p_idxs = findall((p_dict[p] isa Vector) && (length(p_dict[p]) > 1) - for p in parameters(lrs)) - - # Creates the MTKParameters structure and `p_setters` vector (which are used to manage - # the vertex parameter values during the simulations). - 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]] - - # Computes the transport rate type vector and leaving rate matrix. - 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 + jac_transport::Union{Nothing, Matrix{S}, SparseMatrixCSC{S, Int64}}, sparse) 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) + 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. @@ -198,7 +218,7 @@ function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{R,Ve transport_rates = make_sidxs_to_transrate_map(vert_ps, edge_ps, lrs) # Depending on Jacobian and sparsity options, computes the Jacobian transport matrix and prototype. - if sparse && !jac + if !sparse && !jac jac_transport = nothing jac_prototype = nothing else @@ -209,7 +229,7 @@ function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{R,Ve end # Creates the `LatticeTransportODEFunction` functor (if `jac`, sets it as the Jacobian as well). - f = LatticeTransportODEFunction(ofunc_dense, [vert_ps; edge_ps], lrs, transport_rates, jac_transport) + f = LatticeTransportODEFunction(ofunc_dense, [vert_ps; edge_ps], lrs, transport_rates, jac_transport, sparse) J = (jac ? f : nothing) # Extracts the `Symbol` form for species and parameters. Creates and returns the `ODEFunction`. @@ -267,23 +287,95 @@ function build_jac_prototype(ns_jac_prototype::SparseMatrixCSC{Float64, Int64}, end end - # Create a sparse Jacobian prototype with 0-valued 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 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)) + 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 ### + +# Function for rebuilding a `LatticeReactionSystem` `ODEProblem` after it has been updated. +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 + +# 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 + + # 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 + + # 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 + +# 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) + + # Updates the fields, then deletes superfluous fields, or additional ones. + for (i, v) in enumerate(vec2[1:min(l1, l2)]) + vec1[i] = v + end + foreach(idx -> deleteat!(vec1, idx), l1:-1:(l2 + 1)) + foreach(val -> push!(vec1, val), vec2[l1+1:l2]) +end \ No newline at end of file diff --git a/src/spatial_reaction_systems/spatial_reactions.jl b/src/spatial_reaction_systems/spatial_reactions.jl index 7a941761b7..004f0646a8 100644 --- a/src/spatial_reaction_systems/spatial_reactions.jl +++ b/src/spatial_reaction_systems/spatial_reactions.jl @@ -63,6 +63,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 = 4:2:(2 + 2*length(parameters)) + insert!(pexprs.args, idx, :([edgeparameter=true])) + end + quote $pexprs $iv diff --git a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl index e4b838f9ef..3e9544d5fa 100644 --- a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl +++ b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl @@ -547,6 +547,124 @@ let @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. +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] + + # Checks for all combinations of Jacobian and sparsity. + for jac in [false, true], sparse in [false, true] + # Creates simulation through two different separate simulations. + oprob_1_1 = ODEProblem(lrs, u0, (0.0, 5.0), ps_1; jac, sparse) + 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, sparse) + 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, sparse) + 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 +end + ### Tests Special Cases ### # Create network using either graphs or di-graphs. diff --git a/test/spatial_modelling/lattice_reaction_systems_jumps.jl b/test/spatial_modelling/lattice_reaction_systems_jumps.jl index 51c0c8c996..69928e9c12 100644 --- a/test/spatial_modelling/lattice_reaction_systems_jumps.jl +++ b/test/spatial_modelling/lattice_reaction_systems_jumps.jl @@ -200,4 +200,7 @@ let @test abs(d) < reltol * non_spatial_mean[i] end end -end \ No newline at end of file +end + + +### JumpProblem & Integrator Interfacing ### \ No newline at end of file From e231734ff71b10613c5caa3d498f088a042c4016 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 8 Jul 2024 07:43:15 -0400 Subject: [PATCH 38/47] add additional tests, use argumenterror where appropriate --- .../lattice_jump_systems.jl | 2 +- .../lattice_reaction_systems.jl | 18 +-- .../spatial_ODE_systems.jl | 2 +- .../spatial_reactions.jl | 2 +- src/spatial_reaction_systems/utility.jl | 24 ++-- .../lattice_reaction_systems.jl | 115 ++++++++++++++++-- .../lattice_reaction_systems_ODEs.jl | 39 +++++- .../lattice_reaction_systems_jumps.jl | 12 +- .../lattice_reaction_systems_lattice_types.jl | 4 +- 9 files changed, 181 insertions(+), 37 deletions(-) diff --git a/src/spatial_reaction_systems/lattice_jump_systems.jl b/src/spatial_reaction_systems/lattice_jump_systems.jl index 44b26ecbbd..5dd8084bf7 100644 --- a/src/spatial_reaction_systems/lattice_jump_systems.jl +++ b/src/spatial_reaction_systems/lattice_jump_systems.jl @@ -30,7 +30,7 @@ function JumpProcesses.JumpProblem(lrs::LatticeReactionSystem, dprob, aggregator combinatoric_ratelaws = get_combinatoric_ratelaws(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). diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index 7d985cb74c..ca9c88c911 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -51,19 +51,22 @@ struct LatticeReactionSystem{Q,R,S,T} <: MT.AbstractTimeDependentSystem num_verts::Int64, num_edges::Int64, edge_iterator::T) where {Q,R,S,T} # Error checks. if !(R <: AbstractSpatialReaction) - error("The second argument must be a vector of AbstractSpatialReaction subtypes.") + 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)) - error("A non-flattened (hierarchical) `ReactionSystem` was used as input. `LatticeReactionSystem`s can only be based on non-hierarchical `ReactionSystem`s.") + 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)) - error("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).") + 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)) - error("The `ReactionSystem` used as input contain equations (in addition to reactions). This is not permitted.") + 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)) - @warn "The `ReactionSystem` used as input to `LatticeReactionSystem contain events. These will be ignored in any simulations based on the created `LatticeReactionSystem`." + 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`." @@ -295,7 +298,7 @@ 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) -grid_size(lattice::Graph) = error("Grid size is only defined for LatticeReactionSystems with grid-based lattices (not graph-based).") +grid_size(lattice::Graphs.AbstractGraph) = throw(ArgumentError("Grid size is only defined for LatticeReactionSystems with grid-based lattices (not graph-based).")) """ grid_dims(lrs::LatticeReactionSystem) @@ -305,7 +308,7 @@ 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 -grid_dims(lattice::Graph) = error("Grid dimensions is only defined for LatticeReactionSystems with grid-based lattices (not graph-based).") +grid_dims(lattice::Graphs.AbstractGraph) = throw(ArgumentError("Grid dimensions is only defined for LatticeReactionSystems with grid-based lattices (not graph-based).")) """ get_lattice_graph(lrs::LatticeReactionSystem) @@ -333,7 +336,6 @@ reactions(lrs::LatticeReactionSystem) = reactions(reactionsystem(lrs)) 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.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)) diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index b186cc7199..40da3aa6b0 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -208,7 +208,7 @@ function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{R,Ve 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 # Prepares the inputs to the `LatticeTransportODEFunction` functor. diff --git a/src/spatial_reaction_systems/spatial_reactions.jl b/src/spatial_reaction_systems/spatial_reactions.jl index 004f0646a8..222ea4640b 100644 --- a/src/spatial_reaction_systems/spatial_reactions.jl +++ b/src/spatial_reaction_systems/spatial_reactions.jl @@ -165,5 +165,5 @@ function find_parameters_in_rate!(parameters, rateex::ExprValues) 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 4a881118ab..4f4930e3bd 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -55,10 +55,10 @@ end function lattice_process_input(input::Dict{<:Any, T}, syms::Vector) where {T} # Error checks if !isempty(setdiff(keys(input), syms)) - error("You have provided values for the following unrecognised parameters/initial conditions: $(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))) - error("You have not provided values for the following parameters/initial conditions: $(setdiff(syms, keys(input))).") + throw(ArgumentError("You have not provided values for the following parameters/initial conditions: $(setdiff(syms, keys(input))).")) end return [sym => input[sym] for sym in syms] @@ -67,7 +67,7 @@ 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 - error("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).") + 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. @@ -100,7 +100,7 @@ function vertex_value_form(values, lrs::LatticeReactionSystem, sym::BasicSymboli # 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)) - error("You have provided ($(length(values))) values for $sym. This is not equal to the number of vertices ($(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 @@ -113,10 +113,10 @@ end function vertex_value_form(values::AbstractArray, num_verts::Int64, lattice::CartesianGridRej{N,T}, sym::BasicSymbolic) where {N,T} if size(values) != lattice.dims - error("The values for $sym did not have the same format as the lattice. Expected a $(lattice.dims) array, got one of size $(size(values))") + 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 if (length(values) != num_verts) - error("You have provided ($(length(values))) values for $sym. This is not equal to the number of vertices ($(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 @@ -125,7 +125,7 @@ end function vertex_value_form(values::AbstractArray, num_verts::Int64, lattice::Array{Bool,T}, sym::BasicSymbolic) where {T} if size(values) != size(lattice) - error("The values for $sym did not have the same format as the lattice. Expected a $(size(lattice)) array, got one of size $(size(values))") + 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 # Pre-declares a vector with the values in each vertex (return_values). @@ -139,7 +139,7 @@ function vertex_value_form(values::AbstractArray, num_verts::Int64, lattice::Arr # Checks that the correct number of values was provided, and returns the values. if (length(return_values) != num_verts) - error("You have provided ($(length(return_values))) values for $sym. This is not equal to the number of vertices ($(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 @@ -158,10 +158,10 @@ function edge_value_form(values, lrs::LatticeReactionSystem, sym) # Error checks. if nnz(values) != num_edges(lrs) - error("You have provided ($(nnz(values))) values for $sym. This is not equal to the number of edges ($(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)) - error("Values was not provided for some edges for edge parameter $sym.") + 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 @@ -296,7 +296,7 @@ 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.") + throw(ArgumentError("An non-edge parameter was encountered in expressions: $exp. Here, only edge parameters are expected.")) end # Creates a Function tha computes the expressions value for a parameter set. @@ -320,7 +320,7 @@ 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, only 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 that computes the expressions value for a parameter set. diff --git a/test/spatial_modelling/lattice_reaction_systems.jl b/test/spatial_modelling/lattice_reaction_systems.jl index ce8898d18c..4f08f4b723 100644 --- a/test/spatial_modelling/lattice_reaction_systems.jl +++ b/test/spatial_modelling/lattice_reaction_systems.jl @@ -9,6 +9,27 @@ include("../spatial_test_networks.jl") # Pre declares a grid. grids = [very_small_2d_cartesian_grid, very_small_2d_masked_grid, very_small_2d_graph_grid] +### Test Spatial Reactions ### + +# Test creation of TransportReaction with non-parameters in rate. +# Tests that it works even when 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 + +# Checks that the `hash` functions works 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 + ### Tests LatticeReactionSystem Getters Correctness ### # Test case 1. @@ -127,6 +148,37 @@ let end end +# Tests using various more obscure types of getters. +let + # 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 Spatial Reactions Getters Correctness ### # Test case 1. @@ -233,16 +285,6 @@ end ### Tests Error generation ### -# Test creation of TransportReaction with non-parameters in rate. -# Tests that it works even when 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 - # Network where diffusion species is not declared in non-spatial network. let rs = @reaction_network begin @@ -297,6 +339,59 @@ let 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 + @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, [tr], short_path) +end + +# Tests for non-complete input `ReactionSystem`. +let + tr = @transport_reaction D X + rs = @network_component begin + (p,d), 0 <--> X + end + @test_throws ArgumentError LatticeReactionSystem(rs1, [tr], short_path) +end ### Tests Grid Vertex and Edge Number Computation ### diff --git a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl index 3e9544d5fa..b2c3035dbf 100644 --- a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl +++ b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl @@ -547,7 +547,7 @@ let @test all(isequal.(ss_1, ss_2)) end -### ODEProblem & Integrator Interfacing ### +### ODEProblem & Integrator Interfacing ### # Checks that basic interfacing with ODEProblem parameters (getting and setting) works. let @@ -788,4 +788,41 @@ let end end end +end + + +### Error Tests ### + +# 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_modelling/lattice_reaction_systems_jumps.jl b/test/spatial_modelling/lattice_reaction_systems_jumps.jl index 69928e9c12..701922381a 100644 --- a/test/spatial_modelling/lattice_reaction_systems_jumps.jl +++ b/test/spatial_modelling/lattice_reaction_systems_jumps.jl @@ -203,4 +203,14 @@ let end -### JumpProblem & Integrator Interfacing ### \ No newline at end of file +### JumpProblem & Integrator Interfacing ### + + +### 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 index de77d54d5f..bde1bb14f8 100644 --- a/test/spatial_modelling/lattice_reaction_systems_lattice_types.jl +++ b/test/spatial_modelling/lattice_reaction_systems_lattice_types.jl @@ -50,7 +50,7 @@ let @test grid_dims(masked_1d_lrs) == 1 @test grid_dims(masked_2d_lrs) == 2 @test grid_dims(masked_3d_lrs) == 3 - @test_throws Exception grid_dims(graph_lrs) + @test_throws ArgumentError grid_dims(graph_lrs) # Checks grid sizes. @test grid_size(cartesian_1d_lrs) == (5,) @@ -59,7 +59,7 @@ let @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 Exception grid_size(graph_lrs) + @test_throws ArgumentError grid_size(graph_lrs) end # Checks grid dimensions for 2d and 3d grids where some dimension is equal to 1. From a8aae9392ed195492ae39c9b3f5c09913b99eae1 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 8 Jul 2024 07:48:20 -0400 Subject: [PATCH 39/47] remove compute_edge_value function (not used after revamp). --- src/spatial_reaction_systems/utility.jl | 52 ------------------------- 1 file changed, 52 deletions(-) diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index 4f4930e3bd..e242ba3935 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -263,10 +263,6 @@ function get_transport_rate(transport_rate::SparseMatrixCSC{T, Int64}, edge::Pai t_rate_idx_types::Bool) where {T} return t_rate_idx_types ? transport_rate[1,1] : transport_rate[edge[1],edge[2]] end -# Finds the transportation rate for a specific species, LatticeTransportODEf struct, and edge. -function get_transport_rate(trans_s_idx::Int64, f_func, edge::Pair{Int64,Int64}) - get_transport_rate(f_func.transport_rates[trans_s_idx][2], edge, f_func.t_rate_idx_types[trans_s_idx]) -end # For a `LatticeTransportODEFunction`, updates its stored parameters (in `mtk_ps`) so that they # the heterogeneous parameters' values correspond to the values in the specified vertex. @@ -276,42 +272,6 @@ function update_mtk_ps!(lt_ofun::LatticeTransportODEFunction, all_ps::Vector{T}, end end -# Fetches the parameter values that currently are in the work parameter vector and which -# corresponds to the parameters of the non-spatial `ReactionSystem` stored in the `ReactionSystem`. -function nonspatial_ps(lt_ode_func) - return @view lt_ode_func.work_ps[lt_ode_func.nonspatial_rs_p_idxs] -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::Int64) - reshape(expand_component_values(values, n), length(values), n) -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) - throw(ArgumentError("An non-edge parameter was encountered in expressions: $exp. Here, only edge parameters are expected.")) - 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:num_edges(lrs)] -end - # For an expression, computes 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. @@ -339,18 +299,6 @@ 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, and a parameter set, checks if any relevant parameters have a # spatial component. Filters out any parameters that are edge parameters. function has_spatial_vertex_component(exp, ps) From aa81594f0032fd6f632dd7b42b4e9e5cc8ab501c Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 8 Jul 2024 07:54:11 -0400 Subject: [PATCH 40/47] testfix --- test/runtests.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 23cb457f4b..374e598d88 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -9,7 +9,6 @@ using SafeTestsets, Test ### Run Tests ### @time begin - @time @safetestset "Jump Lattice Systems Simulations" begin include("spatial_modelling/lattice_reaction_systems_jumps.jl") end # Tests the `ReactionSystem` structure and its properties. @time @safetestset "Reaction Structure" begin include("reactionsystem_core/reaction.jl") end From 445af774c19fa46a6726d8b466f0872012e40d59 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 8 Jul 2024 08:22:09 -0400 Subject: [PATCH 41/47] split LatticeReactiontests. Move spatial tests to the end of runtests.jl --- test/runtests.jl | 17 +- .../lattice_reaction_systems.jl | 150 ++---------------- test/spatial_modelling/spatial_reactions.jl | 136 ++++++++++++++++ 3 files changed, 158 insertions(+), 145 deletions(-) create mode 100644 test/spatial_modelling/spatial_reactions.jl diff --git a/test/runtests.jl b/test/runtests.jl index 374e598d88..eec0279da7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -50,13 +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 "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 - # Tests network visualisation. @time @safetestset "Latexify" begin include("visualisation/latexify.jl") end # Disable on Macs as can't install GraphViz via jll @@ -69,7 +62,15 @@ 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) + # Transportest 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 + end # @time diff --git a/test/spatial_modelling/lattice_reaction_systems.jl b/test/spatial_modelling/lattice_reaction_systems.jl index 4f08f4b723..d3e067edb4 100644 --- a/test/spatial_modelling/lattice_reaction_systems.jl +++ b/test/spatial_modelling/lattice_reaction_systems.jl @@ -6,29 +6,9 @@ using Catalyst, Graphs, OrdinaryDiffEq, Test # Fetch test networks. include("../spatial_test_networks.jl") -# Pre declares a grid. +# Pre-declares a set of grids. grids = [very_small_2d_cartesian_grid, very_small_2d_masked_grid, very_small_2d_graph_grid] -### Test Spatial Reactions ### - -# Test creation of TransportReaction with non-parameters in rate. -# Tests that it works even when 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 - -# Checks that the `hash` functions works 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 ### Tests LatticeReactionSystem Getters Correctness ### @@ -179,110 +159,6 @@ let @test isequal(independent_variables(lrs), [t]) 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 - -# 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 - 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 - -### Test Interpolation ### - -# Does not currently work. The 3 tr_macro_ lines generate errors. -# Test case 1. -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 - ### Tests Error generation ### # Network where diffusion species is not declared in non-spatial network. @@ -375,13 +251,13 @@ end # Tests for hierarchical input system. let t = default_t() - @parameters d + @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, [tr], short_path) + @test_throws ArgumentError LatticeReactionSystem(rs2, [TransportReaction(D, X)], CartesianGrid((2,2))) end # Tests for non-complete input `ReactionSystem`. @@ -390,7 +266,7 @@ let rs = @network_component begin (p,d), 0 <--> X end - @test_throws ArgumentError LatticeReactionSystem(rs1, [tr], short_path) + @test_throws ArgumentError LatticeReactionSystem(rs, [tr], CartesianGrid((2,2))) end ### Tests Grid Vertex and Edge Number Computation ### @@ -409,14 +285,14 @@ let 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 lrs1.num_edges == iterator_count(edge_iterator(lrs1)) - @test lrs2.num_edges == iterator_count(edge_iterator(lrs2)) + @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 lrs1.num_edges == iterator_count(edge_iterator(lrs1)) + @test num_edges(lrs1) == iterator_count(edge_iterator(lrs1)) end end @@ -462,33 +338,33 @@ let 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())[end][5], n, rtol=1e-6) + @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())[end][5:5:25], n, rtol=1e-6)) + @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())[end][25], n^2, rtol=1e-6) + @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())[end][5:5:125], n, rtol=1e-6)) + @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())[end][25:25:125], n^2, rtol=1e-6)) + @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())[end][125], n^3, rtol=1e-6) + @test isapprox(solve(oprob, Tsit5()).u[end][125], n^3, rtol=1e-6) end # Checks that erroneous input yields errors. diff --git a/test/spatial_modelling/spatial_reactions.jl b/test/spatial_modelling/spatial_reactions.jl new file mode 100644 index 0000000000..32895135da --- /dev/null +++ b/test/spatial_modelling/spatial_reactions.jl @@ -0,0 +1,136 @@ +### Preparations ### + +# Fetch packages. +using Catalyst, Graphs, OrdinaryDiffEq, Test + +# 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] + + +### 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 works 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 From 0a2192d8c527c64131d9bb3ba39ca753c28dcd53 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 8 Jul 2024 09:44:19 -0400 Subject: [PATCH 42/47] reduce test tuntimes --- ...attice_reaction_systems_ODE_performance.jl | 52 ++-- .../lattice_reaction_systems.jl | 2 +- .../lattice_reaction_systems_ODEs.jl | 229 +++++++----------- .../lattice_reaction_systems_jumps.jl | 16 +- test/spatial_modelling/spatial_reactions.jl | 8 +- test/spatial_test_networks.jl | 6 + 6 files changed, 133 insertions(+), 180 deletions(-) diff --git a/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl b/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl index 972ea1529c..804dc4bc05 100644 --- a/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl +++ b/test/performance_benchmarks/lattice_reaction_systems_ODE_performance.jl @@ -20,7 +20,7 @@ include("../spatial_test_networks.jl") # Small grid, small, non-stiff, system. let lrs = LatticeReactionSystem(SIR_system, SIR_srs_2, small_2d_graph_grid) - u0 = [:S => 990.0, :I => 20.0 * rand_v_vals(lattice(lrs)), :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) @@ -35,7 +35,7 @@ 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(lattice(lrs)), :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) @@ -50,7 +50,7 @@ end # Small grid, small, stiff, system. let lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_2d_graph_grid) - u0 = [:X => rand_v_vals(lattice(lrs), 10), :Y => rand_v_vals(lattice(lrs), 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]) @@ -65,7 +65,7 @@ end # Large grid, small, stiff, system. let lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, large_2d_grid) - u0 = [:X => rand_v_vals(lattice(lrs), 10), :Y => rand_v_vals(lattice(lrs), 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]) @@ -82,10 +82,10 @@ let lrs = LatticeReactionSystem(CuH_Amination_system, CuH_Amination_srs_2, small_2d_graph_grid) u0 = [ - :CuoAc => 0.005 .+ rand_v_vals(lattice(lrs), 0.005), - :Ligand => 0.005 .+ rand_v_vals(lattice(lrs), 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(lattice(lrs), 0.5), + :Silane => 0.5 .+ rand_v_vals(lrs, 0.5), :CuHLigand => 0.0, :SilaneOAc => 0.0, :Styrene => 0.16, @@ -113,10 +113,10 @@ let lrs = LatticeReactionSystem(CuH_Amination_system, CuH_Amination_srs_2, large_2d_grid) u0 = [ - :CuoAc => 0.005 .+ rand_v_vals(lattice(lrs), 0.005), - :Ligand => 0.005 .+ rand_v_vals(lattice(lrs), 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(lattice(lrs), 0.5), + :Silane => 0.5 .+ rand_v_vals(lrs, 0.5), :CuHLigand => 0.0, :SilaneOAc => 0.0, :Styrene => 0.16, @@ -143,14 +143,14 @@ end let lrs = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_2d_graph_grid) u0 = [ - :w => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), - :w2 => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), - :w2v => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), - :v => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), - :w2v2 => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), - :vP => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), - :σB => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), - :w2σB => 0.5 .+ rand_v_vals(lattice(lrs), 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, ] @@ -169,14 +169,14 @@ end let lrs = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, large_2d_grid) u0 = [ - :w => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), - :w2 => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), - :w2v => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), - :v => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), - :w2v2 => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), - :vP => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), - :σB => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), - :w2σB => 0.5 .+ rand_v_vals(lattice(lrs), 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, ] diff --git a/test/spatial_modelling/lattice_reaction_systems.jl b/test/spatial_modelling/lattice_reaction_systems.jl index d3e067edb4..52b0ac916b 100644 --- a/test/spatial_modelling/lattice_reaction_systems.jl +++ b/test/spatial_modelling/lattice_reaction_systems.jl @@ -331,7 +331,7 @@ let end n = 5 tr = @transport_reaction D X - tspan = (0.0, 10000.0) + tspan = (0.0, 1000.0) u0 = [:X => 1.0] # Checks the 1d case. diff --git a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl index b2c3035dbf..92d905e169 100644 --- a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl +++ b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl @@ -15,68 +15,33 @@ rng = StableRNG(12345) t = default_t() ### Tests Simulations Don't Error ### -for grid in [small_2d_graph_grid, short_path, small_directed_cycle, - 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_2d_masked_grid] - # 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(lattice(lrs)), :I => 1.0, :R => 0.0] - u0_3 = [ - :S => 500.0 .+ 500.0 * rand_v_vals(lattice(lrs)), - :I => 50 * rand_v_vals(lattice(lrs)), - :R => 50 * rand_v_vals(lattice(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(lattice(lrs))] - pV_3 = [ - :α => 0.1 / 2000 * rand_v_vals(lattice(lrs)), - :β => 0.02 * rand_v_vals(lattice(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]) - @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(lattice(lrs), 10.0), :Y => 2.0] - u0_3 = [:X => rand_v_vals(lattice(lrs), 20), :Y => rand_v_vals(lattice(lrs), 10)] - for u0 in [u0_1, u0_2, u0_3] - p1 = [:A => 1.0, :B => 4.0] - p2 = [:A => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), :B => 4.0] - p3 = [ - :A => 0.5 .+ rand_v_vals(lattice(lrs), 0.5), - :B => 4.0 .+ rand_v_vals(lattice(lrs), 1.0), +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), ] - for pV in [p1, p2, p3] - 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, 0.2), - 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, 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 @@ -192,8 +157,8 @@ end let lrs = LatticeReactionSystem(binding_system, binding_srs, undirected_cycle) u0 = [ - :X => 1.0 .+ rand_v_vals(lattice(lrs)), - :Y => 2.0 * rand_v_vals(lattice(lrs)), + :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) @@ -207,24 +172,18 @@ end # Checks that various combinations of jac and sparse gives the same result. let lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_2d_graph_grid) - u0 = [:X => rand_v_vals(lattice(lrs), 10), :Y => rand_v_vals(lattice(lrs), 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, 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 # Compares Catalyst-generated to hand written one for the brusselator for a line of cells. @@ -300,7 +259,7 @@ let return sparse(jac_prototype_pre) end - num_verts = 5000 + num_verts = 100 u0 = 2 * rand(rng, 2*num_verts) p = [1.0, 4.0, 0.1] tspan = (0.0, 100.0) @@ -353,34 +312,34 @@ let sigmaB_p_spat = [:DσB => 0.05, :Dw => 0.04, :Dv => 0.03] # 1d lattices. - lrs1_cartesian = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_1d_cartesian_grid) - lrs1_masked = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_1d_masked_grid) - lrs1_graph = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_1d_graph_grid) + 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,10.0), [sigmaB_p; sigmaB_p_spat]) - oprob1_masked = ODEProblem(lrs1_masked, sigmaB_u0, (0.0,10.0), [sigmaB_p; sigmaB_p_spat]) - oprob1_graph = ODEProblem(lrs1_graph, sigmaB_u0, (0.0,10.0), [sigmaB_p; sigmaB_p_spat]) - @test solve(oprob1_cartesian, QNDF()) == solve(oprob1_masked, QNDF()) == solve(oprob1_graph, QNDF()) + 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, small_2d_cartesian_grid) - lrs2_masked = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_2d_masked_grid) - lrs2_graph = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_2d_graph_grid) + 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,10.0), [sigmaB_p; sigmaB_p_spat]) - oprob2_masked = ODEProblem(lrs2_masked, sigmaB_u0, (0.0,10.0), [sigmaB_p; sigmaB_p_spat]) - oprob2_graph = ODEProblem(lrs2_graph, sigmaB_u0, (0.0,10.0), [sigmaB_p; sigmaB_p_spat]) - @test solve(oprob2_cartesian, QNDF()) == solve(oprob2_masked, QNDF()) == solve(oprob2_graph, QNDF()) + 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, small_3d_cartesian_grid) - lrs3_masked = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_3d_masked_grid) - lrs3_graph = LatticeReactionSystem(sigmaB_system, sigmaB_srs_2, small_3d_graph_grid) - - oprob3_cartesian = ODEProblem(lrs3_cartesian, sigmaB_u0, (0.0,10.0), [sigmaB_p; sigmaB_p_spat]) - oprob3_masked = ODEProblem(lrs3_masked, sigmaB_u0, (0.0,10.0), [sigmaB_p; sigmaB_p_spat]) - oprob3_graph = ODEProblem(lrs3_graph, sigmaB_u0, (0.0,10.0), [sigmaB_p; sigmaB_p_spat]) - @test solve(oprob3_cartesian, QNDF()) == solve(oprob3_masked, QNDF()) == solve(oprob3_graph, QNDF()) + 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 # Tests that input parameter and u0 values can be given using different types of input for 2d lattices. @@ -457,7 +416,7 @@ let 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.lattice), :R => 0.0] + 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] @@ -474,7 +433,7 @@ let 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.lattice), :R => 0.0] + 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.003, :dS2 => 0.007, :dI1 => 2, :dI2 => 0.005, :dR1 => 1.010050167084168, :dR2 => 1.0755285551056204e-16] @@ -613,6 +572,7 @@ 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 checks for `jac = sparse = true`. let # Prepares problem inputs. lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_2, very_small_2d_cartesian_grid) @@ -638,31 +598,28 @@ let ps_1 = [:A => A1, :B => B1, :dX => dX1, :dY => dY1] ps_2 = [:A => A2, :B => B2, :dX => dX2, :dY => dY2] - # Checks for all combinations of Jacobian and sparsity. - for jac in [false, true], sparse in [false, true] - # Creates simulation through two different separate simulations. - oprob_1_1 = ODEProblem(lrs, u0, (0.0, 5.0), ps_1; jac, sparse) - 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, sparse) - 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, sparse) - 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 + # 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 ### @@ -772,20 +729,16 @@ let # 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, 20.0), ps[1]) - sol_base = solve(oprob_base, QNDF(); abstol=1e-8, reltol=1e-8, saveat=0.1) + 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. - for grid in [very_small_2d_cartesian_grid, very_small_2d_masked_grid, very_small_2d_graph_grid] - lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_2, 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, Tuple(p_base), Dict(p_base)] - for sparse in [false, true], jac in [false, true] - oprob = ODEProblem(lrs, u0, (0.0, 20.0), p; sparse, jac) - sol = solve(oprob, QNDF(); abstol=1e-8, reltol=1e-8, saveat=0.1) - @test sol == sol_base - end - end + 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 end end diff --git a/test/spatial_modelling/lattice_reaction_systems_jumps.jl b/test/spatial_modelling/lattice_reaction_systems_jumps.jl index 701922381a..904bad1173 100644 --- a/test/spatial_modelling/lattice_reaction_systems_jumps.jl +++ b/test/spatial_modelling/lattice_reaction_systems_jumps.jl @@ -16,18 +16,18 @@ let 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 .+ 500 * rand_v_vals(lattice(lrs))), :I => 1, :R => 0] + 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(lattice(lrs))), - :I => round.(Int64, 50 * rand_v_vals(lattice(lrs))), - :R => round.(Int64, 50 * rand_v_vals(lattice(lrs))), + :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(lattice(lrs))] + pV_2 = [:α => 0.1 / 1000, :β => 0.02 * rand_v_vals(lrs)] pV_3 = [ - :α => 0.1 / 2000 * rand_v_vals(lattice(lrs)), - :β => 0.02 * rand_v_vals(lattice(lrs)), + :α => 0.1 / 2000 * rand_v_vals(lrs), + :β => 0.02 * rand_v_vals(lrs), ] for pV in [pV_1, pV_2, pV_3] pE_1 = [sp => 0.01 for sp in spatial_param_syms(lrs)] @@ -103,7 +103,7 @@ let # Create JumpProblem u0 = [:X => 1, :Y => rand(1:10, num_verts(lrs))] tspan = (0.0, 100.0) - ps = [:A => 1.0, :B => 5.0 .+ rand_v_vals(lattice(lrs)), :dX => rand_e_vals(lrs)] + 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()) diff --git a/test/spatial_modelling/spatial_reactions.jl b/test/spatial_modelling/spatial_reactions.jl index 32895135da..395c264444 100644 --- a/test/spatial_modelling/spatial_reactions.jl +++ b/test/spatial_modelling/spatial_reactions.jl @@ -1,13 +1,7 @@ ### Preparations ### # Fetch packages. -using Catalyst, Graphs, OrdinaryDiffEq, Test - -# 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] +using Catalyst, Test ### TransportReaction Creation Tests ### diff --git a/test/spatial_test_networks.jl b/test/spatial_test_networks.jl index 924fecb045..e9322290c1 100644 --- a/test/spatial_test_networks.jl +++ b/test/spatial_test_networks.jl @@ -191,7 +191,9 @@ sigmaB_srs_2 = [sigmaB_tr_σB, sigmaB_tr_w, sigmaB_tr_v] ### Declares Lattices ### # 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)) @@ -202,7 +204,9 @@ 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) @@ -217,7 +221,9 @@ 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]) From 28743ab323587ffef4dd1328bf432f45852d16e7 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 8 Jul 2024 09:45:01 -0400 Subject: [PATCH 43/47] writing fix --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index eec0279da7..6479ae2234 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -62,7 +62,7 @@ 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 - # Transportest stability computation (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. From a98f4be70f6c7c0da3a914d8e913e9e8e914f8ae Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 8 Jul 2024 21:39:12 -0400 Subject: [PATCH 44/47] format --- src/Catalyst.jl | 3 +- .../lattice_jump_systems.jl | 44 +++--- .../lattice_reaction_systems.jl | 130 +++++++++++------- .../spatial_ODE_systems.jl | 122 ++++++++-------- .../spatial_reactions.jl | 11 +- src/spatial_reaction_systems/utility.jl | 86 ++++++------ .../lattice_reaction_systems_jumps.jl | 1 - 7 files changed, 223 insertions(+), 174 deletions(-) diff --git a/src/Catalyst.jl b/src/Catalyst.jl index 8b8895cdea..7c431a90c5 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -176,7 +176,8 @@ include("spatial_reaction_systems/lattice_reaction_systems.jl") export LatticeReactionSystem export spatial_species, vertex_parameters, edge_parameters export CartesianGrid, CartesianGridReJ # (Implemented in JumpProcesses) -export has_cartesian_lattice, has_masked_lattice, has_grid_lattice, has_graph_lattice, grid_dims, grid_size +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 # Specific spatial problem types. diff --git a/src/spatial_reaction_systems/lattice_jump_systems.jl b/src/spatial_reaction_systems/lattice_jump_systems.jl index 5dd8084bf7..97cd86b1ad 100644 --- a/src/spatial_reaction_systems/lattice_jump_systems.jl +++ b/src/spatial_reaction_systems/lattice_jump_systems.jl @@ -9,25 +9,27 @@ function DiffEqBase.DiscreteProblem(lrs::LatticeReactionSystem, u0_in, tspan, # Converts potential symmaps to varmaps. u0_in = symmap_to_varmap(lrs, u0_in) - p_in = 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) + 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) + vert_ps, edge_ps = lattice_process_p(p_in, vertex_parameters(lrs), edge_parameters(lrs), + lrs) # 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 containing a `LatticeReactionSystem`. -function JumpProcesses.JumpProblem(lrs::LatticeReactionSystem, dprob, aggregator, args...; name = nameof(reactionsystem(lrs)), - combinatoric_ratelaws = get_combinatoric_ratelaws(reactionsystem(lrs)), kwargs...) +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) throw(ArgumentError("Unexpected `DiscreteProblem` passed into `JumpProblem`. Was a `LatticeReactionSystem` used as input to the initial `DiscreteProblem`?")) @@ -40,12 +42,13 @@ function JumpProcesses.JumpProblem(lrs::LatticeReactionSystem, dprob, aggregator # 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, num_species(lrs), num_verts(lrs)), 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 , name, kwargs...) + return JumpProblem(non_spat_dprob, aggregator, sma_jumps; + hopping_constants, spatial_system, name, kwargs...) end # Creates the hopping constants from a discrete problem and a lattice reaction system. @@ -59,7 +62,7 @@ function make_hopping_constants(dprob::DiscreteProblem, lrs::LatticeReactionSyst # 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)] + 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 @@ -67,10 +70,9 @@ function make_hopping_constants(dprob::DiscreteProblem, lrs::LatticeReactionSyst # 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) - ] + 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 @@ -79,16 +81,17 @@ end # 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 = [has_spatial_vertex_component(rx.rate, dprob.p) - for rx in reactions(reactionsystem(lrs))] + 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(reactionsystem(lrs))) - count(is_spatials)) + 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, length(reactions(reactionsystem(lrs)))) - net_stoich = Vector{Vector{Pair{Int64, Int64}}}(undef, length(reactions(reactionsystem(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 @@ -101,9 +104,10 @@ function make_spatial_majumps(dprob, lrs::LatticeReactionSystem) cur_rx += 1 end # Loops through reactions with spatial rates, computes their rates and stoichiometries. - for (is_spat, rx) in zip(is_spatials, reactions(reactionsystem(lrs))) + 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; ps = dprob.p) + 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, reactionsystem(lrs)) net_stoich[cur_rx] = int_map(rx.netstoich, reactionsystem(lrs)) diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index ca9c88c911..e26cc6051f 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -1,12 +1,12 @@ ### 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}} +const GridLattice{N, T} = Union{Array{Bool, N}, CartesianGridRej{N, T}} ### Lattice Reaction Network Structure ### # Describes a spatial reaction network over a lattice. -struct LatticeReactionSystem{Q,R,S,T} <: MT.AbstractTimeDependentSystem +struct LatticeReactionSystem{Q, R, S, T} <: MT.AbstractTimeDependentSystem # Input values. """The (non-spatial) reaction system within each vertex.""" reactionsystem::ReactionSystem{Q} @@ -47,8 +47,8 @@ struct LatticeReactionSystem{Q,R,S,T} <: MT.AbstractTimeDependentSystem """ edge_iterator::T - 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} + 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.")) @@ -97,10 +97,13 @@ struct LatticeReactionSystem{Q,R,S,T} <: MT.AbstractTimeDependentSystem ps = [parameters(rs); setdiff([edge_parameters; vertex_parameters], parameters(rs))] # 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) + 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) + 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 @@ -112,11 +115,14 @@ function LatticeReactionSystem(rs, srs, lattice::DiGraph) return LatticeReactionSystem(rs, srs, lattice, num_verts, num_edges, edge_iterator) end # Creates a LatticeReactionSystem from a (undirected) Graph lattice (graph grid). -LatticeReactionSystem(rs, srs, lattice::SimpleGraph) = LatticeReactionSystem(rs, srs, DiGraph(lattice)) +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} +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.") @@ -135,9 +141,10 @@ function LatticeReactionSystem(rs, srs, lattice::GridLattice{N,T}; diagonal_conn # 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) + 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) + 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 @@ -146,24 +153,24 @@ function LatticeReactionSystem(rs, srs, lattice::GridLattice{N,T}; diagonal_conn return LatticeReactionSystem(rs, srs, lattice, num_verts, num_edges, edge_iterator) end - ### 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::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. +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 @@ -174,7 +181,8 @@ function count_edges(grid::Array{Bool, N}; diagonal_connections = false) where { 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)) + num_edges += length(get_neighbours(grid, Tuple(grid_idx), g_size; + diagonal_connections)) end return num_edges end @@ -182,7 +190,7 @@ end # 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} +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)) @@ -200,21 +208,24 @@ function get_index_converters(grid::GridLattice{N,T}, num_verts) where {N,T} 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} +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] + 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] + 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) + 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. @@ -223,16 +234,19 @@ 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} - all(0 < g_idx <= dim_leng for (g_idx, dim_leng) in zip(grid_idx, g_size)) || return false +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 # 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). -get_grid_indices(grid::CartesianGridRej{N,T}) where {N,T} = CartesianIndices(ones(grid_size(grid))) -get_grid_indices(grid::Array{Bool, N}) where {N} = CartesianIndices(grid) - +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 ### @@ -256,7 +270,9 @@ edge_iterator(lrs::LatticeReactionSystem) = getfield(lrs, :edge_iterator) Returns `true` if all spatial reactions in `lrs` are `TransportReaction`s. """ -is_transport_system(lrs::LatticeReactionSystem) = all(sr -> sr isa TransportReaction, spatial_reactions(lrs)) +function is_transport_system(lrs::LatticeReactionSystem) + return all(sr -> sr isa TransportReaction, spatial_reactions(lrs)) +end """ has_cartesian_lattice(lrs::LatticeReactionSystem) @@ -264,7 +280,8 @@ is_transport_system(lrs::LatticeReactionSystem) = all(sr -> sr isa TransportReac 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_cartesian_lattice(lrs::LatticeReactionSystem) = lattice(lrs) isa + CartesianGridRej{N, T} where {N, T} """ has_masked_lattice(lrs::LatticeReactionSystem) @@ -272,14 +289,16 @@ has_cartesian_lattice(lrs::LatticeReactionSystem) = lattice(lrs) isa CartesianGr 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_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`. """ -has_grid_lattice(lrs::LatticeReactionSystem) = (has_cartesian_lattice(lrs) || has_masked_lattice(lrs)) +function has_grid_lattice(lrs::LatticeReactionSystem) + return has_cartesian_lattice(lrs) || has_masked_lattice(lrs) +end """ has_graph_lattice(lrs::LatticeReactionSystem) @@ -296,9 +315,11 @@ Returns the size of `lrs`'s lattice (only if it is a cartesian or masked grid la 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::CartesianGridRej{N, T}) where {N, T} = lattice.dims grid_size(lattice::Array{Bool, N}) where {N} = size(lattice) -grid_size(lattice::Graphs.AbstractGraph) = throw(ArgumentError("Grid size is only defined for LatticeReactionSystems with grid-based lattices (not graph-based).")) +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) @@ -307,8 +328,10 @@ Returns the number of dimensions of `lrs`'s lattice (only if it is a cartesian o 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 -grid_dims(lattice::Graphs.AbstractGraph) = throw(ArgumentError("Grid dimensions is only defined for LatticeReactionSystems with grid-based lattices (not graph-based).")) +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) @@ -317,14 +340,16 @@ Returns lrs's lattice, but in as a graph. Currently does not work for Cartesian """ 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)) + return Graphs.SimpleGraphFromIterator(Graphs.SimpleEdge(e[1], e[2]) + for e in edge_iterator(lrs)) end ### Catalyst-based Getters ### # Get all species. -species(lrs::LatticeReactionSystem) = unique([species(reactionsystem(lrs)); spatial_species(lrs)]) +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)) @@ -341,18 +366,18 @@ 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) +function MT.get_eqs(lrs::LatticeReactionSystem) MT.get_eqs(reactionsystem(lrs)) end -function MT.get_unknowns(lrs::LatticeReactionSystem) +function MT.get_unknowns(lrs::LatticeReactionSystem) MT.get_unknowns(reactionsystem(lrs)) end -function MT.get_ps(lrs::LatticeReactionSystem) +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) +function MT.get_systems(lrs::LatticeReactionSystem) return [] end @@ -412,15 +437,16 @@ function make_edge_p_values(lrs::LatticeReactionSystem, make_edge_p_value::Funct # 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)) + 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]] = eps() - values[e[1],e[2]] = make_edge_p_value(flat_to_grid_idx[e[1]], flat_to_grid_idx[e[2]]) + values[e[1], e[2]] = make_edge_p_value(flat_to_grid_idx[e[1]], + flat_to_grid_idx[e[2]]) end return values @@ -469,8 +495,9 @@ 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} +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.") @@ -497,4 +524,3 @@ function make_directed_edge_values(lrs::LatticeReactionSystem, x_vals::Tuple{T,T # 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/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index 40da3aa6b0..1d31a0ff35 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -2,7 +2,7 @@ # 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} +struct LatticeTransportODEFunction{P, Q, R, S, T} """ The ODEFunction of the (non-spatial) ReactionSystem that generated this LatticeTransportODEFunction instance. @@ -38,7 +38,7 @@ struct LatticeTransportODEFunction{P,Q,R,S,T} 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. """ - transport_rates::Vector{Pair{Int64,SparseMatrixCSC{S, Int64}}} + transport_rates::Vector{Pair{Int64, SparseMatrixCSC{S, Int64}}} """ 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 @@ -63,19 +63,22 @@ struct LatticeTransportODEFunction{P,Q,R,S,T} sparse::Bool """Remove when we add this as problem metadata""" lrs::LatticeReactionSystem - - function LatticeTransportODEFunction(ofunc::P, ps::Vector{<:Pair}, - lrs::LatticeReactionSystem, transport_rates::Vector{Pair{Int64, SparseMatrixCSC{S, Int64}}}, - jac_transport::Union{Nothing, Matrix{S}, SparseMatrixCSC{S, Int64}}, sparse) where {P,S} + + 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) + 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) + 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 @@ -84,7 +87,8 @@ end # 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)) + 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 @@ -94,18 +98,20 @@ function make_mtk_ps_structs(ps, lrs, heterogeneous_vert_p_idxs) 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]] + 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] + 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]) + 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 @@ -119,26 +125,27 @@ function (lt_ofun::LatticeTransportODEFunction)(du::AbstractVector, u, p, t) 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) + 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) + 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) + 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 + 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] + 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 @@ -152,7 +159,7 @@ function (lt_ofun::LatticeTransportODEFunction)(J::AbstractMatrix, u, p, t) 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) @@ -167,52 +174,54 @@ end # Creates an ODEProblem from a LatticeReactionSystem. 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(reactionsystem(lrs)), - remove_conserved = false, checks = false, kwargs...) + p_in = DiffEqBase.NullParameters(), args...; + jac = false, sparse = false, + name = nameof(lrs), include_zero_odes = true, + 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. u0_in = symmap_to_varmap(lrs, u0_in) - p_in = 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) + 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) + 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) + ofun = build_odefunction(lrs, vert_ps, edge_ps, jac, sparse, name, include_zero_odes, + combinatoric_ratelaws, remove_conserved, checks) # 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...) + return ODEProblem(ofun, u0, tspan, ps, args...; kwargs...) end # Builds an ODEFunction for a spatial ODEProblem. -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} +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 + if remove_conserved throw(ArgumentError("Removal of conserved quantities is currently not supported for `LatticeReactionSystem`s")) end # Prepares the inputs to the `LatticeTransportODEFunction` functor. - osys = complete(convert(ODESystem, reactionsystem(lrs); name, combinatoric_ratelaws, include_zero_odes, checks)) + 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) @@ -222,14 +231,16 @@ function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{R,Ve jac_transport = nothing jac_prototype = nothing else - jac_sparse = build_jac_prototype(ofunc_sparse.jac_prototype, transport_rates, lrs; set_nonzero = jac) + 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 # Creates the `LatticeTransportODEFunction` functor (if `jac`, sets it as the Jacobian as well). - f = LatticeTransportODEFunction(ofunc_dense, [vert_ps; edge_ps], lrs, transport_rates, jac_transport, sparse) + f = LatticeTransportODEFunction(ofunc_dense, [vert_ps; edge_ps], lrs, sparse, + jac_transport, transport_rates) J = (jac ? f : nothing) # Extracts the `Symbol` form for species and parameters. Creates and returns the `ODEFunction`. @@ -240,13 +251,14 @@ end # 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} +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) + trans_only_species = filter(s_idx -> !Base.isstored(ns_jac_prototype, s_idx, s_idx), + trans_species) # Finds the indices of all terms in the non-spatial jacobian. ns_jac_prototype_idxs = findnz(ns_jac_prototype) @@ -255,7 +267,7 @@ function build_jac_prototype(ns_jac_prototype::SparseMatrixCSC{Float64, Int64}, # Prepares vectors to store i and j indices of Jacobian entries. idx = 1 - num_entries = num_verts(lrs) * length(ns_i_idxs) + + 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) @@ -301,7 +313,7 @@ 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)) + val = get_transport_rate(rates, e, size(rates) == (1, 1)) # Term due to species leaving source vertex. jac_prototype[idx_src, idx_src] -= val @@ -326,7 +338,8 @@ function rebuild_lat_internals!(integrator) end # Function which rebuilds a `LatticeTransportODEFunction` functor for a new parameter set. -function rebuild_lat_internals!(lt_ofun::LatticeTransportODEFunction, ps_new, lrs::LatticeReactionSystem) +function rebuild_lat_internals!(lt_ofun::LatticeTransportODEFunction, ps_new, + lrs::LatticeReactionSystem) # Computes Jacobian properties. jac = !isnothing(lt_ofun.jac_transport) sparse = lt_ofun.sparse @@ -334,9 +347,10 @@ function rebuild_lat_internals!(lt_ofun::LatticeTransportODEFunction, ps_new, lr # 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) + 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) @@ -377,5 +391,5 @@ function replace_vec!(vec1, vec2) vec1[i] = v end foreach(idx -> deleteat!(vec1, idx), l1:-1:(l2 + 1)) - foreach(val -> push!(vec1, val), vec2[l1+1:l2]) -end \ No newline at end of file + 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 222ea4640b..43ae1fc807 100644 --- a/src/spatial_reaction_systems/spatial_reactions.jl +++ b/src/spatial_reaction_systems/spatial_reactions.jl @@ -17,7 +17,6 @@ function isedgeparameter(x, default = false) Symbolics.getmetadata(x, EdgeParameter, default) end - ### Transport Reaction Structures ### # A transport reaction. These are simple to handle, and should cover most types of spatial reactions. @@ -64,8 +63,8 @@ function make_transport_reaction(rateex, species) trxexpr = :(TransportReaction($rateex, $species)) # Appends `edgeparameter` metadata to all declared parameters. - for idx = 4:2:(2 + 2*length(parameters)) - insert!(pexprs.args, idx, :([edgeparameter=true])) + for idx in 4:2:(2 + 2 * length(parameters)) + insert!(pexprs.args, idx, :([edgeparameter = true])) end quote @@ -83,11 +82,12 @@ ModelingToolkit.parameters(tr::TransportReaction) = Symbolics.get_variables(tr.r spatial_species(tr::TransportReaction) = [tr.species] # Checks that a TransportReactions is valid for a given reaction system. -function check_spatial_reaction_validity(rs::ReactionSystem, tr::TransportReaction; edge_parameters=[]) +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 are of the wrong size). - if !any(isequal(tr.species), species(rs)) + 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 @@ -148,7 +148,6 @@ function hash(tr::TransportReaction, h::UInt) Base.hash(tr.species, h) end - ### Utility ### # Loops through a rate and extracts all parameters. diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index e242ba3935..80a490f912 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -18,7 +18,7 @@ end 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) + 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, @@ -27,22 +27,22 @@ function lattice_process_u0(u0_in, u0_syms::Vector, lrs::LatticeReactionSystem) 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)) + return expand_component_values([entry[2] for entry in u0], num_verts(lrs)) end # From a parameter input, split it into vertex parameters and edge parameters. # Store these in the desired internal format. -function lattice_process_p(ps_in, ps_vertex_syms::Vector, - ps_edge_syms::Vector, lrs::LatticeReactionSystem) +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]) + 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, 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) @@ -71,9 +71,9 @@ function lattice_process_input(input, syms::Vector) 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)] +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 @@ -99,7 +99,7 @@ function vertex_value_form(values, lrs::LatticeReactionSystem, sym::BasicSymboli # 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)) + 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 @@ -110,20 +110,20 @@ function vertex_value_form(values, lrs::LatticeReactionSystem, sym::BasicSymboli end # 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} +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 - if (length(values) != num_verts) + 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 # 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} +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 @@ -132,13 +132,13 @@ function vertex_value_form(values::AbstractArray, num_verts::Int64, lattice::Arr # 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) = enumerate(values) + for (idx, val) in enumerate(values) lattice[idx] || continue return_values[cur_idx += 1] = val end # Checks that the correct number of values was provided, and returns the values. - if (length(return_values) != num_verts) + 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 @@ -155,7 +155,7 @@ end 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))).")) @@ -173,18 +173,19 @@ end # 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} +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 - ] + 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 @@ -192,10 +193,11 @@ end 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(s, p_val_dict, lrs) for s in spatial_species(lrs)] - + 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(lrs). - return sort(unsorted_rates; by = rate -> findfirst(isequal(rate[1]), species(lrs))) + return sort(unsorted_rates; by = rate -> findfirst(isequal(rate[1]), species(lrs))) end # For the expression describing the rate of transport (likely only a single parameter, e.g. `D`), @@ -211,9 +213,9 @@ function compute_transport_rates(s::BasicSymbolic, p_val_dict, lrs::LatticeReact # 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) + 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...)) + return sparse([1], [1], rate_law_func(relevant_p_vals...)) else transport_rates = spzeros(num_verts(lrs), num_verts(lrs)) for e in edge_iterator(lrs) @@ -243,14 +245,17 @@ 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 indices in the u array of all species in vertex vert (when there are num_species species). -get_indexes(vert::Int64, num_species::Int64) = ((vert - 1) * num_species + 1):(vert * num_species) +function get_indexes(vert::Int64, num_species::Int64) + return ((vert - 1) * num_species + 1):(vert * num_species) +end # 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} +function get_edge_value(values::Vector{T}, edge::Pair{Int64, Int64}) where {T} return (length(values) == 1) ? values[1] : values[edge[1]] end -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]] +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 # Returns the value of an initial condition of vertex parameter in a vertex. @@ -259,14 +264,15 @@ function get_vertex_value(values::Vector{T}, vert_idx::Int64) where {T} end # 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]] +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 a `LatticeTransportODEFunction`, updates 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} +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 @@ -293,8 +299,8 @@ function compute_vertex_value(exp, lrs::LatticeReactionSystem; u = [], ps = []) 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_vertex_value(value_dict[sym], vert_idx) for sym in relevant_syms]...) - for vert_idx in 1:num_verts(lrs)] + 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 ### @@ -304,5 +310,5 @@ end 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) + return any(length(value_dict[sym]) > 1 for sym in relevant_syms) end diff --git a/test/spatial_modelling/lattice_reaction_systems_jumps.jl b/test/spatial_modelling/lattice_reaction_systems_jumps.jl index 904bad1173..fe32701b11 100644 --- a/test/spatial_modelling/lattice_reaction_systems_jumps.jl +++ b/test/spatial_modelling/lattice_reaction_systems_jumps.jl @@ -139,7 +139,6 @@ let # Check that higher p gives higher mean. for i = 1:5 sol = solve(jprob, SSAStepper(); saveat = 1.) - println() @test mean(getindex.(sol.u, 1)) < mean(getindex.(sol.u, 2)) < mean(getindex.(sol.u, 3)) < mean(getindex.(sol.u, 4)) end end From c70e716a9fb3e90c680f379ddc086fe3c2e4cc56 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 8 Jul 2024 21:40:43 -0400 Subject: [PATCH 45/47] format update --- .../lattice_jump_systems.jl | 8 ++++---- .../lattice_reaction_systems.jl | 13 +++++++------ .../spatial_ODE_systems.jl | 16 ++++++++-------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/spatial_reaction_systems/lattice_jump_systems.jl b/src/spatial_reaction_systems/lattice_jump_systems.jl index 97cd86b1ad..826a48856f 100644 --- a/src/spatial_reaction_systems/lattice_jump_systems.jl +++ b/src/spatial_reaction_systems/lattice_jump_systems.jl @@ -19,16 +19,16 @@ function DiffEqBase.DiscreteProblem(lrs::LatticeReactionSystem, u0_in, tspan, # 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) + vert_ps, edge_ps = lattice_process_p(p_in, vertex_parameters(lrs), + edge_parameters(lrs), lrs) # 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 containing a `LatticeReactionSystem`. -function JumpProcesses.JumpProblem(lrs::LatticeReactionSystem, dprob, aggregator,args...; - combinatoric_ratelaws = get_combinatoric_ratelaws(reactionsystem(lrs)), +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) diff --git a/src/spatial_reaction_systems/lattice_reaction_systems.jl b/src/spatial_reaction_systems/lattice_reaction_systems.jl index e26cc6051f..7f9e2923cf 100644 --- a/src/spatial_reaction_systems/lattice_reaction_systems.jl +++ b/src/spatial_reaction_systems/lattice_reaction_systems.jl @@ -48,7 +48,8 @@ struct LatticeReactionSystem{Q, R, S, T} <: MT.AbstractTimeDependentSystem edge_iterator::T 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} + 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.")) @@ -121,7 +122,7 @@ 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}; +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.") @@ -144,7 +145,7 @@ function LatticeReactionSystem(rs, srs, lattice::GridLattice{N, T}; 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) + diagonal_connections) cur_vert += 1 edge_iterator[cur_vert] = flat_idx => grid_to_flat_idx[neighbour_grid_idx...] end @@ -270,7 +271,7 @@ edge_iterator(lrs::LatticeReactionSystem) = getfield(lrs, :edge_iterator) Returns `true` if all spatial reactions in `lrs` are `TransportReaction`s. """ -function is_transport_system(lrs::LatticeReactionSystem) +function is_transport_system(lrs::LatticeReactionSystem) return all(sr -> sr isa TransportReaction, spatial_reactions(lrs)) end @@ -296,7 +297,7 @@ has_masked_lattice(lrs::LatticeReactionSystem) = lattice(lrs) isa Array{Bool, N} Returns `true` if `lrs` was created using a cartesian or masked grid lattice. Otherwise, returns `false`. """ -function has_grid_lattice(lrs::LatticeReactionSystem) +function has_grid_lattice(lrs::LatticeReactionSystem) return has_cartesian_lattice(lrs) || has_masked_lattice(lrs) end @@ -445,7 +446,7 @@ function make_edge_p_values(lrs::LatticeReactionSystem, make_edge_p_value::Funct # 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]], + values[e[1], e[2]] = make_edge_p_value(flat_to_grid_idx[e[1]], flat_to_grid_idx[e[2]]) end diff --git a/src/spatial_reaction_systems/spatial_ODE_systems.jl b/src/spatial_reaction_systems/spatial_ODE_systems.jl index 1d31a0ff35..9879e27221 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -66,7 +66,7 @@ struct LatticeTransportODEFunction{P, Q, R, S, T} function LatticeTransportODEFunction(ofunc::P, ps::Vector{<:Pair}, lrs::LatticeReactionSystem, sparse::Bool, - jac_transport::Union{Nothing, Matrix{S}, SparseMatrixCSC{S, Int64}}, + 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) @@ -75,9 +75,9 @@ struct LatticeTransportODEFunction{P, Q, R, S, T} lrs) # Creates and returns the `LatticeTransportODEFunction` functor. - new{P, typeof(mtk_ps), typeof(p_setters), S, typeof(jac_transport)}(ofunc, + 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), + transport_rates, t_rate_idx_types, leaving_rates, Catalyst.edge_iterator(lrs), jac_transport, sparse, lrs) end end @@ -88,7 +88,7 @@ end 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)) + for p in parameters(lrs)) end # Creates the MTKParameters structure and `p_setters` vector (which are used to manage @@ -110,7 +110,7 @@ function make_t_types_and_leaving_rates(transport_rates, 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, + leaving_rates[s_idx, e[1]] += get_transport_rate(tr_pair[2], e, t_rate_idx_types[s_idx]) end end @@ -196,8 +196,8 @@ function DiffEqBase.ODEProblem(lrs::LatticeReactionSystem, u0_in, tspan, # 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) + 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, @@ -347,7 +347,7 @@ function rebuild_lat_internals!(lt_ofun::LatticeTransportODEFunction, ps_new, # 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), + vert_ps, edge_ps = lattice_process_p(ps_new, vertex_parameters(lrs), edge_parameters(lrs), lrs) ps_new = [vert_ps; edge_ps] From 37a14d04af70fbda69218c0f481f52ec0316176e Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 9 Jul 2024 16:55:46 -0400 Subject: [PATCH 46/47] add solution interfacing --- src/Catalyst.jl | 4 +- .../lattice_jump_systems.jl | 14 +- .../lattice_solution_interfacing.jl | 121 +++++++++++ .../spatial_ODE_systems.jl | 51 ++++- test/runtests.jl | 1 + .../lattice_reaction_systems_jumps.jl | 24 ++- .../lattice_solution_interfacing.jl | 196 ++++++++++++++++++ 7 files changed, 402 insertions(+), 9 deletions(-) create mode 100644 src/spatial_reaction_systems/lattice_solution_interfacing.jl create mode 100644 test/spatial_modelling/lattice_solution_interfacing.jl diff --git a/src/Catalyst.jl b/src/Catalyst.jl index 7c431a90c5..7d40cf695c 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -171,7 +171,7 @@ 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 @@ -179,6 +179,8 @@ 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") diff --git a/src/spatial_reaction_systems/lattice_jump_systems.jl b/src/spatial_reaction_systems/lattice_jump_systems.jl index 826a48856f..1754aea0cf 100644 --- a/src/spatial_reaction_systems/lattice_jump_systems.jl +++ b/src/spatial_reaction_systems/lattice_jump_systems.jl @@ -122,7 +122,8 @@ 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 @@ -141,3 +142,14 @@ 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_solution_interfacing.jl b/src/spatial_reaction_systems/lattice_solution_interfacing.jl new file mode 100644 index 0000000000..006b8e2217 --- /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 contain all interfacing functionality. However, +# long-term it should be replaced with a sleeker interface. Ideally as MTK-wider support for +# lattice problems and solutions are 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 contain 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 returns the solution across all saved timesteps. If `t` +instead is a vector (or range of values), returns the solutions interpolated at these timepoints. + +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 timestep. +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 reshapes 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 9879e27221..49893ad20e 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -243,10 +243,10 @@ function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{R, V jac_transport, transport_rates) J = (jac ? f : nothing) - # Extracts the `Symbol` form for species and parameters. Creates and returns the `ODEFunction`. - syms = MT.getname.(species(lrs)) - paramsyms = MT.getname.(parameters(lrs)) - return ODEFunction(f; jac = J, jac_prototype, syms, paramsyms) + # 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. @@ -325,7 +325,48 @@ end ### Functor Updating Functionality ### -# Function for rebuilding a `LatticeReactionSystem` `ODEProblem` after it has been updated. +""" + rebuild_lat_internals!(sciml_struct) + +Rebuilds the internal functions for simulating a LatticeReactionSystem. WHenever a problem or +integrator have had its parameter values updated, thus 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 have been updated. +- When a parameter with spatially homogeneous values have 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 build 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) + +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 diff --git a/test/runtests.jl b/test/runtests.jl index 6479ae2234..4197706e0d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -72,5 +72,6 @@ using SafeTestsets, Test @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/spatial_modelling/lattice_reaction_systems_jumps.jl b/test/spatial_modelling/lattice_reaction_systems_jumps.jl index fe32701b11..f2db71569f 100644 --- a/test/spatial_modelling/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") @@ -204,6 +203,27 @@ end ### JumpProblem & Integrator Interfacing ### +# Currently not supported, check that corresponding functions yields 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 ### diff --git a/test/spatial_modelling/lattice_solution_interfacing.jl b/test/spatial_modelling/lattice_solution_interfacing.jl new file mode 100644 index 0000000000..8bead8ce4a --- /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 effect of system, check that solution correspond to known +# initial condition throughout 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 a 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 solution have 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 value at `t` is inbetween 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 From e19d67d462d7542fae1441e395234ba15c821d44 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 9 Jul 2024 17:13:08 -0400 Subject: [PATCH 47/47] format, improve writing --- .../lattice_jump_systems.jl | 7 ++--- .../lattice_solution_interfacing.jl | 28 +++++++++---------- .../spatial_ODE_systems.jl | 18 ++++++------ .../spatial_reactions.jl | 6 ++-- src/spatial_reaction_systems/utility.jl | 8 +++--- .../lattice_reaction_systems.jl | 4 +-- .../lattice_reaction_systems_ODEs.jl | 10 +++---- .../lattice_reaction_systems_jumps.jl | 6 ++-- .../lattice_reaction_systems_lattice_types.jl | 2 +- .../lattice_solution_interfacing.jl | 10 +++---- test/spatial_modelling/spatial_reactions.jl | 2 +- 11 files changed, 50 insertions(+), 51 deletions(-) diff --git a/src/spatial_reaction_systems/lattice_jump_systems.jl b/src/spatial_reaction_systems/lattice_jump_systems.jl index 1754aea0cf..ebbe367cf1 100644 --- a/src/spatial_reaction_systems/lattice_jump_systems.jl +++ b/src/spatial_reaction_systems/lattice_jump_systems.jl @@ -37,7 +37,7 @@ function JumpProcesses.JumpProblem(lrs::LatticeReactionSystem, dprob, aggregator # Computes hopping constants and mass action jumps (requires some internal juggling). # Currently, the resulting JumpProblem does not depend on parameters (no way to incorporate these). - # Hence the parameters of this one does not 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) @@ -54,7 +54,7 @@ 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. + # 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)] @@ -77,7 +77,7 @@ function make_hopping_constants(dprob::DiscreteProblem, lrs::LatticeReactionSyst 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. +# 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. @@ -143,7 +143,6 @@ end # return ModelingToolkit.assemble_maj(eqs.x[1], statetoid, majpmapper) # end - ### Problem & Integrator Rebuilding ### # Currently not implemented. diff --git a/src/spatial_reaction_systems/lattice_solution_interfacing.jl b/src/spatial_reaction_systems/lattice_solution_interfacing.jl index 006b8e2217..3a286a8a02 100644 --- a/src/spatial_reaction_systems/lattice_solution_interfacing.jl +++ b/src/spatial_reaction_systems/lattice_solution_interfacing.jl @@ -1,7 +1,7 @@ ### Rudimentary Interfacing Function ### -# A single function, `get_lrs_vals`, which contain all interfacing functionality. However, -# long-term it should be replaced with a sleeker interface. Ideally as MTK-wider support for -# lattice problems and solutions are introduced. +# 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) @@ -11,7 +11,7 @@ desired forms. Generally, for `LatticeReactionSystem`s, the values in `sol` is o 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 contain sp's value (either at a time step of time, +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 @@ -22,8 +22,8 @@ Arguments: - `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 returns the solution across all saved timesteps. If `t` -instead is a vector (or range of values), returns the solutions interpolated at these timepoints. +- `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, @@ -48,7 +48,7 @@ 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 timestep. +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 ``` """ @@ -61,7 +61,7 @@ function get_lrs_vals(sol, sp, lrs::LatticeReactionSystem; t = nothing) # 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 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 @@ -79,7 +79,7 @@ function get_lrs_vals(sol, lattice, t::Nothing, sp_idx, sp_tot) 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] + 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 @@ -87,7 +87,8 @@ 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} +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 @@ -98,15 +99,15 @@ function get_lrs_vals(sol, lattice, t::AbstractVector{T}, sp_idx, sp_tot) where 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] + 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 reshapes the vector of values to the correct form (depending +# 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} +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}) @@ -118,4 +119,3 @@ 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 49893ad20e..3dced1e573 100644 --- a/src/spatial_reaction_systems/spatial_ODE_systems.jl +++ b/src/spatial_reaction_systems/spatial_ODE_systems.jl @@ -26,7 +26,7 @@ struct LatticeTransportODEFunction{P, Q, R, S, T} mtk_ps::Q """ Stores a SymbolicIndexingInterface `setp` function for each heterogeneous vertex parameter (i.e. - vertex parameter which value is not identical across the lattice). The `setp` function at index + 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`. """ p_setters::R @@ -82,7 +82,7 @@ struct LatticeTransportODEFunction{P, Q, R, S, T} end end -# `LatticeTransportODEFunction` helper functions (re used by rebuild function later on). +# `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) @@ -226,7 +226,7 @@ function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{R, V ofunc_sparse = ODEFunction(osys; jac = true, sparse = true) transport_rates = make_sidxs_to_transrate_map(vert_ps, edge_ps, lrs) - # Depending on Jacobian and sparsity options, computes the Jacobian transport matrix and prototype. + # Depending on Jacobian and sparsity options, compute the Jacobian transport matrix and prototype. if !sparse && !jac jac_transport = nothing jac_prototype = nothing @@ -249,7 +249,7 @@ function build_odefunction(lrs::LatticeReactionSystem, vert_ps::Vector{Pair{R, V return ODEFunction(f; jac = J, jac_prototype, sys) end -# Builds a jacobian prototype. +# 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}}}, @@ -328,11 +328,11 @@ end """ rebuild_lat_internals!(sciml_struct) -Rebuilds the internal functions for simulating a LatticeReactionSystem. WHenever a problem or -integrator have had its parameter values updated, thus function should be called for the update to +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 have been updated. -- When a parameter with spatially homogeneous values have been given spatially heterogeneous values +- An edge parameter has been updated. +- When a parameter with spatially homogeneous values has been given spatially heterogeneous values (or vice versa). Arguments: @@ -340,7 +340,7 @@ Arguments: Notes: - Currently does not work for `DiscreteProblem`s, `JumpProblem`s, or their integrators. -- The function is not build with performance in mind, so avoid calling it multiple times in +- The function is not built with performance in mind, so avoid calling it multiple times in performance-critical applications. Example: diff --git a/src/spatial_reaction_systems/spatial_reactions.jl b/src/spatial_reaction_systems/spatial_reactions.jl index 43ae1fc807..204d94992a 100644 --- a/src/spatial_reaction_systems/spatial_reactions.jl +++ b/src/spatial_reaction_systems/spatial_reactions.jl @@ -99,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)) diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index 80a490f912..e00f753d8c 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -269,7 +269,7 @@ function get_transport_rate(transport_rate::SparseMatrixCSC{T, Int64}, return t_rate_idx_types ? transport_rate[1, 1] : transport_rate[edge[1], edge[2]] end -# For a `LatticeTransportODEFunction`, updates its stored parameters (in `mtk_ps`) so that they +# 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} @@ -278,7 +278,7 @@ function update_mtk_ps!(lt_ofun::LatticeTransportODEFunction, all_ps::Vector{T}, end 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. @@ -289,7 +289,7 @@ function compute_vertex_value(exp, lrs::LatticeReactionSystem; u = [], ps = []) throw(ArgumentError("An edge parameter was encountered in expressions: $exp. Here, only vertex-based components are expected.")) end - # Creates a Function that 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. @@ -305,7 +305,7 @@ end ### System Property Checks ### -# For a Symbolic expression, and a parameter set, checks if any relevant parameters have a +# 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) diff --git a/test/spatial_modelling/lattice_reaction_systems.jl b/test/spatial_modelling/lattice_reaction_systems.jl index 52b0ac916b..f36a0a5d4c 100644 --- a/test/spatial_modelling/lattice_reaction_systems.jl +++ b/test/spatial_modelling/lattice_reaction_systems.jl @@ -298,7 +298,7 @@ end ### Tests Edge Value Computation Helper Functions ### -# Checks that computes the correct values across various types of grids. +# 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 @@ -323,7 +323,7 @@ let end end -# Checks that all species ends up in the correct place in in a pure flow system (checking various dimensions). +# 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 diff --git a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl index 92d905e169..26da7ba3fc 100644 --- a/test/spatial_modelling/lattice_reaction_systems_ODEs.jl +++ b/test/spatial_modelling/lattice_reaction_systems_ODEs.jl @@ -14,7 +14,7 @@ rng = StableRNG(12345) # Sets defaults t = default_t() -### Tests Simulations Don't Error ### +### 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] @@ -186,7 +186,7 @@ let @test all(isapprox.(ss, solve(oprob_sparse_jac, Rosenbrock23(); abstol = 1e-10, reltol = 1e-10).u[end]; rtol = 0.0001)) end -# Compares Catalyst-generated to hand written one for the brusselator for a line of cells. +# 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 @@ -534,7 +534,7 @@ end # Checks that the `rebuild_lat_internals!` function is correctly applied to an ODEProblem. let - # Creates a brusselator `LatticeReactionSystem`. + # Creates a Brusselator `LatticeReactionSystem`. lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_2, very_small_2d_cartesian_grid) # Checks for all combinations of Jacobian and sparsity. @@ -572,7 +572,7 @@ 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 checks for `jac = sparse = true`. +# 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) @@ -624,7 +624,7 @@ end ### Tests Special Cases ### -# Create network using either graphs or di-graphs. +# 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)) diff --git a/test/spatial_modelling/lattice_reaction_systems_jumps.jl b/test/spatial_modelling/lattice_reaction_systems_jumps.jl index f2db71569f..576e543f40 100644 --- a/test/spatial_modelling/lattice_reaction_systems_jumps.jl +++ b/test/spatial_modelling/lattice_reaction_systems_jumps.jl @@ -88,7 +88,7 @@ end ### SpatialMassActionJump Testing ### -# Checks that the correct structure;s is produced. +# Checks that the correct structures are produced. let # Network for reference: # A, ∅ → X @@ -113,7 +113,7 @@ let # @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 @@ -203,7 +203,7 @@ end ### JumpProblem & Integrator Interfacing ### -# Currently not supported, check that corresponding functions yields errors. +# Currently not supported, check that corresponding functions yield errors. let # Prepare `LatticeReactionSystem`. rs = @reaction_network begin diff --git a/test/spatial_modelling/lattice_reaction_systems_lattice_types.jl b/test/spatial_modelling/lattice_reaction_systems_lattice_types.jl index bde1bb14f8..dbdc233b89 100644 --- a/test/spatial_modelling/lattice_reaction_systems_lattice_types.jl +++ b/test/spatial_modelling/lattice_reaction_systems_lattice_types.jl @@ -9,7 +9,7 @@ include("../spatial_test_networks.jl") ### Run Tests ### -# Test errors when attempting to create networks with dimension > 3. +# 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)) diff --git a/test/spatial_modelling/lattice_solution_interfacing.jl b/test/spatial_modelling/lattice_solution_interfacing.jl index 8bead8ce4a..238700a51f 100644 --- a/test/spatial_modelling/lattice_solution_interfacing.jl +++ b/test/spatial_modelling/lattice_solution_interfacing.jl @@ -5,8 +5,8 @@ using Catalyst, Graphs, JumpProcesses, OrdinaryDiffEq, SparseArrays, Test ### `get_lrs_vals` Tests ### -# Basic test. For simulations without effect of system, check that solution correspond to known -# initial condition throughout solution. +# 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. @@ -33,7 +33,7 @@ let tspan = (0.0, 1.0) ps = [:k1 => 0.0, :k2 => 0.0, :D => 0.0] - # Loops through a lattice cases and check that they are correct. + # 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) @@ -53,7 +53,7 @@ let end # Checks on simulations where the system changes in time. -# Checks that solution have correct initial condition and end point (steady state). +# 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. @@ -84,7 +84,7 @@ let end end -# Checks interpolation when sampling at time point. Check that value at `t` is inbetween the +# 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. diff --git a/test/spatial_modelling/spatial_reactions.jl b/test/spatial_modelling/spatial_reactions.jl index 395c264444..e764c619a3 100644 --- a/test/spatial_modelling/spatial_reactions.jl +++ b/test/spatial_modelling/spatial_reactions.jl @@ -120,7 +120,7 @@ let # @test isequal(tr_3, tr_macro_3) end -# Checks that the `hash` functions works for `TransportReaction`s. +# Checks that the `hash` functions work for `TransportReaction`s. let tr1 = @transport_reaction D1 X tr2 = @transport_reaction D1 X