From a55ad5b4f524e90adb9a359643b1e5a5c5204806 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 22 May 2024 17:09:20 -0400 Subject: [PATCH 01/67] init --- docs/src/api.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/src/api.md b/docs/src/api.md index 9e7086e6f8..25ae93e90f 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -43,6 +43,7 @@ t = default_t() rxs = [Reaction(β, [S,I], [I], [1,1], [2]) Reaction(γ, [I], [R])] @named rs = ReactionSystem(rxs, t) +rs = complete(rs) u₀map = [S => 999.0, I => 1.0, R => 0.0] parammap = [β => 1/10000, γ => 0.01] @@ -50,18 +51,21 @@ tspan = (0.0, 250.0) # solve as ODEs odesys = convert(ODESystem, rs) +odesys = complete(odesys) oprob = ODEProblem(odesys, u₀map, tspan, parammap) sol = solve(oprob, Tsit5()) p1 = plot(sol, title = "ODE") # solve as SDEs sdesys = convert(SDESystem, rs) +sdesys = complete(sdesys) sprob = SDEProblem(sdesys, u₀map, tspan, parammap) sol = solve(sprob, EM(), dt=.01) p2 = plot(sol, title = "SDE") # solve as jump process jumpsys = convert(JumpSystem, rs) +jumpsys = complete(jumpsys) u₀map = [S => 999, I => 1, R => 0] dprob = DiscreteProblem(jumpsys, u₀map, tspan, parammap) jprob = JumpProblem(jumpsys, dprob, Direct()) From 203a7eb84dfc0d892e1e4f32386ff9734370ac88 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 22 May 2024 17:58:36 -0400 Subject: [PATCH 02/67] init --- docs/src/model_creation/parametric_stoichiometry.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/src/model_creation/parametric_stoichiometry.md b/docs/src/model_creation/parametric_stoichiometry.md index 434002f72c..0788f01125 100644 --- a/docs/src/model_creation/parametric_stoichiometry.md +++ b/docs/src/model_creation/parametric_stoichiometry.md @@ -58,6 +58,7 @@ stoichiometries `(F,2*H,2)`. Let's now convert `revsys` to ODEs and look at the resulting equations: ```@example s1 osys = convert(ODESystem, revsys) +osys = complete(osys) equations(osys) show(stdout, MIME"text/plain"(), equations(osys)) # hide ``` @@ -88,6 +89,7 @@ converting to an `ODESystem`). For the previous example this gives the following (different) system of ODEs ```@example s1 osys = convert(ODESystem, revsys; combinatoric_ratelaws = false) +osys = complete(osys) equations(osys) show(stdout, MIME"text/plain"(), equations(osys)) # hide ``` @@ -140,6 +142,7 @@ The parameter `b` does not need to be explicitly declared in the We next convert our network to a jump process representation ```@example s1 jsys = convert(JumpSystem, burstyrn; combinatoric_ratelaws = false) +jsys = complete(jsys) equations(jsys) show(stdout, MIME"text/plain"(), equations(jsys)) # hide ``` From 7a7acc12a27cb76b45e48c4f3a75730e7a549b92 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 22 May 2024 18:04:23 -0400 Subject: [PATCH 03/67] init --- docs/src/model_creation/compositional_modeling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/model_creation/compositional_modeling.md b/docs/src/model_creation/compositional_modeling.md index d5d547ac28..e9a00b5d5c 100644 --- a/docs/src/model_creation/compositional_modeling.md +++ b/docs/src/model_creation/compositional_modeling.md @@ -18,7 +18,7 @@ end Alternatively one can just build the `ReactionSystem` via the symbolic interface. ```@example ex0 @parameters d -@variable t +t = default_t() @species X(t) rx = Reaction(d, [X], nothing) @named degradation_component = ReactionSystem([rs], t) From c31e73b3a1c83441a6e9d3b3c1bfed97e6530509 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 22 May 2024 18:19:11 -0400 Subject: [PATCH 04/67] init --- docs/src/model_creation/examples/hodgkin_huxley_equation.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/model_creation/examples/hodgkin_huxley_equation.md b/docs/src/model_creation/examples/hodgkin_huxley_equation.md index ab1f072b6b..08d98d5b6e 100644 --- a/docs/src/model_creation/examples/hodgkin_huxley_equation.md +++ b/docs/src/model_creation/examples/hodgkin_huxley_equation.md @@ -77,6 +77,7 @@ I = I₀ * sin(2*pi*t/30)^2 # get the gating variables to use in the equation for dV/dt @unpack m,n,h = hhrn +Dₜ = default_time_deriv() eqs = [Dₜ(V) ~ -1/C * (ḡK*n^4*(V-EK) + ḡNa*m^3*h*(V-ENa) + ḡL*(V-EL)) + I/C] @named voltageode = ODESystem(eqs, t) nothing # hide @@ -88,6 +89,7 @@ Finally, we add this ODE into the reaction model as ```@example hh1 @named hhmodel = extend(voltageode, hhrn) +hhmodel = complete(hhmodel) nothing # hide ``` From 007e809aec1bcf990b9213e14ab4f4123d9f8ab0 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 29 May 2024 17:19:09 -0400 Subject: [PATCH 05/67] up --- docs/pages.jl | 46 +++++++++++++++++++++++----------------------- docs/src/api.md | 8 ++++---- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/pages.jl b/docs/pages.jl index fd9cfa18ab..3307989831 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -1,61 +1,61 @@ pages = Any[ "Home" => "index.md", "Introduction to Catalyst" => Any[ - "introduction_to_catalyst/catalyst_for_new_julia_users.md", + #"introduction_to_catalyst/catalyst_for_new_julia_users.md", # "introduction_to_catalyst/introduction_to_catalyst.md" # Advanced introduction. ], "Model Creation and Properties" => Any[ - "model_creation/dsl_basics.md", - "model_creation/dsl_advanced.md", + #"model_creation/dsl_basics.md", + #"model_creation/dsl_advanced.md", #"model_creation/programmatic_CRN_construction.md", #"model_creation/compositional_modeling.md", #"model_creation/constraint_equations.md", # Events. #"model_creation/parametric_stoichiometry.md",# Distributed parameters, rates, and initial conditions. # Loading and writing models to files. - "model_creation/model_visualisation.md", + #"model_creation/model_visualisation.md", #"model_creation/network_analysis.md", - "model_creation/chemistry_related_functionality.md", + #"model_creation/chemistry_related_functionality.md", "Model creation examples" => Any[ - "model_creation/examples/basic_CRN_library.md", - "model_creation/examples/programmatic_generative_linear_pathway.md", + #"model_creation/examples/basic_CRN_library.md", + #"model_creation/examples/programmatic_generative_linear_pathway.md", #"model_creation/examples/hodgkin_huxley_equation.md", #"model_creation/examples/smoluchowski_coagulation_equation.md" ] ], "Model simulation" => Any[ - "model_simulation/simulation_introduction.md", + #"model_simulation/simulation_introduction.md", # Simulation introduction. - "model_simulation/simulation_plotting.md", - "model_simulation/simulation_structure_interfacing.md", - "model_simulation/ensemble_simulations.md", + #"model_simulation/simulation_plotting.md", + #"model_simulation/simulation_structure_interfacing.md", + #"model_simulation/ensemble_simulations.md", # Stochastic simulation statistical analysis. - "model_simulation/ode_simulation_performance.md", + #"model_simulation/ode_simulation_performance.md", # ODE Performance considerations/advice. # SDE Performance considerations/advice. # Jump Performance considerations/advice. # Finite state projection ], "Steady state analysis" => Any[ - "steady_state_functionality/homotopy_continuation.md", - "steady_state_functionality/nonlinear_solve.md", - "steady_state_functionality/steady_state_stability_computation.md", - "steady_state_functionality/bifurcation_diagrams.md", - "steady_state_functionality/dynamical_systems.md" + #"steady_state_functionality/homotopy_continuation.md", + #"steady_state_functionality/nonlinear_solve.md", + #"steady_state_functionality/steady_state_stability_computation.md", + #"steady_state_functionality/bifurcation_diagrams.md", + #"steady_state_functionality/dynamical_systems.md" ], "Inverse Problems" => Any[ # Inverse problems introduction. - "inverse_problems/optimization_ode_param_fitting.md", + #"inverse_problems/optimization_ode_param_fitting.md", # "inverse_problems/petab_ode_param_fitting.md", # ODE parameter fitting using Turing. # SDE/Jump fitting. - "inverse_problems/behaviour_optimisation.md", - "inverse_problems/structural_identifiability.md", + #"inverse_problems/behaviour_optimisation.md", + #"inverse_problems/structural_identifiability.md", # Practical identifiability. - "inverse_problems/global_sensitivity_analysis.md", + #"inverse_problems/global_sensitivity_analysis.md", "Inverse problem examples" => Any[ - "inverse_problems/examples/ode_fitting_oscillation.md" + #"inverse_problems/examples/ode_fitting_oscillation.md" ] ], "Spatial modelling" => Any[ @@ -68,5 +68,5 @@ pages = Any[ # # Repository structure. # ], #"FAQs" => "faqs.md", - #"API" => "api.md" + "API" => "api.md" ] \ No newline at end of file diff --git a/docs/src/api.md b/docs/src/api.md index 25ae93e90f..a0f386f3a2 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -35,7 +35,7 @@ corresponding chemical reaction ODE models, chemical Langevin equation SDE models, and stochastic chemical kinetics jump process models. ```@example ex1 -using Catalyst, DifferentialEquations, Plots +using Catalyst, OrdinaryDiffEq, StochasticDiffEq, JumpProcesses, Plots t = default_t() @parameters β γ @species S(t) I(t) R(t) @@ -60,7 +60,7 @@ p1 = plot(sol, title = "ODE") sdesys = convert(SDESystem, rs) sdesys = complete(sdesys) sprob = SDEProblem(sdesys, u₀map, tspan, parammap) -sol = solve(sprob, EM(), dt=.01) +sol = solve(sprob, EM(), dt=.01, saveat = 2.0) p2 = plot(sol, title = "SDE") # solve as jump process @@ -68,8 +68,8 @@ jumpsys = convert(JumpSystem, rs) jumpsys = complete(jumpsys) u₀map = [S => 999, I => 1, R => 0] dprob = DiscreteProblem(jumpsys, u₀map, tspan, parammap) -jprob = JumpProblem(jumpsys, dprob, Direct()) -sol = solve(jprob, SSAStepper()) +jprob = JumpProblem(jumpsys, dprob, Direct(); save_positions = (false,false)) +sol = solve(jprob, SSAStepper(), saveat = 2.0) p3 = plot(sol, title = "jump") plot(p1, p2, p3; layout = (3,1)) From c54a093e8ff9a9fedd0981c58d3fd7d0427d0e97 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 29 May 2024 17:21:48 -0400 Subject: [PATCH 06/67] up --- docs/pages.jl | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/pages.jl b/docs/pages.jl index 3307989831..1b969b4680 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -1,61 +1,61 @@ pages = Any[ "Home" => "index.md", "Introduction to Catalyst" => Any[ - #"introduction_to_catalyst/catalyst_for_new_julia_users.md", + "introduction_to_catalyst/catalyst_for_new_julia_users.md", # "introduction_to_catalyst/introduction_to_catalyst.md" # Advanced introduction. ], "Model Creation and Properties" => Any[ - #"model_creation/dsl_basics.md", - #"model_creation/dsl_advanced.md", + "model_creation/dsl_basics.md", + "model_creation/dsl_advanced.md", #"model_creation/programmatic_CRN_construction.md", #"model_creation/compositional_modeling.md", #"model_creation/constraint_equations.md", # Events. #"model_creation/parametric_stoichiometry.md",# Distributed parameters, rates, and initial conditions. # Loading and writing models to files. - #"model_creation/model_visualisation.md", + "model_creation/model_visualisation.md", #"model_creation/network_analysis.md", - #"model_creation/chemistry_related_functionality.md", + "model_creation/chemistry_related_functionality.md", "Model creation examples" => Any[ - #"model_creation/examples/basic_CRN_library.md", - #"model_creation/examples/programmatic_generative_linear_pathway.md", + "model_creation/examples/basic_CRN_library.md", + "model_creation/examples/programmatic_generative_linear_pathway.md", #"model_creation/examples/hodgkin_huxley_equation.md", #"model_creation/examples/smoluchowski_coagulation_equation.md" ] ], "Model simulation" => Any[ - #"model_simulation/simulation_introduction.md", + "model_simulation/simulation_introduction.md", # Simulation introduction. - #"model_simulation/simulation_plotting.md", - #"model_simulation/simulation_structure_interfacing.md", - #"model_simulation/ensemble_simulations.md", + "model_simulation/simulation_plotting.md", + "model_simulation/simulation_structure_interfacing.md", + "model_simulation/ensemble_simulations.md", # Stochastic simulation statistical analysis. - #"model_simulation/ode_simulation_performance.md", + "model_simulation/ode_simulation_performance.md", # ODE Performance considerations/advice. # SDE Performance considerations/advice. # Jump Performance considerations/advice. # Finite state projection ], "Steady state analysis" => Any[ - #"steady_state_functionality/homotopy_continuation.md", - #"steady_state_functionality/nonlinear_solve.md", - #"steady_state_functionality/steady_state_stability_computation.md", - #"steady_state_functionality/bifurcation_diagrams.md", - #"steady_state_functionality/dynamical_systems.md" + "steady_state_functionality/homotopy_continuation.md", + "steady_state_functionality/nonlinear_solve.md", + "steady_state_functionality/steady_state_stability_computation.md", + "steady_state_functionality/bifurcation_diagrams.md", + "steady_state_functionality/dynamical_systems.md" ], "Inverse Problems" => Any[ # Inverse problems introduction. - #"inverse_problems/optimization_ode_param_fitting.md", + "inverse_problems/optimization_ode_param_fitting.md", # "inverse_problems/petab_ode_param_fitting.md", # ODE parameter fitting using Turing. # SDE/Jump fitting. - #"inverse_problems/behaviour_optimisation.md", - #"inverse_problems/structural_identifiability.md", + "inverse_problems/behaviour_optimisation.md", + "inverse_problems/structural_identifiability.md", # Practical identifiability. - #"inverse_problems/global_sensitivity_analysis.md", + "inverse_problems/global_sensitivity_analysis.md", "Inverse problem examples" => Any[ - #"inverse_problems/examples/ode_fitting_oscillation.md" + "inverse_problems/examples/ode_fitting_oscillation.md" ] ], "Spatial modelling" => Any[ From 377d711f493c315f690a066da15607b7db39b2a9 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 29 May 2024 17:23:58 -0400 Subject: [PATCH 07/67] up --- docs/pages.jl | 2 +- docs/src/model_creation/examples/hodgkin_huxley_equation.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/pages.jl b/docs/pages.jl index fd9cfa18ab..e4c1441a70 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -20,7 +20,7 @@ pages = Any[ "Model creation examples" => Any[ "model_creation/examples/basic_CRN_library.md", "model_creation/examples/programmatic_generative_linear_pathway.md", - #"model_creation/examples/hodgkin_huxley_equation.md", + "model_creation/examples/hodgkin_huxley_equation.md", #"model_creation/examples/smoluchowski_coagulation_equation.md" ] ], diff --git a/docs/src/model_creation/examples/hodgkin_huxley_equation.md b/docs/src/model_creation/examples/hodgkin_huxley_equation.md index 08d98d5b6e..a2091035b1 100644 --- a/docs/src/model_creation/examples/hodgkin_huxley_equation.md +++ b/docs/src/model_creation/examples/hodgkin_huxley_equation.md @@ -13,7 +13,7 @@ cells such as neurons and muscle cells. We begin by importing some necessary packages. ```@example hh1 using ModelingToolkit, Catalyst, NonlinearSolve -using DifferentialEquations, Symbolics +using OrdinaryDiffEq, Symbolics using Plots t = default_t() D = default_time_deriv() From 535ad6a2f8875b209bf7679fedbea85be0402143 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 29 May 2024 17:29:49 -0400 Subject: [PATCH 08/67] up --- docs/pages.jl | 2 +- docs/src/model_creation/compositional_modeling.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/pages.jl b/docs/pages.jl index fd9cfa18ab..84c4ab7e9f 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -9,7 +9,7 @@ pages = Any[ "model_creation/dsl_basics.md", "model_creation/dsl_advanced.md", #"model_creation/programmatic_CRN_construction.md", - #"model_creation/compositional_modeling.md", + "model_creation/compositional_modeling.md", #"model_creation/constraint_equations.md", # Events. #"model_creation/parametric_stoichiometry.md",# Distributed parameters, rates, and initial conditions. diff --git a/docs/src/model_creation/compositional_modeling.md b/docs/src/model_creation/compositional_modeling.md index e9a00b5d5c..82e9bfc9d3 100644 --- a/docs/src/model_creation/compositional_modeling.md +++ b/docs/src/model_creation/compositional_modeling.md @@ -21,7 +21,7 @@ Alternatively one can just build the `ReactionSystem` via the symbolic interface t = default_t() @species X(t) rx = Reaction(d, [X], nothing) -@named degradation_component = ReactionSystem([rs], t) +@named degradation_component = ReactionSystem([rx], t) ``` We can test whether a system is complete using the `ModelingToolkit.iscomplete` function: ```@example ex0 From 56acc1f21f3ed69cc0acefee50474da67cba9b57 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 29 May 2024 18:16:59 -0400 Subject: [PATCH 09/67] up --- docs/pages.jl | 2 +- docs/src/model_creation/parametric_stoichiometry.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/pages.jl b/docs/pages.jl index fd9cfa18ab..b4daba47bd 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -12,7 +12,7 @@ pages = Any[ #"model_creation/compositional_modeling.md", #"model_creation/constraint_equations.md", # Events. - #"model_creation/parametric_stoichiometry.md",# Distributed parameters, rates, and initial conditions. + "model_creation/parametric_stoichiometry.md",# Distributed parameters, rates, and initial conditions. # Loading and writing models to files. "model_creation/model_visualisation.md", #"model_creation/network_analysis.md", diff --git a/docs/src/model_creation/parametric_stoichiometry.md b/docs/src/model_creation/parametric_stoichiometry.md index 0788f01125..a0d370ef0d 100644 --- a/docs/src/model_creation/parametric_stoichiometry.md +++ b/docs/src/model_creation/parametric_stoichiometry.md @@ -7,7 +7,7 @@ use symbolic stoichiometries, and discuss several caveats to be aware of. Let's first consider a simple reversible reaction where the number of reactants is a parameter, and the number of products is the product of two parameters. ```@example s1 -using Catalyst, Latexify, DifferentialEquations, ModelingToolkit, Plots +using Catalyst, Latexify, OrdinaryDiffEq, ModelingToolkit, Plots revsys = @reaction_network revsys begin k₊, m*A --> (m*n)*B k₋, B --> A @@ -141,6 +141,7 @@ The parameter `b` does not need to be explicitly declared in the We next convert our network to a jump process representation ```@example s1 +using JumpProcesses jsys = convert(JumpSystem, burstyrn; combinatoric_ratelaws = false) jsys = complete(jsys) equations(jsys) From bfa8f907761dc7bc377a428de0b3d2027e253b3f Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 29 May 2024 19:47:10 -0400 Subject: [PATCH 10/67] comment out SI doc (broken on v1.10.3, need 1.10.2 or new Julia release) --- docs/pages.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages.jl b/docs/pages.jl index fd9cfa18ab..abfd24c0f1 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -51,7 +51,7 @@ pages = Any[ # ODE parameter fitting using Turing. # SDE/Jump fitting. "inverse_problems/behaviour_optimisation.md", - "inverse_problems/structural_identifiability.md", + #"inverse_problems/structural_identifiability.md", # Practical identifiability. "inverse_problems/global_sensitivity_analysis.md", "Inverse problem examples" => Any[ From c86c500aeaa1aa60262df0cd80a2b3c57da6fa48 Mon Sep 17 00:00:00 2001 From: Torkel Date: Thu, 30 May 2024 13:39:38 -0400 Subject: [PATCH 11/67] up --- docs/pages.jl | 4 +--- .../steady_state_functionality/bifurcation_diagrams.md | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/pages.jl b/docs/pages.jl index abfd24c0f1..260c491979 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -26,13 +26,11 @@ pages = Any[ ], "Model simulation" => Any[ "model_simulation/simulation_introduction.md", - # Simulation introduction. "model_simulation/simulation_plotting.md", "model_simulation/simulation_structure_interfacing.md", "model_simulation/ensemble_simulations.md", # Stochastic simulation statistical analysis. "model_simulation/ode_simulation_performance.md", - # ODE Performance considerations/advice. # SDE Performance considerations/advice. # Jump Performance considerations/advice. # Finite state projection @@ -51,7 +49,7 @@ pages = Any[ # ODE parameter fitting using Turing. # SDE/Jump fitting. "inverse_problems/behaviour_optimisation.md", - #"inverse_problems/structural_identifiability.md", + #"inverse_problems/structural_identifiability.md", # Broken on Julia v1.10.3, requires v1.10.2 or 1.10.4. # Practical identifiability. "inverse_problems/global_sensitivity_analysis.md", "Inverse problem examples" => Any[ diff --git a/docs/src/steady_state_functionality/bifurcation_diagrams.md b/docs/src/steady_state_functionality/bifurcation_diagrams.md index a76ff91393..cebe51dc1c 100644 --- a/docs/src/steady_state_functionality/bifurcation_diagrams.md +++ b/docs/src/steady_state_functionality/bifurcation_diagrams.md @@ -43,7 +43,7 @@ nothing # hide Finally, we compute our bifurcation diagram using: ```@example ex1 -bif_dia = bifurcationdiagram(bprob, PALC(), 2, opts_br; bothside = true) +bif_dia = bifurcationdiagram(bprob, PALC(), 2, (args...) -> opts_br; bothside = true) nothing # hide ``` Where `PALC()` designates that we wish to use the pseudo arclength continuation method to track our solution. The third argument (`2`) designates the maximum number of recursions when branches of branches are computed (branches appear as continuation encounters certain bifurcation points). For diagrams with highly branched structures (rare for many common small chemical reaction networks) this input is important. Finally, `bothside = true` designates that we wish to perform continuation on both sides of the initial point (which is typically the case). @@ -69,7 +69,7 @@ opt_newton = NewtonPar(tol = 1e-9, max_iterations = 1000) opts_br = ContinuationPar(p_min = p_span[1], p_max = p_span[2], dsmin = 0.001, dsmax = 0.01, max_steps = 1000, newton_options = opt_newton) -bif_dia = bifurcationdiagram(bprob, PALC(), 2, opts_br; bothside=true) +bif_dia = bifurcationdiagram(bprob, PALC(), 2, (args...) -> opts_br; bothside = true) nothing # hide ``` (however, in this case these additional settings have no significant effect on the result) @@ -79,7 +79,7 @@ Let's consider the previous case, but instead compute the bifurcation diagram ov ```@example ex1 p_span = (2.0, 15.0) opts_br = ContinuationPar(p_min = p_span[1], p_max = p_span[2], max_steps = 1000) -bif_dia = bifurcationdiagram(bprob, PALC(), 2, opts_br= true) +bif_dia = bifurcationdiagram(bprob, PALC(), 2, (args...) -> opts_br; bothside = true) plot(bif_dia; xguide = "k1", yguide = "X") ``` Here, in the bistable region, we only see a single branch. The reason is that the continuation algorithm starts at our initial guess (here made at $k1 = 4.0$ for $(X,Y) = (5.0,2.0)$) and tracks the diagram from there. However, with the upper bound set at $k1=15.0$ the bifurcation diagram has a disjoint branch structure, preventing the full diagram from being computed by continuation alone. In this case it could be solved by increasing the bound from $k1=15.0$, however, this is not possible in all cases. In these cases, *deflation* can be used. This is described in the [BifurcationKit documentation](https://bifurcationkit.github.io/BifurcationKitDocs.jl/dev/tutorials/tutorials2/#Snaking-computed-with-deflation). @@ -103,7 +103,7 @@ bprob = BifurcationProblem(kinase_model, u_guess, p_start, :d; plot_var = :Xp, u p_span = (0.1, 10.0) opts_br = ContinuationPar(p_min = p_span[1], p_max = p_span[2], max_steps = 1000) -bif_dia = bifurcationdiagram(bprob, PALC(), 2, opts_br; bothside = true) +bif_dia = bifurcationdiagram(bprob, PALC(), 2, (args...) -> opts_br; bothside = true) plot(bif_dia; xguide = "d", yguide = "Xp") ``` This bifurcation diagram does not contain any interesting features (such as bifurcation points), and only shows how the steady state concentration of $Xp$ is reduced as $d$ increases. From c5398de85d2999198388dde3c8fae6d56aa02de4 Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 31 May 2024 10:47:15 -0400 Subject: [PATCH 12/67] add DiffEq doc dependency again --- docs/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Project.toml b/docs/Project.toml index 246727a23e..ebb086fda6 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -5,6 +5,7 @@ CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Catalyst = "479239e8-5488-4da2-87a7-35f2df7eef83" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DiffEqParamEstim = "1130ab10-4a5a-5621-a13d-e4788d82bd4c" +DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DynamicalSystems = "61744808-ddfa-5f27-97ff-6e42cc95d634" From cf83b4a4b20022ba77d1d2d5cd2b6794b9eded31 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sat, 2 Dec 2023 12:52:26 -0500 Subject: [PATCH 13/67] init. --- src/Catalyst.jl | 9 +- .../lattice_jump_systems.jl | 81 +++++++++ .../lattice_reaction_systems_jumps.jl | 157 ++++++++++++++++++ 3 files changed, 243 insertions(+), 4 deletions(-) create mode 100644 src/spatial_reaction_systems/lattice_jump_systems.jl create mode 100644 test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl diff --git a/src/Catalyst.jl b/src/Catalyst.jl index d80c115119..ad889bef22 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -166,20 +166,21 @@ export make_si_ode ### Spatial Reaction Networks ### -# spatial reactions +# Spatial reactions. 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 -# variosu utility functions +# Various utility functions include("spatial_reaction_systems/utility.jl") -# spatial lattice ode systems. +# Specific spatial problem types. include("spatial_reaction_systems/spatial_ODE_systems.jl") +include("spatial_reaction_systems/lattice_jump_systems.jl") end # module diff --git a/src/spatial_reaction_systems/lattice_jump_systems.jl b/src/spatial_reaction_systems/lattice_jump_systems.jl new file mode 100644 index 0000000000..2ecfe3fa56 --- /dev/null +++ b/src/spatial_reaction_systems/lattice_jump_systems.jl @@ -0,0 +1,81 @@ +### JumpProblem ### + +# Builds a spatial DiscreteProblem from a Lattice Reaction System. +function DiffEqBase.DiscreteProblem(lrs::LatticeReactionSystem, u0_in, tspan, p_in = DiffEqBase.NullParameters(), args...; kwargs...) + is_transport_system(lrs) || error("Currently lattice Jump simulations only supported when all spatial reactions are transport reactions.") + + # 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) + + # 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). + 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(lrs.rs, u0, tspan, [vert_ps, edge_ps], args...; kwargs...) +end + +# Builds a spatial JumpProblem from a DiscreteProblem containg a Lattice Reaction System. +function JumpProcesses.JumpProblem(lrs::LatticeReactionSystem, dprob, aggregator, args...; name = nameof(lrs.rs), + combinatoric_ratelaws = get_combinatoric_ratelaws(lrs.rs), kwargs...) + # Error checks. + (dprob.p isa Vector{Vector{Vector{Float64}}}) || dprob.p isa Vector{Vector} || error("Parameters in input DiscreteProblem is of an unexpected type: $(typeof(dprob.p)). Was a LatticeReactionProblem passed into the DiscreteProblem when it was created?") # The second check (Vector{Vector} is needed becaus on the CI server somehow the Tuple{..., ...} is covnerted into a Vector[..., ...]). It does not happen when I run tests locally, so no ideal how to fix. + any(length.(dprob.p[1]) .> 1) && error("Spatial reaction rates are currently not supported in lattice jump simulations.") + + # Computes hopping constants and mass action jumps (requires some internal juggling). + # The non-spatial DiscreteProblem have a u0 matrix with entries for all combinations of species and vertexes. + # Currently, JumpProcesses requires uniform vertex parameters (hence `p=first.(dprob.p[1])`). + hopping_constants = make_hopping_constants(dprob, lrs) + non_spat_dprob = DiscreteProblem(reshape(dprob.u0, lrs.num_species, lrs.num_verts), dprob.tspan, first.(dprob.p[1])) + majumps = make_majumps(non_spat_dprob, lrs.rs) + + return JumpProblem(non_spat_dprob, aggregator, majumps; + hopping_constants, spatial_system = lrs.lattice, 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)) + all_diff_rates = [haskey(spatial_rates_dict, s) ? spatial_rates_dict[s] : [0.0] for s in species(lrs)] + + # Creates the hopping constant Matrix. It contains one element for each combination of species and vertex. + # Each element is a Vector, containing the outgoing hopping rates for that species, from that vertex, on that edge. + hopping_constants = [Vector{Float64}(undef, length(lrs.lattice.fadjlist[j])) + for i in 1:(lrs.num_species), j in 1:(lrs.num_verts)] + + # For each edge, finds each position in `hopping_constants`. + for (e_idx, e) in enumerate(edges(lrs.lattice)) + dst_idx = findfirst(isequal(e.dst), lrs.lattice.fadjlist[e.src]) + # For each species, sets that hopping rate. + for s_idx in 1:(lrs.num_species) + hopping_constants[s_idx, e.src][dst_idx] = get_component_value(all_diff_rates[s_idx], e_idx) + end + end + + return hopping_constants +end + +# Creates the (non-spatial) mass action jumps from a (non-spatial) DiscreteProblem (and its Reaction System of origin). +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))) + eqs = equations(js) + invttype = non_spat_dprob.tspan[1] === nothing ? Float64 : typeof(1 / non_spat_dprob.tspan[2]) + + # Assembles the mass action jumps. + p = (non_spat_dprob.p isa DiffEqBase.NullParameters || non_spat_dprob.p === nothing) ? Num[] : non_spat_dprob.p + majpmapper = ModelingToolkit.JumpSysMajParamMapper(js, p; jseqs = eqs, rateconsttype = invttype) + return ModelingToolkit.assemble_maj(eqs.x[1], statetoid, majpmapper) +end \ No newline at end of file diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl b/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl new file mode 100644 index 0000000000..f41f1d3090 --- /dev/null +++ b/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl @@ -0,0 +1,157 @@ +### Preparations ### + +# Fetch packages. +using JumpProcesses +using Random, Statistics, SparseArrays, Test + +# Fetch test networks. +include("../spatial_test_networks.jl") + +### Correctness Tests ### + +# 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 srs in [Vector{TransportReaction}(), SIR_srs_1, SIR_srs_2] + lrs = LatticeReactionSystem(SIR_system, srs, grid) + u0_1 = [:S => 999, :I => 1, :R => 0] + u0_2 = [:S => round.(Int64, 500.0 .+ 500.0 * rand_v_vals(lrs.lattice)), :I => 1, :R => 0, ] + u0_3 = [:S => 950, :I => round.(Int64, 50 * rand_v_vals(lrs.lattice)), :R => round.(Int64, 50 * rand_v_vals(lrs.lattice))] + u0_4 = [:S => round.(500.0 .+ 500.0 * rand_v_vals(lrs.lattice)), :I => round.(50 * rand_v_vals(lrs.lattice)), :R => round.(50 * rand_v_vals(lrs.lattice))] + u0_5 = make_u0_matrix(u0_3, vertices(lrs.lattice), map(s -> Symbol(s.f), species(lrs.rs))) + for u0 in [u0_1, u0_2, u0_3, u0_4, u0_5] + p1 = [:α => 0.1 / 1000, :β => 0.01] + p2 = [:α => 0.1 / 1000, :β => 0.02 * rand_v_vals(lrs.lattice)] + p3 = [ + :α => 0.1 / 2000 * rand_v_vals(lrs.lattice), + :β => 0.02 * rand_v_vals(lrs.lattice), + ] + p4 = make_u0_matrix(p1, vertices(lrs.lattice), Symbol.(parameters(lrs.rs))) + for pV in [p1] #, p2, p3, p4] # Removed until spatial non-diffusion parameters are supported. + pE_1 = map(sp -> sp => 0.01, ModelingToolkit.getname.(edge_parameters(lrs))) + pE_2 = map(sp -> sp => 0.01, ModelingToolkit.getname.(edge_parameters(lrs))) + pE_3 = map(sp -> sp => rand_e_vals(lrs.lattice, 0.01), ModelingToolkit.getname.(edge_parameters(lrs))) + pE_4 = make_u0_matrix(pE_3, edges(lrs.lattice), ModelingToolkit.getname.(edge_parameters(lrs))) + for pE in [pE_1, pE_2, pE_3, pE_4] + dprob = DiscreteProblem(lrs, u0, (0.0, 100.0), (pV, pE)) + jprob = JumpProblem(lrs, dprob, NSM()) + @test SciMLBase.successful_retcode(solve(jprob, SSAStepper())) + end + end + end + end + end +end + +### Input Handling Tests ### + +# Tests that the correct hopping rates and initial conditions are generated. +# 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) + + # 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)))) + + # 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)))) + + # 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)))) + + # 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_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.scaled_rates == true_maj_scaled_rates + @test jprob.massaction_jump.reactant_stoch == true_maj_reactant_stoch + @test 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] + dprob = DiscreteProblem(lrs, u0, (0.0, 100.0), [pE; pV]) + 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.scaled_rates == true_maj_scaled_rates + @test jprob.massaction_jump.reactant_stoch == true_maj_reactant_stoch + @test jprob.massaction_jump.net_stoch == true_maj_net_stoch + end + end +end + +### ABC Model Test (from JumpProcesses) ### +let + # Preparations (stuff used in JumpProcesses examples ported over here, could be written directly into code). + Nsims = 100 + reltol = 0.05 + non_spatial_mean = [65.7395, 65.7395, 434.2605] #mean of 10,000 simulations + dim = 1 + linear_size = 5 + num_nodes = linear_size^dim + dims = Tuple(repeat([linear_size], dim)) + domain_size = 1.0 #μ-meter + mesh_size = domain_size / linear_size + rates = [0.1 / mesh_size, 1.0] + diffusivity = 1.0 + num_species = 3 + + # Make model. + rn = @reaction_network begin + (kB,kD), A + B <--> C + end + tr_1 = @transport_reaction D A + tr_2 = @transport_reaction D B + tr_3 = @transport_reaction D C + lattice = Graphs.grid(dims) + lrs = LatticeReactionSystem(rn, [tr_1, tr_2, tr_3], lattice) + + # Set simulation parameters and create problems + u0 = [:A => [0,0,500,0,0], :B => [0,0,500,0,0], :C => 0] + tspan = (0.0, 10.0) + pV = [:kB => rates[1], :kD => rates[2]] + pE = [:D => diffusivity] + dprob = DiscreteProblem(lrs, u0, tspan, (pV, pE)) + jump_problems = [JumpProblem(lrs, dprob, alg(); save_positions = (false, false)) for alg in [NSM, DirectCRDirect]] # NRM doesn't work. Might need Cartesian grid. + + # Tests. + function get_mean_end_state(jump_prob, Nsims) + end_state = zeros(size(jump_prob.prob.u0)) + for i in 1:Nsims + sol = solve(jump_prob, SSAStepper()) + end_state .+= sol.u[end] + end + end_state / Nsims + end + for jprob in jump_problems + solution = solve(jprob, SSAStepper()) + mean_end_state = get_mean_end_state(jprob, Nsims) + mean_end_state = reshape(mean_end_state, num_species, num_nodes) + diff = sum(mean_end_state, dims = 2) - non_spatial_mean + for (i, d) in enumerate(diff) + @test abs(d) < reltol * non_spatial_mean[i] + end + end +end \ No newline at end of file From f19582be1d5c8aa5394c4e2a6b1b20b23ee12b81 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sat, 2 Dec 2023 13:00:22 -0500 Subject: [PATCH 14/67] test up --- .../lattice_reaction_systems_jumps.jl | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl b/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl index f41f1d3090..8b7db9c105 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl @@ -102,17 +102,19 @@ let end end -### ABC Model Test (from JumpProcesses) ### +### Tests taken from JumpProcesses ### + +# ABC Model Test let # Preparations (stuff used in JumpProcesses examples ported over here, could be written directly into code). Nsims = 100 reltol = 0.05 - non_spatial_mean = [65.7395, 65.7395, 434.2605] #mean of 10,000 simulations + non_spatial_mean = [65.7395, 65.7395, 434.2605] # Mean of 10,000 simulations. dim = 1 linear_size = 5 num_nodes = linear_size^dim dims = Tuple(repeat([linear_size], dim)) - domain_size = 1.0 #μ-meter + domain_size = 1.0 # μ-meter. mesh_size = domain_size / linear_size rates = [0.1 / mesh_size, 1.0] diffusivity = 1.0 @@ -128,15 +130,16 @@ let lattice = Graphs.grid(dims) lrs = LatticeReactionSystem(rn, [tr_1, tr_2, tr_3], lattice) - # Set simulation parameters and create problems + # Set simulation parameters and create problems. u0 = [:A => [0,0,500,0,0], :B => [0,0,500,0,0], :C => 0] tspan = (0.0, 10.0) pV = [:kB => rates[1], :kD => rates[2]] pE = [:D => diffusivity] dprob = DiscreteProblem(lrs, u0, tspan, (pV, pE)) - jump_problems = [JumpProblem(lrs, dprob, alg(); save_positions = (false, false)) for alg in [NSM, DirectCRDirect]] # NRM doesn't work. Might need Cartesian grid. + # 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]] - # Tests. + # Run tests. function get_mean_end_state(jump_prob, Nsims) end_state = zeros(size(jump_prob.prob.u0)) for i in 1:Nsims From fcff2e8e6c3c498995ef3b3647fb1162f77bab34 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sat, 30 Dec 2023 11:55:20 +0100 Subject: [PATCH 15/67] up --- .../lattice_reaction_systems_jumps.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl b/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl index 8b7db9c105..1eb1566c30 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl @@ -7,7 +7,8 @@ using Random, Statistics, SparseArrays, Test # Fetch test networks. include("../spatial_test_networks.jl") -### Correctness Tests ### + +### General Tests ### # Tests that there are no errors during runs for a variety of input forms. let @@ -43,6 +44,7 @@ let end end + ### Input Handling Tests ### # Tests that the correct hopping rates and initial conditions are generated. @@ -102,6 +104,7 @@ let end end + ### Tests taken from JumpProcesses ### # ABC Model Test From 50c20beb5ea32ed93c2823e38b8bd60f67053515 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 31 Dec 2023 13:44:32 +0100 Subject: [PATCH 16/67] init --- .../lattice_jump_systems.jl | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/spatial_reaction_systems/lattice_jump_systems.jl b/src/spatial_reaction_systems/lattice_jump_systems.jl index 2ecfe3fa56..462c926294 100644 --- a/src/spatial_reaction_systems/lattice_jump_systems.jl +++ b/src/spatial_reaction_systems/lattice_jump_systems.jl @@ -36,9 +36,9 @@ function JumpProcesses.JumpProblem(lrs::LatticeReactionSystem, dprob, aggregator # Currently, JumpProcesses requires uniform vertex parameters (hence `p=first.(dprob.p[1])`). hopping_constants = make_hopping_constants(dprob, lrs) non_spat_dprob = DiscreteProblem(reshape(dprob.u0, lrs.num_species, lrs.num_verts), dprob.tspan, first.(dprob.p[1])) - majumps = make_majumps(non_spat_dprob, lrs.rs) + sma_jumps = make_spatial_majumps(non_spat_dprob, dprob, lrs) - return JumpProblem(non_spat_dprob, aggregator, majumps; + return JumpProblem(non_spat_dprob, aggregator, sma_jumps; hopping_constants, spatial_system = lrs.lattice, name, kwargs...) end @@ -66,6 +66,12 @@ function make_hopping_constants(dprob::DiscreteProblem, lrs::LatticeReactionSyst return hopping_constants end +# Creates the (spatial) mass action jumps from a (spatial) DiscreteProblem its non-spatial version, and a LatticeReactionSystem. +function make_spatial_majumps(non_spat_dprob, dprob, rs::LatticeReactionSystem) + ma_jumps = make_majumps(non_spat_dprob, lrs.rs) + +end + # Creates the (non-spatial) mass action jumps from a (non-spatial) DiscreteProblem (and its Reaction System of origin). function make_majumps(non_spat_dprob, rs::ReactionSystem) # Computes various required inputs for assembling the mass action jumps. @@ -74,8 +80,8 @@ function make_majumps(non_spat_dprob, rs::ReactionSystem) eqs = equations(js) invttype = non_spat_dprob.tspan[1] === nothing ? Float64 : typeof(1 / non_spat_dprob.tspan[2]) - # Assembles the mass action jumps. + # Assembles the non-spatial mass action jumps. p = (non_spat_dprob.p isa DiffEqBase.NullParameters || non_spat_dprob.p === nothing) ? Num[] : non_spat_dprob.p majpmapper = ModelingToolkit.JumpSysMajParamMapper(js, p; jseqs = eqs, rateconsttype = invttype) return ModelingToolkit.assemble_maj(eqs.x[1], statetoid, majpmapper) -end \ No newline at end of file +end From 38c28691f7e1d2d13b62be11ba1355e9e7b8e4e7 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 3 Jan 2024 14:36:39 +0100 Subject: [PATCH 17/67] Use SpatialMassActionJump --- src/Catalyst.jl | 6 +- .../lattice_jump_systems.jl | 87 +++++++++++++---- src/spatial_reaction_systems/utility.jl | 96 +++++++++++++++++++ .../lattice_reaction_systems_jumps.jl | 56 +++++++++++ 4 files changed, 222 insertions(+), 23 deletions(-) diff --git a/src/Catalyst.jl b/src/Catalyst.jl index ad889bef22..2aab60fb54 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -6,9 +6,9 @@ module Catalyst using DocStringExtensions using SparseArrays, DiffEqBase, Reexport, Setfield using LaTeXStrings, Latexify, Requires -using JumpProcesses: JumpProcesses, - JumpProblem, MassActionJump, ConstantRateJump, - VariableRateJump +using JumpProcesses: JumpProcesses, JumpProblem, + MassActionJump, ConstantRateJump, VariableRateJump, + SpatialMassActionJump # ModelingToolkit imports and convenience functions we use using ModelingToolkit diff --git a/src/spatial_reaction_systems/lattice_jump_systems.jl b/src/spatial_reaction_systems/lattice_jump_systems.jl index 462c926294..6287936889 100644 --- a/src/spatial_reaction_systems/lattice_jump_systems.jl +++ b/src/spatial_reaction_systems/lattice_jump_systems.jl @@ -28,15 +28,19 @@ end function JumpProcesses.JumpProblem(lrs::LatticeReactionSystem, dprob, aggregator, args...; name = nameof(lrs.rs), combinatoric_ratelaws = get_combinatoric_ratelaws(lrs.rs), kwargs...) # Error checks. - (dprob.p isa Vector{Vector{Vector{Float64}}}) || dprob.p isa Vector{Vector} || error("Parameters in input DiscreteProblem is of an unexpected type: $(typeof(dprob.p)). Was a LatticeReactionProblem passed into the DiscreteProblem when it was created?") # The second check (Vector{Vector} is needed becaus on the CI server somehow the Tuple{..., ...} is covnerted into a Vector[..., ...]). It does not happen when I run tests locally, so no ideal how to fix. - any(length.(dprob.p[1]) .> 1) && error("Spatial reaction rates are currently not supported in lattice jump simulations.") + # The second check (Vector{Vector} is needed because on the CI server somehow the Tuple{..., ...} is converted into a Vector[..., ...]). + # It does not happen when I run tests locally, so no ideal how to fix. + (dprob.p isa Vector{Vector{Vector{Float64}}}) || dprob.p isa Vector{Vector} || error("Parameters in input DiscreteProblem is of an unexpected type: $(typeof(dprob.p)). Was a LatticeReactionProblem passed into the DiscreteProblem when it was created?") # Computes hopping constants and mass action jumps (requires some internal juggling). - # The non-spatial DiscreteProblem have a u0 matrix with entries for all combinations of species and vertexes. # 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 + # handle parameters this can be updated and improved. + # The non-spatial DiscreteProblem have a u0 matrix with entries for all combinations of species and vertexes. hopping_constants = make_hopping_constants(dprob, lrs) + sma_jumps = make_spatial_majumps(dprob, lrs) non_spat_dprob = DiscreteProblem(reshape(dprob.u0, lrs.num_species, lrs.num_verts), dprob.tspan, first.(dprob.p[1])) - sma_jumps = make_spatial_majumps(non_spat_dprob, dprob, lrs) return JumpProblem(non_spat_dprob, aggregator, sma_jumps; hopping_constants, spatial_system = lrs.lattice, name, kwargs...) @@ -66,22 +70,65 @@ function make_hopping_constants(dprob::DiscreteProblem, lrs::LatticeReactionSyst return hopping_constants end -# Creates the (spatial) mass action jumps from a (spatial) DiscreteProblem its non-spatial version, and a LatticeReactionSystem. -function make_spatial_majumps(non_spat_dprob, dprob, rs::LatticeReactionSystem) - ma_jumps = make_majumps(non_spat_dprob, lrs.rs) - +# 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. +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)] + + # 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))) + + # 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)) + 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) + 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)) + 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) + cur_rx += 1 + end + # SpatialMassActionJump expects empty rate containers to be nothing. + isempty(u_rates) && (u_rates = nothing) + (count(is_spatials)==0) && (s_rates = nothing) + + return SpatialMassActionJump(u_rates, s_rates, reactant_stoich, net_stoich) end -# Creates the (non-spatial) mass action jumps from a (non-spatial) DiscreteProblem (and its Reaction System of origin). -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))) - eqs = equations(js) - invttype = non_spat_dprob.tspan[1] === nothing ? Float64 : typeof(1 / non_spat_dprob.tspan[2]) - - # Assembles the non-spatial mass action jumps. - p = (non_spat_dprob.p isa DiffEqBase.NullParameters || non_spat_dprob.p === nothing) ? Num[] : non_spat_dprob.p - majpmapper = ModelingToolkit.JumpSysMajParamMapper(js, p; jseqs = eqs, rateconsttype = invttype) - return ModelingToolkit.assemble_maj(eqs.x[1], statetoid, majpmapper) +### Extra ### + +# Temporary. Awaiting implementation in SII, or proper implementation withinCatalyst (with more general functionality). +function int_map(map_in, sys) where {T,S} + return [ModelingToolkit.variable_index(sys, pair[1]) => pair[2] for pair in map_in] end + +# Currently unused. If we want to create certain types of MassActionJumps (instead of SpatialMassActionJumps) we can take this one back. +# Creates the (non-spatial) mass action jumps from a (non-spatial) DiscreteProblem (and its Reaction System of origin). +# 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))) +# eqs = equations(js) +# invttype = non_spat_dprob.tspan[1] === nothing ? Float64 : typeof(1 / non_spat_dprob.tspan[2]) +# +# # Assembles the non-spatial mass action jumps. +# p = (non_spat_dprob.p isa DiffEqBase.NullParameters || non_spat_dprob.p === nothing) ? Num[] : non_spat_dprob.p +# majpmapper = ModelingToolkit.JumpSysMajParamMapper(js, p; jseqs = eqs, rateconsttype = invttype) +# return ModelingToolkit.assemble_maj(eqs.x[1], statetoid, majpmapper) +# end diff --git a/src/spatial_reaction_systems/utility.jl b/src/spatial_reaction_systems/utility.jl index 03271562e8..691ba79a3c 100644 --- a/src/spatial_reaction_systems/utility.jl +++ b/src/spatial_reaction_systems/utility.jl @@ -295,3 +295,99 @@ end function matrix_expand_component_values(values::Vector{<:Vector}, n) 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) + error("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:lrs.num_edges] +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. +# 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. + 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.") + 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. + 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) + + # 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]...)] + end + return [exp_func([get_component_value(sym_val_dict[sym], idxV) for sym in relevant_syms]...) + for idxV in 1:lrs.num_verts] +end + +### System Property Checks ### + +# For a Symbolic expression, a LatticeReactionSystem, and a parameter list of the internal format: +# Checks if any edge parameter in the expression have a spatial component (that is, is not uniform). +function has_spatial_edge_component(exp, lrs::LatticeReactionSystem, edge_ps) + # Finds the edge parameters in the expression. Computes their indexes. + exp_syms = Symbolics.get_variables(exp) + exp_edge_ps = filter(sym -> any(isequal(sym), edge_parameters(lrs)), exp_syms) + p_idxs = [findfirst(isequal(sym, edge_p) for edge_p in edge_parameters(lrs)) for sym in exp_syms] + # Checks if any of the corresponding value vectors have length != 1 (that is, is not uniform). + return any(length(edge_ps[p_idx]) != 1 for p_idx in p_idxs) +end + +# For a Symbolic expression, a LatticeReactionSystem, and a parameter list of the internal format (vector of vectors): +# Checks if any vertex parameter in the expression have a spatial component (that is, is not uniform). +function has_spatial_vertex_component(exp, lrs::LatticeReactionSystem; u=nothing, vert_ps=nothing) + # Finds all the symbols in the expression. + exp_syms = Symbolics.get_variables(exp) + + # If vertex parameter values where given, checks if any of these have non-uniform values. + if !isnothing(vert_ps) + exp_vert_ps = filter(sym -> any(isequal(sym), vertex_parameters(lrs)), exp_syms) + p_idxs = [ModelingToolkit.parameter_index(lrs.rs, sym) for sym in exp_vert_ps] + any(length(vert_ps[p_idx]) != 1 for p_idx in p_idxs) && return true + end + + # If states values where given, checks if any of these have non-uniform values. + if !isnothing(u) + exp_u = filter(sym -> any(isequal(sym), species(lrs)), exp_syms) + u_idxs = [ModelingToolkit.variable_index(lrs.rs, sym) for sym in exp_u] + any(length(u[u_idx]) != 1 for u_idx in u_idxs) && return true + end + + return false +end \ No newline at end of file diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl b/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl index 1eb1566c30..6721e707cd 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl @@ -105,6 +105,62 @@ let end +### SpatialMassActionJump Testing ### + +# Checks that the correct structure is produced. +let + # Network for reference: + # A, ∅ → X + # 1, 2X + Y → 3X + # B, X → Y + # 1, X → ∅ + # srs = [@transport_reaction dX X] + # Create LatticeReactionSystem + lrs = LatticeReactionSystem(brusselator_system, brusselator_srs_1, small_3d_grid) + + # Create JumpProblem + u0 = [:X => 1, :Y => rand(1:10, lrs.num_verts)] + tspan = (0.0, 100.0) + ps = [:A => 1.0, :B => 5.0 .+ rand(lrs.num_verts), :dX => rand(lrs.num_edges)] + dprob = DiscreteProblem(lrs, u0, tspan, ps) + jprob = JumpProblem(lrs, dprob, NSM()) + + # Checks internal structures. + 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) + + # Checks that problem can be simulated. + @test SciMLBase.successful_retcode(solve(jprob, SSAStepper())) +end + +# Checks that simulations gives a correctly heterogeneous solution. +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) + + # Create JumpProblem. + u0 = [:X => 1] + tspan = (0.0, 100.0) + ps = [:p => [0.1, 1.0, 10.0, 100.0], :d => 1.0, :D => 0.0] + dprob = DiscreteProblem(lrs, u0, tspan, ps) + jprob = JumpProblem(lrs, dprob, NSM()) + + # 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) + @test mean(getindex.(sol.u, 1)) < mean(getindex.(sol.u, 2)) < mean(getindex.(sol.u, 3)) < mean(getindex.(sol.u, 4)) + end +end + + ### Tests taken from JumpProcesses ### # ABC Model Test From 89fcdf30d9c2c3916378c69a9adf1805655153b1 Mon Sep 17 00:00:00 2001 From: Torkel Date: Thu, 25 Jan 2024 17:42:34 -0500 Subject: [PATCH 18/67] test up --- .../lattice_reaction_systems_jumps.jl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl b/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl index 6721e707cd..8fc019d8bf 100644 --- a/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl +++ b/test/spatial_reaction_systems/lattice_reaction_systems_jumps.jl @@ -87,9 +87,8 @@ let 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.scaled_rates == true_maj_scaled_rates @test jprob.massaction_jump.reactant_stoch == true_maj_reactant_stoch - @test jprob.massaction_jump.net_stoch == true_maj_net_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] @@ -97,9 +96,8 @@ let 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.scaled_rates == true_maj_scaled_rates @test jprob.massaction_jump.reactant_stoch == true_maj_reactant_stoch - @test jprob.massaction_jump.net_stoch == true_maj_net_stoch + @test all(issetequal(ns1, ns2) for (ns1, ns2) in zip(jprob.massaction_jump.net_stoch, true_maj_net_stoch)) end end end From 16938691527001b74a5608ea2119586631e01d58 Mon Sep 17 00:00:00 2001 From: Torkel Date: Thu, 16 May 2024 08:41:50 -0400 Subject: [PATCH 19/67] update for new MTK Parameter structure --- .../lattice_jump_systems.jl | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/spatial_reaction_systems/lattice_jump_systems.jl b/src/spatial_reaction_systems/lattice_jump_systems.jl index 6287936889..818da05d1a 100644 --- a/src/spatial_reaction_systems/lattice_jump_systems.jl +++ b/src/spatial_reaction_systems/lattice_jump_systems.jl @@ -2,7 +2,9 @@ # Builds a spatial DiscreteProblem from a Lattice Reaction System. function DiffEqBase.DiscreteProblem(lrs::LatticeReactionSystem, u0_in, tspan, p_in = DiffEqBase.NullParameters(), args...; kwargs...) - is_transport_system(lrs) || error("Currently lattice Jump simulations only supported when all spatial reactions are transport reactions.") + if !is_transport_system(lrs) + 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. @@ -18,20 +20,20 @@ function DiffEqBase.DiscreteProblem(lrs::LatticeReactionSystem, u0_in, tspan, p_ # 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(lrs.rs, u0, tspan, [vert_ps, edge_ps], args...; kwargs...) + return DiscreteProblem(u0, tspan, [vert_ps, edge_ps], args...; kwargs...) end # Builds a spatial JumpProblem from a DiscreteProblem containg a Lattice Reaction System. function JumpProcesses.JumpProblem(lrs::LatticeReactionSystem, dprob, aggregator, args...; name = nameof(lrs.rs), combinatoric_ratelaws = get_combinatoric_ratelaws(lrs.rs), kwargs...) # Error checks. - # The second check (Vector{Vector} is needed because on the CI server somehow the Tuple{..., ...} is converted into a Vector[..., ...]). - # It does not happen when I run tests locally, so no ideal how to fix. - (dprob.p isa Vector{Vector{Vector{Float64}}}) || dprob.p isa Vector{Vector} || error("Parameters in input DiscreteProblem is of an unexpected type: $(typeof(dprob.p)). Was a LatticeReactionProblem passed into the DiscreteProblem when it was created?") - + if !isnothing(dprob.f.sys) + error("Unexpected `DiscreteProblem` passed into `JumpProblem`. Was a `LatticeReactionSystem` used as input to the initial `DiscreteProblem`?") + end + # Computes hopping constants and mass action jumps (requires some internal juggling). # Currently, JumpProcesses requires uniform vertex parameters (hence `p=first.(dprob.p[1])`). # Currently, the resulting JumpProblem does not depend on parameters (no way to incorporate these). From 09bd10a0d365df5c67c2786d9f95fef1b0b2371c Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 31 May 2024 14:28:27 -0400 Subject: [PATCH 20/67] rebase fix --- test/runtests.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/runtests.jl b/test/runtests.jl index 8f255921d7..9196ca91c8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -61,6 +61,7 @@ using SafeTestsets, Test @time @safetestset "PDE Systems Simulations" begin include("spatial_modelling/simulate_PDEs.jl") end @time @safetestset "Lattice Reaction Systems" begin include("spatial_modelling/lattice_reaction_systems.jl") end @time @safetestset "ODE Lattice Systems Simulations" begin include("spatial_modelling/lattice_reaction_systems_ODEs.jl") end + @time @safetestset "Jump Lattice Systems Simulations" begin include("spatial_reaction_systems/lattice_reaction_systems_jumps.jl") end #end #if GROUP == "All" || GROUP == "Visualisation-Extensions" From 67ab88808507ec639466c2f50a0a53eaf3948324 Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 31 May 2024 16:02:51 -0400 Subject: [PATCH 21/67] up --- docs/src/assets/Project.toml | 1 + docs/src/model_simulation/simulation_introduction.md | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/src/assets/Project.toml b/docs/src/assets/Project.toml index 246727a23e..ebb086fda6 100644 --- a/docs/src/assets/Project.toml +++ b/docs/src/assets/Project.toml @@ -5,6 +5,7 @@ CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Catalyst = "479239e8-5488-4da2-87a7-35f2df7eef83" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DiffEqParamEstim = "1130ab10-4a5a-5621-a13d-e4788d82bd4c" +DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DynamicalSystems = "61744808-ddfa-5f27-97ff-6e42cc95d634" diff --git a/docs/src/model_simulation/simulation_introduction.md b/docs/src/model_simulation/simulation_introduction.md index 6234dc4a0a..81894ac74e 100644 --- a/docs/src/model_simulation/simulation_introduction.md +++ b/docs/src/model_simulation/simulation_introduction.md @@ -210,6 +210,9 @@ Next, let us consider a simulation for another parameter set: ```@example simulation_intro_sde sprob = remake(sprob; u0 = [:X1 => 100.0, :X2 => 200.0], p = [:k1 => 200.0, :k2 => 500.0]) sol = solve(sprob, STrapezoid()) +nothing # hide +``` +```@example simulation_intro_sde sol = solve(sprob, STrapezoid(); seed = 12345) # hide plot(sol) ``` @@ -317,9 +320,9 @@ plot(sol) ### [Designating aggregators and simulation methods for jump simulations](@id simulation_intro_jumps_solver_designation) Jump simulations (just like ODEs and SDEs) are performed using solver methods. Unlike ODEs and SDEs, jump simulations are carried out by two different types of methods acting in tandem. First, an *aggregator* method is used to (after each reaction) determine the time to, and type of, the next reaction. Next, a simulation method is used to actually carry out the simulation. -Several different aggregators are available (a full list is provided [here](https://docs.sciml.ai/JumpProcesses/stable/jump_types/#Jump-Aggregators-for-Exact-Simulation)). To designate a specific one, provide it as the third argument to the `JumpProblem`. E.g. to designate that Gillespie's direct method (`Direct`) should be used, use: +Several different aggregators are available (a full list is provided [here](https://docs.sciml.ai/JumpProcesses/stable/jump_types/#Jump-Aggregators-for-Exact-Simulation)). To designate a specific one, provide it as the third argument to the `JumpProblem`. E.g. to designate that the sorting direct method (`SortingDirect`) should be used, use: ```@example simulation_intro_jumps -jprob = JumpProblem(two_state_model, dprob, Direct()) +jprob = JumpProblem(two_state_model, dprob, SortingDirect()) nothing # hide ``` Especially for large systems, the choice of aggregator is relevant to simulation performance. A guide for aggregator selection is provided [here](@ref ref). From abd6dcafddb1ddabdd7598f4229c24401e77d0f7 Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 31 May 2024 16:03:41 -0400 Subject: [PATCH 22/67] up --- docs/pages.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/pages.jl b/docs/pages.jl index 96e6587bf3..941e906c3a 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -18,7 +18,7 @@ pages = Any[ "model_creation/network_analysis.md", "model_creation/chemistry_related_functionality.md", "Model creation examples" => Any[ - "model_creation/examples/basic_CRN_library.md", + #"model_creation/examples/basic_CRN_library.md", "model_creation/examples/programmatic_generative_linear_pathway.md", "model_creation/examples/hodgkin_huxley_equation.md", "model_creation/examples/smoluchowski_coagulation_equation.md" @@ -29,9 +29,9 @@ pages = Any[ # Simulation introduction. "model_simulation/simulation_plotting.md", "model_simulation/simulation_structure_interfacing.md", - "model_simulation/ensemble_simulations.md", + #"model_simulation/ensemble_simulations.md", # Stochastic simulation statistical analysis. - "model_simulation/ode_simulation_performance.md", + #"model_simulation/ode_simulation_performance.md", # ODE Performance considerations/advice. # SDE Performance considerations/advice. # Jump Performance considerations/advice. @@ -39,7 +39,7 @@ pages = Any[ ], "Steady state analysis" => Any[ "steady_state_functionality/homotopy_continuation.md", - "steady_state_functionality/nonlinear_solve.md", + #"steady_state_functionality/nonlinear_solve.md", "steady_state_functionality/steady_state_stability_computation.md", "steady_state_functionality/bifurcation_diagrams.md", "steady_state_functionality/dynamical_systems.md" From b7047cf99109e5dbe5b65d80017329e392e155c5 Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 31 May 2024 17:35:06 -0400 Subject: [PATCH 23/67] up --- .github/workflows/Documentation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml index 7e9af3e1dd..fc1871b19a 100644 --- a/.github/workflows/Documentation.yml +++ b/.github/workflows/Documentation.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@latest with: - version: '1' + version: '1.10.2' - name: Install dependencies run: julia --project=docs/ -e 'ENV["JULIA_PKG_SERVER"] = ""; using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' - name: Build and deploy From 088e3129ca6dd491e2043dad0bb694f53e1f61b6 Mon Sep 17 00:00:00 2001 From: CompatHelper Julia Date: Sat, 1 Jun 2024 00:20:36 +0000 Subject: [PATCH 24/67] CompatHelper: add new compat entry for DynamicPolynomials at version 0.5, (keep existing compat) --- Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Project.toml b/Project.toml index 2b90ff7b2a..3c9572d1c4 100644 --- a/Project.toml +++ b/Project.toml @@ -40,6 +40,7 @@ BifurcationKit = "0.3" DataStructures = "0.18" DiffEqBase = "6.83.0" DocStringExtensions = "0.8, 0.9" +DynamicPolynomials = "0.5" DynamicQuantities = "0.13.2" Graphs = "1.4" HomotopyContinuation = "2.9" From 013d58d364de49f4142a98e5a15890fbc1899473 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 1 Jun 2024 04:49:58 +0200 Subject: [PATCH 25/67] Update pages.jl comment out graphviz stuff --- docs/pages.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/pages.jl b/docs/pages.jl index 1a54135768..bd716f4327 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -14,7 +14,7 @@ pages = Any[ # Events. #"model_creation/parametric_stoichiometry.md",# Distributed parameters, rates, and initial conditions. # Loading and writing models to files. - "model_creation/model_visualisation.md", + #"model_creation/model_visualisation.md", #"model_creation/network_analysis.md", "model_creation/chemistry_related_functionality.md", "Model creation examples" => Any[ @@ -67,4 +67,4 @@ pages = Any[ # ], #"FAQs" => "faqs.md", #"API" => "api.md" -] \ No newline at end of file +] From c4264299868945eb756956a5e1f331e9df59ff86 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sat, 1 Jun 2024 14:24:47 -0400 Subject: [PATCH 26/67] add back model visualization doc page --- docs/pages.jl | 10 +++++----- .../assets/network_graphs/brusselator_graph.png | Bin 0 -> 14630 bytes .../repressilator_complex_graph.png | Bin 0 -> 13569 bytes .../network_graphs/repressilator_graph.png | Bin 0 -> 12564 bytes docs/src/model_creation/model_visualisation.md | 7 +++++++ 5 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 docs/src/assets/network_graphs/brusselator_graph.png create mode 100644 docs/src/assets/network_graphs/repressilator_complex_graph.png create mode 100644 docs/src/assets/network_graphs/repressilator_graph.png diff --git a/docs/pages.jl b/docs/pages.jl index bd716f4327..65ea437951 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -14,11 +14,11 @@ pages = Any[ # Events. #"model_creation/parametric_stoichiometry.md",# Distributed parameters, rates, and initial conditions. # Loading and writing models to files. - #"model_creation/model_visualisation.md", + "model_creation/model_visualisation.md", #"model_creation/network_analysis.md", "model_creation/chemistry_related_functionality.md", "Model creation examples" => Any[ - #"model_creation/examples/basic_CRN_library.md", + "model_creation/examples/basic_CRN_library.md", "model_creation/examples/programmatic_generative_linear_pathway.md", #"model_creation/examples/hodgkin_huxley_equation.md", #"model_creation/examples/smoluchowski_coagulation_equation.md" @@ -28,7 +28,7 @@ pages = Any[ "model_simulation/simulation_introduction.md", "model_simulation/simulation_plotting.md", "model_simulation/simulation_structure_interfacing.md", - #"model_simulation/ensemble_simulations.md", + "model_simulation/ensemble_simulations.md", # Stochastic simulation statistical analysis. "model_simulation/ode_simulation_performance.md", # SDE Performance considerations/advice. @@ -37,7 +37,7 @@ pages = Any[ ], "Steady state analysis" => Any[ "steady_state_functionality/homotopy_continuation.md", - #"steady_state_functionality/nonlinear_solve.md", + "steady_state_functionality/nonlinear_solve.md", "steady_state_functionality/steady_state_stability_computation.md", "steady_state_functionality/bifurcation_diagrams.md", "steady_state_functionality/dynamical_systems.md" @@ -49,7 +49,7 @@ pages = Any[ # ODE parameter fitting using Turing. # SDE/Jump fitting. "inverse_problems/behaviour_optimisation.md", - #"inverse_problems/structural_identifiability.md", # Broken on Julia v1.10.3, requires v1.10.2 or 1.10.4. + "inverse_problems/structural_identifiability.md", # Broken on Julia v1.10.3, requires v1.10.2 or 1.10.4. # Practical identifiability. "inverse_problems/global_sensitivity_analysis.md", "Inverse problem examples" => Any[ diff --git a/docs/src/assets/network_graphs/brusselator_graph.png b/docs/src/assets/network_graphs/brusselator_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..599d23615817fc169112d0b0d9170ddc9499f83d GIT binary patch literal 14630 zcmZ`=WmHyMw8blmfS@3vl!$amgS2#aBO#KK(hVvCA|Tx*jdXWNOLv!aclTT08}IKM zjw@Wn^X;?uin->T+fQCr3F&~2`S#w^%qf9j~yQc@r>RA^LJQ8utwH(I&6`L{YP{z?D6(Z5TN zI?)NEUY6#)_+97vII^zdfR_)x%0CmPeMs~L@tv*k;4@@_ETRAJ%Qt+x)n20dn+vmx z<4s>Lue+n8qfXAwOdeNGA3l6|NR%j#@;!~=0oz)FhiBhki+U zvhmp@(T!C2W4?t@UtW{_3m;1{=Ii| zb35B_Csv?|@WGVGnh}=Kef%)JurN3{`0>@%m2lHD(*P6r@jF=~rcKSwOdK4>hbukN zF)?^tWXYC~>D>hwzeVYK#4Id)ctuLuMSi84r)hn#Bym@iV%QKxx|L4!Z1N#uF-3NjglcgWSl7${`ycr0g zg=2C2{!>ff@waMW_$QCQaS7Wkp$W3d=Zq|^u5R+FQQ?eFPU17Z5}+att$(TfHqwxp zg{7mjbD=GeV7k^h47Pjb^t>mIBg8OfdRnVglb$BxM@mWx3SW6(V4!7IMqM2*E-vny zH*cmLODZbb8yoM4iHXIsnTeJd^|bg2Wii7lhCkxr;b~Rbcy~oHY~D1S7D=WLUdN}W zlgrA=vYHNmhtFEv+6pKsVSzuO!a>{G+6pE^H8V5I&CO+GVWH(9Q%W5Oj*3E&-)RYs zi<44Nz?ztth?pHVE!(l)7|HRCizASUV^9A$lu=nzv$(PGqo#(N{#|*Rh(B(0T-;cH z4rTPqqJOdS99haV5J!CzCB}X~f38ed*=p6h#XuM!ZHyJj!FKK)9Hgk^s8$CNa9Bz& zwgqBLHuZLNkdTskm6o!?&PYf}k@E6tpOzgR+1c3GAj#82R900D6dOwM@bZ3~C{Ev5 zSzZ>Yb2)x^!h@XpGPGX)a=OknDmwakN@}Wo#o3wDuqoTg$%);@u;1s;NVRo!`v(V2 zU0vwUo;?%t)h4g1sv;vNU$<1t*COHMjGPfrg9F)=a3+TQVTYkRxz zXx_V_(vM1a`R^wCK-xst*VgiQTpz1p5^=T6H~abp29A)Qeg6E}hC@zHPRRG&;tzj+ z30c{d!E`AyGO|zWgX9Z~ixSe(WZc{YpM5Y0He``JJUq@WE*5rogM58`)vrq&uq1RD zdGFo3N671<9JQ31nmS-wR{C+sr{4V%`}uQq<0n^()y{_*JIKM&(c)TKg!}vZX=!OM zyO>o~RZA^rG##-kPd&RL>03L0Wk`^@H=g|-G5tx%*>rP#$?0~+m?@hen5Vk9x+*qT z?~aOw_KJ$?0VO4+kguj04h|0F&s!}mt*WV!95wh{b#?VMNnu!GbzD@`GYB~!Ox>(_ z@r$q4)wZjct|wc1mnYj>6D7X!@r1CgUF7E!GO^FOy1B&C6+_@rZf-o!7<7iOlj7r{ z4zx{A$3vb)&|;NY&fvqw=H}&L5Y@why_e3{sq2I{952#GaXsA;Nge3Vd&kk#)byO) z{CVEH3U;VI?CT3+(r;t5SrS($$Lj3p>l^C{sE5kB<8XdpxHDaK z_j5!roW3a4wdZAc<`t&cxI8hHy=xAr= z-LTF*FIpO!SJc#g-@YNwTzSFoueB=QN=Tq0l6QMDLZ{A!$!YIBm&2C&JGuX$b;!Mj zB4Jg3?z3Be`7<|{&gUUs)9`O4R8&-~)Yr&i(0;nBY**jdZ{R@KFZ}s(SF6E;2x8{` zqZi$K^UZjyCh`^=q>m+Zo!ox^{_S?qLFsmTeL6lh#bVfrlOdb%BQ7p3S?I|(8YslJ ztGz|8rO-c?d;QQ-TLm$f(A<(EAy~Iv$5BZ+h$6XdI|qlSKB)yXeA^i zKP>$Xsl7g#g4$JVIm0z$#MUNF=AIl64RxhAL29)(0ebE#)Oxyiq*V z9zXsAS+7xQg4J+yK?~TyIi(%jFV6-G3+rfYz-w)7%_c4rJ)>i0CIK3W_05&@^z`&| zW`hS__c0)zI{K4U#D|aWvEpN(YaF_xnY?i6 zH9uWn*h7y?e8p}-6Cqhq?gl;8w8zYrMNMQSp~hf2`!79RvT$BfP3 z*5a}ZzdDS-ZS$-MrKHcKVn6pbatcD>ZH0xAS&X_FGm5jG7i)~Ar=>9h@`AQ>el#TK zyj?bS#QdbFoPyn*WqC|AHKHmyvS(uH9a-9{yt?uaWU8mw;=dvcOEu-G$jIJ?k`2ce z$5sTU6}!tqoSub97qvyJDc|{HJ=CtycB6UQ;o+(_gJFA(mx=; zU}N|KhhJVs^q`qi%4p%L7QU}X-`mJ8&5Mf^<#287lLK2l@y`UKxMi7(x{-$U zR|ir?J+UH5eD1J*Vw360qU0tli8=XF&6t2KLCdAAznv-y^2(CdzkL(~aS6~_=(;1u z$12i_^4MgEHrMm+*};K{2ECB6vty-I&3!*zY3<@Xjlz`PqBYe_()_%Spj$%}nHak~k zYuua2+a<+N*YZoi>+GP5!5OyGj0baIu;32fg!dMAQtx?AMX(`WO!2py%5EHN)4Tv7 zcd3gqGs#Cu6wz_Klf*56PVo3WcmxCla^F?ti=HO3MU3W>JU>L7!@*4b8*FCVkYn{u zXJtw6Z?38LYq1xMm6sDC^7l_0ZWET4mMAakI*Pl+h}{ukJCrH6s(X8(Cey2rwVbdK zo}k;3@uWjbyhAiO_(=1viuij&JyEA9+#{2jZzvd*=ZH0L=2b-VI5Pv{EZFx`YzrSB z=ETD>zWP~C^~lhjk}l3s@T0zQp;N$m*?CC{Hinerwd}>VU|qcxag-v-{B(aTN7JO@ zTxVhPi?Gm8r^6l&2?>e$ApV4yi9nLWA-!7;q9>`Cq?c*JgdcVHICNqqUD z={a_sYJ|{9y?0iq+*dLZ>hNDGUStXl%`D?5Zdei;hZ3#Nb>bQn%}VKJ?7!q_oSR@o z@9iqkkZEmg?w=NxtH;t};ELII^KyFL@Bj*6H6IU7^1O-3kco@Dy!r8z_WO??9{?e# zaP=mAPb;uG&sJ0=lKy97^qTFsiRS4`3A8rk@+IosVud>*A|leUtbP_#wVKj*6VJ<`Zk` zr`UWl+fIhT4UH7`Dr3`OfJE+N5(1_`1xS?+g%JtS;UM5qK0|ATnx5lECvDNlrmx$+ ztm=Vh@(%3IRMv97xFzO9Hxei$t zv3i>jr=iz`5?qGAqr>@w=3H2%*^Nv=K|wt98yTFE!LI=myCl!k8Amp3A;X{KT9jPp zkqYE)R0RExlRSLAiq`M@Cs+Hh)l^mOL&y1GY%ErwQ}^A^Pj56=qdSq8KqHcgg+;$3 zl-z|oQKIzdSX(Lw6R!eY1N)e})ihAR>{lpq2a0N#(+Xpa!#05Wu9w!rA|hD0xT^D~ z_17owU{-Bycc}%m1~noO2Ed)2ol=V_Tz-Cj%?iuB8kO>&GAoX$131%e{}!dt$Vcg2 z=ieN8wg1RruZmD>`6@GD!e$f5uKB`OFf^Jo<*p3T1|I9rjY`3;o*r^m)^I=_`C3(q zmaVcxqcijI!ePJDT~lXhXQ=C!18S4^`Wn&7d~6x1$gj zsLFrZOb%i&V>2)&Q*5Yw4xd}sY9aY5i1JIo0EHWpMCkeY7;Wvf-KA!QLr_^68_aiv z{2o{JRHI}@}rbnH$tX`2qfDM(H&uyY~TdMWl=>!?ibDeZWzk&jW)xIRp zwZ2)Y6EyFyFmyb|TIqdEoP&aC7J(z#=6jmjZj1H5LtUm^5(PayMxf|j=weS`KF!@* zSy)JkTD?evp)?34X$XiQ_c>l;r4XD@fwo0{zLsTO{aiHQluV0}x= ze5s8H07GKGm6^>(=@7k6LYnNnAx|WdEn-$|)N~&MUxYH~QcaMKo}QeZJ`f7AO$Ait z5SZ@-1n$VIIs)MNSe5gQUzc9lanW!nL&oi5JySY?D*{m7G{;o|vzpbCEgNlvrv4Bm zUS&kitJl|g1P7S^V(RyEr5iXS&^gK;^Jr>0WThDPj6R9^YM5-_t@S9ZvORTX{-49BC>0G%7TNUr2LVKK2M&z@yoR%T^Au(h=XlIy{P2Y=?~h@pH`J8XBROGT~C)SS8prOQ(V z8^!>;aC5!usa9!Or5e$da5u^Jv~!?%y_+h;0D}r=c=wF1e1?*g`q*>+-2^IvYv8{* z6B^Sy9rO6cKL=p+#m6A8lkKS|1Oz#kfM5yX$hsIBy5l)*X{2vV10x;Bu0)Hf4|0Bq z*}p$-Zy}x%+1&AZ;4xo!S+5&y8-{_dW3aSkW%6%4t+Sm)N!*Qfp4y9UKwjIKijObx z`}YS>kG{slOt^QW+6l)CGR)!3%Qg+ahVpmOez(ETZ+H(qm#SayPe-HRi-dD zC4fRxR8q2+xYh}kla;;S8JdukM11GYooccrM=zubVnNrIHync z_Vi=F-FCnI+u6zA95)*;Bso7njrzC~Q8Vkbhyg40uDiaKe5IDZd|)`d>*CBO;$4;N z?#TmR0-TJg^L4hn+&%j^u-?>MK4x6wtRsAcnt{2On&%qf%fwf{(JKTq;0Nd`gnVw# zSy)PJ+M%(KLcUSRB(Akkhu(yZyR%0m^`*xM)84*dN&gy@xT%?!mjcuVOri4m%N{3N z8o*pa*gLtpZffqp7TE1fpBHiS$FW;T8W~Z*-vD94=(wW=3l7xe=vD**EizIyU+KKB z!nI?jsp-E*j~>Cfj#pUC-6cNMN&0J6P}c_hNl{sup_(0yYhPySz((esCyuN?yv+Do z8%MmDu;xD!}xtv!qrqpEWyx7${;yqo0sdu{C-v7?s z`^wb`3&8atVEG$>C6ejbfudejRZuFr^?=hf@jsxFLbdfbY~><>#N-~O5x4=vA;9E3cI}L<}%*M zsdwKEq}q=#YbrqJDN47BV8n=B17>jl%a| zNH|g%v*5sOD8DFW$`+Z8<%?ApKx1ZSXRke9iDNbA{+=`8a`i6MkewmEw~V%7Xffua z9U7i*M(H>EL&^cG1-e)l$Nsq@B#zblwTE_>zPG=yG>bmmV`#WyE-x<+AmI2}q~AUW zQ|6_^XsBTfu$22dHM<*c_Dz_lVt)9S@5&I2>O=fIW=H{?ey(x67S>`Sc0lMBz6k;@p<*rfkXfP%=N< zK>R&br<(R^T^*elMMXu+n!7OnF2X!r-P_R+Y0Sf$Q?Oj-d}sva!(?n{KT*u0!sFTn zD(C6=lZD*PsX^zz!jnhN_~}(17jBluWF_;>9aKF%cpRSe)?z7t`|5S{5*o7UdLrmt z>T70+D=2c+i@vJqx~q)2FwoPdE2N4du%q>2+d#6=`I9uwJfq`{k=omvOQ1Uv`r6O; z&7&>}ctZytitviX2E6;yv{64lnZYDG7u}LMH;`whtHO>!w8ndaapH0M=x463pzU1m zI4fO_9PV7tYJZUb1Luz(5l{HS%W!Z0c4lS?dqs$}hH(Sf=_;FM(5V#%ny;8n-^@%3 z5@GN3)EKw{WMt(2-%6C`=H@hRxf=)FEPcak`_dwcirBy+K8mlUkidq>EYl1mlK?JP zZzRnetDtsjtkCMec-)Q=Ixj8@en_!FzY;B*y;-ar zL$NPtW~Zg~hl3CGMt?R4jBujs4kv7E<~v*S&Twj;sR|w`x zobCExy3KM7G|w3)TVS33`|rQ!oHl9sJixx5ij#^XH;YS3(l{RH$WwJt6{pUTRlk3fjUCuQas4AaJRHfe zVaW65FsVC+GBNt z2eKW3!j`+|4~`R9<*7o*?iGs$HWl+Z2zaI@?jl|DPvINDi2^4Di+bPm4VC9|tKc`l1x74O*MNV53I-l`X%5_igK3N$A!9wvH}nvMMgDrxO_|6X$pJYu)umM?+Z zv905G6DCixZ1wF%ZBts3`+KHh)w>b0@1i<5YjNUKzj*TcM-ys9+HYoWO;rTdxt^vd z)8H|_M?#Q{u!`oUl-ziG+Jdaf1(O+f#;88RS5S>uk9XdZIuh3r3q>Xyj(z#&>o|C+ zua=R0>4z4yvJ4{E*5z}XXEg4=jaOSz%J}Ey=Z<`~@}YpN5A7CI-A>doT>uiRKKQ9J z?Si&=`c|Z*>sGipJE-a6CT8~Zsn>AT7*(D1`W@7U0Jqp|kdP3uRoo6+zy4V&l)YXx9m~q~Bv~AE&QIjmq;HpxP8 zoi;9g_QdJk?Hx8!MJT9aDn8*+XO9G(9Ez^!Y}PZMa{WO=N1rIvy99kql)gH^f@2#@i_NN;dWnY(m6rYj~$aauwAm%lFGoIt)KcMJKYS&`+7&)1(FY4B4YDF+|*5XSN=78d8~ z&31ESt@1CSCT7p8l$Xu&uB@y8wu2J=oX_n|DrA4U{j*WR`jk`e@E}VqNoWF<=lu8H z&aKYs&D8plC0^kb^j@0``_tt!A z%Dxl%+JdpfbC2=plKHcS&@(!M%g;yumR?eK7oBXKoAmVxQ$drAz6ujqGJd6TYneP!K!MUMzIPR`++sq5c4U+-8?sK@T4 z7yj~`%`c}Y^BGS1Sjs&6+DC&3>5##= ziYrDMwYKaOiX6BteQBfK&k)pxQN+s9^HZKhA|(Qq^12hD5x}fqyEV=P(lCO7J#tyr z(i`M(v=m8IkdnXg{4EyR9UN|GOzLefM2OOpp%a(vg5LBPHZYWIAXV&9B>g)$6prya zR}RR=oKFFKpM~6MyKyeod(=5?k>_sQ8ShPKTsiJscb}=gZ%JH2q6-DU< zlYO$!m7DCjqKR_WIWw0^pDQW--8ZL7_ODzy){lWAPp&cLSntC`?V56V&R#;g z=zLFgdfUFQCMofut1SthcprpfQhxrV5K^&)wKZ`_BeT)mf$a{OVHr}_ za^cVb*M)RCsetW~&MbYm;N#6P1Q7+{HeT<}9TE}(%+#34!&mG4UuHe>sidjaq2t$G zYZaC=HV+JhfJ|1b-;SBDUDI~3+;s=R4Cm+PSMSce8@iML!jnV-JCqEb@% zQLcVFRo*=wU!AE~Ov-@N-AoGm;pXIjYQ8_j#_&pjxO$9@-2{t%fR2t3fg^S*-pxo` z)(GqG&jFv+w`Hh?{XUUfgxVH7fJ z6D5?O@1;sby#@d--{5gANxTTMz}94$e^wTi!|rU)OpW9EcoAuGax$y~gf;mmaw1uJ zBINjyJJoNizxYzplA{`Po6SD3kaDNJwxm(k&ev_|g#~ETIM56EVj2Rdt*qHJzSZLe zc`3Voj#$oIvRt9``;8>2WFz8;%d(h z)CkE|7*PFw{1`k&MX-jlNbVO$zwPF!8jr%W%G@gW6I+f5OYe(6ffGqpxU2JG$r!tf z612lYyLCmrt34re3k!{615{w#eK0o*fc?(~F(JvEQw#`0YjO|N#O)mkVox>M_%{L| zE7?iZzcMm1^i(!2!$(I)2N2p00<`u=DV@}TPYW%mlEt`}3?hwV z&MgcEZRP5C|1W-N1&=BY{MAd@?yLCW=V6g~pgu623dfq^vD1B5VuS(5<&t(ui-QJg zxu2h(z;Cxwd{aTe|E4Oej8AuF;@?<*1u+#kY8jy3*XJ7eLDzX|*?V0Hrzitx5Mu^U zbrqDtLg49fvLv(1tW)Jb2YPeryOyPBqqm!K|Evd_#Qw>Y6%4{J1cihUTlV6`i$@Kq z^B_hrfT~32cagB57%1@WDim3}4xgpIV$7Wd*SI&n1JcXhFtL+ZXaN{n5a%!bKFI%8 zCggSqNEHjRftKTOzWfa2R47_$#(jy%_wR$%L9p2;sDZ2{oLWh}%7*+2h4lORChwqO zCi*ooKh3V$!;ggDd)|(AnjA%Q?ew^=54A;*Buoc*+|GjEX&An*>Q_($ZJ*s~?kDv))f{rLasUEV(cC~dyspmn zp^z$C=@}Xu+n(*|VG_Ri1C>xhT>Q`AU?_~^Lj}70FmJ3+mTi)1V_u$}Asji~-Q9)7 z#S39_eDe`%I?hY9*vT7cKO{NbNWyd(bc9s0MM)&&jy%-F(F`=&2DJnp%~2d#O@;eB zdai2z5cnXeNV@sdAifm6W*?Bv5WQx$Vk!dbiG=PPJVwKvDx~Eo9-m-1RXJS71%E2% zsdn9q>4ToWzDxk9KvAnipKXrhwEtGh+&etX$^X>x_wPS&DiFT;^tsvhQCMv358y_K zxa@wxK37laULvERp%s>u(L1Kw6yTJBi0yLZR7%vg6k22Q<3M3@eC)?leGHM*2tT=+ z=82pNaq&J9nD3M2eupEWYD5m08sea#`W64K$CsJd%}ef(RZgfInL-H zwG2LJC-y{){09dIofeM^lxf!clkbAvWM#&2Zh6=9)-|y9cLB757#3qY@?=mK!L{(! z-yi+UmoL?!@-Sz@LDucw{4ZbC3~l-xj1#o<=hHH8-p;PuhtJ;J4GtkA$Ca7-Fs08N znQxJODlvMe;^4>MS|xGSpFgH+$f0-uHvpns5v$qoJic}$GnuPajg?MiT zMy`jEpCpf%PSiTHAeQKPwT}YqID)x@Qi;!1nvE^dK%rG*8wE_I_00x-DU#;7pCll! zZ=$q|{eOPkY{w4HWHPO&RMOpCY!=vGqKSusV~Nsf<9Bh-1Pcp`-}mqDN3vC*KNLFc ze{eb8IBsdrMhg^eh9R{Vau>8WYDarTEEuSP=LL@^1`q)McG8=tXRm`$q*KCzaj<15 za$VARMZG7baz={vn(wdlCCLG`11bOV)vKm)u_YeNkR!9uY+;0(q2s)_Uk^GefoRE7?Lud${S#oxe7u{%;}l%g6mWmrS8vSN zWtpFJSW`0(U~W0WIs+BzIky8HLjQ$<*9TK(PI!<5Cr_2)P?m(lMj4zPRPqsuxuf#g zqaQ5veA2zKeJ>(e%gY)^8Z#zE*&u}4f{6Rzz`axTmlsn`w0OtLcDlWGxu8WZoFR!tTkcYA#nGmi~gLF@(Q@Z+KvK`EMdDn|vLEoy5qK;{%A3N! zA#d-#x3CBV^CvjtN=--5!09;C)7*Sls_W8`t&3{>Tlop!Z=AmUHva@L2VQFZmkfEBgY=OpkD)O z0&?EQ_4L0TotvknPlGPL6f5wQ*Xbk+v>Rt*XT1Lg*3MgyTr|qf@fxlU&;fd^Sb1I& zJfW0L6S#YQ92D~a{KVP$m6g$8X=L%dakHGO?@5ygi{^IBAE2vLjZNqbB-Rxi&8FSy zR-sZDwf$o_h)>4yWcpS;OCH)W!tM)fs)3>50CW!>&ue>Vr6JPpYR8md2>itl&Rm26 zt&i_A0CrW!Wu1K1Ws^QtU8HY#1I7}IuZM+lEuVTc|G_MX zK!*{uYBoSOP0!3Y-<+>H&wD=$Sa3H%1~1u>9PAA!wZ)L7K(*rWy0C(i=odh+`+-vL z!o}vjwQ(|&#I&OWVN%ZJO=`+GMj}feNBN_?ys}bkF~ttX=7|a`V$esiNJaOW&>8yS z1(Y&1(od3kxsLaYeq_J~}hp~ua+G2i7D3s_)aiu2?y6-PA zFe6?UH2M1&_+%$~`LQMj0*t>vv~q@7&1QGD&hYKY{(cBQyf!B(X*0OX{J~_57wqNb zwG2f|BVDlrj4KCVW;J~4ak*WALG&T8#(p!h)^vo%8FI@8{6Pj1p-Zo|tN(%_Em&V) zAA?B8!dA5O=w2!o0YMkSAPIi`4wy2@$i7L0Q6Tmm%$do}L;hghm@K!D0=I7!ua}gh zB<@E5Ov-dVaWSZEK5|t`Pp(U%y_?q?l#AhItf(yZPNMpAS7<{iDK`T7GW?WBiJcY(SVm! zRDa1h#QM6r7Ek9qjZd~F42+D5?Kf3{NuJ3gC@CqKC^LJ8kmvgnd2w)Y2LX$KrB)RD zhH#O9!|uC%0vMM+R)3oR?_IgM9$0`{+S*{~{R=ej12DM!`Fw|h3E2+JfdJ!>9^>G& zgpx}=EEkYb(W-M%Q7h-PS$Y7u1aGFDVl^sFXQd|$St>mTH5D-d0I$8cHjoNlcQDp~ zVTR((n`nqR{Oy(m2%ONcFoZ$s9ME_S6rLU7_qkBtxwtf=*5L4AEY;up*Z7ctJpl0P zP@$fXkT10|!cXZyO+&K)akMqpkOVgoY(at~lK{*1cjF}YNEq7rKKU{V;WK?eM$|w= zwl_95`e0_pLw7-V(gC3$9M#Y(!J-dsLKZdyW-YA6RT(ew3&Cv%xaNUC$5_$a{B=0afHVnK41gez5OQ<22sl1 zSxc{m;Pwh^X_i9jb2d{PaIwQ)GCCg^SkBgVK)112+v}|2VCU!No_fy|&+nOt7)l}W z023CLmZs@8cpyF>5|7EF9v5u6SuAixps(TPm;u3npb)$L0iDy#&HM~3dx&H=8=Gx- zwhPGq&Kg+k16!5A@(Mc$3w%XG^8;=VXlZNXaoJHJ_;=X8_O`Z+wgs&0BzX4(7BFJ2 zbjLJ&Srf{)D@%+mms5U^f#bH6~hAz$j?@^p7B!UAL# zX%iDFP=W1Tu9GHZhmX9uRzc#l+noh3odKx%rbpw>&d&eG0RWpV4jM72UuS1$ds`)a z2e7ZuN%Kw@b!wdyAik^@kl}9;qZB@V=XWepKsi;jLDbL55tli5p+YWYCV9M~r-z!T zOb{Wdl5ZZCnD`UCGl*H<$LB|}%nUh~jd%4jka~2w_3_|*#^=JR16!14RdJwXdJ3qm z(lN~5pvJhHBOF>9Hd#&{kaw-$dJ`g;T;Bg1zZgnxjQ3)Y&v^$ps{|4Hg zwY@!?UG!ILEPD6koio7ah>Y`nM9e<%Eh#Fh9h%1kaEV~vr2tzt^m=ByHF<;^7zWs! zbh)=#ypS*(-#`dqwq23tyE)T`VGM()S0C<87(&u+&DO=~xBmq63mgMFtk!U?KRLym zlm{1DxM`iP$GJD6>Kk=G2eIq+Iaj=IXI~$~SS%orcCa0QO$XushS@35ff}`O&2NAk zA{+*MC^HD^1-$VblgFn}*(M+-z@kRTY4Z)bAlS*pKYe0^T?8-jU2j3XHO6~G2(!Oy z0tuhUX0z*C4djuU=lL=;+O@t*!lfXk|f& z+&Y4IXEN!33GWDCzC>ZG-)?_FM7v`JQ6)x9tL@gC{xy2x@i;NUhT!HoCs%{*ewYt( zN&jy!3C!Gaz+2&&hwXDa?tEJ7Cpp=f>4Hd?kdbNa=|AFYKvV<<7Ga==&wizO_>~0lfiK|vj9{OE z4JrbW-gLKg#Do?^F!~00o(a*mF$V?)jyA`F z5OgD0+Eu40)}iB%lAi-e2}wwhK`>Ht4Gpgc{9C(_Nh#o-#|40JFwAg3)|gL~%M_3& zz@48@|Nh+3PLY$5N%y$E0B81kXl+A7Lo^|IG#YgEp-8xVO1s)l4?zpwzdu;xXa!|QSEldnW79}LsT4V-r8vgM#y97E zp3G1_QEtrj^hm6&t)YQn6A|^m=nfDH)G!=JP!);0U`${8)6RF-Iaqj2$y1T7MHxMrQz;b znHOEx(()hH^B-)oz~ha_|M!LE-_K;P$H)SYO@(i6 a@9^xLhq~`uABnc5&;e5gOU;hW(JB5q@ literal 0 HcmV?d00001 diff --git a/docs/src/assets/network_graphs/repressilator_complex_graph.png b/docs/src/assets/network_graphs/repressilator_complex_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..a5302659175f874a2bc1df82f6a790ee86c96cea GIT binary patch literal 13569 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 literal 0 HcmV?d00001 diff --git a/docs/src/model_creation/model_visualisation.md b/docs/src/model_creation/model_visualisation.md index 6efb71b4bb..80e33d9aef 100644 --- a/docs/src/model_creation/model_visualisation.md +++ b/docs/src/model_creation/model_visualisation.md @@ -45,7 +45,10 @@ brusselator = @reaction_network begin 1, X --> ∅ end Graph(brusselator) +nothing # hide ``` +!["Brusselator Graph"](../assets/network_graphs/brusselator_graph.png) + The network graph represents species as blue nodes and reactions as orange dots. Black arrows from species to reactions indicate substrates, and are labelled with their respective stoichiometries. Similarly, black arrows from reactions to species indicate products (also labelled with their respective stoichiometries). If there are any reactions where a species affect the rate, but does not participate as a reactant, this is displayed with a dashed red arrow. This can be seen in the following [Repressilator model](@ref basic_CRN_library_repressilator): ```@example visualisation_graphs repressilator = @reaction_network begin @@ -55,7 +58,9 @@ repressilator = @reaction_network begin d, (X, Y, Z) --> ∅ end Graph(repressilator) +nothing # hide ``` +!["Repressilator Graph"](../assets/network_graphs/repressilator_graph.png) A generated graph can be saved using the `savegraph` function: ```@example visualisation_graphs @@ -67,5 +72,7 @@ rm("repressilator_graph.png") # hide Finally, a [network's reaction complexes](@ref network_analysis_reaction_complexes) (and the reactions in between these) can be displayed using the `complexgraph(brusselator)` function: ```@example visualisation_graphs complexgraph(brusselator) +nothing # hide ``` +!["Repressilator Complex Graph"](../assets/network_graphs/repressilator_complex_graph.png) Here, reaction complexes are displayed as blue nodes, and reactions in between these as black arrows. \ No newline at end of file From a1e5bfa23ade462ed4e4f91f57ed40689b8fcbfe Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 1 Jun 2024 21:38:24 +0200 Subject: [PATCH 27/67] don't attempt to save graph (documenter cannot handle) --- docs/src/model_creation/model_visualisation.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/src/model_creation/model_visualisation.md b/docs/src/model_creation/model_visualisation.md index 80e33d9aef..a13f348749 100644 --- a/docs/src/model_creation/model_visualisation.md +++ b/docs/src/model_creation/model_visualisation.md @@ -63,10 +63,9 @@ nothing # hide !["Repressilator Graph"](../assets/network_graphs/repressilator_graph.png) A generated graph can be saved using the `savegraph` function: -```@example visualisation_graphs +```julia repressilator_graph = Graph(repressilator) savegraph(repressilator_graph, "repressilator_graph.png") -rm("repressilator_graph.png") # hide ``` Finally, a [network's reaction complexes](@ref network_analysis_reaction_complexes) (and the reactions in between these) can be displayed using the `complexgraph(brusselator)` function: @@ -75,4 +74,4 @@ complexgraph(brusselator) nothing # hide ``` !["Repressilator Complex Graph"](../assets/network_graphs/repressilator_complex_graph.png) -Here, reaction complexes are displayed as blue nodes, and reactions in between these as black arrows. \ No newline at end of file +Here, reaction complexes are displayed as blue nodes, and reactions in between these as black arrows. From 6d02cfe51f26b012d93a9f2e83ef536c6e3467fb Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Mon, 12 Feb 2024 19:21:39 -0500 Subject: [PATCH 28/67] init --- src/reactionsystem.jl | 6 ++++-- test/extensions/structural_identifiability.jl | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/reactionsystem.jl b/src/reactionsystem.jl index 3be6b6c06c..a5182438dc 100644 --- a/src/reactionsystem.jl +++ b/src/reactionsystem.jl @@ -1338,7 +1338,8 @@ function MT.flatten(rs::ReactionSystem; name = nameof(rs)) balanced_bc_check = false, spatial_ivs = get_sivs(rs), continuous_events = MT.continuous_events(rs), - discrete_events = MT.discrete_events(rs)) + discrete_events = MT.discrete_events(rs), + complete = false) end """ @@ -1395,7 +1396,8 @@ function ModelingToolkit.extend(sys::MT.AbstractSystem, rs::ReactionSystem; balanced_bc_check = false, spatial_ivs = sivs, continuous_events, - discrete_events) + discrete_events, + complete = false) end ### Units Handling ### diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl index 8ef35c293b..4fe1bad359 100644 --- a/test/extensions/structural_identifiability.jl +++ b/test/extensions/structural_identifiability.jl @@ -191,7 +191,6 @@ let si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[pₑ, pₚ], known_p=[pₑ]) # Tests using model.component style (have to make system complete first). - gw_osc_complt = complete(goodwind_oscillator_catalyst) @test make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M]) isa ODE @test make_si_ode(gw_osc_complt; known_p=[gw_osc_complt.pₑ]) isa ODE @test make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M], known_p=[gw_osc_complt.pₑ]) isa ODE From 3f65047e7fe446af722fd37edd05aebc393e45f2 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Mon, 12 Feb 2024 19:21:46 -0500 Subject: [PATCH 29/67] up --- test/extensions/structural_identifiability.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/extensions/structural_identifiability.jl b/test/extensions/structural_identifiability.jl index 4fe1bad359..8ef35c293b 100644 --- a/test/extensions/structural_identifiability.jl +++ b/test/extensions/structural_identifiability.jl @@ -191,6 +191,7 @@ let si_catalyst_ode = make_si_ode(goodwind_oscillator_catalyst; measured_quantities=[pₑ, pₚ], known_p=[pₑ]) # Tests using model.component style (have to make system complete first). + gw_osc_complt = complete(goodwind_oscillator_catalyst) @test make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M]) isa ODE @test make_si_ode(gw_osc_complt; known_p=[gw_osc_complt.pₑ]) isa ODE @test make_si_ode(gw_osc_complt; measured_quantities=[gw_osc_complt.M], known_p=[gw_osc_complt.pₑ]) isa ODE From e912c133fe26ab5dfc7f6cb8c3906d4d1f9cf7d0 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 18 Mar 2024 19:06:43 -0400 Subject: [PATCH 30/67] up --- src/reactionsystem.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/reactionsystem.jl b/src/reactionsystem.jl index a5182438dc..39dde75b9c 100644 --- a/src/reactionsystem.jl +++ b/src/reactionsystem.jl @@ -437,6 +437,7 @@ end # Two-argument constructor (reactions/equations and time variable). # Calls the `make_ReactionSystem_internal`, which in turn calls the four-argument constructor. function ReactionSystem(rxs::Vector, iv = Catalyst.DEFAULT_IV; kwargs...) + println("Complete 3: ", complete) make_ReactionSystem_internal(rxs, iv, Vector{Num}(), Vector{Num}(); kwargs...) end @@ -1320,7 +1321,7 @@ Notes: - The default value of `combinatoric_ratelaws` will be the logical or of all `ReactionSystem`s. """ -function MT.flatten(rs::ReactionSystem; name = nameof(rs)) +function MT.flatten(rs::ReactionSystem; name = nameof(rs), complete = false) isempty(get_systems(rs)) && return rs # right now only NonlinearSystems and ODESystems can be handled as subsystems @@ -1339,7 +1340,7 @@ function MT.flatten(rs::ReactionSystem; name = nameof(rs)) spatial_ivs = get_sivs(rs), continuous_events = MT.continuous_events(rs), discrete_events = MT.discrete_events(rs), - complete = false) + complete = complete) end """ From ea448a1728d49b5d50d21161617d282bb0ad83c3 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 18 Mar 2024 20:15:51 -0400 Subject: [PATCH 31/67] tests shoudl start passing --- src/reactionsystem.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/reactionsystem.jl b/src/reactionsystem.jl index 39dde75b9c..f2ac871bc9 100644 --- a/src/reactionsystem.jl +++ b/src/reactionsystem.jl @@ -437,7 +437,6 @@ end # Two-argument constructor (reactions/equations and time variable). # Calls the `make_ReactionSystem_internal`, which in turn calls the four-argument constructor. function ReactionSystem(rxs::Vector, iv = Catalyst.DEFAULT_IV; kwargs...) - println("Complete 3: ", complete) make_ReactionSystem_internal(rxs, iv, Vector{Num}(), Vector{Num}(); kwargs...) end From ef94f700fe0b37ed259e4083e27eb2b112792883 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 25 Mar 2024 18:23:10 -0400 Subject: [PATCH 32/67] rewors --- test/dsl/dsl_options.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/dsl/dsl_options.jl b/test/dsl/dsl_options.jl index 93ce90b541..f88c366fa2 100644 --- a/test/dsl/dsl_options.jl +++ b/test/dsl/dsl_options.jl @@ -16,7 +16,7 @@ t = default_t() ### Tests `@parameters`, `@species`, and `@variables` Options ### -# Test creating networks with/without options. +# Test creating networks with/without options. Compares they are all identical. let @reaction_network begin (k1, k2), A <--> B end @reaction_network begin @@ -155,7 +155,6 @@ end # Test inferring with stoichiometry symbols and interpolation. let @parameters k g h gg X y [isconstantspecies = true] - t = Catalyst.DEFAULT_IV @species A(t) B(t) BB(t) C(t) rni = @reaction_network inferred begin @@ -496,7 +495,7 @@ let @test_broken false # plot(sol; idxs=[:X, :Y]).series_list[2].plotattributes[:y][end] ≈ 3.0 end -# Compares programmatic and DSL system with observables. +# Compares programmatic and DSL systems with observables. let # Model declarations. rn_dsl = @reaction_network begin From 21b87b81017671d6bff58574d7d7f9f13606df57 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 25 Mar 2024 18:48:31 -0400 Subject: [PATCH 33/67] up --- test/dsl/dsl_options.jl | 777 +++++++++++++++++----------------------- 1 file changed, 325 insertions(+), 452 deletions(-) diff --git a/test/dsl/dsl_options.jl b/test/dsl/dsl_options.jl index f88c366fa2..bdd71e2cfc 100644 --- a/test/dsl/dsl_options.jl +++ b/test/dsl/dsl_options.jl @@ -1,4 +1,4 @@ -#! format: off +### Prepares Tests ### ### Prepares Tests ### @@ -16,334 +16,214 @@ t = default_t() ### Tests `@parameters`, `@species`, and `@variables` Options ### -# Test creating networks with/without options. Compares they are all identical. -let - @reaction_network begin (k1, k2), A <--> B end - @reaction_network begin - @parameters k1 k2 - (k1, k2), A <--> B - end - @reaction_network begin - @parameters k1 k2 - @species A(t) B(t) - (k1, k2), A <--> B - end - @reaction_network begin - @species A(t) B(t) - (k1, k2), A <--> B - end - - @reaction_network begin - @parameters begin - k1 - k2 - end - (k1, k2), A <--> B - end - @reaction_network begin - @species begin - A(t) - B(t) - end - (k1, k2), A <--> B - end - @reaction_network begin - @parameters begin - k1 - k2 - end - @species begin - A(t) - B(t) - end - (k1, k2), A <--> B - end - - n1 = @reaction_network rnname begin (k1, k2), A <--> B end - n2 = @reaction_network rnname begin - @parameters k1 k2 - (k1, k2), A <--> B - end - n3 = @reaction_network rnname begin - @species A(t) B(t) - (k1, k2), A <--> B - end - n4 = @reaction_network rnname begin - @parameters k1 k2 - @species A(t) B(t) - (k1, k2), A <--> B - end - n5 = @reaction_network rnname begin - (k1, k2), A <--> B - @parameters k1 k2 - end - n6 = @reaction_network rnname begin - (k1, k2), A <--> B - @species A(t) B(t) - end - n7 = @reaction_network rnname begin - (k1, k2), A <--> B - @parameters k1 k2 - @species A(t) B(t) - end - n8 = @reaction_network rnname begin - @parameters begin - k1 - k2 - end - (k1, k2), A <--> B - end - n9 = @reaction_network rnname begin - @species begin - A(t) - B(t) - end - (k1, k2), A <--> B - end - n10 = @reaction_network rnname begin - @parameters begin - k1 - k2 - end - @species begin - A(t) - B(t) - end - (k1, k2), A <--> B - end - @test all(==(n1), (n2, n3, n4, n5, n6, n7, n8, n9, n10)) -end - -# Tests that when either @species or @parameters is given, the other is inferred properly. -let - rn1 = @reaction_network begin - k*X, A + B --> 0 - end - @test issetequal(species(rn1), @species A(t) B(t)) - @test issetequal(parameters(rn1), @parameters k X) - - rn2 = @reaction_network begin - @species A(t) B(t) X(t) - k*X, A + B --> 0 - end - @test issetequal(species(rn2), @species A(t) B(t) X(t)) - @test issetequal(parameters(rn2), @parameters k) +# Sets the default `t` to use. +t = default_t() - rn3 = @reaction_network begin - @parameters k - k*X, A + B --> 0 - end - @test issetequal(species(rn3), @species A(t) B(t)) - @test issetequal(parameters(rn3), @parameters k X) +# Fetch test networks and functions. +include("../test_networks.jl") +include("../test_functions.jl") - rn4 = @reaction_network begin - @species A(t) B(t) X(t) - @parameters k - k*X, A + B --> 0 - end - @test issetequal(species(rn4), @species A(t) B(t) X(t)) - @test issetequal(parameters(rn4), @parameters k) +### Declares Testing Functions ### - rn5 = @reaction_network begin - @parameters k B [isconstantspecies=true] - k*X, A + B --> 0 - end - @test issetequal(species(rn5), @species A(t)) - @test issetequal(parameters(rn5), @parameters k B X) +function unpacksys(sys) + get_eqs(sys), get_iv(sys), get_unknowns(sys), get_ps(sys), nameof(sys), get_systems(sys) end -# Test inferring with stoichiometry symbols and interpolation. -let - @parameters k g h gg X y [isconstantspecies = true] - @species A(t) B(t) BB(t) C(t) - - rni = @reaction_network inferred begin - $k*X, $y + g*A + h*($gg)*B + $BB * C --> k*C - end - @test issetequal(species(rni), [A, B, BB, C]) - @test issetequal(parameters(rni), [k, g, h, gg, X, y]) +opname(x) = istree(x) ? nameof(operation(x)) : nameof(x) +alleq(xs, ys) = all(isequal(x, y) for (x, y) in zip(xs, ys)) - rnii = @reaction_network inferred begin - @species BB(t) - @parameters y [isconstantspecies = true] - k*X, y + g*A + h*($gg)*B + BB * C --> k*C +# Gets all the reactants in a set of equations. +function all_reactants(eqs) + all_reactants = [] + for eq in eqs + append!(all_reactants, opname.(eq.substrates)) + append!(all_reactants, opname.(eq.products)) end - @test rnii == rni + return Set{Symbol}(unique(all_reactants)) end -# Tests that when some species or parameters are left out, the others are set properly. -let - rn6 = @reaction_network begin - @species A(t) - k*X, A + B --> 0 - end - @test issetequal(species(rn6), @species A(t) B(t)) - @test issetequal(parameters(rn6), @parameters k X) - - rn7 = @reaction_network begin - @species A(t) X(t) - k*X, A + B --> 0 - end - @test issetequal(species(rn7), @species A(t) X(t) B(t)) - @test issetequal(parameters(rn7), @parameters k) - - rn7 = @reaction_network begin - @parameters B [isconstantspecies=true] - k*X, A + B --> 0 - end - @test issetequal(species(rn7), @species A(t)) - @test issetequal(parameters(rn7), @parameters B k X) - - rn8 = @reaction_network begin - @parameters B [isconstantspecies=true] k - k*X, A + B --> 0 - end - @test issetequal(species(rn8), @species A(t)) - @test issetequal(parameters(rn8), @parameters B k X) +# Gets all parameters (where every reaction rate is constant) +function all_parameters(eqs) + return Set(unique(map(eq -> opname(eq.rate), eqs))) +end - rn9 = @reaction_network begin - @parameters k1 X1 - @species A1(t) B1(t) - k1*X1, A1 + B1 --> 0 - k2*X2, A2 + B2 --> 0 - end - @test issetequal(species(rn9), @species A1(t) B1(t) A2(t) B2(t)) - @test issetequal(parameters(rn9), @parameters k1 X1 k2 X2) +# Perform basic tests. +function basic_test(rn, N, unknowns_syms, p_syms) + eqs, iv, unknowns, ps, name, systems = unpacksys(rn) + @test length(eqs) == N + @test opname(iv) == :t + @test length(unknowns) == length(unknowns_syms) + @test issetequal(map(opname, unknowns), unknowns_syms) + @test all_reactants(eqs) == Set(unknowns_syms) + @test length(ps) == length(p_syms) + @test issetequal(map(opname, ps), p_syms) +end - rn10 = @reaction_network begin - @parameters k1 X2 B2 [isconstantspecies=true] - @species A1(t) X1(t) - k1*X1, A1 + B1 --> 0 - k2*X2, A2 + B2 --> 0 - end - @test issetequal(species(rn10), @species A1(t) X1(t) B1(t) A2(t)) - @test issetequal(parameters(rn10), @parameters k1 X2 B2 k2) +### Basic Tests ### - rn11 = @reaction_network begin - @parameters k1 k2 - @species X1(t) - k1*X1, A1 + B1 --> 0 - k2*X2, A2 + B2 --> 0 - end - @test issetequal(species(rn11), @species X1(t) A1(t) A2(t) B1(t) B2(t)) - @test issetequal(parameters(rn11), @parameters k1 k2 X2) +# Test basic properties of networks. +let + basic_test(reaction_networks_standard[1], 10, [:X1, :X2, :X3], + [:p1, :p2, :p3, :k1, :k2, :k3, :k4, :d1, :d2, :d3]) + @test all_parameters(get_eqs(reaction_networks_standard[1])) == + Set([:p1, :p2, :p3, :k1, :k2, :k3, :k4, :d1, :d2, :d3]) + basic_test(reaction_networks_standard[2], 3, [:X1, :X2], [:v1, :K1, :v2, :K2, :d]) + basic_test(reaction_networks_standard[3], 10, [:X1, :X2, :X3, :X4], + [:v1, :K1, :v2, :K2, :k1, :k2, :k3, :k4, :d]) + basic_test(reaction_networks_standard[4], 8, [:X1, :X2, :X3, :X4], + [:v1, :K1, :v2, :K2, :v3, :K3, :v4, :K4, :d1, :d2, :d3, :d4]) + basic_test(reaction_networks_standard[5], 8, [:X1, :X2, :X3, :X4], + [:p, :k1, :k2, :k3, :k4, :k5, :k6, :d]) + @test all_parameters(get_eqs(reaction_networks_standard[5])) == + Set([:p, :k1, :k2, :k3, :k4, :k5, :k6, :d]) + basic_test(reaction_networks_hill[1], 4, [:X1, :X2], + [:v1, :v2, :K1, :K2, :n1, :n2, :d1, :d2]) + basic_test(reaction_networks_constraint[1], 6, [:X1, :X2, :X3], + [:k1, :k2, :k3, :k4, :k5, :k6]) + basic_test(reaction_networks_real[1], 4, [:X, :Y], [:A, :B]) + basic_test(reaction_networks_weird[1], 2, [:X], [:p, :d]) + basic_test(reaction_networks_weird[2], 4, [:X, :Y, :Z], [:k1, :k2, :k3, :k4]) end -# Checks that some created networks are identical. +# Compares networks to networks created using different arrow types. let - rn12 = @reaction_network rnname begin (k1, k2), A <--> B end - rn13 = @reaction_network rnname begin - @parameters k1 k2 - (k1, k2), A <--> B - end - rn14 = @reaction_network rnname begin - @species A(t) B(t) - (k1, k2), A <--> B - end - rn15 = @reaction_network rnname begin - @parameters k1 k2 - @species A(t) B(t) - (k1, k2), A <--> B + networks_1 = [] + networks_2 = [] + + different_arrow_1 = @reaction_network begin + (p1, p2, p3), ∅ > (X1, X2, X3) + (k1, k2), X2 ↔ X1 + 2X3 + (k3, k4), X1 ⟷ X3 + (d1, d2, d3), (X1, X2, X3) → ∅ + end + push!(networks_1, reaction_networks_standard[1]) + push!(networks_2, different_arrow_1) + + different_arrow_2 = @reaction_network begin + mmr(X2, v1, K1), ∅ → X1 + mm(X1, v2, K2), ∅ ↣ X2 + d, X1 + X2 ↦ ∅ + end + push!(networks_1, reaction_networks_standard[2]) + push!(networks_2, different_arrow_2) + + different_arrow_3 = @reaction_network begin + mm(X2, v1, K1), ∅ ⇾ X1 + mm(X3, v2, K2), ∅ ⟶ X2 + (k1, k2), X1 ⇄ X3 + (k3, k4), X3 + X2 ⇆ X4 + X1 + d, (X1, X2, X3, X4) ⟼ ∅ + end + push!(networks_1, reaction_networks_standard[3]) + push!(networks_2, different_arrow_3) + + different_arrow_4 = @reaction_network begin + mmr(X4, v1, K1), ∅ ⥟ X1 + mmr(X1, v2, K2), ∅ ⥟ X2 + mmr(X2, v3, K3), ∅ ⇀ X3 + mmr(X3, v4, K4), ∅ ⇁ X4 + (d1, d2, d3, d4), (X1, X2, X3, X4) --> ∅ + end + push!(networks_1, reaction_networks_standard[4]) + push!(networks_2, different_arrow_4) + + # Yes the name is different, I wanted one with several single direction arrows. + different_arrow_8 = @reaction_network begin + p, 2X1 < ∅ + k1, X2 ← X1 + (k2, k3), X3 ⟻ X2 + d, ∅ ↼ X3 + end + push!(networks_1, reaction_networks_standard[8]) + push!(networks_2, different_arrow_8) + + for (rn_1, rn_2) in zip(networks_1, networks_2) + for factor in [1e-2, 1e-1, 1e0, 1e1, 1e2, 1e3] + u0 = rnd_u0(rn_1, rng; factor) + p = rnd_ps(rn_1, rng; factor) + t = rand(rng) + + @test f_eval(rn_1, u0, p, t) ≈ f_eval(rn_2, u0, p, t) + @test jac_eval(rn_1, u0, p, t) ≈ jac_eval(rn_2, u0, p, t) + @test g_eval(rn_1, u0, p, t) ≈ g_eval(rn_2, u0, p, t) + end end - @test all(==(rn12), (rn13, rn14, rn15)) end -# Checks that the rights things are put in vectors. +# Checks that some created networks are identical. let - rn18 = @reaction_network rnname begin - @parameters p d1 d2 - @species A(t) B(t) - p, 0 --> A - 1, A --> B - (d1, d2), (A, B) --> 0 - end - rn19 = @reaction_network rnname begin - p, 0 --> A - 1, A --> B - (d1, d2), (A, B) --> 0 - end - @test rn18 == rn19 - - @parameters p d1 d2 - @species A(t) B(t) - @test isequal(parameters(rn18)[1], p) - @test isequal(parameters(rn18)[2], d1) - @test isequal(parameters(rn18)[3], d2) - @test isequal(species(rn18)[1], A) - @test isequal(species(rn18)[2], B) - - rn20 = @reaction_network rnname begin - @species X(t) - @parameters S - mm(X,v,K), 0 --> Y - (k1,k2), 2Y <--> Y2 - d*Y, S*(Y2+Y) --> 0 - end - rn21 = @reaction_network rnname begin - @species X(t) Y(t) Y2(t) - @parameters v K k1 k2 d S - mm(X,v,K), 0 --> Y - (k1,k2), 2Y <--> Y2 - d*Y, S*(Y2+Y) --> 0 - end - rn22 = @reaction_network rnname begin - @species X(t) Y2(t) - @parameters d k1 - mm(X,v,K), 0 --> Y - (k1,k2), 2Y <--> Y2 - d*Y, S*(Y2+Y) --> 0 - end - @test all(==(rn20), (rn21, rn22)) - @parameters v K k1 k2 d S - @species X(t) Y(t) Y2(t) - @test issetequal(parameters(rn22),[v K k1 k2 d S]) - @test issetequal(species(rn22), [X Y Y2]) + # Declares network. + differently_written_5 = @reaction_network begin + q, ∅ → Y1 + (l1, l2), Y1 ⟷ Y2 + (l3, l4), Y2 ⟷ Y3 + (l5, l6), Y3 ⟷ Y4 + c, Y4 → ∅ + end + + # Computes initial conditions/parameter values. + u0_vals = rand(rng, length(species(differently_written_5))) + ps_vals = rand(rng, length(parameters(differently_written_5))) + u0_1 = [:X1 => u0_vals[1], :X2 => u0_vals[2], :X3 => u0_vals[3], :X4 => u0_vals[4]] + u0_2 = [:Y1 => u0_vals[1], :Y2 => u0_vals[2], :Y3 => u0_vals[3], :Y4 => u0_vals[4]] + ps_1 = [:p => ps_vals[1], :k1 => ps_vals[2], :k2 => ps_vals[3], :k3 => ps_vals[4], + :k4 => ps_vals[5], :k5 => ps_vals[6], :k6 => ps_vals[7], :d => ps_vals[8]] + ps_2 = [:q => ps_vals[1], :l1 => ps_vals[2], :l2 => ps_vals[3], :l3 => ps_vals[4], + :l4 => ps_vals[5], :l5 => ps_vals[6], :l6 => ps_vals[7], :c => ps_vals[8]] + t = rand(rng) + + # Checks equivalence. + rn_1 = reaction_networks_standard[5] + rn_2 = differently_written_5 + @test f_eval(rn_1, u0_1, ps_1, t) ≈ f_eval(rn_2, u0_2, ps_2, t) + @test jac_eval(rn_1, u0_1, ps_1, t) ≈ jac_eval(rn_2, u0_2, ps_2, t) + @test g_eval(rn_1, u0_1, ps_1, t) ≈ g_eval(rn_2, u0_2, ps_2, t) end -# Tests that defaults work. +# Compares networks to networks written in different ways. let - rn26 = @reaction_network rnname begin - @parameters p=1.0 d1 d2=5 - @species A(t) B(t)=4 - p, 0 --> A - 1, A --> B - (d1, d2), (A, B) --> 0 - end - - rn27 = @reaction_network rnname begin - @parameters p1=1.0 p2=2.0 k1=4.0 k2=5.0 v=8.0 K=9.0 n=3 d=10.0 - @species X(t)=4.0 Y(t)=3.0 X2Y(t)=2.0 Z(t)=1.0 - (p1,p2), 0 --> (X,Y) - (k1,k2), 2X + Y --> X2Y - hill(X2Y,v,K,n), 0 --> Z - d, (X,Y,X2Y,Z) --> 0 - end - u0_27 = [] - p_27 = [] - - rn28 = @reaction_network rnname begin - @parameters p1=1.0 p2 k1=4.0 k2 v=8.0 K n=3 d - @species X(t)=4.0 Y(t) X2Y(t) Z(t)=1.0 - (p1,p2), 0 --> (X,Y) - (k1,k2), 2X + Y --> X2Y - hill(X2Y,v,K,n), 0 --> Z - d, (X,Y,X2Y,Z) --> 0 - end - u0_28 = symmap_to_varmap(rn28, [:p2=>2.0, :k2=>5.0, :K=>9.0, :d=>10.0]) - p_28 = symmap_to_varmap(rn28, [:Y=>3.0, :X2Y=>2.0]) - defs28 = Dict(Iterators.flatten((u0_28, p_28))) - - rn29 = @reaction_network rnname begin - @parameters p1 p2 k1 k2 v K n d - @species X(t) Y(t) X2Y(t) Z(t) - (p1,p2), 0 --> (X,Y) - (k1,k2), 2X + Y --> X2Y - hill(X2Y,v,K,n), 0 --> Z - d, (X,Y,X2Y,Z) --> 0 - end - u0_29 = symmap_to_varmap(rn29, [:p1=>1.0, :p2=>2.0, :k1=>4.0, :k2=>5.0, :v=>8.0, :K=>9.0, :n=>3, :d=>10.0]) - p_29 = symmap_to_varmap(rn29, [:X=>4.0, :Y=>3.0, :X2Y=>2.0, :Z=>1.0]) - defs29 = Dict(Iterators.flatten((u0_29, p_29))) + networks_1 = [] + networks_2 = [] + + # Unfold reactions. + differently_written_6 = @reaction_network begin + p1, ∅ → X1 + p2, ∅ → X2 + k1, 2X1 → X3 + k2, X3 → 2X1 + k3, X2 + X3 → 4X4 + k4, 4X4 → X2 + X3 + k5, X4 + X1 → 2X3 + k6, 2X3 → X4 + X1 + d, X1 → ∅ + d, X2 → ∅ + d, X3 → ∅ + d, X4 → ∅ + end + push!(networks_1, reaction_networks_standard[6]) + push!(networks_2, differently_written_6) + + # Ignore mass action. + differently_written_7 = @reaction_network begin + @parameters p1 p2 p3 k1 k2 k3 v1 K1 d1 d2 d3 d4 d5 + (p1, p2, p3), ∅ ⇒ (X1, X2, X3) + (k1 * X1 * X2^2 / 2, k2 * X4), X1 + 2X2 ⟺ X4 + (mm(X3, v1, K1) * X4, k3 * X5), X4 ⇔ X5 + (d1 * X1, d2 * X2, d3 * X3, d4 * X4, d5 * X5), ∅ ⟽ (X1, X2, X3, X4, X5) + end + push!(networks_1, reaction_networks_standard[7]) + push!(networks_2, differently_written_7) + + # Ignore mass action new arrows. + differently_written_8 = @reaction_network begin + @parameters p1 p2 p3 k1 k2 k3 v1 K1 d1 d2 d3 d4 d5 + (p1, p2, p3), ∅ => (X1, X2, X3) + (k1 * X1 * X2^2 / 2, k2 * X4), X1 + 2X2 ⟺ X4 + (mm(X3, v1, K1) * X4, k3 * X5), X4 ⇔ X5 + (d1 * X1, d2 * X2, d3 * X3, d4 * X4, d5 * X5), ∅ <= (X1, X2, X3, X4, X5) + end + push!(networks_1, reaction_networks_standard[7]) + push!(networks_2, differently_written_8) @test ModelingToolkit.defaults(rn27) == defs29 @test merge(ModelingToolkit.defaults(rn28), defs28) == ModelingToolkit.defaults(rn27) @@ -458,9 +338,6 @@ let X ~ Xi + Xa Y ~ Y1 + Y2 end - (p,d), 0 <--> Xi - (k1,k2), Xi <--> Xa - (k3,k4), Y1 <--> Y2 end @unpack X, Xi, Xa, Y, Y1, Y2, p, d, k1, k2, k3, k4 = rn @@ -495,18 +372,24 @@ let @test_broken false # plot(sol; idxs=[:X, :Y]).series_list[2].plotattributes[:y][end] ≈ 3.0 end -# Compares programmatic and DSL systems with observables. +# Compares networks to networks written without parameters, let - # Model declarations. - rn_dsl = @reaction_network begin - @observables begin - X ~ x + 2x2y - Y ~ y + x2y - end - k, 0 --> (x, y) - (kB, kD), 2x + y <--> x2y - d, (x,y,x2y) --> 0 - end + networks_1 = [] + networks_2 = [] + parameter_sets = [] + + # Different parameter and variable names. + no_parameters_9 = @reaction_network begin + (1.5, 1, 2), ∅ ⟶ (X1, X2, X3) + (0.01, 2.3, 1001), (X1, X2, X3) ⟶ ∅ + (π, 42), X1 + X2 ⟷ X3 + (19.9, 999.99), X3 ⟷ X4 + (sqrt(3.7), exp(1.9)), X4 ⟷ X1 + X2 + end + push!(networks_1, reaction_networks_standard[9]) + push!(networks_2, no_parameters_9) + push!(parameter_sets, [:p1 => 1.5, :p2 => 1, :p3 => 2, :d1 => 0.01, :d2 => 2.3, :d3 => 1001, + :k1 => π, :k2 => 42, :k3 => 19.9, :k4 => 999.99, :k5 => sqrt(3.7), :k6 => exp(1.9)]) @variables X(t) Y(t) @species x(t), y(t), x2y(t) @@ -522,21 +405,17 @@ let @named rn_prog = ReactionSystem([r1, r2, r3, r4, r5, r6, r7], t, [x, y, x2y], [k, kB, kD, d]; observed = obs_eqs) rn_prog = complete(rn_prog) - # Make simulations. - u0 = [x => 1.0, y => 0.5, x2y => 0.0] - tspan = (0.0, 15.0) - ps = [k => 1.0, kD => 0.1, kB => 0.5, d => 5.0] - oprob_dsl = ODEProblem(rn_dsl, u0, tspan, ps) - oprob_prog = ODEProblem(rn_prog, u0, tspan, ps) - - sol_dsl = solve(oprob_dsl, Tsit5(); saveat=0.1) - sol_prog = solve(oprob_prog, Tsit5(); saveat=0.1) - - # Tests observables equal in both cases. - @test oprob_dsl[:X] == oprob_prog[:X] - @test oprob_dsl[:Y] == oprob_prog[:Y] - @test sol_dsl[:X] == sol_prog[:X] - @test sol_dsl[:Y] == sol_prog[:Y] + + for (rn_1, rn_2, p_1) in zip(networks_1, networks_2, parameter_sets) + for factor in [1e-2, 1e-1, 1e0, 1e1, 1e2, 1e3] + u0 = rnd_u0(rn_1, rng; factor) + t = rand(rng) + + @test f_eval(rn_1, u0, p_1, t) ≈ f_eval(rn_2, u0, [], t) + @test jac_eval(rn_1, u0, p_1, t) ≈ jac_eval(rn_2, u0, [], t) + @test g_eval(rn_1, u0, p_1, t) ≈ g_eval(rn_2, u0, [], t) + end + end end # Tests for complicated observable formula. @@ -556,32 +435,51 @@ let u0 = Dict([:X1 => 1.0, :X2 => 2.0, :X3 => 3.0, :X4 => 4.0]) ps = Dict([:p => 1.0, :d => 0.2, :k1 => 1.5, :k2 => 1.5, :k3 => 5.0, :k4 => 5.0, :op_1 => 1.5, :op_2 => 1.5]) - oprob = ODEProblem(rn, u0, (0.0, 1000.0), ps) - sol = solve(oprob, Tsit5()) + rxs_1 = [Reaction(p, nothing, [X1], nothing, [2]), + Reaction(k1, [X1], [X2], [1], [1]), + Reaction(k2, [X2], [X3], [1], [1]), + Reaction(k3, [X2], [X3], [1], [1]), + Reaction(d, [X3], nothing, [1], nothing)] + @named rs_1 = ReactionSystem(rxs_1, t, [X1, X2, X3], [p, k1, k2, k3, d]) + push!(identical_networks_4, reaction_networks_standard[8] => rs_1) @test sol[:X][1] == u0[:X1]^2 + ps[:op_1]*(u0[:X2] + 2*u0[:X3]) + u0[:X1]*u0[:X4]/ps[:op_2] + ps[:p] end -# Checks that ivs are correctly found. -let - rn = @reaction_network begin - @ivs t x y - @species V1(t) V2(t,x) V3(t, y) W1(t) W2(t, y) - @observables begin - V ~ V1 + 2V2 + 3V3 - W ~ W1 + W2 + rxs_3 = [Reaction(k1, [X1], [X2], [1], [1]), + Reaction(0, [X2], [X3], [1], [1]), + Reaction(k2, [X3], [X4], [1], [1]), + Reaction(k3, [X4], [X5], [1], [1])] + @named rs_3 = ReactionSystem(rxs_3, t, [X1, X2, X3, X4, X5], [k1, k2, k3]) + push!(identical_networks_4, reaction_networks_weird[7] => rs_3) + + for networks in identical_networks_4 + @test isequal(get_iv(networks[1]), get_iv(networks[2])) + @test alleq(get_unknowns(networks[1]), get_unknowns(networks[2])) + @test alleq(get_ps(networks[1]), get_ps(networks[2])) + @test ModelingToolkit.get_systems(networks[1]) == + ModelingToolkit.get_systems(networks[2]) + @test length(get_eqs(networks[1])) == length(get_eqs(networks[2])) + for (e1, e2) in zip(get_eqs(networks[1]), get_eqs(networks[2])) + @test isequal(e1.rate, e2.rate) + @test isequal(e1.substrates, e2.substrates) + @test isequal(e1.products, e2.products) + @test isequal(e1.substoich, e2.substoich) + @test isequal(e1.prodstoich, e2.prodstoich) + @test isequal(e1.netstoich, e2.netstoich) + @test isequal(e1.only_use_rate, e2.only_use_rate) end end - V,W = getfield.(observed(rn), :lhs) - @test isequal(arguments(ModelingToolkit.unwrap(V)), Any[rn.iv, rn.sivs[1], rn.sivs[2]]) - @test isequal(arguments(ModelingToolkit.unwrap(W)), Any[rn.iv, rn.sivs[2]]) end -# Checks that metadata is written properly. +### Tests Usage of Various Symbols ### + +# Tests that time is handled properly. let - rn = @reaction_network rn_observed begin - @observables (X, [description="my_description"]) ~ X1 + X2 - k, 0 --> X1 + X2 + time_network = @reaction_network begin + (t, k2), X1 ↔ X2 + (k3, t), X2 ↔ X3 + (t, k6), X3 ↔ X1 end @test ModelingToolkit.getdescription(observed(rn)[1].lhs) == "my_description" end @@ -604,64 +502,62 @@ let @test isequal(observed(rn1)[1].lhs.metadata, observed(rn2)[1].lhs.metadata) @test isequal(unknowns(rn1), unknowns(rn2)) - # Case with metadata. - rn3 = @reaction_network rn_observed begin - @observables (X, [description="description"]) ~ X1 + X2 - k, 0 --> X1 + X2 - end - rn4 = @reaction_network rn_observed begin - @variables X(t) [description="description"] - @observables X ~ X1 + X2 - k, 0 --> X1 + X2 + @test f_eval(reaction_networks_constraint[1], u, p_1, τ) ≈ f_eval(time_network, u, p_2, τ) + @test jac_eval(reaction_networks_constraint[1], u, p_1, τ) ≈ jac_eval(time_network, u, p_2, τ) + @test g_eval(reaction_networks_constraint[1], u, p_1, τ) ≈ g_eval(time_network, u, p_2, τ) end - @test isequal(observed(rn3)[1].rhs, observed(rn4)[1].rhs) - @test isequal(observed(rn3)[1].lhs.metadata, observed(rn4)[1].lhs.metadata) - @test isequal(unknowns(rn3), unknowns(rn4)) end -# Tests for interpolation into the observables option. +# Check that various symbols can be used as species/parameter names. let - # Interpolation into lhs. - @species X [description="An observable"] - rn1 = @reaction_network begin - @observables $X ~ X1 + X2 - (k1, k2), X1 <--> X2 + @reaction_network begin + (a, A), n ⟷ N + (b, B), o ⟷ O + (c, C), p ⟷ P + (d, D), q ⟷ Q + (e, E), r ⟷ R + (f, F), s ⟷ S + (g, G), u ⟷ U + (h, H), v ⟷ V + (j, J), w ⟷ W + (k, K), x ⟷ X + (l, L), y ⟷ Y + (m, M), z ⟷ Z end @test isequal(observed(rn1)[1].lhs, X) @test ModelingToolkit.getdescription(rn1.X) == "An observable" @test isspecies(rn1.X) @test length(unknowns(rn1)) == 2 - # Interpolation into rhs. - @parameters n [description="A parameter"] - @species S(t) - rn2 = @reaction_network begin - @observables Stot ~ $S + $n*Sn - (kB, kD), $n*S <--> Sn - end - @unpack Stot, Sn, kD, kB = rn2 + @reaction_network begin (1.0, 1.0), i ⟷ T end - u0 = Dict([S => 5.0, Sn => 1.0]) - ps = Dict([n => 2, kB => 1.0, kD => 1.0]) - oprob = ODEProblem(rn2, u0, (0.0, 1.0), ps) + @reaction_network begin + (å, Å), ü ⟷ Ü + (ä, Ä), ñ ⟷ Ñ + (ö, Ö), æ ⟷ Æ + end - @test issetequal(Symbolics.get_variables(observed(rn2)[1].rhs), [S, n, Sn]) - @test oprob[Stot] == u0[S] + ps[n]*u0[Sn] - @test length(unknowns(rn2)) == 2 + @reaction_network begin + (α, Α), ν ⟷ Ν + (β, Β), ξ ⟷ Ξ + (γ, γ), ο ⟷ Ο + (δ, Δ), Π ⟷ Π + (ϵ, Ε), ρ ⟷ Ρ + (ζ, Ζ), σ ⟷ Σ + (η, Η), τ ⟷ Τ + (θ, Θ), υ ⟷ Υ + (ι, Ι), ϕ ⟷ Φ + (κ, κ), χ ⟷ Χ + (λ, Λ), ψ ↔ Ψ + (μ, Μ), ω ⟷ Ω + end end # Tests specific declaration of observables as species/variables. let rn = @reaction_network begin - @species X(t) - @variables Y(t) - @observables begin - X ~ X + 2X2 - Y ~ Y1 + Y2 - Z ~ X + Y - end - (kB,kD), 2X <--> X2 - (k1,k2), Y1 <--> Y2 + k1, S + I --> 2I + k2, I --> R end @test isspecies(rn.X) @@ -677,69 +573,46 @@ let k, 0 --> X1 + X2 end - # System with observable in observable formula. - @test_throws Exception @eval @reaction_network begin - @observables begin - X ~ X1 + X2 - X2 ~ 2X - end - (p,d), 0 <--> X1 + X2 - end - - # Multiple @observables options - @test_throws Exception @eval @reaction_network begin - @observables X ~ X1 + X2 - @observables Y ~ Y1 + Y2 - k, 0 --> X1 + X2 - k, 0 --> Y1 + Y2 - end - @test_throws Exception @eval @reaction_network begin - @observables begin - X ~ X1 + X2 - end - @observables begin - X ~ 2(X1 + X2) - end - (p,d), 0 <--> X1 + X2 - end - - # Default value for compound. - @test_throws Exception @eval @reaction_network begin - @observables (X = 1.0) ~ X1 + X2 - k, 0 --> X1 + X2 - end - - # Forbidden symbols as observable names. - @test_throws Exception @eval @reaction_network begin - @observables t ~ t1 + t2 - k, 0 --> t1 + t2 - end - @test_throws Exception @eval @reaction_network begin - @observables im ~ i + m - k, 0 --> i + m - end - - # Non-trivial observables expression. - @test_throws Exception @eval @reaction_network begin - @observables X - X1 ~ X2 - k, 0 --> X1 + X2 + rn2 = @reaction_network arrowtest begin + a1, C --> 0 + a2, 0 --> C + k1, A + B --> C + k2, C --> A + B + b1, B --> 0 end - # Occurrence of undeclared dependants. - @test_throws Exception @eval @reaction_network begin - @observables X ~ X1 + X2 - k, 0 --> X1 - end + @test rn1 == rn2 +end - # Interpolation and explicit declaration of an observable. - @variables X(t) - @test_throws Exception @eval @reaction_network begin - @variables X(t) - @observables $X ~ X1 + X2 - (k1,k2), X1 <--> X2 - end +# Tests arrow variants in "@reaction" macro . +let + @test isequal((@reaction k, 0 --> X), (@reaction k, X <-- 0)) + @test isequal((@reaction k, 0 --> X), (@reaction k, X ⟻ 0)) + @test isequal((@reaction k, 0 --> X), (@reaction k, 0 → X)) + @test isequal((@reaction k, 0 --> X), (@reaction k, 0 ⥟ X)) end +# Test that symbols with special mean, or that are forbidden, are handled properly. +let + test_network = @reaction_network begin t * k, X --> ∅ end + @test length(species(test_network)) == 1 + @test length(parameters(test_network)) == 1 + + test_network = @reaction_network begin π, X --> ∅ end + @test length(species(test_network)) == 1 + @test length(parameters(test_network)) == 0 + @test reactions(test_network)[1].rate == π + + test_network = @reaction_network begin pi, X --> ∅ end + @test length(species(test_network)) == 1 + @test length(parameters(test_network)) == 0 + @test reactions(test_network)[1].rate == pi + + test_network = @reaction_network begin ℯ, X --> ∅ end + @test length(species(test_network)) == 1 + @test length(parameters(test_network)) == 0 + @test reactions(test_network)[1].rate == ℯ + ### Test `@equations` Option for Coupled CRN/Equations Models ### From 33b68076cc44a821095ad6a6be1ae90836b504b5 Mon Sep 17 00:00:00 2001 From: Torkel Date: Thu, 28 Mar 2024 14:20:34 -0400 Subject: [PATCH 34/67] init --- src/Catalyst.jl | 5 + .../serialisation_support.jl | 232 ++++++++++++++++++ .../serialise_reactionsystem.jl | 75 ++++++ 3 files changed, 312 insertions(+) create mode 100644 src/model_serialisation/serialisation_support.jl create mode 100644 src/model_serialisation/serialise_reactionsystem.jl diff --git a/src/Catalyst.jl b/src/Catalyst.jl index 42433bcd2f..fd2f10e2d6 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -153,6 +153,11 @@ export balance_reaction, balance_system include("steady_state_stability.jl") export steady_state_stability, steady_state_jac +# ReactionSystem serialisation. +include("model_serialisation/serialise_reactionsystem.jl") +include("model_serialisation/serialisation_support.jl") +export save_reaction_network + ### Extensions ### # HomotopyContinuation diff --git a/src/model_serialisation/serialisation_support.jl b/src/model_serialisation/serialisation_support.jl new file mode 100644 index 0000000000..43805ce126 --- /dev/null +++ b/src/model_serialisation/serialisation_support.jl @@ -0,0 +1,232 @@ +### Generic Functions ### + +# Function which handles the addition of a single component to the file string. +function push_component(file_text::String, rn::ReactionSystem, annotate::Bool, comp_funcs::Tuple) + has_component, get_comp_string, get_comp_annotation = comp_funcs + has_component(rn) || (return (file_text, false)) + write_string = "\n" * get_comp_string(rn) + annotate && (write_string = "\n\n# " * get_comp_annotation(rn) * write_string) + return (file_text * write_string, true) +end + +# Converts a Num to a String. +function wrapped_num_2_string(num) + return String(Symbol(num)) +end + +# Converts a vector of Nums to a single string with all (e.g. [X(t), Y(t)] becomes " X(t) Y(t)"). +function wrapped_nums_2_string(nums) + return prod([" " * wrapped_num_2_string(num) for num in nums]) +end + +# Converts a vector of Symbolics to a (uncalled) vector (e.g. [X(t), Y(t), p] becomes " [X, Y, p]"). +function nums_2_uncalled_strin_vec(nums) + return "$(convert(Vector{Any}, uncall.(nums)))"[4:end] +end + +# Function for converting a Symbolics of the form `X(t)` to the form `X`. +function uncall(var) + istree(var) ? Sym{Real}(Symbolics.getname(var)) : var +end + +function nums_2_uncalled_strin_vec(nums) + "$(convert(Vector{Any}, uncall.(nums)))"[4:end] +end + +# Converts a numeric expression to a string. `uncall_dict` is required to convert stuff like +# `p1*X(t) + p2` to `p1*X + p2`. +function num_2_string(num; uncall_dict = make_uncall_dict(Symbolics.get_variables(num))) + uncalled_num = substitute(num, uncall_dict) + return repr(uncalled_num) +end + +# For a list of symbolics, makes an "uncall dict". It permits the conversion of e.g. `X(t)` to `X`. +function make_uncall_dict(syms) + return Dict([sym => uncall(Symbolics.unwrap(sym)) for sym in syms]) +end + +### Generic Unsupported Component FUnctions ### + +# Generic function for creating an string for an unsupported argument. +function get_unsupported_comp_string(component::String) + @warn "Writing ReactionSystem models with $(component) is currently not supported. This field is not written to the file." + return "" +end + +# Generic function for creating the annotation string for an unsupported argument. +function get_unsupported_comp_annotation(component::String) + return "$(component): (OBS: Currently not supported, and hence empty)" +end + + +### Handles Independent Variables ### + +# Checks if the reaction system have any independent variable. True for all valid reaction systems. +function has_iv(rn::ReactionSystem) + return true +end + +# Extract a string which declares the system's independent variable. +function get_iv_string(rn::ReactionSystem) + return "@variables $(num_2_string(ModelingToolkit.get_iv(rn)))" +end + +# Creates an annotation for the system's independent variable. +function get_iv_annotation(rn::ReactionSystem) + return "Independent variable:" +end + +# Combines the 3 independent variable-related functions in a constant tuple. +IV_FS = (has_iv, get_iv_string, get_iv_annotation) + + +### Handles Spatial Independent Variables ### + +# Checks if the reaction system have any spatial independent variables. +function has_sivs(rn::ReactionSystem) + return length(rn.sivs) != 0 +end + +# Extract a string which declares the system's spatial independent variables. +function get_sivs_string(rn::ReactionSystem) + return "sivs = @variables$(wrapped_nums_2_string(rn.sivs))" +end + +# Creates an annotation for the system's spatial independent variables. +function get_sivs_annotation(rn::ReactionSystem) + return "Spatial independent variables:" +end + +# Combines the 3 independent variables-related functions in a constant tuple. +SIVS_FS = (has_sivs, get_sivs_string, get_sivs_annotation) + + +### Handles Species ### + +# Checks if the reaction system have any species. +function has_species(rn::ReactionSystem) + return length(species(rn)) != 0 +end + +# Extract a string which declares the system's species. +function get_species_string(rn::ReactionSystem) + return "sps = @species$(wrapped_nums_2_string(species(rn)))" +end + +# Creates an annotation for the system's species. +function get_species_annotation(rn::ReactionSystem) + return "Species:" +end + +# Combines the 3 species-related functions in a constant tuple. +SPECIES_FS = (has_species, get_species_string, get_species_annotation) + + +### Handles Variables ### + +# Checks if the reaction system have any variables. +function has_variables(rn::ReactionSystem) + return length(unknowns(rn)) > length(species(rn)) +end + +# Extract a string which declares the system's variables. +function get_variables_string(rn::ReactionSystem) + variables = filter(!isspecies, unknowns(rn)) + return "vars = @variables$(wrapped_nums_2_string(variables))" +end + +# Creates an annotation for the system's . +function get_variables_annotation(rn::ReactionSystem) + return "Variables:" +end + +# Combines the 3 variables-related functions in a constant tuple. +VARIABLES_FS = (has_variables, get_variables_string, get_variables_annotation) + + +### Handles Parameters ### + +# Checks if the reaction system have any parameters. +function has_parameters(rn::ReactionSystem) + return length(parameters(rn)) != 0 +end + +# Extract a string which declares the system's parameters. +function get_parameters_string(rn::ReactionSystem) + return "ps = @parameters$(wrapped_nums_2_string(parameters(rn)))" +end + +# Creates an annotation for the system's parameters. +function get_parameters_annotation(rn::ReactionSystem) + return "Parameters:" +end + +# Combines the 3 parameters-related functions in a constant tuple. +PARAMETERS_FS = (has_parameters, get_parameters_string, get_parameters_annotation) + + +### Handles Reactions ### + +# Checks if the reaction system have any reactions. +function has_reactions(rn::ReactionSystem) + return length(reactions(rn)) != 0 +end + +# Extract a string which declares the system's reactions. +function get_reactions_string(rn::ReactionSystem) + uncall_dict = make_uncall_dict(unknowns(rn)) + rxs_string = "rxs = [" + for rx in reactions(rn) + rxs_string = rxs_string * "\n\t" * reaction_string(rx, uncall_dict) * "," + end + + # Updates the string (including removing the last `,`) and returns in. + return rxs_string[1:end-1] * "\n]" +end + + +# Creates a string that corresponds to the declaration of a single `Reaction`. +function reaction_string(rx::Reaction, uncall_dict) + # Prepares the `Reaction` declaration components. + rate = num_2_string(rx.rate; uncall_dict) + substrates = isempty(rx.substrates) ? "nothing" : nums_2_uncalled_strin_vec(rx.substrates) + products = isempty(rx.products) ? "nothing" : nums_2_uncalled_strin_vec(rx.products) + substoich = isempty(rx.substoich) ? "nothing" : nums_2_uncalled_strin_vec(rx.substoich) + prodstoich = isempty(rx.prodstoich) ? "nothing" : nums_2_uncalled_strin_vec(rx.prodstoich) + + # Creates the full expression, including adding kwargs (`only_use_rate` and `metadata`). + rx_string = "Reaction($rate, $(substrates), $(products), $(substoich), $(prodstoich)" + if rx.only_use_rate + rx_string = rx_string * "; only_use_rate = true" + end + return rx_string * ")" +end + +# Creates an annotation for the system's reactions. +function get_reactions_annotation(rn::ReactionSystem) + return "Reactions:" +end + +# Combines the 3 reactions-related functions in a constant tuple. +REACTIONS_FS = (has_reactions, get_reactions_string, get_reactions_annotation) + + +### Handles Equations ### + +# Checks if the reaction system have any equations. +function has_equations(rn::ReactionSystem) + return length(equations(rn)) > length(reactions(rn)) +end + +# Extract a string which declares the system's equations. +function get_equations_string(rn::ReactionSystem) + get_unsupported_comp_string("equations") +end + +# Creates an annotation for the system's equations. +function get_equations_annotation(rn::ReactionSystem) + get_unsupported_comp_annotation("Equations") +end + +# Combines the 3 equations-related functions in a constant tuple. +EQUATIONS_FS = (has_equations, get_equations_string, get_equations_annotation) \ No newline at end of file diff --git a/src/model_serialisation/serialise_reactionsystem.jl b/src/model_serialisation/serialise_reactionsystem.jl new file mode 100644 index 0000000000..b42d34bbe5 --- /dev/null +++ b/src/model_serialisation/serialise_reactionsystem.jl @@ -0,0 +1,75 @@ +function save_reaction_network(filename::String, rn::ReactionSystem; annotate = true) + # Initiates the file string. + file_text = "" + + # Goes through each type of system component, potentially adding it to the string. + file_text, has_iv = push_component(file_text, rn, annotate, IV_FS) + file_text, has_sivs = push_component(file_text, rn, annotate, SIVS_FS) + file_text, has_species = push_component(file_text, rn, annotate, SPECIES_FS) + file_text, has_variables = push_component(file_text, rn, annotate, VARIABLES_FS) + file_text, has_parameters = push_component(file_text, rn, annotate, PARAMETERS_FS) + file_text, has_reactions = push_component(file_text, rn, annotate, REACTIONS_FS) + file_text, has_equations = push_component(file_text, rn, annotate, EQUATIONS_FS) + + # Finalises the system. Creates the final `ReactionSystem` call. + rs_creation_code = make_reaction_system_call(rn, file_text, annotate, + has_sivs, has_species, has_variables, + has_parameters, has_reactions, has_equations) + annotate || (file_text = "\n" * file_text) + file_text = "let" * file_text * "\n\n" * rs_creation_code * "\n\nend" + + # Writes the model to a file. Then, returns nothing. + open(filename, "w") do file + write(file, file_text) + end + return nothing +end + +# Takes the actual text which creates the model, and wraps it in a `let ... end` statement and A +# ReactionSystem call, to create the final file text. +function make_reaction_system_call(rs::ReactionSystem, file_text, annotate, has_sivs, has_species, has_variables, has_parameters, has_reactions, has_equations) + # Creates base call. + iv = Catalyst.get_iv(rs) + if has_reactions && has_equations + eqs = "[rxs; eqs]" + elseif has_reactions + eqs = "rxs" + elseif has_equations + eqs = "eqs" + else + eqs = "[]" + end + if has_species && has_variables + unknowns = "[sps; vars]" + elseif has_species + unknowns = "sps" + elseif has_variables + unknowns = "vars" + else + unknowns = "[]" + end + if has_parameters + ps = "ps" + else + ps = "[]" + end + reaction_system_string = "ReactionSystem($eqs, $iv, $unknowns, $ps" + + # Appends additional (optional) arguments. + if Base.isidentifier(Catalyst.getname(rs)) + rs_name = ":$(Catalyst.getname(rs))" + else + rs_name = "Symbol(\"$(Catalyst.getname(rs))\")" + end + reaction_system_string = reaction_system_string * "; name = $(rs_name)" + reaction_system_string = reaction_system_string * ")" + + # Returns the full call. + if !ModelingToolkit.iscomplete(rs) + reaction_system_string = "rs = $(reaction_system_string)\ncomplete(rs)" + end + if annotate + reaction_system_string = "# Declares ReactionSystem model:\n" * reaction_system_string + end + return reaction_system_string +end \ No newline at end of file From 0c5ac53ea91086fc17c4dbe1234cfc504cdf9e84 Mon Sep 17 00:00:00 2001 From: Torkel Date: Thu, 28 Mar 2024 15:28:36 -0400 Subject: [PATCH 35/67] up --- .../serialisation_support.jl | 218 ++---------------- src/model_serialisation/serialise_fields.jl | 171 ++++++++++++++ 2 files changed, 193 insertions(+), 196 deletions(-) create mode 100644 src/model_serialisation/serialise_fields.jl diff --git a/src/model_serialisation/serialisation_support.jl b/src/model_serialisation/serialisation_support.jl index 43805ce126..43e8f4c033 100644 --- a/src/model_serialisation/serialisation_support.jl +++ b/src/model_serialisation/serialisation_support.jl @@ -1,4 +1,4 @@ -### Generic Functions ### +### Field Serialisation Support Functions ### # Function which handles the addition of a single component to the file string. function push_component(file_text::String, rn::ReactionSystem, annotate::Bool, comp_funcs::Tuple) @@ -9,6 +9,20 @@ function push_component(file_text::String, rn::ReactionSystem, annotate::Bool, c return (file_text * write_string, true) end +# Generic function for creating an string for an unsupported argument. +function get_unsupported_comp_string(component::String) + @warn "Writing ReactionSystem models with $(component) is currently not supported. This field is not written to the file." + return "" +end + +# Generic function for creating the annotation string for an unsupported argument. +function get_unsupported_comp_annotation(component::String) + return "$(component): (OBS: Currently not supported, and hence empty)" +end + + +### Symbolics String Conversions ### + # Converts a Num to a String. function wrapped_num_2_string(num) return String(Symbol(num)) @@ -24,15 +38,6 @@ function nums_2_uncalled_strin_vec(nums) return "$(convert(Vector{Any}, uncall.(nums)))"[4:end] end -# Function for converting a Symbolics of the form `X(t)` to the form `X`. -function uncall(var) - istree(var) ? Sym{Real}(Symbolics.getname(var)) : var -end - -function nums_2_uncalled_strin_vec(nums) - "$(convert(Vector{Any}, uncall.(nums)))"[4:end] -end - # Converts a numeric expression to a string. `uncall_dict` is required to convert stuff like # `p1*X(t) + p2` to `p1*X + p2`. function num_2_string(num; uncall_dict = make_uncall_dict(Symbolics.get_variables(num))) @@ -40,193 +45,14 @@ function num_2_string(num; uncall_dict = make_uncall_dict(Symbolics.get_variable return repr(uncalled_num) end -# For a list of symbolics, makes an "uncall dict". It permits the conversion of e.g. `X(t)` to `X`. -function make_uncall_dict(syms) - return Dict([sym => uncall(Symbolics.unwrap(sym)) for sym in syms]) -end - -### Generic Unsupported Component FUnctions ### - -# Generic function for creating an string for an unsupported argument. -function get_unsupported_comp_string(component::String) - @warn "Writing ReactionSystem models with $(component) is currently not supported. This field is not written to the file." - return "" -end - -# Generic function for creating the annotation string for an unsupported argument. -function get_unsupported_comp_annotation(component::String) - return "$(component): (OBS: Currently not supported, and hence empty)" -end - - -### Handles Independent Variables ### - -# Checks if the reaction system have any independent variable. True for all valid reaction systems. -function has_iv(rn::ReactionSystem) - return true -end - -# Extract a string which declares the system's independent variable. -function get_iv_string(rn::ReactionSystem) - return "@variables $(num_2_string(ModelingToolkit.get_iv(rn)))" -end - -# Creates an annotation for the system's independent variable. -function get_iv_annotation(rn::ReactionSystem) - return "Independent variable:" -end - -# Combines the 3 independent variable-related functions in a constant tuple. -IV_FS = (has_iv, get_iv_string, get_iv_annotation) - - -### Handles Spatial Independent Variables ### - -# Checks if the reaction system have any spatial independent variables. -function has_sivs(rn::ReactionSystem) - return length(rn.sivs) != 0 -end - -# Extract a string which declares the system's spatial independent variables. -function get_sivs_string(rn::ReactionSystem) - return "sivs = @variables$(wrapped_nums_2_string(rn.sivs))" -end - -# Creates an annotation for the system's spatial independent variables. -function get_sivs_annotation(rn::ReactionSystem) - return "Spatial independent variables:" -end +### Generic Expression Handling ### -# Combines the 3 independent variables-related functions in a constant tuple. -SIVS_FS = (has_sivs, get_sivs_string, get_sivs_annotation) - - -### Handles Species ### - -# Checks if the reaction system have any species. -function has_species(rn::ReactionSystem) - return length(species(rn)) != 0 -end - -# Extract a string which declares the system's species. -function get_species_string(rn::ReactionSystem) - return "sps = @species$(wrapped_nums_2_string(species(rn)))" -end - -# Creates an annotation for the system's species. -function get_species_annotation(rn::ReactionSystem) - return "Species:" -end - -# Combines the 3 species-related functions in a constant tuple. -SPECIES_FS = (has_species, get_species_string, get_species_annotation) - - -### Handles Variables ### - -# Checks if the reaction system have any variables. -function has_variables(rn::ReactionSystem) - return length(unknowns(rn)) > length(species(rn)) -end - -# Extract a string which declares the system's variables. -function get_variables_string(rn::ReactionSystem) - variables = filter(!isspecies, unknowns(rn)) - return "vars = @variables$(wrapped_nums_2_string(variables))" -end - -# Creates an annotation for the system's . -function get_variables_annotation(rn::ReactionSystem) - return "Variables:" -end - -# Combines the 3 variables-related functions in a constant tuple. -VARIABLES_FS = (has_variables, get_variables_string, get_variables_annotation) - - -### Handles Parameters ### - -# Checks if the reaction system have any parameters. -function has_parameters(rn::ReactionSystem) - return length(parameters(rn)) != 0 -end - -# Extract a string which declares the system's parameters. -function get_parameters_string(rn::ReactionSystem) - return "ps = @parameters$(wrapped_nums_2_string(parameters(rn)))" -end - -# Creates an annotation for the system's parameters. -function get_parameters_annotation(rn::ReactionSystem) - return "Parameters:" -end - -# Combines the 3 parameters-related functions in a constant tuple. -PARAMETERS_FS = (has_parameters, get_parameters_string, get_parameters_annotation) - - -### Handles Reactions ### - -# Checks if the reaction system have any reactions. -function has_reactions(rn::ReactionSystem) - return length(reactions(rn)) != 0 -end - -# Extract a string which declares the system's reactions. -function get_reactions_string(rn::ReactionSystem) - uncall_dict = make_uncall_dict(unknowns(rn)) - rxs_string = "rxs = [" - for rx in reactions(rn) - rxs_string = rxs_string * "\n\t" * reaction_string(rx, uncall_dict) * "," - end - - # Updates the string (including removing the last `,`) and returns in. - return rxs_string[1:end-1] * "\n]" -end - - -# Creates a string that corresponds to the declaration of a single `Reaction`. -function reaction_string(rx::Reaction, uncall_dict) - # Prepares the `Reaction` declaration components. - rate = num_2_string(rx.rate; uncall_dict) - substrates = isempty(rx.substrates) ? "nothing" : nums_2_uncalled_strin_vec(rx.substrates) - products = isempty(rx.products) ? "nothing" : nums_2_uncalled_strin_vec(rx.products) - substoich = isempty(rx.substoich) ? "nothing" : nums_2_uncalled_strin_vec(rx.substoich) - prodstoich = isempty(rx.prodstoich) ? "nothing" : nums_2_uncalled_strin_vec(rx.prodstoich) - - # Creates the full expression, including adding kwargs (`only_use_rate` and `metadata`). - rx_string = "Reaction($rate, $(substrates), $(products), $(substoich), $(prodstoich)" - if rx.only_use_rate - rx_string = rx_string * "; only_use_rate = true" - end - return rx_string * ")" -end - -# Creates an annotation for the system's reactions. -function get_reactions_annotation(rn::ReactionSystem) - return "Reactions:" -end - -# Combines the 3 reactions-related functions in a constant tuple. -REACTIONS_FS = (has_reactions, get_reactions_string, get_reactions_annotation) - - -### Handles Equations ### - -# Checks if the reaction system have any equations. -function has_equations(rn::ReactionSystem) - return length(equations(rn)) > length(reactions(rn)) -end - -# Extract a string which declares the system's equations. -function get_equations_string(rn::ReactionSystem) - get_unsupported_comp_string("equations") +# Function for converting a Symbolics of the form `X(t)` to the form `X`. +function uncall(var) + istree(var) ? Sym{Real}(Symbolics.getname(var)) : var end -# Creates an annotation for the system's equations. -function get_equations_annotation(rn::ReactionSystem) - get_unsupported_comp_annotation("Equations") +# For a list of symbolics, makes an "uncall dict". It permits the conversion of e.g. `X(t)` to `X`. +function make_uncall_dict(syms) + return Dict([sym => uncall(Symbolics.unwrap(sym)) for sym in syms]) end - -# Combines the 3 equations-related functions in a constant tuple. -EQUATIONS_FS = (has_equations, get_equations_string, get_equations_annotation) \ No newline at end of file diff --git a/src/model_serialisation/serialise_fields.jl b/src/model_serialisation/serialise_fields.jl new file mode 100644 index 0000000000..195c52e769 --- /dev/null +++ b/src/model_serialisation/serialise_fields.jl @@ -0,0 +1,171 @@ +### Handles Independent Variables ### + +# Checks if the reaction system have any independent variable. True for all valid reaction systems. +function has_iv(rn::ReactionSystem) + return true +end + +# Extract a string which declares the system's independent variable. +function get_iv_string(rn::ReactionSystem) + return "@variables $(num_2_string(ModelingToolkit.get_iv(rn)))" +end + +# Creates an annotation for the system's independent variable. +function get_iv_annotation(rn::ReactionSystem) + return "Independent variable:" +end + +# Combines the 3 independent variable-related functions in a constant tuple. +IV_FS = (has_iv, get_iv_string, get_iv_annotation) + + +### Handles Spatial Independent Variables ### + +# Checks if the reaction system have any spatial independent variables. +function has_sivs(rn::ReactionSystem) + return length(rn.sivs) != 0 +end + +# Extract a string which declares the system's spatial independent variables. +function get_sivs_string(rn::ReactionSystem) + return "sivs = @variables$(wrapped_nums_2_string(rn.sivs))" +end + +# Creates an annotation for the system's spatial independent variables. +function get_sivs_annotation(rn::ReactionSystem) + return "Spatial independent variables:" +end + +# Combines the 3 independent variables-related functions in a constant tuple. +SIVS_FS = (has_sivs, get_sivs_string, get_sivs_annotation) + + +### Handles Species ### + +# Checks if the reaction system have any species. +function has_species(rn::ReactionSystem) + return length(species(rn)) != 0 +end + +# Extract a string which declares the system's species. +function get_species_string(rn::ReactionSystem) + return "sps = @species$(wrapped_nums_2_string(species(rn)))" +end + +# Creates an annotation for the system's species. +function get_species_annotation(rn::ReactionSystem) + return "Species:" +end + +# Combines the 3 species-related functions in a constant tuple. +SPECIES_FS = (has_species, get_species_string, get_species_annotation) + + +### Handles Variables ### + +# Checks if the reaction system have any variables. +function has_variables(rn::ReactionSystem) + return length(unknowns(rn)) > length(species(rn)) +end + +# Extract a string which declares the system's variables. +function get_variables_string(rn::ReactionSystem) + variables = filter(!isspecies, unknowns(rn)) + return "vars = @variables$(wrapped_nums_2_string(variables))" +end + +# Creates an annotation for the system's . +function get_variables_annotation(rn::ReactionSystem) + return "Variables:" +end + +# Combines the 3 variables-related functions in a constant tuple. +VARIABLES_FS = (has_variables, get_variables_string, get_variables_annotation) + + +### Handles Parameters ### + +# Checks if the reaction system have any parameters. +function has_parameters(rn::ReactionSystem) + return length(parameters(rn)) != 0 +end + +# Extract a string which declares the system's parameters. +function get_parameters_string(rn::ReactionSystem) + return "ps = @parameters$(wrapped_nums_2_string(parameters(rn)))" +end + +# Creates an annotation for the system's parameters. +function get_parameters_annotation(rn::ReactionSystem) + return "Parameters:" +end + +# Combines the 3 parameters-related functions in a constant tuple. +PARAMETERS_FS = (has_parameters, get_parameters_string, get_parameters_annotation) + + +### Handles Reactions ### + +# Checks if the reaction system have any reactions. +function has_reactions(rn::ReactionSystem) + return length(reactions(rn)) != 0 +end + +# Extract a string which declares the system's reactions. +function get_reactions_string(rn::ReactionSystem) + uncall_dict = make_uncall_dict(unknowns(rn)) + rxs_string = "rxs = [" + for rx in reactions(rn) + rxs_string = rxs_string * "\n\t" * reaction_string(rx, uncall_dict) * "," + end + + # Updates the string (including removing the last `,`) and returns in. + return rxs_string[1:end-1] * "\n]" +end + + +# Creates a string that corresponds to the declaration of a single `Reaction`. +function reaction_string(rx::Reaction, uncall_dict) + # Prepares the `Reaction` declaration components. + rate = num_2_string(rx.rate; uncall_dict) + substrates = isempty(rx.substrates) ? "nothing" : nums_2_uncalled_strin_vec(rx.substrates) + products = isempty(rx.products) ? "nothing" : nums_2_uncalled_strin_vec(rx.products) + substoich = isempty(rx.substoich) ? "nothing" : nums_2_uncalled_strin_vec(rx.substoich) + prodstoich = isempty(rx.prodstoich) ? "nothing" : nums_2_uncalled_strin_vec(rx.prodstoich) + + # Creates the full expression, including adding kwargs (`only_use_rate` and `metadata`). + rx_string = "Reaction($rate, $(substrates), $(products), $(substoich), $(prodstoich)" + if rx.only_use_rate + rx_string = rx_string * "; only_use_rate = true" + end + return rx_string * ")" +end + +# Creates an annotation for the system's reactions. +function get_reactions_annotation(rn::ReactionSystem) + return "Reactions:" +end + +# Combines the 3 reactions-related functions in a constant tuple. +REACTIONS_FS = (has_reactions, get_reactions_string, get_reactions_annotation) + + +### Handles Equations ### + +# Checks if the reaction system have any equations. +function has_equations(rn::ReactionSystem) + return length(equations(rn)) > length(reactions(rn)) +end + +# Extract a string which declares the system's equations. +function get_equations_string(rn::ReactionSystem) + get_unsupported_comp_string("equations") +end + +# Creates an annotation for the system's equations. +function get_equations_annotation(rn::ReactionSystem) + get_unsupported_comp_annotation("Equations") +end + +# Combines the 3 equations-related functions in a constant tuple. +EQUATIONS_FS = (has_equations, get_equations_string, get_equations_annotation) \ No newline at end of file From 9b4bbdd3dcc15db8cc5ab43923dc1458eb760baf Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 29 Mar 2024 08:24:28 -0400 Subject: [PATCH 36/67] save progress --- src/Catalyst.jl | 13 +- .../serialisation_support.jl | 188 +++++++++++++++--- src/model_serialisation/serialise_fields.jl | 158 +++++++++++++-- .../serialise_reactionsystem.jl | 94 ++++++--- 4 files changed, 384 insertions(+), 69 deletions(-) diff --git a/src/Catalyst.jl b/src/Catalyst.jl index fd2f10e2d6..7adaf341b3 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -153,11 +153,6 @@ export balance_reaction, balance_system include("steady_state_stability.jl") export steady_state_stability, steady_state_jac -# ReactionSystem serialisation. -include("model_serialisation/serialise_reactionsystem.jl") -include("model_serialisation/serialisation_support.jl") -export save_reaction_network - ### Extensions ### # HomotopyContinuation @@ -187,4 +182,12 @@ include("spatial_reaction_systems/utility.jl") include("spatial_reaction_systems/spatial_ODE_systems.jl") include("spatial_reaction_systems/lattice_jump_systems.jl") + +### ReactionSystem Serialisation ### +# Has to be at the end (because it uses records of all metadata declared by Catalyst). +include("model_serialisation/serialisation_support.jl") +include("model_serialisation/serialise_fields.jl") +include("model_serialisation/serialise_reactionsystem.jl") +export save_reaction_network + end # module diff --git a/src/model_serialisation/serialisation_support.jl b/src/model_serialisation/serialisation_support.jl index 43e8f4c033..29f0aa33a8 100644 --- a/src/model_serialisation/serialisation_support.jl +++ b/src/model_serialisation/serialisation_support.jl @@ -1,11 +1,35 @@ +## String Handling ### + +# Appends stuff to a string. +# E.g `@string_append! str_base str1 str2` becomes `str_base = str_base * str1 * str2`. +macro string_append!(string, inputs...) + rhs = :($string * $(inputs[1])) + for input in inputs[2:end] + push!(rhs.args, input) + end + return esc(:($string = $rhs)) +end + +# Prepends stuff to a string. Can only take 1 or 2 inputs. +# E.g `@string_prepend! str1 str_base` becomes `str_base = str1 * str_base`. +macro string_prepend!(input, string) + rhs = :($input * $string) + return esc(:($string = $rhs)) +end +macro string_prepend!(input1, input2, string) + rhs = :($input1 * $input2 * $string) + return esc(:($string = $rhs)) +end + + ### Field Serialisation Support Functions ### # Function which handles the addition of a single component to the file string. -function push_component(file_text::String, rn::ReactionSystem, annotate::Bool, comp_funcs::Tuple) +function push_field(file_text::String, rn::ReactionSystem, annotate::Bool, comp_funcs::Tuple) has_component, get_comp_string, get_comp_annotation = comp_funcs has_component(rn) || (return (file_text, false)) write_string = "\n" * get_comp_string(rn) - annotate && (write_string = "\n\n# " * get_comp_annotation(rn) * write_string) + annotate && (@string_prepend! "\n\n# " get_comp_annotation(rn) write_string) return (file_text * write_string, true) end @@ -21,38 +45,154 @@ function get_unsupported_comp_annotation(component::String) end -### Symbolics String Conversions ### +### String Conversion ### -# Converts a Num to a String. -function wrapped_num_2_string(num) - return String(Symbol(num)) +# Converts a numeric expression (e.g. p*X + 2Y) to a string (e.g. "p*X + 2Y"). Also ensures that for +# any variables (e.g. X(t)) the call part is stripped, and only variable name (e.g. X) is written. +function expression_2_string(expr; strip_call_dict = make_strip_call_dict(Symbolics.get_variables(expr))) + strip_called_expr = substitute(expr, strip_call_dict) + return repr(strip_called_expr) end -# Converts a vector of Nums to a single string with all (e.g. [X(t), Y(t)] becomes " X(t) Y(t)"). -function wrapped_nums_2_string(nums) - return prod([" " * wrapped_num_2_string(num) for num in nums]) +# Converts a vector of symbolics (e.g. the species or parameter vectors) to a string vector. Strips +# any calls (e.g. X(t) becomes X). E.g. a species vector [X, Y, Z] is converted to "[X, Y, Z]". +function syms_2_strings(syms) + strip_called_syms = [strip_call(Symbolics.unwrap(sym)) for sym in syms] + return "$(convert(Vector{Any}, strip_called_syms))"[4:end] +end + +# Converts a vector of symbolics (e.g. the species or parameter vectors) to a string corresponding to +# the code required to declare them (potential @parameters or @species commands must still be added). +# The `multiline_format` option formats it with a `begin ... end` block and declarations on separate lines. +function syms_2_declaration_string(syms; multiline_format = false) + decs_string = (multiline_format ? "begin" : "") + for sym in syms + delimiter = (multiline_format ? "\n\t" : " ") + @string_append! decs_string delimiter sym_2_declaration_string(sym; multiline_format) + end + multiline_format && (@string_append! decs_string "\nend") + return decs_string end -# Converts a vector of Symbolics to a (uncalled) vector (e.g. [X(t), Y(t), p] becomes " [X, Y, p]"). -function nums_2_uncalled_strin_vec(nums) - return "$(convert(Vector{Any}, uncall.(nums)))"[4:end] +# Converts a symbolic (e.g. a species or parameter) to a string corresponding to how it would be declared +# in code. Takes default values and metadata into account. Example output "p=2.0 [bounds=(0.0, 1.0)]". +# The `multiline_format` option formats the string as if it is part of a `begin .. end` block. +function sym_2_declaration_string(sym; multiline_format = false) + # Creates the basic symbol. The `"$(sym)"` ensures that we get e.g. "X(t)" and not "X". + dec_string = "$(sym)" + + # If there is a default value, adds this to the declaration. + if ModelingToolkit.hasdefault(sym) + def_val = x_2_string(ModelingToolkit.getdefault(sym)) + separator = (multiline_format ? " = " : "=") + @string_append! dec_string separator "$(def_val)" + end + + # Adds any metadata to the declaration. + metadata_to_declare = get_metadata_to_declare(sym) + if !isempty(metadata_to_declare) + metadata_string = (multiline_format ? ", [" : " [") + for metadata in metadata_to_declare + @string_append! metadata_string metadata_2_string(sym, metadata) ", " + end + @string_append! dec_string metadata_string[1:end-2] "]" + end + + # Returns the declaration entry for the symbol. + return dec_string end -# Converts a numeric expression to a string. `uncall_dict` is required to convert stuff like -# `p1*X(t) + p2` to `p1*X + p2`. -function num_2_string(num; uncall_dict = make_uncall_dict(Symbolics.get_variables(num))) - uncalled_num = substitute(num, uncall_dict) - return repr(uncalled_num) +# Converts a generic value to a String. Handles each type of value separately. Unsupported values might +# not necessarily generate valid code, and hence throw errors. Primarily used to write default values +# and metadata values (which hopefully almost exclusively) has simple, supported, types. Ideally, +# more supported types can be added here. +x_2_string(x::Num) = expression_2_string(x) +x_2_string(x::SymbolicUtils.BasicSymbolic{<:Real}) = expression_2_string(x) +x_2_string(x::String) = "\"$x\"" +x_2_string(x::Char) = "\'$x\'" +x_2_string(x::Symbol) = ":$x" +x_2_string(x::Number) = string(x) +function x_2_string(x::Vector) + output = "[" + for val in x + output = output * x_2_string(val) * ", " + end + return output[1:end-2] * "]" +end +function x_2_string(x::Tuple) + output = "(" + for val in x + output = output * x_2_string(val) * ", " + end + return output[1:end-2] * ")" +end +x_2_string(x::Pair) = "$(x_2_string(x[1])) => $(x_2_string(x[2]))" +x_2_string(x::Nothing) = "nothing" +x_2_string(x) = error("Tried to write an unsupported value ($(x)) of an unsupported type ($(typeof(x))) to a string.") + + +### Symbolics Metadata Handling ### + +# For a Symbolic, retrieve all metadata that needs to be added to its declaration. Certain metadata +# (such as default values and whether a variable is a species or not) are skipped (these are stored +# in the `SKIPPED_METADATA` constant). +# Because it is impossible to retrieve the keyword used to declare individual metadata from the +# metadata entry, these must be stored manually (in `RECOGNISED_METADATA`). If one of these are +# encountered, a warning is thrown and it is skipped (we could also throw an error). I have asked +# Shashi, and he claims there is not alternative (general) solution. +function get_metadata_to_declare(sym) + metadata_keys = collect(keys(sym.metadata)) + metadata_keys = filter(mdk -> !(mdk in SKIPPED_METADATA), metadata_keys) + if any(!(mdk in keys(RECOGNISED_METADATA)) for mdk in metadata_keys) + @warn "The following unrecognised metadata entries: $(setdiff(metadata_keys, keys(RECOGNISED_METADATA))) are not recognised for species/variable/parameter $sym. If you raise an issue at https://github.com/SciML/Catalyst.jl, we can add support for this metadata type." + metadata_keys = filter(mdk -> in(mdk, keys(RECOGNISED_METADATA)), metadata_keys) + end + return metadata_keys +end + +# Converts a given metadata into the string used to declare it. +function metadata_2_string(sym, metadata) + return RECOGNISED_METADATA[metadata] * " = " * x_2_string(sym.metadata[metadata]) end +# List of all recognised metadata (we should add as many as possible), and th keyword used to declare +# them in code. +const RECOGNISED_METADATA = Dict([ + Catalyst.ParameterConstantSpecies => "isconstantspecies" + Catalyst.VariableBCSpecies => "isbcspecies" + Catalyst.VariableSpecies => "isspecies" + Catalyst.EdgeParameter => "edgeparameter" + Catalyst.CompoundSpecies => "iscompound" + Catalyst.CompoundComponents => "components" + Catalyst.CompoundCoefficients => "coefficients" + + ModelingToolkit.VariableDescription => "description" + ModelingToolkit.VariableBounds => "bounds" + ModelingToolkit.VariableUnit => "unit" + ModelingToolkit.VariableConnectType => "connect" + ModelingToolkit.VariableNoiseType => "noise" + ModelingToolkit.VariableInput => "input" + ModelingToolkit.VariableOutput => "output" + ModelingToolkit.VariableIrreducible => "irreducible" + ModelingToolkit.VariableStatePriority => "state_priority" + ModelingToolkit.VariableMisc => "misc" + ModelingToolkit.TimeDomain => "timedomain" +]) + +# List of metadata that does not need to be explicitly declared to be added (or which is handled separately). +const SKIPPED_METADATA = [ModelingToolkit.MTKVariableTypeCtx, Symbolics.VariableSource, + Symbolics.VariableDefaultValue, Catalyst.VariableSpecies] + + ### Generic Expression Handling ### -# Function for converting a Symbolics of the form `X(t)` to the form `X`. -function uncall(var) - istree(var) ? Sym{Real}(Symbolics.getname(var)) : var +# Potentially strips the call for a symbolics. E.g. X(t) becomes X (but p remains p). This is used +# when variables are written to files, as in code they are used without the call part. +function strip_call(sym) + return istree(sym) ? Sym{Real}(Symbolics.getname(sym)) : sym end -# For a list of symbolics, makes an "uncall dict". It permits the conversion of e.g. `X(t)` to `X`. -function make_uncall_dict(syms) - return Dict([sym => uncall(Symbolics.unwrap(sym)) for sym in syms]) -end +# For an vector of symbolics, creates a dictionary taking each symbolics to each call-stripped form. +function make_strip_call_dict(syms) + return Dict([sym => strip_call(Symbolics.unwrap(sym)) for sym in syms]) +end \ No newline at end of file diff --git a/src/model_serialisation/serialise_fields.jl b/src/model_serialisation/serialise_fields.jl index 195c52e769..c108dcf117 100644 --- a/src/model_serialisation/serialise_fields.jl +++ b/src/model_serialisation/serialise_fields.jl @@ -7,7 +7,8 @@ end # Extract a string which declares the system's independent variable. function get_iv_string(rn::ReactionSystem) - return "@variables $(num_2_string(ModelingToolkit.get_iv(rn)))" + iv_dec = ModelingToolkit.get_iv(rn) + return "@variables $(iv_dec)" end # Creates an annotation for the system's independent variable. @@ -28,7 +29,7 @@ end # Extract a string which declares the system's spatial independent variables. function get_sivs_string(rn::ReactionSystem) - return "sivs = @variables$(wrapped_nums_2_string(rn.sivs))" + return "spatial_ivs = @variables$(get_species_string(rn.sivs))" end # Creates an annotation for the system's spatial independent variables. @@ -49,7 +50,7 @@ end # Extract a string which declares the system's species. function get_species_string(rn::ReactionSystem) - return "sps = @species$(wrapped_nums_2_string(species(rn)))" + return "sps = @species$(syms_2_declaration_string(species(rn)))" end # Creates an annotation for the system's species. @@ -71,7 +72,7 @@ end # Extract a string which declares the system's variables. function get_variables_string(rn::ReactionSystem) variables = filter(!isspecies, unknowns(rn)) - return "vars = @variables$(wrapped_nums_2_string(variables))" + return "vars = @variables$(syms_2_declaration_string(variables))" end # Creates an annotation for the system's . @@ -92,7 +93,7 @@ end # Extract a string which declares the system's parameters. function get_parameters_string(rn::ReactionSystem) - return "ps = @parameters$(wrapped_nums_2_string(parameters(rn)))" + return "ps = @parameters$(syms_2_declaration_string(parameters(rn)))" end # Creates an annotation for the system's parameters. @@ -113,31 +114,49 @@ end # Extract a string which declares the system's reactions. function get_reactions_string(rn::ReactionSystem) - uncall_dict = make_uncall_dict(unknowns(rn)) + # Creates a dictionary for converting symbolics to their call-stripped form (e.g. X(t) to X). + strip_call_dict = make_strip_call_dict(unknowns(rn)) + + # Handles the case with zero and one reaction separately. Only effect is nicer formatting. + (length(reactions(rn)) == 0) && (return "rxs = []") + (length(reactions(rn)) == 1) && (return "rxs = [$(reaction_string(rx, strip_call_dict))]") + + # Creates the string corresponding to the code which generates the system's reactions. rxs_string = "rxs = [" for rx in reactions(rn) - rxs_string = rxs_string * "\n\t" * reaction_string(rx, uncall_dict) * "," + @string_append! rxs_string "\n\t" * reaction_string(rx, strip_call_dict) "," end - # Updates the string (including removing the last `,`) and returns in. + # Updates the string (including removing the last `,`) and returns it. return rxs_string[1:end-1] * "\n]" end - # Creates a string that corresponds to the declaration of a single `Reaction`. -function reaction_string(rx::Reaction, uncall_dict) +function reaction_string(rx::Reaction, strip_call_dict) # Prepares the `Reaction` declaration components. - rate = num_2_string(rx.rate; uncall_dict) - substrates = isempty(rx.substrates) ? "nothing" : nums_2_uncalled_strin_vec(rx.substrates) - products = isempty(rx.products) ? "nothing" : nums_2_uncalled_strin_vec(rx.products) - substoich = isempty(rx.substoich) ? "nothing" : nums_2_uncalled_strin_vec(rx.substoich) - prodstoich = isempty(rx.prodstoich) ? "nothing" : nums_2_uncalled_strin_vec(rx.prodstoich) + rate = expression_2_string(rx.rate; strip_call_dict) + substrates = isempty(rx.substrates) ? "nothing" : x_2_string(rx.substrates) + products = isempty(rx.products) ? "nothing" : x_2_string(rx.products) + substoich = isempty(rx.substoich) ? "nothing" : x_2_string(rx.substoich) + prodstoich = isempty(rx.prodstoich) ? "nothing" : x_2_string(rx.prodstoich) # Creates the full expression, including adding kwargs (`only_use_rate` and `metadata`). rx_string = "Reaction($rate, $(substrates), $(products), $(substoich), $(prodstoich)" if rx.only_use_rate - rx_string = rx_string * "; only_use_rate = true" + @string_append! rx_string "; only_use_rate = true" + isempty(getmetadata_dict(rx)) || (rx_string = rx_string * ", ") + end + if !isempty(getmetadata_dict(rx)) + rx.only_use_rate || (@string_append! rx_string "; ") + @string_append! rx_string "metadata = [" + for entry in getmetadata_dict(rx) + metadata_entry = "$(x_2_string(entry)), " + @string_append! rx_string metadata_entry + end + rx_string = rx_string[1:end-2] * "]" end + + # Returns the Reaction string. return rx_string * ")" end @@ -168,4 +187,109 @@ function get_equations_annotation(rn::ReactionSystem) end # Combines the 3 equations-related functions in a constant tuple. -EQUATIONS_FS = (has_equations, get_equations_string, get_equations_annotation) \ No newline at end of file +EQUATIONS_FS = (has_equations, get_equations_string, get_equations_annotation) + + +### Handles Observables ### + +# Checks if the reaction system have any observables. +function has_observed(rn::ReactionSystem) + return false +end + +# Extract a string which declares the system's observables. +function get_observed_string(rn::ReactionSystem) + get_unsupported_comp_string("observables") +end + +# Creates an annotation for the system's observables. +function get_observed_annotation(rn::ReactionSystem) + get_unsupported_comp_annotation("Observables:") +end + +# Combines the 3 -related functions in a constant tuple. +OBSERVED_FS = (has_observed, get_observed_string, get_observed_annotation) + + +### Handles Continuous Events ### + +# Checks if the reaction system have any continuous events. +function has_continuous_events(rn::ReactionSystem) + return false +end + +# Extract a string which declares the system's continuous events. +function get_continuous_events_string(rn::ReactionSystem) + get_unsupported_comp_string("continuous events") +end + +# Creates an annotation for the system's continuous events. +function get_continuous_events_annotation(rn::ReactionSystem) + get_unsupported_comp_annotation("Continuous events:") +end + +# Combines the 3 -related functions in a constant tuple. +CONTINUOUS_EVENTS_FS = (has_continuous_events, get_continuous_events_string, get_continuous_events_annotation) + + +### Handles Discrete Events ### + +# Checks if the reaction system have any discrete events. +function has_discrete_events(rn::ReactionSystem) + return false +end + +# Extract a string which declares the system's discrete events. +function get_discrete_events_string(rn::ReactionSystem) + get_unsupported_comp_string("discrete events") +end + +# Creates an annotation for the system's discrete events. +function get_discrete_events_annotation(rn::ReactionSystem) + get_unsupported_comp_annotation("Discrete events:") +end + +# Combines the 3 -related functions in a constant tuple. +DISCRETE_EVENTS_FS = (has_discrete_events, get_discrete_events_string, get_discrete_events_annotation) + + +### Handles Systems ### + +# Checks if the reaction system have any systems. +function has_systems(rn::ReactionSystem) + return false +end + +# Extract a string which declares the system's systems. +function get_systems_string(rn::ReactionSystem) + get_unsupported_comp_string("systems") +end + +# Creates an annotation for the system's systems. +function get_systems_annotation(rn::ReactionSystem) + get_unsupported_comp_annotation("Systems:") +end + +# Combines the 3 systems-related functions in a constant tuple. +SYSTEMS_FS = (has_systems, get_systems_string, get_systems_annotation) + + +### Handles Connection Types ### + +# Checks if the reaction system have any connection types. +function has_connection_type(rn::ReactionSystem) + return false +end + +# Extract a string which declares the system's connection types. +function get_connection_type_string(rn::ReactionSystem) + get_unsupported_comp_string("connection types") +end + +# Creates an annotation for the system's connection types. +function get_connection_type_annotation(rn::ReactionSystem) + get_unsupported_comp_annotation("Connection types:") +end + +# Combines the 3 connection types-related functions in a constant tuple. +CONNECTION_TYPE_FS = (has_connection_type, get_connection_type_string, get_connection_type_annotation) \ No newline at end of file diff --git a/src/model_serialisation/serialise_reactionsystem.jl b/src/model_serialisation/serialise_reactionsystem.jl index b42d34bbe5..572776dd86 100644 --- a/src/model_serialisation/serialise_reactionsystem.jl +++ b/src/model_serialisation/serialise_reactionsystem.jl @@ -1,22 +1,44 @@ +""" + save_reaction_network(filename::String, rn::ReactionSystem; annotate = true) + +Save a `ReactionSystem` model to a file. + +Work in progress, currently missing features: +- Problems with ordering of declarations of species/variables/parameters that have defaults that are other species/variables/parameters. +- Saving of the `sivs` field has not been fully implemented. +- Saving of the `observed` field has not been fully implemented. +- Saving of the `continuous_events` field has not been fully implemented. +- Saving of the `discrete_events` field has not been fully implemented. +- Saving of the `systems` field has not been fully implemented. +- Saving of the `connection_type` field has not been fully implemented. +""" function save_reaction_network(filename::String, rn::ReactionSystem; annotate = true) # Initiates the file string. file_text = "" # Goes through each type of system component, potentially adding it to the string. - file_text, has_iv = push_component(file_text, rn, annotate, IV_FS) - file_text, has_sivs = push_component(file_text, rn, annotate, SIVS_FS) - file_text, has_species = push_component(file_text, rn, annotate, SPECIES_FS) - file_text, has_variables = push_component(file_text, rn, annotate, VARIABLES_FS) - file_text, has_parameters = push_component(file_text, rn, annotate, PARAMETERS_FS) - file_text, has_reactions = push_component(file_text, rn, annotate, REACTIONS_FS) - file_text, has_equations = push_component(file_text, rn, annotate, EQUATIONS_FS) - + file_text, _ = push_field(file_text, rn, annotate, IV_FS) + file_text, has_sivs = push_field(file_text, rn, annotate, SIVS_FS) + file_text, has_species = push_field(file_text, rn, annotate, SPECIES_FS) + file_text, has_variables = push_field(file_text, rn, annotate, VARIABLES_FS) + file_text, has_parameters = push_field(file_text, rn, annotate, PARAMETERS_FS) + file_text, has_reactions = push_field(file_text, rn, annotate, REACTIONS_FS) + file_text, has_equations = push_field(file_text, rn, annotate, EQUATIONS_FS) + file_text, has_observed = push_field(file_text, rn, annotate, OBSERVED_FS) + file_text, has_continuous_events = push_field(file_text, rn, annotate, CONTINUOUS_EVENTS_FS) + file_text, has_discrete_events = push_field(file_text, rn, annotate, DISCRETE_EVENTS_FS) + file_text, has_systems = push_field(file_text, rn, annotate, SYSTEMS_FS) + file_text, has_connection_type = push_field(file_text, rn, annotate, CONNECTION_TYPE_FS) + # Finalises the system. Creates the final `ReactionSystem` call. rs_creation_code = make_reaction_system_call(rn, file_text, annotate, - has_sivs, has_species, has_variables, - has_parameters, has_reactions, has_equations) - annotate || (file_text = "\n" * file_text) - file_text = "let" * file_text * "\n\n" * rs_creation_code * "\n\nend" + has_sivs, has_species, has_variables, has_parameters, + has_reactions, has_equations, has_observed, + has_discrete_events, has_continuous_events, + has_systems, has_connection_type) + annotate || (@string_prepend! "\n" file_text) + @string_prepend! "let" file_text + @string_append! file_text "\n\n" rs_creation_code "\n\nend" # Writes the model to a file. Then, returns nothing. open(filename, "w") do file @@ -25,11 +47,17 @@ function save_reaction_network(filename::String, rn::ReactionSystem; annotate = return nothing end -# Takes the actual text which creates the model, and wraps it in a `let ... end` statement and A -# ReactionSystem call, to create the final file text. -function make_reaction_system_call(rs::ReactionSystem, file_text, annotate, has_sivs, has_species, has_variables, has_parameters, has_reactions, has_equations) - # Creates base call. - iv = Catalyst.get_iv(rs) +# Takes the actual text which creates the model, and wraps it in a `let ... end` statement and a +# ReactionSystem call. This creates the finalised text that is written to a file. +function make_reaction_system_call(rs::ReactionSystem, file_text, annotate, has_sivs, has_species, + has_variables, has_parameters, has_reactions, has_equations, + has_observed, has_continuous_events, has_discrete_events, + has_systems, has_connection_type) + + # Gets the independent variable input. + iv = x_2_string(get_iv(rs)) + + # Gets the equations (reactions + equations) input. if has_reactions && has_equations eqs = "[rxs; eqs]" elseif has_reactions @@ -39,6 +67,8 @@ function make_reaction_system_call(rs::ReactionSystem, file_text, annotate, has_ else eqs = "[]" end + + # Gets the unknowns (species + variables) input. if has_species && has_variables unknowns = "[sps; vars]" elseif has_species @@ -48,28 +78,46 @@ function make_reaction_system_call(rs::ReactionSystem, file_text, annotate, has_ else unknowns = "[]" end + + # Gets the parameters input. if has_parameters ps = "ps" else ps = "[]" end + + # Initiates the ReactionSystem call with the mandatory inputs. reaction_system_string = "ReactionSystem($eqs, $iv, $unknowns, $ps" - # Appends additional (optional) arguments. + # Appends the reaction system name. Also initiates the optional argument part of the call. if Base.isidentifier(Catalyst.getname(rs)) rs_name = ":$(Catalyst.getname(rs))" else rs_name = "Symbol(\"$(Catalyst.getname(rs))\")" end - reaction_system_string = reaction_system_string * "; name = $(rs_name)" - reaction_system_string = reaction_system_string * ")" + @string_append! reaction_system_string "; name = $(rs_name)" + + # Goes through various fields that might exists, and if so, adds them to the string. + has_sivs && (@string_append! reaction_system_string ", spatial_ivs") + has_observed && (@string_append! reaction_system_string ", observed") + has_continuous_events && (@string_append! reaction_system_string ", continuous_events") + has_discrete_events && (@string_append! reaction_system_string ", discrete_events") + has_systems && (@string_append! reaction_system_string ", systems") + has_connection_type && (@string_append! reaction_system_string ", connection_type") + + # Potentially appends a combinatorial combinatoric_ratelaws statement. + Symbolics.unwrap(rs.combinatoric_ratelaws) || (@string_append! reaction_system_string ", combinatoric_ratelaws = false") + + # Potentially appends `ReactionSystem` metadata value(s). Weird composite types are not supported. + isnothing(rs.metadata) || (@string_append! reaction_system_string ", metadata = $(x_2_string(rs.metadata))") - # Returns the full call. + # Finalises the call. Appends potential annotation. If the system is complete, add a call for this. + @string_append! reaction_system_string ")" if !ModelingToolkit.iscomplete(rs) - reaction_system_string = "rs = $(reaction_system_string)\ncomplete(rs)" + @string_append! reaction_system_string "rs = $(reaction_system_string)\ncomplete(rs)" end if annotate - reaction_system_string = "# Declares ReactionSystem model:\n" * reaction_system_string + @string_prepend! "# Declares ReactionSystem model:\n" reaction_system_string end return reaction_system_string end \ No newline at end of file From 501727dd8ed8de353e8da8eef154acd8be429f18 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 14 Apr 2024 16:59:16 -0400 Subject: [PATCH 37/67] add continiuous and discrete events --- src/model_serialisation/serialise_fields.jl | 111 ++++++++++++++++-- .../serialise_reactionsystem.jl | 27 ++--- 2 files changed, 111 insertions(+), 27 deletions(-) diff --git a/src/model_serialisation/serialise_fields.jl b/src/model_serialisation/serialise_fields.jl index c108dcf117..afb2671a26 100644 --- a/src/model_serialisation/serialise_fields.jl +++ b/src/model_serialisation/serialise_fields.jl @@ -24,12 +24,12 @@ IV_FS = (has_iv, get_iv_string, get_iv_annotation) # Checks if the reaction system have any spatial independent variables. function has_sivs(rn::ReactionSystem) - return length(rn.sivs) != 0 + return !isempty(get_sivs(rn)) end # Extract a string which declares the system's spatial independent variables. function get_sivs_string(rn::ReactionSystem) - return "spatial_ivs = @variables$(get_species_string(rn.sivs))" + return "spatial_ivs = @variables$(get_species_string(get_sivs(rn)))" end # Creates an annotation for the system's spatial independent variables. @@ -117,8 +117,7 @@ function get_reactions_string(rn::ReactionSystem) # Creates a dictionary for converting symbolics to their call-stripped form (e.g. X(t) to X). strip_call_dict = make_strip_call_dict(unknowns(rn)) - # Handles the case with zero and one reaction separately. Only effect is nicer formatting. - (length(reactions(rn)) == 0) && (return "rxs = []") + # Handles the case with one reaction separately. Only effect is nicer formatting. (length(reactions(rn)) == 1) && (return "rxs = [$(reaction_string(rx, strip_call_dict))]") # Creates the string corresponding to the code which generates the system's reactions. @@ -178,12 +177,27 @@ end # Extract a string which declares the system's equations. function get_equations_string(rn::ReactionSystem) - get_unsupported_comp_string("equations") + # Creates a dictionary for converting symbolics to their call-stripped form (e.g. X(t) to X). + strip_call_dict = make_strip_call_dict(unknowns(rn)) + + # Handles the case with one equation separately. Only effect is nicer formatting. + if length(equations(rn)) - length(reactions(rn)) == 1 + return "eqs = [$(expression_2_string(equations(rn)[end]; strip_call_dict))]" + end + + # Creates the string corresponding to the code which generates the system's reactions. + eqs_string = "rxs = [" + for eq in reactions(rn)[length(reactions(rn)) + 1:end] + @string_append! eqs_string "\n\t" expression_2_string(eq; strip_call_dict) "," + end + + # Updates the string (including removing the last `,`) and returns it. + return eqs_string[1:end-1] * "\n]" end # Creates an annotation for the system's equations. function get_equations_annotation(rn::ReactionSystem) - get_unsupported_comp_annotation("Equations") + return "Equations:" end # Combines the 3 equations-related functions in a constant tuple. @@ -204,7 +218,7 @@ end # Creates an annotation for the system's observables. function get_observed_annotation(rn::ReactionSystem) - get_unsupported_comp_annotation("Observables:") + return "Observables:" end # Combines the 3 -related functions in a constant tuple. @@ -215,17 +229,53 @@ OBSERVED_FS = (has_observed, get_observed_string, get_observed_annotation) # Checks if the reaction system have any continuous events. function has_continuous_events(rn::ReactionSystem) - return false + return length(rn.continuous_events) > 0 end # Extract a string which declares the system's continuous events. function get_continuous_events_string(rn::ReactionSystem) - get_unsupported_comp_string("continuous events") + # Creates a dictionary for converting symbolics to their call-stripped form (e.g. X(t) to X). + strip_call_dict = make_strip_call_dict(unknowns(rn)) + + # Handles the case with one event separately. Only effect is nicer formatting. + if length(rn.continuous_events) == 1 + return "continuous_events = [$(continuous_event_string(rn.continuous_events.value[1], strip_call_dict))]" + end + + # Creates the string corresponding to the code which generates the system's reactions. + continuous_events_string = "continuous_events = [" + for continuous_event in rn.continuous_events.value + @string_append! continuous_events_string "\n\t" continuous_event_string(continuous_event, strip_call_dict) "," + end + + # Updates the string (including removing the last `,`) and returns it. + return continuous_events_string[1:end-1] * "\n]" +end + +# Creates a string that corresponds to the declaration of a single continuous event. +function continuous_event_string(continuous_event, strip_call_dict) + # Creates the string corresponding to the equations (i.e. conditions). + eqs_string = "[" + for eq in continuous_event.eqs + @string_append! eqs_string expression_2_string(eq; strip_call_dict) ", " + end + eqs_string = eqs_string[1:end-2] * "]" + + # Creates the string corresponding to the affects. + # Continuous events' `affect` field should probably be called `affects`. Likely the `s` was + # dropped by mistake in MTK. + affects_string = "[" + for affect in continuous_event.affect + @string_append! affects_string expression_2_string(affect; strip_call_dict) ", " + end + affects_string = affects_string[1:end-2] * "]" + + return eqs_string * " => " * affects_string end # Creates an annotation for the system's continuous events. function get_continuous_events_annotation(rn::ReactionSystem) - get_unsupported_comp_annotation("Continuous events:") + return "Continuous events:" end # Combines the 3 -related functions in a constant tuple. @@ -236,17 +286,52 @@ CONTINUOUS_EVENTS_FS = (has_continuous_events, get_continuous_events_string, get # Checks if the reaction system have any discrete events. function has_discrete_events(rn::ReactionSystem) - return false + return length(rn.discrete_events) > 0 end # Extract a string which declares the system's discrete events. function get_discrete_events_string(rn::ReactionSystem) - get_unsupported_comp_string("discrete events") + # Creates a dictionary for converting symbolics to their call-stripped form (e.g. X(t) to X). + strip_call_dict = make_strip_call_dict(unknowns(rn)) + + # Handles the case with one event separately. Only effect is nicer formatting. + if length(rn.discrete_events) == 1 + return "discrete_events = [$(discrete_event_string(rn.discrete_events.value[1], strip_call_dict))]" + end + + # Creates the string corresponding to the code which generates the system's reactions. + discrete_events_string = "discrete_events = [" + for discrete_event in rn.discrete_events.value + @string_append! discrete_events_string "\n\t" discrete_event_string(discrete_event, strip_call_dict) "," + end + + # Updates the string (including removing the last `,`) and returns it. + return discrete_events_string[1:end-1] * "\n]" +end + +# Creates a string that corresponds to the declaration of a single discrete event. +function discrete_event_string(discrete_event, strip_call_dict) + # Creates the string corresponding to the conditions. The special check is if the condition is + # an expression like `X > 5.0`. Here, "(...)" is added for purely aesthetic reasons. + condition_string = x_2_string(discrete_event.condition) + if discrete_event.condition isa SymbolicUtils.BasicSymbolic + @string_prepend! "(" condition_string + @string_append! condition_string ")" + end + + # Creates the string corresponding to the affects. + affects_string = "[" + for affect in discrete_event.affects + @string_append! affects_string expression_2_string(affect; strip_call_dict) ", " + end + affects_string = affects_string[1:end-2] * "]" + + return condition_string * " => " * affects_string end # Creates an annotation for the system's discrete events. function get_discrete_events_annotation(rn::ReactionSystem) - get_unsupported_comp_annotation("Discrete events:") + return "Discrete events:" end # Combines the 3 -related functions in a constant tuple. diff --git a/src/model_serialisation/serialise_reactionsystem.jl b/src/model_serialisation/serialise_reactionsystem.jl index 572776dd86..84443ef48c 100644 --- a/src/model_serialisation/serialise_reactionsystem.jl +++ b/src/model_serialisation/serialise_reactionsystem.jl @@ -5,12 +5,10 @@ Save a `ReactionSystem` model to a file. Work in progress, currently missing features: - Problems with ordering of declarations of species/variables/parameters that have defaults that are other species/variables/parameters. -- Saving of the `sivs` field has not been fully implemented. - Saving of the `observed` field has not been fully implemented. -- Saving of the `continuous_events` field has not been fully implemented. -- Saving of the `discrete_events` field has not been fully implemented. - Saving of the `systems` field has not been fully implemented. - Saving of the `connection_type` field has not been fully implemented. +- equations and reactions are used instead of get_rxs and get_eqs. """ function save_reaction_network(filename::String, rn::ReactionSystem; annotate = true) # Initiates the file string. @@ -31,11 +29,11 @@ function save_reaction_network(filename::String, rn::ReactionSystem; annotate = file_text, has_connection_type = push_field(file_text, rn, annotate, CONNECTION_TYPE_FS) # Finalises the system. Creates the final `ReactionSystem` call. - rs_creation_code = make_reaction_system_call(rn, file_text, annotate, - has_sivs, has_species, has_variables, has_parameters, - has_reactions, has_equations, has_observed, - has_discrete_events, has_continuous_events, - has_systems, has_connection_type) + # Enclose everything ing a `let ... end` block. + rs_creation_code = make_reaction_system_call(rn, annotate, has_sivs, has_species, has_variables, + has_parameters, has_reactions, has_equations, + has_observed, has_continuous_events, + has_discrete_events, has_systems, has_connection_type) annotate || (@string_prepend! "\n" file_text) @string_prepend! "let" file_text @string_append! file_text "\n\n" rs_creation_code "\n\nend" @@ -47,9 +45,9 @@ function save_reaction_network(filename::String, rn::ReactionSystem; annotate = return nothing end -# Takes the actual text which creates the model, and wraps it in a `let ... end` statement and a -# ReactionSystem call. This creates the finalised text that is written to a file. -function make_reaction_system_call(rs::ReactionSystem, file_text, annotate, has_sivs, has_species, +# Creates a ReactionSystem call for creating the model. Adds all the correct inputs to it. The input +# `has_` `Bool`s described which inputs are used. If model is `complete`, this is handled here. +function make_reaction_system_call(rs::ReactionSystem, annotate, has_sivs, has_species, has_variables, has_parameters, has_reactions, has_equations, has_observed, has_continuous_events, has_discrete_events, has_systems, has_connection_type) @@ -105,7 +103,7 @@ function make_reaction_system_call(rs::ReactionSystem, file_text, annotate, has_ has_systems && (@string_append! reaction_system_string ", systems") has_connection_type && (@string_append! reaction_system_string ", connection_type") - # Potentially appends a combinatorial combinatoric_ratelaws statement. + # Potentially appends a combinatoric_ratelaws statement. Symbolics.unwrap(rs.combinatoric_ratelaws) || (@string_append! reaction_system_string ", combinatoric_ratelaws = false") # Potentially appends `ReactionSystem` metadata value(s). Weird composite types are not supported. @@ -113,8 +111,9 @@ function make_reaction_system_call(rs::ReactionSystem, file_text, annotate, has_ # Finalises the call. Appends potential annotation. If the system is complete, add a call for this. @string_append! reaction_system_string ")" - if !ModelingToolkit.iscomplete(rs) - @string_append! reaction_system_string "rs = $(reaction_system_string)\ncomplete(rs)" + if ModelingToolkit.iscomplete(rs) + @string_prepend! "rs = " reaction_system_string + @string_append! reaction_system_string "\nrs = complete(rs)" end if annotate @string_prepend! "# Declares ReactionSystem model:\n" reaction_system_string From 03bbfe327f24f45223a2669a49e567944ee37d43 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 14 Apr 2024 17:08:15 -0400 Subject: [PATCH 38/67] fixes for hierarchial systems --- .../serialisation_support.jl | 3 ++ src/model_serialisation/serialise_fields.jl | 32 +++++++++---------- .../serialise_reactionsystem.jl | 1 - 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/model_serialisation/serialisation_support.jl b/src/model_serialisation/serialisation_support.jl index 29f0aa33a8..dfe92a82b0 100644 --- a/src/model_serialisation/serialisation_support.jl +++ b/src/model_serialisation/serialisation_support.jl @@ -195,4 +195,7 @@ end # For an vector of symbolics, creates a dictionary taking each symbolics to each call-stripped form. function make_strip_call_dict(syms) return Dict([sym => strip_call(Symbolics.unwrap(sym)) for sym in syms]) +end +function make_strip_call_dict(rn::ReactionSystem) + return make_strip_call_dict(get_unknowns(rn)) end \ No newline at end of file diff --git a/src/model_serialisation/serialise_fields.jl b/src/model_serialisation/serialise_fields.jl index afb2671a26..a4f696ae2e 100644 --- a/src/model_serialisation/serialise_fields.jl +++ b/src/model_serialisation/serialise_fields.jl @@ -45,12 +45,12 @@ SIVS_FS = (has_sivs, get_sivs_string, get_sivs_annotation) # Checks if the reaction system have any species. function has_species(rn::ReactionSystem) - return length(species(rn)) != 0 + return !isempty(get_species(rn)) end # Extract a string which declares the system's species. function get_species_string(rn::ReactionSystem) - return "sps = @species$(syms_2_declaration_string(species(rn)))" + return "sps = @species$(syms_2_declaration_string(get_species(rn)))" end # Creates an annotation for the system's species. @@ -66,12 +66,12 @@ SPECIES_FS = (has_species, get_species_string, get_species_annotation) # Checks if the reaction system have any variables. function has_variables(rn::ReactionSystem) - return length(unknowns(rn)) > length(species(rn)) + return length(get_unknowns(rn)) > length(get_species(rn)) end # Extract a string which declares the system's variables. function get_variables_string(rn::ReactionSystem) - variables = filter(!isspecies, unknowns(rn)) + variables = filter(!isspecies, get_unknowns(rn)) return "vars = @variables$(syms_2_declaration_string(variables))" end @@ -88,12 +88,12 @@ VARIABLES_FS = (has_variables, get_variables_string, get_variables_annotation) # Checks if the reaction system have any parameters. function has_parameters(rn::ReactionSystem) - return length(parameters(rn)) != 0 + return length(get_ps(rn)) != 0 end # Extract a string which declares the system's parameters. function get_parameters_string(rn::ReactionSystem) - return "ps = @parameters$(syms_2_declaration_string(parameters(rn)))" + return "ps = @parameters$(syms_2_declaration_string(get_ps(rn)))" end # Creates an annotation for the system's parameters. @@ -115,14 +115,14 @@ end # Extract a string which declares the system's reactions. function get_reactions_string(rn::ReactionSystem) # Creates a dictionary for converting symbolics to their call-stripped form (e.g. X(t) to X). - strip_call_dict = make_strip_call_dict(unknowns(rn)) + strip_call_dict = make_strip_call_dict(rn) # Handles the case with one reaction separately. Only effect is nicer formatting. - (length(reactions(rn)) == 1) && (return "rxs = [$(reaction_string(rx, strip_call_dict))]") + (length(get_rxs(rn)) == 1) && (return "rxs = [$(reaction_string(rx, strip_call_dict))]") # Creates the string corresponding to the code which generates the system's reactions. rxs_string = "rxs = [" - for rx in reactions(rn) + for rx in get_rxs(rn) @string_append! rxs_string "\n\t" * reaction_string(rx, strip_call_dict) "," end @@ -172,22 +172,22 @@ REACTIONS_FS = (has_reactions, get_reactions_string, get_reactions_annotation) # Checks if the reaction system have any equations. function has_equations(rn::ReactionSystem) - return length(equations(rn)) > length(reactions(rn)) + return length(get_eqs(rn)) > length(get_rxs(rn)) end # Extract a string which declares the system's equations. function get_equations_string(rn::ReactionSystem) # Creates a dictionary for converting symbolics to their call-stripped form (e.g. X(t) to X). - strip_call_dict = make_strip_call_dict(unknowns(rn)) + strip_call_dict = make_strip_call_dict(rn) # Handles the case with one equation separately. Only effect is nicer formatting. - if length(equations(rn)) - length(reactions(rn)) == 1 - return "eqs = [$(expression_2_string(equations(rn)[end]; strip_call_dict))]" + if length(get_eqs(rn)) - length(get_rxs(rn)) == 1 + return "eqs = [$(expression_2_string(get_eqs(rn)[end]; strip_call_dict))]" end # Creates the string corresponding to the code which generates the system's reactions. eqs_string = "rxs = [" - for eq in reactions(rn)[length(reactions(rn)) + 1:end] + for eq in get_eqs(rn)[length(get_rxs(rn)) + 1:end] @string_append! eqs_string "\n\t" expression_2_string(eq; strip_call_dict) "," end @@ -235,7 +235,7 @@ end # Extract a string which declares the system's continuous events. function get_continuous_events_string(rn::ReactionSystem) # Creates a dictionary for converting symbolics to their call-stripped form (e.g. X(t) to X). - strip_call_dict = make_strip_call_dict(unknowns(rn)) + strip_call_dict = make_strip_call_dict(rn) # Handles the case with one event separately. Only effect is nicer formatting. if length(rn.continuous_events) == 1 @@ -292,7 +292,7 @@ end # Extract a string which declares the system's discrete events. function get_discrete_events_string(rn::ReactionSystem) # Creates a dictionary for converting symbolics to their call-stripped form (e.g. X(t) to X). - strip_call_dict = make_strip_call_dict(unknowns(rn)) + strip_call_dict = make_strip_call_dict(rn) # Handles the case with one event separately. Only effect is nicer formatting. if length(rn.discrete_events) == 1 diff --git a/src/model_serialisation/serialise_reactionsystem.jl b/src/model_serialisation/serialise_reactionsystem.jl index 84443ef48c..36b574b977 100644 --- a/src/model_serialisation/serialise_reactionsystem.jl +++ b/src/model_serialisation/serialise_reactionsystem.jl @@ -8,7 +8,6 @@ Work in progress, currently missing features: - Saving of the `observed` field has not been fully implemented. - Saving of the `systems` field has not been fully implemented. - Saving of the `connection_type` field has not been fully implemented. -- equations and reactions are used instead of get_rxs and get_eqs. """ function save_reaction_network(filename::String, rn::ReactionSystem; annotate = true) # Initiates the file string. From 713c1519aca1e496faa06e9e3a39cc83dd1007ed Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 14 Apr 2024 21:15:24 -0400 Subject: [PATCH 39/67] Support hierarchial systems, some fixes, start adding tests --- src/Catalyst.jl | 6 +- .../serialisation_support.jl | 57 ++++- .../serialise_fields.jl | 228 +++++++++++++++--- .../serialise_reactionsystem.jl | 60 +++-- .../reactionsystem_serialisation.jl | 92 +++++++ 5 files changed, 386 insertions(+), 57 deletions(-) rename src/{model_serialisation => reactionsystem_serialisation}/serialisation_support.jl (79%) rename src/{model_serialisation => reactionsystem_serialisation}/serialise_fields.jl (56%) rename src/{model_serialisation => reactionsystem_serialisation}/serialise_reactionsystem.jl (72%) create mode 100644 test/miscellaneous_tests/reactionsystem_serialisation.jl diff --git a/src/Catalyst.jl b/src/Catalyst.jl index 7adaf341b3..ea1a9f2c23 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -185,9 +185,9 @@ include("spatial_reaction_systems/lattice_jump_systems.jl") ### ReactionSystem Serialisation ### # Has to be at the end (because it uses records of all metadata declared by Catalyst). -include("model_serialisation/serialisation_support.jl") -include("model_serialisation/serialise_fields.jl") -include("model_serialisation/serialise_reactionsystem.jl") +include("reactionsystem_serialisation/serialisation_support.jl") +include("reactionsystem_serialisation/serialise_fields.jl") +include("reactionsystem_serialisation/serialise_reactionsystem.jl") export save_reaction_network end # module diff --git a/src/model_serialisation/serialisation_support.jl b/src/reactionsystem_serialisation/serialisation_support.jl similarity index 79% rename from src/model_serialisation/serialisation_support.jl rename to src/reactionsystem_serialisation/serialisation_support.jl index dfe92a82b0..7c4d08b6ac 100644 --- a/src/model_serialisation/serialisation_support.jl +++ b/src/reactionsystem_serialisation/serialisation_support.jl @@ -65,7 +65,7 @@ end # the code required to declare them (potential @parameters or @species commands must still be added). # The `multiline_format` option formats it with a `begin ... end` block and declarations on separate lines. function syms_2_declaration_string(syms; multiline_format = false) - decs_string = (multiline_format ? "begin" : "") + decs_string = (multiline_format ? " begin" : "") for sym in syms delimiter = (multiline_format ? "\n\t" : " ") @string_append! decs_string delimiter sym_2_declaration_string(sym; multiline_format) @@ -81,6 +81,17 @@ function sym_2_declaration_string(sym; multiline_format = false) # Creates the basic symbol. The `"$(sym)"` ensures that we get e.g. "X(t)" and not "X". dec_string = "$(sym)" + # If the symbol have a non-default type, appends the declaration of this. + # Assumes that the type is on the form `SymbolicUtils.BasicSymbolic{X}`. Contain error checks + # to ensure that this is the case. + if !(sym isa SymbolicUtils.BasicSymbolic{Real}) + sym_type = String(Symbol(typeof(Symbolics.unwrap(k2)))) + if (sym_type[1:28] != "SymbolicUtils.BasicSymbolic{") || (sym_type[end] != '}') + error("Encountered symbolic of unexpected type: $sym_type.") + end + @string_append! dec_string "::" sym_type[29:end-1] + end + # If there is a default value, adds this to the declaration. if ModelingToolkit.hasdefault(sym) def_val = x_2_string(ModelingToolkit.getdefault(sym)) @@ -198,4 +209,48 @@ function make_strip_call_dict(syms) end function make_strip_call_dict(rn::ReactionSystem) return make_strip_call_dict(get_unknowns(rn)) +end + + +### Handle Parameters/Species/Variables Declaration Dependencies ### + +# Gets a vector with the symbolics a symbolic depends on (currently only considers defaults). +function get_dep_syms(sym) + ModelingToolkit.hasdefault(sym) || return [] + return Symbolics.get_variables(ModelingToolkit.getdefault(sym)) +end + +# Checks if a symbolic depends on an symbolics in a vector being declared. +# Because Symbolics has to utilise `isequal`, the `isdisjoint` function cannot be used. +function depends_on(sym, syms) + dep_syms = get_dep_syms(sym) + for s1 in dep_syms + for s2 in syms + isequal(s1, s2) && return true + end + end + return false +end + +# For a set of remaining parameters/species/variables (remaining_syms), return this split into +# two sets: +# One with those that does not depend on any sym in `all_remaining_syms`. +# One with those that does depend on at least one sym in `all_remaining_syms`. +function dependency_split(all_remaining_syms, remaining_syms) + writable_syms = filter(sym -> !depends_on(sym, all_remaining_syms), remaining_syms) + nonwritable_syms = filter(sym -> depends_on(sym, all_remaining_syms), remaining_syms) + return writable_syms, nonwritable_syms +end + + +### Other Functions ### + + +# Checks if a symbolic's declaration is "complicated". The declaration is considered complicated +# if it have metadata, default value, or type designation that must be declared. +function complicated_declaration(sym) + isempty(get_metadata_to_declare(sym)) || (return true) + ModelingToolkit.hasdefault(sym) && (return true) + (sym isa SymbolicUtils.BasicSymbolic{Real}) || (return true) + return false end \ No newline at end of file diff --git a/src/model_serialisation/serialise_fields.jl b/src/reactionsystem_serialisation/serialise_fields.jl similarity index 56% rename from src/model_serialisation/serialise_fields.jl rename to src/reactionsystem_serialisation/serialise_fields.jl index a4f696ae2e..1b31c7e879 100644 --- a/src/model_serialisation/serialise_fields.jl +++ b/src/reactionsystem_serialisation/serialise_fields.jl @@ -41,16 +41,130 @@ end SIVS_FS = (has_sivs, get_sivs_string, get_sivs_annotation) +### Handles Species, Variables, and Parameters ### + +# Function which handles the addition of species, variable, and parameter declarations to the file +# text. These must be handled as a unity in case there are default value dependencies between these. +function handle_us_n_ps(file_text::String, rn::ReactionSystem, annotate::Bool) + # Fetches the systems parameters, species, and variables. Computes the `has_` `Bool`s. + ps_all = get_ps(rn) + sps_all = get_species(rn) + vars_all = filter(!isspecies, get_unknowns(rn)) + has_ps = has_parameters(rn) + has_sps = has_species(rn) + has_vars = has_variables(rn) + + # Checks which sets have dependencies which requires managing. + p_deps = any(depends_on(p, [ps_all; sps_all; vars_all]) for p in ps_all) + sp_deps = any(depends_on(sp, [sps_all; vars_all]) for sp in sps_all) + var_deps = any(depends_on(var, vars_all) for var in vars_all) + + # Makes the initial declaration. + if !p_deps && has_ps + annotate && (@string_append! file_text "\n\n# " get_parameters_annotation(rn)) + @string_append! file_text "\nps = " get_parameters_string(ps_all) + end + if !sp_deps && has_sps + annotate && (@string_append! file_text "\n\n# " get_species_annotation(rn)) + @string_append! file_text "\nsps = " get_species_string(sps_all) + end + if !var_deps && has_vars + annotate && (@string_append! file_text "\n\n# " get_variables_annotation(rn)) + @string_append! file_text "\nvars = " get_variables_string(vars_all) + end + + # If any set have dependencies, handle these. + # There are cases where the dependent syms come after their dependencies in the vector + # (e.g. corresponding to `@parameters p1 p2=p1`) + # which would not require this special treatment. However, this is currently not considered. + # Considering it would make the written code prettier, but would also require additional + # work in these functions to handle these cases (can be sorted out in the future). + if p_deps || sp_deps || var_deps + # Builds an annotation mentioning specially handled stuff. + if annotate + @string_append! file_text "\n\n# Some " + p_deps && (@string_append! file_text "parameters, ") + sp_deps && (@string_append! file_text "species, ") + var_deps && (@string_append! file_text "variables, ") + file_text = file_text[1:end-2] + @string_append! file_text " depends on the declaration of other parameters, species, and/or variables.\n# These are specially handled here.\n" + end + + # Pre-declares the sets with written/remaining parameters/species/variables. + # Whenever all/none are written depends on whether there were any initial dependencies. + remaining_ps = (p_deps ? ps_all : []) + remaining_sps = (sp_deps ? sps_all : []) + remaining_vars = (var_deps ? vars_all : []) + + # Iteratively loops through all parameters, species, and/or variables. In each iteration, + # adds the declaration of those that can still be declared. + while !(isempty(remaining_ps) && isempty(remaining_sps) && isempty(remaining_vars)) + # Checks which parameters/species/variables can be written. + writable_ps, nonwritable_ps = dependency_split([remaining_ps; remaining_sps; remaining_vars], remaining_ps) + writable_sps, nonwritable_sps = dependency_split([remaining_sps; remaining_vars], remaining_sps) + writable_vars, nonwritable_vars = dependency_split(remaining_vars, remaining_vars) + + # Writes those that can be written. + isempty(writable_ps) || @string_append! file_text get_parameters_string(writable_ps) "\n" + isempty(writable_sps) || @string_append! file_text get_species_string(writable_sps) "\n" + isempty(writable_vars) || @string_append! file_text get_variables_string(writable_vars) "\n" + + # Updates the remaining parameters/species/variables sets. + remaining_ps = nonwritable_ps + remaining_sps = nonwritable_sps + remaining_vars = nonwritable_vars + end + + # For parameters, species, and/or variables with dependencies, creates final vectors. + p_deps && (@string_append! file_text "ps = " syms_2_strings(ps_all) "\n") + sp_deps && (@string_append! file_text "sps = " syms_2_strings(sps_all) "\n") + var_deps && (@string_append! file_text "vars = " syms_2_strings(vars_all) "\n") + file_text = file_text[1:end-1] + end + + # Returns the finalised output. + return file_text, has_ps, has_sps, has_vars +end + + +### Handles Parameters ### +# Unlike most other fields, there are not called via `push_field`, but rather via `handle_us_n_ps`. +# Hence they work slightly differently. + +# Checks if the reaction system have any parameters. +function has_parameters(rn::ReactionSystem) + return !isempty(get_ps(rn)) +end + +# Extract a string which declares the system's parameters. Uses multiline declaration (a +# `begin ... end` block) if more than 3 parameters have a "complicated" declaration (if they +# have metadata, default value, or type designation). +function get_parameters_string(ps) + multiline_format = count(complicated_declaration(p) for p in ps) > 3 + return "@parameters$(syms_2_declaration_string(ps; multiline_format))" +end + +# Creates an annotation for the system's parameters. +function get_parameters_annotation(rn::ReactionSystem) + return "Parameters:" +end + + ### Handles Species ### +# Unlike most other fields, there are not called via `push_field`, but rather via `handle_us_n_ps`. +# Hence they work slightly differently. # Checks if the reaction system have any species. function has_species(rn::ReactionSystem) return !isempty(get_species(rn)) end -# Extract a string which declares the system's species. -function get_species_string(rn::ReactionSystem) - return "sps = @species$(syms_2_declaration_string(get_species(rn)))" +# Extract a string which declares the system's species. Uses multiline declaration (a +# `begin ... end` block) if more than 3 species have a "complicated" declaration (if they +# have metadata, default value, or type designation). +function get_species_string(sps) + multiline_format = count(complicated_declaration(sp) for sp in sps) > 3 + return "@species$(syms_2_declaration_string(sps; multiline_format))" end # Creates an annotation for the system's species. @@ -58,21 +172,22 @@ function get_species_annotation(rn::ReactionSystem) return "Species:" end -# Combines the 3 species-related functions in a constant tuple. -SPECIES_FS = (has_species, get_species_string, get_species_annotation) - ### Handles Variables ### +# Unlike most other fields, there are not called via `push_field`, but rather via `handle_us_n_ps`. +# Hence they work slightly differently. # Checks if the reaction system have any variables. function has_variables(rn::ReactionSystem) return length(get_unknowns(rn)) > length(get_species(rn)) end -# Extract a string which declares the system's variables. -function get_variables_string(rn::ReactionSystem) - variables = filter(!isspecies, get_unknowns(rn)) - return "vars = @variables$(syms_2_declaration_string(variables))" +# Extract a string which declares the system's variables. Uses multiline declaration (a +# `begin ... end` block) if more than 3 variables have a "complicated" declaration (if they +# have metadata, default value, or type designation). +function get_variables_string(vars) + multiline_format = count(complicated_declaration(var) for var in vars) > 3 + return "@variables$(syms_2_declaration_string(vars; multiline_format))" end # Creates an annotation for the system's . @@ -84,27 +199,6 @@ end VARIABLES_FS = (has_variables, get_variables_string, get_variables_annotation) -### Handles Parameters ### - -# Checks if the reaction system have any parameters. -function has_parameters(rn::ReactionSystem) - return length(get_ps(rn)) != 0 -end - -# Extract a string which declares the system's parameters. -function get_parameters_string(rn::ReactionSystem) - return "ps = @parameters$(syms_2_declaration_string(get_ps(rn)))" -end - -# Creates an annotation for the system's parameters. -function get_parameters_annotation(rn::ReactionSystem) - return "Parameters:" -end - -# Combines the 3 parameters-related functions in a constant tuple. -PARAMETERS_FS = (has_parameters, get_parameters_string, get_parameters_annotation) - - ### Handles Reactions ### # Checks if the reaction system have any reactions. @@ -208,12 +302,42 @@ EQUATIONS_FS = (has_equations, get_equations_string, get_equations_annotation) # Checks if the reaction system have any observables. function has_observed(rn::ReactionSystem) - return false + return !isempty(observed(rn)) end # Extract a string which declares the system's observables. function get_observed_string(rn::ReactionSystem) - get_unsupported_comp_string("observables") + # Finds the observable species and variables. + observed_unknowns = [obs_eq.lhs for obs_eq in observed(rn)] + observed_species = filter(isspecies, observed_unknowns) + observed_variables = filter(!isspecies, observed_unknowns) + + # Creates a dictionary for converting symbolics to their call-stripped form (e.g. X(t) to X). + strip_call_dict = make_strip_call_dict([get_unknowns(rn); observed_unknowns]) + + # Initialises the observables string with declaring the observable species/variables. + observed_string = "" + if !isempty(observed_species) + @string_append! observed_string "@species$(syms_2_declaration_string(observed_species))\n" + end + if !isempty(observed_variables) + @string_append! observed_string "@variables$(syms_2_declaration_string(observed_variables))\n" + end + + # Handles the case with one observable separately. Only effect is nicer formatting. + if length(observed(rn)) == 1 + @string_append! observed_string "observed = [$(expression_2_string(observed(rn)[1]; strip_call_dict))]" + return observed_string + end + + # Appends with the code which will generate observables equations. + @string_append! observed_string "observed = [" + for obs in observed(rn) + @string_append! observed_string "\n\t" expression_2_string(obs, strip_call_dict) "," + end + + # Updates the string (including removing the last `,`) and returns it. + return observed_string[1:end-1] * "\n]" end # Creates an annotation for the system's observables. @@ -340,19 +464,49 @@ DISCRETE_EVENTS_FS = (has_discrete_events, get_discrete_events_string, get_discr ### Handles Systems ### +# Specific `push_field` function, which is used for the system field (where the annotation option +# must be passed to the `get_component_string` function). Since non-ReactionSystem systems cannot be +# written to file, this functions throws an error if any such systems are encountered. +function push_systems_field(file_text::String, rn::ReactionSystem, annotate::Bool) + # Checks whther there are any subsystems, and if these are ReactionSystems. + has_systems(rn) || (return (file_text, false)) + if any(!(system isa ReactionSystem) for system in ModelingToolkit.get_systems(rn)) + error("Tries to write a ReactionSystem to file which have non-ReactionSystem subs-systems. This is currently not possible.") + end + + # Adds the system declaration string to the file string. + write_string = "\n" * get_systems_string(rn, annotate) + annotate && (@string_prepend! "\n\n# " get_systems_annotation(rn) write_string) + return (file_text * write_string, true) +end + # Checks if the reaction system have any systems. function has_systems(rn::ReactionSystem) - return false + return !isempty(ModelingToolkit.get_systems(rn)) end # Extract a string which declares the system's systems. -function get_systems_string(rn::ReactionSystem) - get_unsupported_comp_string("systems") +function get_systems_string(rn::ReactionSystem, annotate::Bool) + # Initiates the `systems` string. It is pre-declared vector, into which the systems are added. + systems_string = "systems = Vector(undef, $(length(ModelingToolkit.get_systems(rn))))" + + # Loops through all systems, adding their declaration to the system string. + for (idx, system) in enumerate(ModelingToolkit.get_systems(rn)) + annotate && (@string_append! systems_string "\n\n# Declares subsystem: $(getname(system))") + + # Manipulates the subsystem declaration to make it nicer. + subsystem_string = get_full_system_string(system, annotate) + subsystem_string = replace(subsystem_string, "\n" => "\n\t") + subsystem_string = "let\n" * subsystem_string[7:end-6] * "end" + @string_append! systems_string "\nsystems[$idx] = " subsystem_string + end + + return systems_string end # Creates an annotation for the system's systems. function get_systems_annotation(rn::ReactionSystem) - get_unsupported_comp_annotation("Systems:") + return "Subystems:" end # Combines the 3 systems-related functions in a constant tuple. diff --git a/src/model_serialisation/serialise_reactionsystem.jl b/src/reactionsystem_serialisation/serialise_reactionsystem.jl similarity index 72% rename from src/model_serialisation/serialise_reactionsystem.jl rename to src/reactionsystem_serialisation/serialise_reactionsystem.jl index 36b574b977..bcaa89643f 100644 --- a/src/model_serialisation/serialise_reactionsystem.jl +++ b/src/reactionsystem_serialisation/serialise_reactionsystem.jl @@ -1,30 +1,62 @@ """ save_reaction_network(filename::String, rn::ReactionSystem; annotate = true) -Save a `ReactionSystem` model to a file. - -Work in progress, currently missing features: -- Problems with ordering of declarations of species/variables/parameters that have defaults that are other species/variables/parameters. -- Saving of the `observed` field has not been fully implemented. -- Saving of the `systems` field has not been fully implemented. -- Saving of the `connection_type` field has not been fully implemented. +Save a `ReactionSystem` model to a file. The `ReactionSystem` is saved as runnable Julia code. This +can both be used to save a `ReactionSystem` model, but also to write it to a file for easy inspection. + +Arguments: +- `filename`: The name of the file to which the `ReactionSystem` is saved. +- `rn`: The `ReactionSystem` which should be saved to a file. +- `annotate = true`: Whether annotation should be added to the file. + +Example: +```julia +rn = @reaction_network begin + (p,d), 0 <--> X +end +save_reaction_network("rn.jls", rn) +``` +The model can now be loaded using +```julia +rn = include("rn.jls") +``` + +Notes: +- `ReactionSystem`s with the `connection_type` field has this ignored (saving of this field has not + been implemented yet). +- `ReactionSystem`s with non-`ReactionSystem` sub-systems (e.g. `ODESystem`s) cannot be saved. +- The `ReactionSystem` is saved using *programmatic* (not DSL) format for model creation. """ function save_reaction_network(filename::String, rn::ReactionSystem; annotate = true) + open(filename, "w") do file + write(file, get_full_system_string(rn, annotate)) + end + return nothing +end + +# Gets the full string which corresponds to the declaration of a system. Might be called recursively +# for systems with subsystems. +function get_full_system_string(rn::ReactionSystem, annotate::Bool) # Initiates the file string. file_text = "" + # Sub-systems must (unfortunately) be declared first (as the variables written in their internal + # let blocks otherwise will overwrite those of the main system). + # Systems are uses custom `push_field` function as these requires the annotation `Bool` + # to be passed to the function that creates the next sub-system declarations. + file_text, has_systems = push_systems_field(file_text, rn, annotate) + # Goes through each type of system component, potentially adding it to the string. + # Species, variables, and parameters must be handled differently in case there is default-values + # dependencies between them. file_text, _ = push_field(file_text, rn, annotate, IV_FS) file_text, has_sivs = push_field(file_text, rn, annotate, SIVS_FS) - file_text, has_species = push_field(file_text, rn, annotate, SPECIES_FS) - file_text, has_variables = push_field(file_text, rn, annotate, VARIABLES_FS) - file_text, has_parameters = push_field(file_text, rn, annotate, PARAMETERS_FS) + file_text, has_parameters, has_species, has_variables = handle_us_n_ps(file_text, rn, annotate) file_text, has_reactions = push_field(file_text, rn, annotate, REACTIONS_FS) file_text, has_equations = push_field(file_text, rn, annotate, EQUATIONS_FS) file_text, has_observed = push_field(file_text, rn, annotate, OBSERVED_FS) file_text, has_continuous_events = push_field(file_text, rn, annotate, CONTINUOUS_EVENTS_FS) file_text, has_discrete_events = push_field(file_text, rn, annotate, DISCRETE_EVENTS_FS) - file_text, has_systems = push_field(file_text, rn, annotate, SYSTEMS_FS) file_text, has_connection_type = push_field(file_text, rn, annotate, CONNECTION_TYPE_FS) # Finalises the system. Creates the final `ReactionSystem` call. @@ -37,11 +69,7 @@ function save_reaction_network(filename::String, rn::ReactionSystem; annotate = @string_prepend! "let" file_text @string_append! file_text "\n\n" rs_creation_code "\n\nend" - # Writes the model to a file. Then, returns nothing. - open(filename, "w") do file - write(file, file_text) - end - return nothing + return file_text end # Creates a ReactionSystem call for creating the model. Adds all the correct inputs to it. The input diff --git a/test/miscellaneous_tests/reactionsystem_serialisation.jl b/test/miscellaneous_tests/reactionsystem_serialisation.jl new file mode 100644 index 0000000000..791a296dc3 --- /dev/null +++ b/test/miscellaneous_tests/reactionsystem_serialisation.jl @@ -0,0 +1,92 @@ +### Prepare Tests ### + +# Fetch packages. +using Catalyst + +# Sets the default `t` and `D` to use. +t = default_t() +D = default_time_deriv() + + +### Basic Test ### + +# Checks for a simple reaction network (containing variables, equations, and observables). +# Checks that declaration via DSL works. +# Checks annotated and non-annotated files against manually written ones. +let + # Creates and serialises the model. + rn = @reaction_system begin + @observables X2 ~ 2X + @equations D(V) ~ 1 - V + (p,d), 0 <--> X + end + save_reaction_network("test_serialisation_annotated.jl", rn) + save_reaction_network("test_serialisation.jl", rn; annotate = false) + + # Checks equivalence. + file_string_annotated = read("test_serialisation_annotated.jl", String) + file_string = read("test_serialisation.jl", String) + file_string_annotated_real = "" + file_string_real = "" + @test file_string_annotated == file_string_annotated_real + @test file == file_string_real + + # Deletes the files. + rm("test_serialisation_annotated.jl") + rm("test_serialisation.jl") +end + +# Tests for hierarchical system created programmatically. +# Checks that the species, variables, and parameters have their non-default types, default values, +# and metadata recorded correctly (these are not considered for system equality is tested). +let + +end + +# Tests for complicated hierarchical system. Tests with non-default independent variable, +# spatial independent variables, variables, (differential and algebraic) equations, observables +# (continuous and discrete) events, and with various species/variables/parameter metadata. +let + +end + + +### Other Tests ### + +# Tests that an error is generated when non-`ReactionSystem` subs-systems are used. +let + @variables V(t) + @species X(t) + @parameters p d V_max + + rxs = [ + Reaction(p, [], [X]), + Reaction(d, [X], []) + ] + eq = D(V) ~ V_max - V + + @named osys = ODESystem([eq], t) + @named rs = ReactionSystem(rxs, t; systems = [osys]) + @test_throws Exception save_reaction_network("failed_serialisation.jl", rs) +end + +# Checks that completeness is recorded correctly. +let + # Checks for complete system. + rs_complete = @reaction_network begin + (p,d), 0 <--> X + end + save_reaction_network("serialised_rs_complete.jl", rs_complete) + rs_complete_loaded = include("serialised_rs_complete.jl") + @test ModelingToolkit.iscomplete(rs_complete_loaded) + rn("serialised_rs_complete.jl") + + # Checks for non-complete system. + rs_incomplete = @network_component begin + (p,d), 0 <--> X + end + save_reaction_network("serialised_rs_incomplete.jl", rs_incomplete) + rs_incomplete_loaded = include("serialised_rs_incomplete.jl") + @test !ModelingToolkit.iscomplete(rs_incomplete_loaded) + rn("serialised_rs_incomplete.jl") +end \ No newline at end of file From 68d22e6d1d0a49a4df04b3e786c4b53796656abe Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 15 Apr 2024 15:03:31 -0400 Subject: [PATCH 40/67] add safety_check function, finish tests, finish docstring --- .../serialisation_support.jl | 35 +- .../serialise_fields.jl | 66 ++-- .../serialise_reactionsystem.jl | 17 +- .../reactionsystem_serialisation.jl | 304 +++++++++++++++++- 4 files changed, 372 insertions(+), 50 deletions(-) diff --git a/src/reactionsystem_serialisation/serialisation_support.jl b/src/reactionsystem_serialisation/serialisation_support.jl index 7c4d08b6ac..2e41e24f46 100644 --- a/src/reactionsystem_serialisation/serialisation_support.jl +++ b/src/reactionsystem_serialisation/serialisation_support.jl @@ -119,26 +119,46 @@ end # more supported types can be added here. x_2_string(x::Num) = expression_2_string(x) x_2_string(x::SymbolicUtils.BasicSymbolic{<:Real}) = expression_2_string(x) +x_2_string(x::Bool) = string(x) x_2_string(x::String) = "\"$x\"" x_2_string(x::Char) = "\'$x\'" x_2_string(x::Symbol) = ":$x" x_2_string(x::Number) = string(x) +x_2_string(x::Pair) = "$(x_2_string(x[1])) => $(x_2_string(x[2]))" +x_2_string(x::Nothing) = "nothing" function x_2_string(x::Vector) output = "[" for val in x - output = output * x_2_string(val) * ", " + @string_append! output x_2_string(val) ", " end return output[1:end-2] * "]" end function x_2_string(x::Tuple) output = "(" for val in x - output = output * x_2_string(val) * ", " + @string_append! output x_2_string(val) ", " end return output[1:end-2] * ")" end -x_2_string(x::Pair) = "$(x_2_string(x[1])) => $(x_2_string(x[2]))" -x_2_string(x::Nothing) = "nothing" +function x_2_string(x::Dict) + output = "Dict([" + for key in keys(x) + @string_append! output x_2_string(key) " => " x_2_string(x[key]) ", " + end + return output[1:end-2] * "])" +end +function x_2_string(x::Union{Matrix, Symbolics.Arr{Any, 2}}) + output = "[" + for j = 1:size(x)[1] + for i = 1:size(x)[2] + @string_append! output x_2_string(x[j,i]) " " + end + output = output[1:end-1] * "; " + end + return output[1:end-2] *"]" +end + + x_2_string(x) = error("Tried to write an unsupported value ($(x)) of an unsupported type ($(typeof(x))) to a string.") @@ -236,10 +256,11 @@ end # two sets: # One with those that does not depend on any sym in `all_remaining_syms`. # One with those that does depend on at least one sym in `all_remaining_syms`. -function dependency_split(all_remaining_syms, remaining_syms) +# The first set is returned. Next `remaining_syms` is updated to be the second set. +function dependency_split!(remaining_syms, all_remaining_syms) writable_syms = filter(sym -> !depends_on(sym, all_remaining_syms), remaining_syms) - nonwritable_syms = filter(sym -> depends_on(sym, all_remaining_syms), remaining_syms) - return writable_syms, nonwritable_syms + filter!(sym -> depends_on(sym, all_remaining_syms), remaining_syms) + return writable_syms end diff --git a/src/reactionsystem_serialisation/serialise_fields.jl b/src/reactionsystem_serialisation/serialise_fields.jl index 1b31c7e879..3c52265979 100644 --- a/src/reactionsystem_serialisation/serialise_fields.jl +++ b/src/reactionsystem_serialisation/serialise_fields.jl @@ -7,7 +7,7 @@ end # Extract a string which declares the system's independent variable. function get_iv_string(rn::ReactionSystem) - iv_dec = ModelingToolkit.get_iv(rn) + iv_dec = MT.get_iv(rn) return "@variables $(iv_dec)" end @@ -29,7 +29,7 @@ end # Extract a string which declares the system's spatial independent variables. function get_sivs_string(rn::ReactionSystem) - return "spatial_ivs = @variables$(get_species_string(get_sivs(rn)))" + return "spatial_ivs = @variables$(syms_2_declaration_string(get_sivs(rn)))" end # Creates an annotation for the system's spatial independent variables. @@ -92,27 +92,24 @@ function handle_us_n_ps(file_text::String, rn::ReactionSystem, annotate::Bool) # Pre-declares the sets with written/remaining parameters/species/variables. # Whenever all/none are written depends on whether there were any initial dependencies. - remaining_ps = (p_deps ? ps_all : []) - remaining_sps = (sp_deps ? sps_all : []) - remaining_vars = (var_deps ? vars_all : []) + # `deepcopy` is required as these gets mutated by `dependency_split!`. + remaining_ps = (p_deps ? deepcopy(ps_all) : []) + remaining_sps = (sp_deps ? deepcopy(sps_all) : []) + remaining_vars = (var_deps ? deepcopy(vars_all) : []) # Iteratively loops through all parameters, species, and/or variables. In each iteration, # adds the declaration of those that can still be declared. while !(isempty(remaining_ps) && isempty(remaining_sps) && isempty(remaining_vars)) - # Checks which parameters/species/variables can be written. - writable_ps, nonwritable_ps = dependency_split([remaining_ps; remaining_sps; remaining_vars], remaining_ps) - writable_sps, nonwritable_sps = dependency_split([remaining_sps; remaining_vars], remaining_sps) - writable_vars, nonwritable_vars = dependency_split(remaining_vars, remaining_vars) + # Checks which parameters/species/variables can be written. The `dependency_split` + # function updates the `remaining_` input. + writable_ps = dependency_split!(remaining_ps, [remaining_ps; remaining_sps; remaining_vars]) + writable_sps = dependency_split!(remaining_sps, [remaining_ps; remaining_sps; remaining_vars]) + writable_vars = dependency_split!(remaining_vars, [remaining_ps; remaining_sps; remaining_vars]) # Writes those that can be written. isempty(writable_ps) || @string_append! file_text get_parameters_string(writable_ps) "\n" isempty(writable_sps) || @string_append! file_text get_species_string(writable_sps) "\n" isempty(writable_vars) || @string_append! file_text get_variables_string(writable_vars) "\n" - - # Updates the remaining parameters/species/variables sets. - remaining_ps = nonwritable_ps - remaining_sps = nonwritable_sps - remaining_vars = nonwritable_vars end # For parameters, species, and/or variables with dependencies, creates final vectors. @@ -280,7 +277,7 @@ function get_equations_string(rn::ReactionSystem) end # Creates the string corresponding to the code which generates the system's reactions. - eqs_string = "rxs = [" + eqs_string = "eqs = [" for eq in get_eqs(rn)[length(get_rxs(rn)) + 1:end] @string_append! eqs_string "\n\t" expression_2_string(eq; strip_call_dict) "," end @@ -308,7 +305,7 @@ end # Extract a string which declares the system's observables. function get_observed_string(rn::ReactionSystem) # Finds the observable species and variables. - observed_unknowns = [obs_eq.lhs for obs_eq in observed(rn)] + observed_unknowns = [obs_eq.lhs for obs_eq in MT.get_observed(rn)] observed_species = filter(isspecies, observed_unknowns) observed_variables = filter(!isspecies, observed_unknowns) @@ -325,15 +322,15 @@ function get_observed_string(rn::ReactionSystem) end # Handles the case with one observable separately. Only effect is nicer formatting. - if length(observed(rn)) == 1 - @string_append! observed_string "observed = [$(expression_2_string(observed(rn)[1]; strip_call_dict))]" + if length(MT.get_observed(rn)) == 1 + @string_append! observed_string "observed = [$(expression_2_string(MT.get_observed(rn)[1]; strip_call_dict))]" return observed_string end # Appends with the code which will generate observables equations. @string_append! observed_string "observed = [" - for obs in observed(rn) - @string_append! observed_string "\n\t" expression_2_string(obs, strip_call_dict) "," + for obs in MT.get_observed(rn) + @string_append! observed_string "\n\t" expression_2_string(obs; strip_call_dict) "," end # Updates the string (including removing the last `,`) and returns it. @@ -353,7 +350,7 @@ OBSERVED_FS = (has_observed, get_observed_string, get_observed_annotation) # Checks if the reaction system have any continuous events. function has_continuous_events(rn::ReactionSystem) - return length(rn.continuous_events) > 0 + return !isempty(MT.get_continuous_events(rn)) end # Extract a string which declares the system's continuous events. @@ -362,13 +359,13 @@ function get_continuous_events_string(rn::ReactionSystem) strip_call_dict = make_strip_call_dict(rn) # Handles the case with one event separately. Only effect is nicer formatting. - if length(rn.continuous_events) == 1 - return "continuous_events = [$(continuous_event_string(rn.continuous_events.value[1], strip_call_dict))]" + if length(MT.get_continuous_events(rn)) == 1 + return "continuous_events = [$(continuous_event_string(MT.get_continuous_events(rn)[1], strip_call_dict))]" end # Creates the string corresponding to the code which generates the system's reactions. continuous_events_string = "continuous_events = [" - for continuous_event in rn.continuous_events.value + for continuous_event in MT.get_continuous_events(rn) @string_append! continuous_events_string "\n\t" continuous_event_string(continuous_event, strip_call_dict) "," end @@ -410,7 +407,7 @@ CONTINUOUS_EVENTS_FS = (has_continuous_events, get_continuous_events_string, get # Checks if the reaction system have any discrete events. function has_discrete_events(rn::ReactionSystem) - return length(rn.discrete_events) > 0 + return !isempty(MT.get_discrete_events(rn)) end # Extract a string which declares the system's discrete events. @@ -419,13 +416,13 @@ function get_discrete_events_string(rn::ReactionSystem) strip_call_dict = make_strip_call_dict(rn) # Handles the case with one event separately. Only effect is nicer formatting. - if length(rn.discrete_events) == 1 - return "discrete_events = [$(discrete_event_string(rn.discrete_events.value[1], strip_call_dict))]" + if length(MT.get_discrete_events(rn)) == 1 + return "discrete_events = [$(discrete_event_string(MT.get_discrete_events(rn)[1], strip_call_dict))]" end # Creates the string corresponding to the code which generates the system's reactions. discrete_events_string = "discrete_events = [" - for discrete_event in rn.discrete_events.value + for discrete_event in MT.get_discrete_events(rn) @string_append! discrete_events_string "\n\t" discrete_event_string(discrete_event, strip_call_dict) "," end @@ -470,7 +467,7 @@ DISCRETE_EVENTS_FS = (has_discrete_events, get_discrete_events_string, get_discr function push_systems_field(file_text::String, rn::ReactionSystem, annotate::Bool) # Checks whther there are any subsystems, and if these are ReactionSystems. has_systems(rn) || (return (file_text, false)) - if any(!(system isa ReactionSystem) for system in ModelingToolkit.get_systems(rn)) + if any(!(system isa ReactionSystem) for system in MT.get_systems(rn)) error("Tries to write a ReactionSystem to file which have non-ReactionSystem subs-systems. This is currently not possible.") end @@ -482,23 +479,26 @@ end # Checks if the reaction system have any systems. function has_systems(rn::ReactionSystem) - return !isempty(ModelingToolkit.get_systems(rn)) + return !isempty(MT.get_systems(rn)) end # Extract a string which declares the system's systems. +# The systems variable (`systems_X`) is the only variable where we append an identifier. This is +# to avoid it getting overwritten by other systems variables (not required for other variables +# due to the order they appear in). function get_systems_string(rn::ReactionSystem, annotate::Bool) # Initiates the `systems` string. It is pre-declared vector, into which the systems are added. - systems_string = "systems = Vector(undef, $(length(ModelingToolkit.get_systems(rn))))" + systems_string = "systems_$(getname(rn)) = Vector(undef, $(length(MT.get_systems(rn))))" # Loops through all systems, adding their declaration to the system string. - for (idx, system) in enumerate(ModelingToolkit.get_systems(rn)) + for (idx, system) in enumerate(MT.get_systems(rn)) annotate && (@string_append! systems_string "\n\n# Declares subsystem: $(getname(system))") # Manipulates the subsystem declaration to make it nicer. subsystem_string = get_full_system_string(system, annotate) subsystem_string = replace(subsystem_string, "\n" => "\n\t") subsystem_string = "let\n" * subsystem_string[7:end-6] * "end" - @string_append! systems_string "\nsystems[$idx] = " subsystem_string + @string_append! systems_string "\nsystems_$(getname(rn))[$idx] = " subsystem_string end return systems_string diff --git a/src/reactionsystem_serialisation/serialise_reactionsystem.jl b/src/reactionsystem_serialisation/serialise_reactionsystem.jl index bcaa89643f..d346c53fc5 100644 --- a/src/reactionsystem_serialisation/serialise_reactionsystem.jl +++ b/src/reactionsystem_serialisation/serialise_reactionsystem.jl @@ -1,5 +1,5 @@ """ - save_reaction_network(filename::String, rn::ReactionSystem; annotate = true) + save_reaction_network(filename::String, rn::ReactionSystem; annotate = true, safety_check = true) Save a `ReactionSystem` model to a file. The `ReactionSystem` is saved as runnable Julia code. This can both be used to save a `ReactionSystem` model, but also to write it to a file for easy inspection. @@ -8,6 +8,10 @@ Arguments: - `filename`: The name of the file to which the `ReactionSystem` is saved. - `rn`: The `ReactionSystem` which should be saved to a file. - `annotate = true`: Whether annotation should be added to the file. +- `safety_check = true`: After serialisation, Catalyst will automatically load the serialised + `ReactionSystem` and check that it is equal to `rn`. If it is not, an error will be thrown. For + models without the `connection_type` field, this should not happen. If performance is required + (i.e. when saving a large number of models), this can be disabled by setting `safety_check = false`. Example: ```julia @@ -25,12 +29,17 @@ Notes: - `ReactionSystem`s with the `connection_type` field has this ignored (saving of this field has not been implemented yet). - `ReactionSystem`s with non-`ReactionSystem` sub-systems (e.g. `ODESystem`s) cannot be saved. +- Reaction systems with components that have units cannot currently be saved. - The `ReactionSystem` is saved using *programmatic* (not DSL) format for model creation. """ -function save_reaction_network(filename::String, rn::ReactionSystem; annotate = true) +function save_reaction_network(filename::String, rn::ReactionSystem; annotate = true, safety_check = true) open(filename, "w") do file write(file, get_full_system_string(rn, annotate)) end + if safety_check && !isequal(rn, include(filename)) + rm(filename) + error("The serialised `ReactionSystem` is not equal to the original one. Please make a report (including the full system) at https://github.com/SciML/Catalyst.jl/issues. To disable this behaviour, please pass the `safety_check = false` argument to `save_reaction_network` (warning, this will permit the serialisation of an erroneous system).") + end return nothing end @@ -127,7 +136,7 @@ function make_reaction_system_call(rs::ReactionSystem, annotate, has_sivs, has_s has_observed && (@string_append! reaction_system_string ", observed") has_continuous_events && (@string_append! reaction_system_string ", continuous_events") has_discrete_events && (@string_append! reaction_system_string ", discrete_events") - has_systems && (@string_append! reaction_system_string ", systems") + has_systems && (@string_append! reaction_system_string ", systems = systems_$(getname(rs))") has_connection_type && (@string_append! reaction_system_string ", connection_type") # Potentially appends a combinatoric_ratelaws statement. @@ -140,7 +149,7 @@ function make_reaction_system_call(rs::ReactionSystem, annotate, has_sivs, has_s @string_append! reaction_system_string ")" if ModelingToolkit.iscomplete(rs) @string_prepend! "rs = " reaction_system_string - @string_append! reaction_system_string "\nrs = complete(rs)" + @string_append! reaction_system_string "\ncomplete(rs)" end if annotate @string_prepend! "# Declares ReactionSystem model:\n" reaction_system_string diff --git a/test/miscellaneous_tests/reactionsystem_serialisation.jl b/test/miscellaneous_tests/reactionsystem_serialisation.jl index 791a296dc3..7059c56088 100644 --- a/test/miscellaneous_tests/reactionsystem_serialisation.jl +++ b/test/miscellaneous_tests/reactionsystem_serialisation.jl @@ -2,6 +2,11 @@ # Fetch packages. using Catalyst +using ModelingToolkit: getdefault, getdescription, get_metadata + +# Creates missing getters for MTK metadata (can be removed once added to MTK). +getmisc(x) = SymbolicUtils.getmetadata(Symbolics.unwrap(x), ModelingToolkit.VariableMisc, nothing) +getinput(x) = SymbolicUtils.getmetadata(Symbolics.unwrap(x), ModelingToolkit.VariableInput, nothing) # Sets the default `t` and `D` to use. t = default_t() @@ -20,8 +25,8 @@ let @equations D(V) ~ 1 - V (p,d), 0 <--> X end - save_reaction_network("test_serialisation_annotated.jl", rn) - save_reaction_network("test_serialisation.jl", rn; annotate = false) + save_reaction_network("test_serialisation_annotated.jl", rn; safety_check = false) + save_reaction_network("test_serialisation.jl", rn; annotate = false, safety_check = false) # Checks equivalence. file_string_annotated = read("test_serialisation_annotated.jl", String) @@ -39,17 +44,303 @@ end # Tests for hierarchical system created programmatically. # Checks that the species, variables, and parameters have their non-default types, default values, # and metadata recorded correctly (these are not considered for system equality is tested). +# Checks that various types (processed by the `x_2_string` function) are serialised properly. +# Checks that `ReactionSystem` and `Reaction` metadata fields are recorded properly. let - + # Prepares various stuff to add as metadata. + bool_md = false + int_md = 3 + float_md = 1.2 + rat_md = 4//5 + sym_md = :sym + c_md = 'c' + str_md = "A string" + nothing_md = nothing + @parameters s r + symb_md = s + expr_md = 2s + r^3 + pair_md = rat_md => symb_md + tup_md = (float_md, str_md, expr_md) + vec_md = [float_md, sym_md, tup_md] + dict_md = Dict([c_md => str_md, symb_md => vec_md]) + mat_md = [rat_md sym_md; symb_md tup_md] + + # Creates parameters, variables, and species (with various metadata and default values). + @parameters begin + a, [input=bool_md] + b, [misc=int_md] + c = float_md, [misc=rat_md] + d1, [misc=c_md] + d2, [description=str_md] + e1, [misc=nothing_md] + e2, [misc=symb_md] + end + @variables begin + A(t) = float_md + B(t), [misc=expr_md] + C(t), [misc=pair_md] + D1(t), [misc=tup_md] + D2(t), [misc=vec_md] + E1(t), [misc=dict_md] + E2(t), [misc=mat_md] + end + @species begin + X(t), [input=bool_md] + Y(t), [misc=int_md] + Z(t), [misc=float_md] + V1(t), [description=str_md] + V2(t), [misc=dict_md] + W1(t), [misc=mat_md] + W2(t) = float_md + end + + # Creates the hierarchical model. Adds metadata to both reactions and the systems. + rxs1 = [ + Reaction(a + A, [X], [], metadata = [:misc => bool_md]) + Reaction(b + B, [Y], [], metadata = [:misc => int_md]) + Reaction(c + C, [Z], [], metadata = [:misc => sym_md]) + Reaction(d1 + D1, [V1], [], metadata = [:misc => str_md]) + Reaction(e1 + E1, [W1], [], metadata = [:misc => nothing_md]) + ] + rxs2 = [ + Reaction(a + A, [X], [], metadata = [:misc => expr_md]) + Reaction(b + B, [Y], [], metadata = [:misc => tup_md]) + Reaction(c + C, [Z], [], metadata = [:misc => vec_md]) + Reaction(d2 + D2, [V2], [], metadata = [:misc => dict_md]) + Reaction(e2 + E2, [W2], [], metadata = [:misc => mat_md]) + ] + @named rs2 = ReactionSystem(rxs2, t; metadata = dict_md) + @named rs1 = ReactionSystem(rxs1, t; systems = [rs2], metadata = mat_md) + rs = complete(rs1) + + # Loads the model and checks that it is correct. Removes the saved file + save_reaction_network("serialised_rs.jl", rs; safety_check = false) + rs_loaded = include("serialised_rs.jl") + @test rs == rs_loaded + rm("serialised_rs.jl") + + # Checks that parameters/species/variables metadata fields are correct. + @test isequal(getinput(rs_loaded.a), bool_md) + @test isequal(getmisc(rs_loaded.b), int_md) + @test isequal(getdefault(rs_loaded.c), float_md) + @test isequal(getmisc(rs_loaded.c), rat_md) + @test isequal(getmisc(rs_loaded.d1), c_md) + @test isequal(getdescription(rs_loaded.rs2.d2), str_md) + @test isequal(getmisc(rs_loaded.e1), nothing_md) + @test isequal(getmisc(rs_loaded.rs2.e2), symb_md) + + @test isequal(getdefault(rs.A), float_md) + @test isequal(getmisc(rs_loaded.B), expr_md) + @test isequal(getmisc(rs_loaded.C), pair_md) + @test isequal(getmisc(rs_loaded.D1), tup_md) + @test isequal(getmisc(rs_loaded.rs2.D2), vec_md) + @test isequal(getmisc(rs_loaded.E1), dict_md) + @test isequal(getmisc(rs_loaded.rs2.E2), mat_md) + + @test isequal(getinput(rs_loaded.X), bool_md) + @test isequal(getmisc(rs_loaded.Y), int_md) + @test isequal(getmisc(rs_loaded.Z), float_md) + @test isequal(getdescription(rs_loaded.V1), str_md) + @test isequal(getmisc(rs_loaded.rs2.V2), dict_md) + @test isequal(getmisc(rs_loaded.W1), mat_md) + @test isequal(getdefault(rs_loaded.rs2.W2), float_md) + + # Checks that `Reaction` metadata fields are correct. + @test isequal(getmetadata(get_rxs(rs_loaded)[1], :misc), bool_md) + @test isequal(getmetadata(get_rxs(rs_loaded)[2], :misc), int_md) + @test isequal(getmetadata(get_rxs(rs_loaded)[3], :misc), sym_md) + @test isequal(getmetadata(get_rxs(rs_loaded)[4], :misc), str_md) + @test isequal(getmetadata(get_rxs(rs_loaded)[5], :misc), nothing_md) + @test isequal(getmetadata(get_rxs(rs_loaded.rs2)[1], :misc), expr_md) + @test isequal(getmetadata(get_rxs(rs_loaded.rs2)[2], :misc), tup_md) + @test isequal(getmetadata(get_rxs(rs_loaded.rs2)[3], :misc), vec_md) + @test isequal(getmetadata(get_rxs(rs_loaded.rs2)[4], :misc), dict_md) + @test isequal(getmetadata(get_rxs(rs_loaded.rs2)[5], :misc), mat_md) + + # Checks that `ReactionSystem` metadata fields are correct. + @test isequal(get_metadata(rs_loaded), mat_md) + @test isequal(get_metadata(rs_loaded.rs2), dict_md) end -# Tests for complicated hierarchical system. Tests with non-default independent variable, -# spatial independent variables, variables, (differential and algebraic) equations, observables -# (continuous and discrete) events, and with various species/variables/parameter metadata. +# Checks systems where parameters/species/variables have complicated interdependency are correctly +# serialised. +# Checks for system with non-default independent variable. +let + # Prepares parameters/variables/species with complicated dependencies. + @variables τ + @parameters begin + b = 3.0 + c + f + end + @variables begin + A(τ) = c + B(τ) = c + A + f + C(τ) = 2.0 + D(τ) = C + G(τ) + end + @species begin + Y(τ) = f + Z(τ) + U(τ) = G + Z + V(τ) + end + @parameters begin + a = G + D + e = U + end + @variables begin + E(τ) = G + Z + F(τ) = f + G + end + @species begin + X(τ) = f + a + end + @parameters d = X + @species W(τ) = d + e + + # Creates model and checks it against serialised version. + @named rs = ReactionSystem([], τ, [X, Y, Z, U, V, W, A, B, C, D, E, F, G], [a, b, c, d, e, f]) + save_reaction_network("serialised_rs.jl", rs; safety_check = false) + @test rs == include("serialised_rs.jl") + rm("serialised_rs.jl") +end + + +# Tests for multi-layered hierarchical system. Tests with spatial independent variables, +# variables, (differential and algebraic) equations, observables (continuous and discrete) events, +# and with various species/variables/parameter/reaction/system metadata. +# Tests for complete and incomplete system. let + # Prepares spatial independent variables (technically not used and only supplied to systems). + sivs = @variables x y z [description="A spatial independent variable."] + # Prepares parameters, species, and variables. + @parameters p d k1_1 k2_1 k1_2 k2_2 k1_3 k2_3 k1_4 k2_4 a b_1 b_2 b_3 b_4 η + @parameters begin + t_1 = 2.0 + t_2::Float64 + t_3, [description="A parameter."] + t_4::Float32 = p, [description="A parameter."] + end + @species X(t) X2_1(t) X2_2(t) X2_3(t) X2_4(t)=p [description="A species."] + @variables A(t)=p [description="A variable."] B_1(t) B_2(t) B_3(t) B_4(t) O_1(t) O_2(t) O_3(t) O_4(t) + + # Prepares all equations. + eqs_1 = [ + Reaction(p, [], [X]; metadata = [:description => "A reaction"]), + Reaction(d, [X], []; metadata = [:noise_scaling => η]), + Reaction(k1_1, [X], [X2_1], [2], [1]), + Reaction(k2_1, [X2_1], [X], [1], [2]), + D(A) ~ a - A, + A + 2B_1^3 ~ b_1 * X + ] + eqs_2 = [ + Reaction(p, [], [X]; metadata = [:description => "A reaction"]), + Reaction(d, [X], []; metadata = [:noise_scaling => η]), + Reaction(k1_2, [X], [X2_2], [2], [1]), + Reaction(k2_2, [X2_2], [X], [1], [2]), + D(A) ~ a - A, + A + 2B_2^3 ~ b_2 * X + ] + eqs_3 = [ + Reaction(p, [], [X]; metadata = [:description => "A reaction"]), + Reaction(d, [X], []; metadata = [:noise_scaling => η]), + Reaction(k1_3, [X], [X2_3], [2], [1]), + Reaction(k2_3, [X2_3], [X], [1], [2]), + D(A) ~ a - A, + A + 2B_3^3 ~ b_3 * X + ] + eqs_4 = [ + Reaction(p, [], [X]; metadata = [:description => "A reaction"]), + Reaction(d, [X], []; metadata = [:noise_scaling => η]), + Reaction(k1_4, [X], [X2_4], [2], [1]), + Reaction(k2_4, [X2_4], [X], [1], [2]), + D(A) ~ a - A, + A + 2B_4^3 ~ b_4 * X + ] + + # Prepares all observables. + observed_1 = [O_1 ~ X + 2*X2_1] + observed_2 = [O_2 ~ X + 2*X2_2] + observed_3 = [O_3 ~ X + 2*X2_3] + observed_4 = [O_4 ~ X + 2*X2_4] + + # Prepares all events. + continuous_events_1 = [(A ~ t_1) => [A ~ A + 2.0, X ~ X/2]] + continuous_events_2 = [(A ~ t_2) => [A ~ A + 2.0, X ~ X/2]] + continuous_events_3 = [(A ~ t_3) => [A ~ A + 2.0, X ~ X/2]] + continuous_events_4 = [(A ~ t_4) => [A ~ A + 2.0, X ~ X/2]] + discrete_events_1 = [ + 10.0 => [X2_1 ~ X2_1 + 1.0] + [5.0, 10.0] => [b_1 ~ 2 * b_1] + (X > 5.0) => [X2_1 ~ X2_1 + 1.0, X ~ X - 1] + ] + discrete_events_2 = [ + 10.0 => [X2_2 ~ X2_2 + 1.0] + [5.0, 10.0] => [b_2 ~ 2 * b_2] + (X > 5.0) => [X2_2 ~ X2_2 + 1.0, X ~ X - 1] + ] + discrete_events_3 = [ + 10.0 => [X2_3 ~ X2_3 + 1.0] + [5.0, 10.0] => [b_3 ~ 2 * b_3] + (X > 5.0) => [X2_3 ~ X2_3 + 1.0, X ~ X - 1] + ] + discrete_events_4 = [ + 10.0 => [X2_4 ~ X2_4 + 1.0] + [5.0, 10.0] => [b_4 ~ 2 * b_4] + (X > 5.0) => [X2_4 ~ X2_4 + 1.0, X ~ X - 1] + ] + + # Creates the systems. + @named rs_4 = ReactionSystem(eqs_4, t; observed = observed_4, continuous_events = continuous_events_4, + discrete_events = discrete_events_4, spatial_ivs = sivs, + metadata = "System 4", systems = []) + @named rs_2 = ReactionSystem(eqs_2, t; observed = observed_2, continuous_events = continuous_events_2, + discrete_events = discrete_events_2, spatial_ivs = sivs, + metadata = "System 2", systems = []) + @named rs_3 = ReactionSystem(eqs_3, t; observed = observed_3, continuous_events = continuous_events_3, + discrete_events = discrete_events_3, spatial_ivs = sivs, + metadata = "System 3", systems = [rs_4]) + @named rs_1 = ReactionSystem(eqs_1, t; observed = observed_1, continuous_events = continuous_events_1, + discrete_events = discrete_events_1, spatial_ivs = sivs, + metadata = "System 1", systems = [rs_2, rs_3]) + rs = complete(rs_1) + + # Checks that the correct system is saved (both complete and incomplete ones). + save_reaction_network("serialised_rs_incomplete.jl", rs_1; safety_check = false) + @test isequal(rs_1, include("serialised_rs_incomplete.jl")) + save_reaction_network("serialised_rs_complete.jl", rs; safety_check = false) + @test isequal(rs, include("serialised_rs_complete.jl")) + rm("serialised_rs_incomplete.jl") + rm("serialised_rs_complete.jl") end +# Tests for (slightly more) complicate system created via the DSL. +# Tests for cases where the number of input is untested (i.e. multiple observables and continuous +# events, but single equations and discrete events). +let + # Declares the model. + rs = @reaction_network begin + @equations D(V) ~ 1 - V + @observables begin + X2 ~ 2*X + X3 ~ 3*X + end + @continuous_events begin + X2 < 5.0 => [X ~ X + 1.0] + X3 > 20.0 => [X ~ X - 1.0] + end + @discrete_events [5.0 => d ~ d/2] + d, X --> 0 + end + + # Checks that serialisation works. + save_reaction_network("serialised_rs", rs; safety_check = false) + @test isequal(rs, include("serialised_rs.jl")) + rm("serialised_rs.jl") +end ### Other Tests ### @@ -71,6 +362,7 @@ let end # Checks that completeness is recorded correctly. +# Checks without turning off the `safety_check` option. let # Checks for complete system. rs_complete = @reaction_network begin From 68b5d72e15204aebfe8ab5d9d2eb12f1b4b22c8c Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 16 Apr 2024 14:14:29 -0400 Subject: [PATCH 41/67] redo how hirearchial systems are handled. --- .../serialisation_support.jl | 18 ++++- .../serialise_fields.jl | 67 ++++++++++--------- .../serialise_reactionsystem.jl | 44 ++++++------ 3 files changed, 74 insertions(+), 55 deletions(-) diff --git a/src/reactionsystem_serialisation/serialisation_support.jl b/src/reactionsystem_serialisation/serialisation_support.jl index 2e41e24f46..17a9fe35c8 100644 --- a/src/reactionsystem_serialisation/serialisation_support.jl +++ b/src/reactionsystem_serialisation/serialisation_support.jl @@ -25,10 +25,24 @@ end ### Field Serialisation Support Functions ### # Function which handles the addition of a single component to the file string. -function push_field(file_text::String, rn::ReactionSystem, annotate::Bool, comp_funcs::Tuple) +function push_field(file_text::String, rn::ReactionSystem, annotate::Bool, top_level::Bool, comp_funcs::Tuple) has_component, get_comp_string, get_comp_annotation = comp_funcs has_component(rn) || (return (file_text, false)) - write_string = "\n" * get_comp_string(rn) + + # Prepares the text creating the field. For non-top level systems, adds `local `. Observables + # must be handled differently (as the declaration is not at the beginning of the code for these). + # The independent variables is not declared as a variable, and also should not have a `1ocal `. + write_string = get_comp_string(rn) + if !(top_level || comp_funcs == IV_FS) + if comp_funcs == OBSERVED_FS + write_string = replace(write_string, "\nobserved = [" => "\nlocal observed = [") + else + @string_prepend! "local " write_string + end + end + @string_prepend! "\n" write_string + + # Adds (potential) annotation. Returns the expanded file text, and a Bool that this field was added. annotate && (@string_prepend! "\n\n# " get_comp_annotation(rn) write_string) return (file_text * write_string, true) end diff --git a/src/reactionsystem_serialisation/serialise_fields.jl b/src/reactionsystem_serialisation/serialise_fields.jl index 3c52265979..59c92eac45 100644 --- a/src/reactionsystem_serialisation/serialise_fields.jl +++ b/src/reactionsystem_serialisation/serialise_fields.jl @@ -45,7 +45,7 @@ SIVS_FS = (has_sivs, get_sivs_string, get_sivs_annotation) # Function which handles the addition of species, variable, and parameter declarations to the file # text. These must be handled as a unity in case there are default value dependencies between these. -function handle_us_n_ps(file_text::String, rn::ReactionSystem, annotate::Bool) +function handle_us_n_ps(file_text::String, rn::ReactionSystem, annotate::Bool, top_level::Bool) # Fetches the systems parameters, species, and variables. Computes the `has_` `Bool`s. ps_all = get_ps(rn) sps_all = get_species(rn) @@ -60,17 +60,18 @@ function handle_us_n_ps(file_text::String, rn::ReactionSystem, annotate::Bool) var_deps = any(depends_on(var, vars_all) for var in vars_all) # Makes the initial declaration. + us_n_ps_string = "" if !p_deps && has_ps - annotate && (@string_append! file_text "\n\n# " get_parameters_annotation(rn)) - @string_append! file_text "\nps = " get_parameters_string(ps_all) + annotate && (@string_append! us_n_ps_string "\n\n# " get_parameters_annotation(rn)) + @string_append! us_n_ps_string "\nps = " get_parameters_string(ps_all) end if !sp_deps && has_sps - annotate && (@string_append! file_text "\n\n# " get_species_annotation(rn)) - @string_append! file_text "\nsps = " get_species_string(sps_all) + annotate && (@string_append! us_n_ps_string "\n\n# " get_species_annotation(rn)) + @string_append! us_n_ps_string "\nsps = " get_species_string(sps_all) end if !var_deps && has_vars - annotate && (@string_append! file_text "\n\n# " get_variables_annotation(rn)) - @string_append! file_text "\nvars = " get_variables_string(vars_all) + annotate && (@string_append! us_n_ps_string "\n\n# " get_variables_annotation(rn)) + @string_append! us_n_ps_string "\nvars = " get_variables_string(vars_all) end # If any set have dependencies, handle these. @@ -82,12 +83,12 @@ function handle_us_n_ps(file_text::String, rn::ReactionSystem, annotate::Bool) if p_deps || sp_deps || var_deps # Builds an annotation mentioning specially handled stuff. if annotate - @string_append! file_text "\n\n# Some " - p_deps && (@string_append! file_text "parameters, ") - sp_deps && (@string_append! file_text "species, ") - var_deps && (@string_append! file_text "variables, ") - file_text = file_text[1:end-2] - @string_append! file_text " depends on the declaration of other parameters, species, and/or variables.\n# These are specially handled here.\n" + @string_append! us_n_ps_string "\n\n# Some " + p_deps && (@string_append! us_n_ps_string "parameters, ") + sp_deps && (@string_append! us_n_ps_string "species, ") + var_deps && (@string_append! us_n_ps_string "variables, ") + us_n_ps_string = us_n_ps_string[1:end-2] + @string_append! us_n_ps_string " depends on the declaration of other parameters, species, and/or variables.\n# These are specially handled here.\n" end # Pre-declares the sets with written/remaining parameters/species/variables. @@ -107,20 +108,27 @@ function handle_us_n_ps(file_text::String, rn::ReactionSystem, annotate::Bool) writable_vars = dependency_split!(remaining_vars, [remaining_ps; remaining_sps; remaining_vars]) # Writes those that can be written. - isempty(writable_ps) || @string_append! file_text get_parameters_string(writable_ps) "\n" - isempty(writable_sps) || @string_append! file_text get_species_string(writable_sps) "\n" - isempty(writable_vars) || @string_append! file_text get_variables_string(writable_vars) "\n" + isempty(writable_ps) || @string_append! us_n_ps_string get_parameters_string(writable_ps) "\n" + isempty(writable_sps) || @string_append! us_n_ps_string get_species_string(writable_sps) "\n" + isempty(writable_vars) || @string_append! us_n_ps_string get_variables_string(writable_vars) "\n" end # For parameters, species, and/or variables with dependencies, creates final vectors. - p_deps && (@string_append! file_text "ps = " syms_2_strings(ps_all) "\n") - sp_deps && (@string_append! file_text "sps = " syms_2_strings(sps_all) "\n") - var_deps && (@string_append! file_text "vars = " syms_2_strings(vars_all) "\n") - file_text = file_text[1:end-1] + p_deps && (@string_append! us_n_ps_string "ps = " syms_2_strings(ps_all) "\n") + sp_deps && (@string_append! us_n_ps_string "sps = " syms_2_strings(sps_all) "\n") + var_deps && (@string_append! us_n_ps_string "vars = " syms_2_strings(vars_all) "\n") + us_n_ps_string = us_n_ps_string[1:end-1] end - # Returns the finalised output. - return file_text, has_ps, has_sps, has_vars + # If this is not a top-level system, `local ` must be added to all declarations. + if !top_level + us_n_ps_string = replace(us_n_ps_string, "\nps = " => "\nlocal ps = ") + us_n_ps_string = replace(us_n_ps_string, "\nsps = " => "\nlocal sps = ") + us_n_ps_string = replace(us_n_ps_string, "\nvars = " => "\nlocal vars = ") + end + + # Merges the file text with `us_n_ps_string` and return the final outputs. + return file_text * us_n_ps_string, has_ps, has_sps, has_vars end @@ -464,7 +472,7 @@ DISCRETE_EVENTS_FS = (has_discrete_events, get_discrete_events_string, get_discr # Specific `push_field` function, which is used for the system field (where the annotation option # must be passed to the `get_component_string` function). Since non-ReactionSystem systems cannot be # written to file, this functions throws an error if any such systems are encountered. -function push_systems_field(file_text::String, rn::ReactionSystem, annotate::Bool) +function push_systems_field(file_text::String, rn::ReactionSystem, annotate::Bool, top_level::Bool) # Checks whther there are any subsystems, and if these are ReactionSystems. has_systems(rn) || (return (file_text, false)) if any(!(system isa ReactionSystem) for system in MT.get_systems(rn)) @@ -472,7 +480,9 @@ function push_systems_field(file_text::String, rn::ReactionSystem, annotate::Boo end # Adds the system declaration string to the file string. - write_string = "\n" * get_systems_string(rn, annotate) + write_string = "\n" + top_level || (@string_append! write_string "local ") + @string_append! write_string get_systems_string(rn, annotate) annotate && (@string_prepend! "\n\n# " get_systems_annotation(rn) write_string) return (file_text * write_string, true) end @@ -483,22 +493,19 @@ function has_systems(rn::ReactionSystem) end # Extract a string which declares the system's systems. -# The systems variable (`systems_X`) is the only variable where we append an identifier. This is -# to avoid it getting overwritten by other systems variables (not required for other variables -# due to the order they appear in). function get_systems_string(rn::ReactionSystem, annotate::Bool) # Initiates the `systems` string. It is pre-declared vector, into which the systems are added. - systems_string = "systems_$(getname(rn)) = Vector(undef, $(length(MT.get_systems(rn))))" + systems_string = "systems = Vector(undef, $(length(MT.get_systems(rn))))" # Loops through all systems, adding their declaration to the system string. for (idx, system) in enumerate(MT.get_systems(rn)) annotate && (@string_append! systems_string "\n\n# Declares subsystem: $(getname(system))") # Manipulates the subsystem declaration to make it nicer. - subsystem_string = get_full_system_string(system, annotate) + subsystem_string = get_full_system_string(system, annotate, false) subsystem_string = replace(subsystem_string, "\n" => "\n\t") subsystem_string = "let\n" * subsystem_string[7:end-6] * "end" - @string_append! systems_string "\nsystems_$(getname(rn))[$idx] = " subsystem_string + @string_append! systems_string "\nsystems[$idx] = " subsystem_string end return systems_string diff --git a/src/reactionsystem_serialisation/serialise_reactionsystem.jl b/src/reactionsystem_serialisation/serialise_reactionsystem.jl index d346c53fc5..1fdf48e866 100644 --- a/src/reactionsystem_serialisation/serialise_reactionsystem.jl +++ b/src/reactionsystem_serialisation/serialise_reactionsystem.jl @@ -34,7 +34,7 @@ Notes: """ function save_reaction_network(filename::String, rn::ReactionSystem; annotate = true, safety_check = true) open(filename, "w") do file - write(file, get_full_system_string(rn, annotate)) + write(file, get_full_system_string(rn, annotate, true)) end if safety_check && !isequal(rn, include(filename)) rm(filename) @@ -45,34 +45,31 @@ end # Gets the full string which corresponds to the declaration of a system. Might be called recursively # for systems with subsystems. -function get_full_system_string(rn::ReactionSystem, annotate::Bool) +function get_full_system_string(rn::ReactionSystem, annotate::Bool, top_level::Bool) # Initiates the file string. file_text = "" - - # Sub-systems must (unfortunately) be declared first (as the variables written in their internal - # let blocks otherwise will overwrite those of the main system). - # Systems are uses custom `push_field` function as these requires the annotation `Bool` - # to be passed to the function that creates the next sub-system declarations. - file_text, has_systems = push_systems_field(file_text, rn, annotate) - + # Goes through each type of system component, potentially adding it to the string. # Species, variables, and parameters must be handled differently in case there is default-values # dependencies between them. - file_text, _ = push_field(file_text, rn, annotate, IV_FS) - file_text, has_sivs = push_field(file_text, rn, annotate, SIVS_FS) - file_text, has_parameters, has_species, has_variables = handle_us_n_ps(file_text, rn, annotate) - file_text, has_reactions = push_field(file_text, rn, annotate, REACTIONS_FS) - file_text, has_equations = push_field(file_text, rn, annotate, EQUATIONS_FS) - file_text, has_observed = push_field(file_text, rn, annotate, OBSERVED_FS) - file_text, has_continuous_events = push_field(file_text, rn, annotate, CONTINUOUS_EVENTS_FS) - file_text, has_discrete_events = push_field(file_text, rn, annotate, DISCRETE_EVENTS_FS) - file_text, has_connection_type = push_field(file_text, rn, annotate, CONNECTION_TYPE_FS) + # Systems uses custom `push_field` function as these requires the annotation `Bool`to be passed + # to the function that creates the next sub-system declarations. + file_text, _ = push_field(file_text, rn, annotate, top_level, IV_FS) + file_text, has_sivs = push_field(file_text, rn, annotate, top_level, SIVS_FS) + file_text, has_parameters, has_species, has_variables = handle_us_n_ps(file_text, rn, annotate, top_level) + file_text, has_reactions = push_field(file_text, rn, annotate, top_level, REACTIONS_FS) + file_text, has_equations = push_field(file_text, rn, annotate, top_level, EQUATIONS_FS) + file_text, has_observed = push_field(file_text, rn, annotate, top_level, OBSERVED_FS) + file_text, has_continuous_events = push_field(file_text, rn, annotate, top_level, CONTINUOUS_EVENTS_FS) + file_text, has_discrete_events = push_field(file_text, rn, annotate, top_level, DISCRETE_EVENTS_FS) + file_text, has_systems = push_systems_field(file_text, rn, annotate, top_level) + file_text, has_connection_type = push_field(file_text, rn, annotate, top_level, CONNECTION_TYPE_FS) # Finalises the system. Creates the final `ReactionSystem` call. # Enclose everything ing a `let ... end` block. - rs_creation_code = make_reaction_system_call(rn, annotate, has_sivs, has_species, has_variables, - has_parameters, has_reactions, has_equations, - has_observed, has_continuous_events, + rs_creation_code = make_reaction_system_call(rn, annotate, top_level, has_sivs, has_species, + has_variables, has_parameters, has_reactions, + has_equations, has_observed, has_continuous_events, has_discrete_events, has_systems, has_connection_type) annotate || (@string_prepend! "\n" file_text) @string_prepend! "let" file_text @@ -83,7 +80,7 @@ end # Creates a ReactionSystem call for creating the model. Adds all the correct inputs to it. The input # `has_` `Bool`s described which inputs are used. If model is `complete`, this is handled here. -function make_reaction_system_call(rs::ReactionSystem, annotate, has_sivs, has_species, +function make_reaction_system_call(rs::ReactionSystem, annotate, top_level, has_sivs, has_species, has_variables, has_parameters, has_reactions, has_equations, has_observed, has_continuous_events, has_discrete_events, has_systems, has_connection_type) @@ -136,7 +133,7 @@ function make_reaction_system_call(rs::ReactionSystem, annotate, has_sivs, has_s has_observed && (@string_append! reaction_system_string ", observed") has_continuous_events && (@string_append! reaction_system_string ", continuous_events") has_discrete_events && (@string_append! reaction_system_string ", discrete_events") - has_systems && (@string_append! reaction_system_string ", systems = systems_$(getname(rs))") + has_systems && (@string_append! reaction_system_string ", systems") has_connection_type && (@string_append! reaction_system_string ", connection_type") # Potentially appends a combinatoric_ratelaws statement. @@ -149,6 +146,7 @@ function make_reaction_system_call(rs::ReactionSystem, annotate, has_sivs, has_s @string_append! reaction_system_string ")" if ModelingToolkit.iscomplete(rs) @string_prepend! "rs = " reaction_system_string + top_level || (@string_prepend! "local " reaction_system_string) @string_append! reaction_system_string "\ncomplete(rs)" end if annotate From 1be73da6a40ff05296ad71137f6e12da8214336e Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 21 May 2024 14:02:55 -0400 Subject: [PATCH 42/67] rebase fix --- src/reactionsystem.jl | 8 +++----- src/reactionsystem_serialisation/serialisation_support.jl | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/reactionsystem.jl b/src/reactionsystem.jl index f2ac871bc9..3be6b6c06c 100644 --- a/src/reactionsystem.jl +++ b/src/reactionsystem.jl @@ -1320,7 +1320,7 @@ Notes: - The default value of `combinatoric_ratelaws` will be the logical or of all `ReactionSystem`s. """ -function MT.flatten(rs::ReactionSystem; name = nameof(rs), complete = false) +function MT.flatten(rs::ReactionSystem; name = nameof(rs)) isempty(get_systems(rs)) && return rs # right now only NonlinearSystems and ODESystems can be handled as subsystems @@ -1338,8 +1338,7 @@ function MT.flatten(rs::ReactionSystem; name = nameof(rs), complete = false) balanced_bc_check = false, spatial_ivs = get_sivs(rs), continuous_events = MT.continuous_events(rs), - discrete_events = MT.discrete_events(rs), - complete = complete) + discrete_events = MT.discrete_events(rs)) end """ @@ -1396,8 +1395,7 @@ function ModelingToolkit.extend(sys::MT.AbstractSystem, rs::ReactionSystem; balanced_bc_check = false, spatial_ivs = sivs, continuous_events, - discrete_events, - complete = false) + discrete_events) end ### Units Handling ### diff --git a/src/reactionsystem_serialisation/serialisation_support.jl b/src/reactionsystem_serialisation/serialisation_support.jl index 17a9fe35c8..e466bc7835 100644 --- a/src/reactionsystem_serialisation/serialisation_support.jl +++ b/src/reactionsystem_serialisation/serialisation_support.jl @@ -99,7 +99,7 @@ function sym_2_declaration_string(sym; multiline_format = false) # Assumes that the type is on the form `SymbolicUtils.BasicSymbolic{X}`. Contain error checks # to ensure that this is the case. if !(sym isa SymbolicUtils.BasicSymbolic{Real}) - sym_type = String(Symbol(typeof(Symbolics.unwrap(k2)))) + sym_type = String(Symbol(typeof(Symbolics.unwrap(sym)))) if (sym_type[1:28] != "SymbolicUtils.BasicSymbolic{") || (sym_type[end] != '}') error("Encountered symbolic of unexpected type: $sym_type.") end From 51d1dc7cd9628621c68970c4e61098b2420917de Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 21 May 2024 20:21:44 -0400 Subject: [PATCH 43/67] rebase fix again --- test/dsl/dsl_options.jl | 778 +++++++++++++++++++++++----------------- 1 file changed, 453 insertions(+), 325 deletions(-) diff --git a/test/dsl/dsl_options.jl b/test/dsl/dsl_options.jl index bdd71e2cfc..93ce90b541 100644 --- a/test/dsl/dsl_options.jl +++ b/test/dsl/dsl_options.jl @@ -1,4 +1,4 @@ -### Prepares Tests ### +#! format: off ### Prepares Tests ### @@ -16,214 +16,335 @@ t = default_t() ### Tests `@parameters`, `@species`, and `@variables` Options ### -# Sets the default `t` to use. -t = default_t() - -# Fetch test networks and functions. -include("../test_networks.jl") -include("../test_functions.jl") +# Test creating networks with/without options. +let + @reaction_network begin (k1, k2), A <--> B end + @reaction_network begin + @parameters k1 k2 + (k1, k2), A <--> B + end + @reaction_network begin + @parameters k1 k2 + @species A(t) B(t) + (k1, k2), A <--> B + end + @reaction_network begin + @species A(t) B(t) + (k1, k2), A <--> B + end -### Declares Testing Functions ### + @reaction_network begin + @parameters begin + k1 + k2 + end + (k1, k2), A <--> B + end + @reaction_network begin + @species begin + A(t) + B(t) + end + (k1, k2), A <--> B + end + @reaction_network begin + @parameters begin + k1 + k2 + end + @species begin + A(t) + B(t) + end + (k1, k2), A <--> B + end -function unpacksys(sys) - get_eqs(sys), get_iv(sys), get_unknowns(sys), get_ps(sys), nameof(sys), get_systems(sys) + n1 = @reaction_network rnname begin (k1, k2), A <--> B end + n2 = @reaction_network rnname begin + @parameters k1 k2 + (k1, k2), A <--> B + end + n3 = @reaction_network rnname begin + @species A(t) B(t) + (k1, k2), A <--> B + end + n4 = @reaction_network rnname begin + @parameters k1 k2 + @species A(t) B(t) + (k1, k2), A <--> B + end + n5 = @reaction_network rnname begin + (k1, k2), A <--> B + @parameters k1 k2 + end + n6 = @reaction_network rnname begin + (k1, k2), A <--> B + @species A(t) B(t) + end + n7 = @reaction_network rnname begin + (k1, k2), A <--> B + @parameters k1 k2 + @species A(t) B(t) + end + n8 = @reaction_network rnname begin + @parameters begin + k1 + k2 + end + (k1, k2), A <--> B + end + n9 = @reaction_network rnname begin + @species begin + A(t) + B(t) + end + (k1, k2), A <--> B + end + n10 = @reaction_network rnname begin + @parameters begin + k1 + k2 + end + @species begin + A(t) + B(t) + end + (k1, k2), A <--> B + end + @test all(==(n1), (n2, n3, n4, n5, n6, n7, n8, n9, n10)) end -opname(x) = istree(x) ? nameof(operation(x)) : nameof(x) -alleq(xs, ys) = all(isequal(x, y) for (x, y) in zip(xs, ys)) +# Tests that when either @species or @parameters is given, the other is inferred properly. +let + rn1 = @reaction_network begin + k*X, A + B --> 0 + end + @test issetequal(species(rn1), @species A(t) B(t)) + @test issetequal(parameters(rn1), @parameters k X) -# Gets all the reactants in a set of equations. -function all_reactants(eqs) - all_reactants = [] - for eq in eqs - append!(all_reactants, opname.(eq.substrates)) - append!(all_reactants, opname.(eq.products)) + rn2 = @reaction_network begin + @species A(t) B(t) X(t) + k*X, A + B --> 0 end - return Set{Symbol}(unique(all_reactants)) -end + @test issetequal(species(rn2), @species A(t) B(t) X(t)) + @test issetequal(parameters(rn2), @parameters k) -# Gets all parameters (where every reaction rate is constant) -function all_parameters(eqs) - return Set(unique(map(eq -> opname(eq.rate), eqs))) -end + rn3 = @reaction_network begin + @parameters k + k*X, A + B --> 0 + end + @test issetequal(species(rn3), @species A(t) B(t)) + @test issetequal(parameters(rn3), @parameters k X) -# Perform basic tests. -function basic_test(rn, N, unknowns_syms, p_syms) - eqs, iv, unknowns, ps, name, systems = unpacksys(rn) - @test length(eqs) == N - @test opname(iv) == :t - @test length(unknowns) == length(unknowns_syms) - @test issetequal(map(opname, unknowns), unknowns_syms) - @test all_reactants(eqs) == Set(unknowns_syms) - @test length(ps) == length(p_syms) - @test issetequal(map(opname, ps), p_syms) -end + rn4 = @reaction_network begin + @species A(t) B(t) X(t) + @parameters k + k*X, A + B --> 0 + end + @test issetequal(species(rn4), @species A(t) B(t) X(t)) + @test issetequal(parameters(rn4), @parameters k) -### Basic Tests ### + rn5 = @reaction_network begin + @parameters k B [isconstantspecies=true] + k*X, A + B --> 0 + end + @test issetequal(species(rn5), @species A(t)) + @test issetequal(parameters(rn5), @parameters k B X) +end -# Test basic properties of networks. +# Test inferring with stoichiometry symbols and interpolation. let - basic_test(reaction_networks_standard[1], 10, [:X1, :X2, :X3], - [:p1, :p2, :p3, :k1, :k2, :k3, :k4, :d1, :d2, :d3]) - @test all_parameters(get_eqs(reaction_networks_standard[1])) == - Set([:p1, :p2, :p3, :k1, :k2, :k3, :k4, :d1, :d2, :d3]) - basic_test(reaction_networks_standard[2], 3, [:X1, :X2], [:v1, :K1, :v2, :K2, :d]) - basic_test(reaction_networks_standard[3], 10, [:X1, :X2, :X3, :X4], - [:v1, :K1, :v2, :K2, :k1, :k2, :k3, :k4, :d]) - basic_test(reaction_networks_standard[4], 8, [:X1, :X2, :X3, :X4], - [:v1, :K1, :v2, :K2, :v3, :K3, :v4, :K4, :d1, :d2, :d3, :d4]) - basic_test(reaction_networks_standard[5], 8, [:X1, :X2, :X3, :X4], - [:p, :k1, :k2, :k3, :k4, :k5, :k6, :d]) - @test all_parameters(get_eqs(reaction_networks_standard[5])) == - Set([:p, :k1, :k2, :k3, :k4, :k5, :k6, :d]) - basic_test(reaction_networks_hill[1], 4, [:X1, :X2], - [:v1, :v2, :K1, :K2, :n1, :n2, :d1, :d2]) - basic_test(reaction_networks_constraint[1], 6, [:X1, :X2, :X3], - [:k1, :k2, :k3, :k4, :k5, :k6]) - basic_test(reaction_networks_real[1], 4, [:X, :Y], [:A, :B]) - basic_test(reaction_networks_weird[1], 2, [:X], [:p, :d]) - basic_test(reaction_networks_weird[2], 4, [:X, :Y, :Z], [:k1, :k2, :k3, :k4]) + @parameters k g h gg X y [isconstantspecies = true] + t = Catalyst.DEFAULT_IV + @species A(t) B(t) BB(t) C(t) + + rni = @reaction_network inferred begin + $k*X, $y + g*A + h*($gg)*B + $BB * C --> k*C + end + @test issetequal(species(rni), [A, B, BB, C]) + @test issetequal(parameters(rni), [k, g, h, gg, X, y]) + + rnii = @reaction_network inferred begin + @species BB(t) + @parameters y [isconstantspecies = true] + k*X, y + g*A + h*($gg)*B + BB * C --> k*C + end + @test rnii == rni end -# Compares networks to networks created using different arrow types. +# Tests that when some species or parameters are left out, the others are set properly. let - networks_1 = [] - networks_2 = [] - - different_arrow_1 = @reaction_network begin - (p1, p2, p3), ∅ > (X1, X2, X3) - (k1, k2), X2 ↔ X1 + 2X3 - (k3, k4), X1 ⟷ X3 - (d1, d2, d3), (X1, X2, X3) → ∅ - end - push!(networks_1, reaction_networks_standard[1]) - push!(networks_2, different_arrow_1) - - different_arrow_2 = @reaction_network begin - mmr(X2, v1, K1), ∅ → X1 - mm(X1, v2, K2), ∅ ↣ X2 - d, X1 + X2 ↦ ∅ - end - push!(networks_1, reaction_networks_standard[2]) - push!(networks_2, different_arrow_2) - - different_arrow_3 = @reaction_network begin - mm(X2, v1, K1), ∅ ⇾ X1 - mm(X3, v2, K2), ∅ ⟶ X2 - (k1, k2), X1 ⇄ X3 - (k3, k4), X3 + X2 ⇆ X4 + X1 - d, (X1, X2, X3, X4) ⟼ ∅ - end - push!(networks_1, reaction_networks_standard[3]) - push!(networks_2, different_arrow_3) - - different_arrow_4 = @reaction_network begin - mmr(X4, v1, K1), ∅ ⥟ X1 - mmr(X1, v2, K2), ∅ ⥟ X2 - mmr(X2, v3, K3), ∅ ⇀ X3 - mmr(X3, v4, K4), ∅ ⇁ X4 - (d1, d2, d3, d4), (X1, X2, X3, X4) --> ∅ - end - push!(networks_1, reaction_networks_standard[4]) - push!(networks_2, different_arrow_4) - - # Yes the name is different, I wanted one with several single direction arrows. - different_arrow_8 = @reaction_network begin - p, 2X1 < ∅ - k1, X2 ← X1 - (k2, k3), X3 ⟻ X2 - d, ∅ ↼ X3 - end - push!(networks_1, reaction_networks_standard[8]) - push!(networks_2, different_arrow_8) - - for (rn_1, rn_2) in zip(networks_1, networks_2) - for factor in [1e-2, 1e-1, 1e0, 1e1, 1e2, 1e3] - u0 = rnd_u0(rn_1, rng; factor) - p = rnd_ps(rn_1, rng; factor) - t = rand(rng) - - @test f_eval(rn_1, u0, p, t) ≈ f_eval(rn_2, u0, p, t) - @test jac_eval(rn_1, u0, p, t) ≈ jac_eval(rn_2, u0, p, t) - @test g_eval(rn_1, u0, p, t) ≈ g_eval(rn_2, u0, p, t) - end + rn6 = @reaction_network begin + @species A(t) + k*X, A + B --> 0 + end + @test issetequal(species(rn6), @species A(t) B(t)) + @test issetequal(parameters(rn6), @parameters k X) + + rn7 = @reaction_network begin + @species A(t) X(t) + k*X, A + B --> 0 + end + @test issetequal(species(rn7), @species A(t) X(t) B(t)) + @test issetequal(parameters(rn7), @parameters k) + + rn7 = @reaction_network begin + @parameters B [isconstantspecies=true] + k*X, A + B --> 0 + end + @test issetequal(species(rn7), @species A(t)) + @test issetequal(parameters(rn7), @parameters B k X) + + rn8 = @reaction_network begin + @parameters B [isconstantspecies=true] k + k*X, A + B --> 0 + end + @test issetequal(species(rn8), @species A(t)) + @test issetequal(parameters(rn8), @parameters B k X) + + rn9 = @reaction_network begin + @parameters k1 X1 + @species A1(t) B1(t) + k1*X1, A1 + B1 --> 0 + k2*X2, A2 + B2 --> 0 + end + @test issetequal(species(rn9), @species A1(t) B1(t) A2(t) B2(t)) + @test issetequal(parameters(rn9), @parameters k1 X1 k2 X2) + + rn10 = @reaction_network begin + @parameters k1 X2 B2 [isconstantspecies=true] + @species A1(t) X1(t) + k1*X1, A1 + B1 --> 0 + k2*X2, A2 + B2 --> 0 + end + @test issetequal(species(rn10), @species A1(t) X1(t) B1(t) A2(t)) + @test issetequal(parameters(rn10), @parameters k1 X2 B2 k2) + + rn11 = @reaction_network begin + @parameters k1 k2 + @species X1(t) + k1*X1, A1 + B1 --> 0 + k2*X2, A2 + B2 --> 0 end + @test issetequal(species(rn11), @species X1(t) A1(t) A2(t) B1(t) B2(t)) + @test issetequal(parameters(rn11), @parameters k1 k2 X2) end # Checks that some created networks are identical. let - # Declares network. - differently_written_5 = @reaction_network begin - q, ∅ → Y1 - (l1, l2), Y1 ⟷ Y2 - (l3, l4), Y2 ⟷ Y3 - (l5, l6), Y3 ⟷ Y4 - c, Y4 → ∅ - end - - # Computes initial conditions/parameter values. - u0_vals = rand(rng, length(species(differently_written_5))) - ps_vals = rand(rng, length(parameters(differently_written_5))) - u0_1 = [:X1 => u0_vals[1], :X2 => u0_vals[2], :X3 => u0_vals[3], :X4 => u0_vals[4]] - u0_2 = [:Y1 => u0_vals[1], :Y2 => u0_vals[2], :Y3 => u0_vals[3], :Y4 => u0_vals[4]] - ps_1 = [:p => ps_vals[1], :k1 => ps_vals[2], :k2 => ps_vals[3], :k3 => ps_vals[4], - :k4 => ps_vals[5], :k5 => ps_vals[6], :k6 => ps_vals[7], :d => ps_vals[8]] - ps_2 = [:q => ps_vals[1], :l1 => ps_vals[2], :l2 => ps_vals[3], :l3 => ps_vals[4], - :l4 => ps_vals[5], :l5 => ps_vals[6], :l6 => ps_vals[7], :c => ps_vals[8]] - t = rand(rng) - - # Checks equivalence. - rn_1 = reaction_networks_standard[5] - rn_2 = differently_written_5 - @test f_eval(rn_1, u0_1, ps_1, t) ≈ f_eval(rn_2, u0_2, ps_2, t) - @test jac_eval(rn_1, u0_1, ps_1, t) ≈ jac_eval(rn_2, u0_2, ps_2, t) - @test g_eval(rn_1, u0_1, ps_1, t) ≈ g_eval(rn_2, u0_2, ps_2, t) + rn12 = @reaction_network rnname begin (k1, k2), A <--> B end + rn13 = @reaction_network rnname begin + @parameters k1 k2 + (k1, k2), A <--> B + end + rn14 = @reaction_network rnname begin + @species A(t) B(t) + (k1, k2), A <--> B + end + rn15 = @reaction_network rnname begin + @parameters k1 k2 + @species A(t) B(t) + (k1, k2), A <--> B + end + @test all(==(rn12), (rn13, rn14, rn15)) end -# Compares networks to networks written in different ways. +# Checks that the rights things are put in vectors. let - networks_1 = [] - networks_2 = [] - - # Unfold reactions. - differently_written_6 = @reaction_network begin - p1, ∅ → X1 - p2, ∅ → X2 - k1, 2X1 → X3 - k2, X3 → 2X1 - k3, X2 + X3 → 4X4 - k4, 4X4 → X2 + X3 - k5, X4 + X1 → 2X3 - k6, 2X3 → X4 + X1 - d, X1 → ∅ - d, X2 → ∅ - d, X3 → ∅ - d, X4 → ∅ - end - push!(networks_1, reaction_networks_standard[6]) - push!(networks_2, differently_written_6) - - # Ignore mass action. - differently_written_7 = @reaction_network begin - @parameters p1 p2 p3 k1 k2 k3 v1 K1 d1 d2 d3 d4 d5 - (p1, p2, p3), ∅ ⇒ (X1, X2, X3) - (k1 * X1 * X2^2 / 2, k2 * X4), X1 + 2X2 ⟺ X4 - (mm(X3, v1, K1) * X4, k3 * X5), X4 ⇔ X5 - (d1 * X1, d2 * X2, d3 * X3, d4 * X4, d5 * X5), ∅ ⟽ (X1, X2, X3, X4, X5) - end - push!(networks_1, reaction_networks_standard[7]) - push!(networks_2, differently_written_7) - - # Ignore mass action new arrows. - differently_written_8 = @reaction_network begin - @parameters p1 p2 p3 k1 k2 k3 v1 K1 d1 d2 d3 d4 d5 - (p1, p2, p3), ∅ => (X1, X2, X3) - (k1 * X1 * X2^2 / 2, k2 * X4), X1 + 2X2 ⟺ X4 - (mm(X3, v1, K1) * X4, k3 * X5), X4 ⇔ X5 - (d1 * X1, d2 * X2, d3 * X3, d4 * X4, d5 * X5), ∅ <= (X1, X2, X3, X4, X5) - end - push!(networks_1, reaction_networks_standard[7]) - push!(networks_2, differently_written_8) + rn18 = @reaction_network rnname begin + @parameters p d1 d2 + @species A(t) B(t) + p, 0 --> A + 1, A --> B + (d1, d2), (A, B) --> 0 + end + rn19 = @reaction_network rnname begin + p, 0 --> A + 1, A --> B + (d1, d2), (A, B) --> 0 + end + @test rn18 == rn19 + + @parameters p d1 d2 + @species A(t) B(t) + @test isequal(parameters(rn18)[1], p) + @test isequal(parameters(rn18)[2], d1) + @test isequal(parameters(rn18)[3], d2) + @test isequal(species(rn18)[1], A) + @test isequal(species(rn18)[2], B) + + rn20 = @reaction_network rnname begin + @species X(t) + @parameters S + mm(X,v,K), 0 --> Y + (k1,k2), 2Y <--> Y2 + d*Y, S*(Y2+Y) --> 0 + end + rn21 = @reaction_network rnname begin + @species X(t) Y(t) Y2(t) + @parameters v K k1 k2 d S + mm(X,v,K), 0 --> Y + (k1,k2), 2Y <--> Y2 + d*Y, S*(Y2+Y) --> 0 + end + rn22 = @reaction_network rnname begin + @species X(t) Y2(t) + @parameters d k1 + mm(X,v,K), 0 --> Y + (k1,k2), 2Y <--> Y2 + d*Y, S*(Y2+Y) --> 0 + end + @test all(==(rn20), (rn21, rn22)) + @parameters v K k1 k2 d S + @species X(t) Y(t) Y2(t) + @test issetequal(parameters(rn22),[v K k1 k2 d S]) + @test issetequal(species(rn22), [X Y Y2]) +end + +# Tests that defaults work. +let + rn26 = @reaction_network rnname begin + @parameters p=1.0 d1 d2=5 + @species A(t) B(t)=4 + p, 0 --> A + 1, A --> B + (d1, d2), (A, B) --> 0 + end + + rn27 = @reaction_network rnname begin + @parameters p1=1.0 p2=2.0 k1=4.0 k2=5.0 v=8.0 K=9.0 n=3 d=10.0 + @species X(t)=4.0 Y(t)=3.0 X2Y(t)=2.0 Z(t)=1.0 + (p1,p2), 0 --> (X,Y) + (k1,k2), 2X + Y --> X2Y + hill(X2Y,v,K,n), 0 --> Z + d, (X,Y,X2Y,Z) --> 0 + end + u0_27 = [] + p_27 = [] + + rn28 = @reaction_network rnname begin + @parameters p1=1.0 p2 k1=4.0 k2 v=8.0 K n=3 d + @species X(t)=4.0 Y(t) X2Y(t) Z(t)=1.0 + (p1,p2), 0 --> (X,Y) + (k1,k2), 2X + Y --> X2Y + hill(X2Y,v,K,n), 0 --> Z + d, (X,Y,X2Y,Z) --> 0 + end + u0_28 = symmap_to_varmap(rn28, [:p2=>2.0, :k2=>5.0, :K=>9.0, :d=>10.0]) + p_28 = symmap_to_varmap(rn28, [:Y=>3.0, :X2Y=>2.0]) + defs28 = Dict(Iterators.flatten((u0_28, p_28))) + + rn29 = @reaction_network rnname begin + @parameters p1 p2 k1 k2 v K n d + @species X(t) Y(t) X2Y(t) Z(t) + (p1,p2), 0 --> (X,Y) + (k1,k2), 2X + Y --> X2Y + hill(X2Y,v,K,n), 0 --> Z + d, (X,Y,X2Y,Z) --> 0 + end + u0_29 = symmap_to_varmap(rn29, [:p1=>1.0, :p2=>2.0, :k1=>4.0, :k2=>5.0, :v=>8.0, :K=>9.0, :n=>3, :d=>10.0]) + p_29 = symmap_to_varmap(rn29, [:X=>4.0, :Y=>3.0, :X2Y=>2.0, :Z=>1.0]) + defs29 = Dict(Iterators.flatten((u0_29, p_29))) @test ModelingToolkit.defaults(rn27) == defs29 @test merge(ModelingToolkit.defaults(rn28), defs28) == ModelingToolkit.defaults(rn27) @@ -338,6 +459,9 @@ let X ~ Xi + Xa Y ~ Y1 + Y2 end + (p,d), 0 <--> Xi + (k1,k2), Xi <--> Xa + (k3,k4), Y1 <--> Y2 end @unpack X, Xi, Xa, Y, Y1, Y2, p, d, k1, k2, k3, k4 = rn @@ -372,24 +496,18 @@ let @test_broken false # plot(sol; idxs=[:X, :Y]).series_list[2].plotattributes[:y][end] ≈ 3.0 end -# Compares networks to networks written without parameters, +# Compares programmatic and DSL system with observables. let - networks_1 = [] - networks_2 = [] - parameter_sets = [] - - # Different parameter and variable names. - no_parameters_9 = @reaction_network begin - (1.5, 1, 2), ∅ ⟶ (X1, X2, X3) - (0.01, 2.3, 1001), (X1, X2, X3) ⟶ ∅ - (π, 42), X1 + X2 ⟷ X3 - (19.9, 999.99), X3 ⟷ X4 - (sqrt(3.7), exp(1.9)), X4 ⟷ X1 + X2 - end - push!(networks_1, reaction_networks_standard[9]) - push!(networks_2, no_parameters_9) - push!(parameter_sets, [:p1 => 1.5, :p2 => 1, :p3 => 2, :d1 => 0.01, :d2 => 2.3, :d3 => 1001, - :k1 => π, :k2 => 42, :k3 => 19.9, :k4 => 999.99, :k5 => sqrt(3.7), :k6 => exp(1.9)]) + # Model declarations. + rn_dsl = @reaction_network begin + @observables begin + X ~ x + 2x2y + Y ~ y + x2y + end + k, 0 --> (x, y) + (kB, kD), 2x + y <--> x2y + d, (x,y,x2y) --> 0 + end @variables X(t) Y(t) @species x(t), y(t), x2y(t) @@ -405,17 +523,21 @@ let @named rn_prog = ReactionSystem([r1, r2, r3, r4, r5, r6, r7], t, [x, y, x2y], [k, kB, kD, d]; observed = obs_eqs) rn_prog = complete(rn_prog) - - for (rn_1, rn_2, p_1) in zip(networks_1, networks_2, parameter_sets) - for factor in [1e-2, 1e-1, 1e0, 1e1, 1e2, 1e3] - u0 = rnd_u0(rn_1, rng; factor) - t = rand(rng) - - @test f_eval(rn_1, u0, p_1, t) ≈ f_eval(rn_2, u0, [], t) - @test jac_eval(rn_1, u0, p_1, t) ≈ jac_eval(rn_2, u0, [], t) - @test g_eval(rn_1, u0, p_1, t) ≈ g_eval(rn_2, u0, [], t) - end - end + # Make simulations. + u0 = [x => 1.0, y => 0.5, x2y => 0.0] + tspan = (0.0, 15.0) + ps = [k => 1.0, kD => 0.1, kB => 0.5, d => 5.0] + oprob_dsl = ODEProblem(rn_dsl, u0, tspan, ps) + oprob_prog = ODEProblem(rn_prog, u0, tspan, ps) + + sol_dsl = solve(oprob_dsl, Tsit5(); saveat=0.1) + sol_prog = solve(oprob_prog, Tsit5(); saveat=0.1) + + # Tests observables equal in both cases. + @test oprob_dsl[:X] == oprob_prog[:X] + @test oprob_dsl[:Y] == oprob_prog[:Y] + @test sol_dsl[:X] == sol_prog[:X] + @test sol_dsl[:Y] == sol_prog[:Y] end # Tests for complicated observable formula. @@ -435,51 +557,32 @@ let u0 = Dict([:X1 => 1.0, :X2 => 2.0, :X3 => 3.0, :X4 => 4.0]) ps = Dict([:p => 1.0, :d => 0.2, :k1 => 1.5, :k2 => 1.5, :k3 => 5.0, :k4 => 5.0, :op_1 => 1.5, :op_2 => 1.5]) - rxs_1 = [Reaction(p, nothing, [X1], nothing, [2]), - Reaction(k1, [X1], [X2], [1], [1]), - Reaction(k2, [X2], [X3], [1], [1]), - Reaction(k3, [X2], [X3], [1], [1]), - Reaction(d, [X3], nothing, [1], nothing)] - @named rs_1 = ReactionSystem(rxs_1, t, [X1, X2, X3], [p, k1, k2, k3, d]) - push!(identical_networks_4, reaction_networks_standard[8] => rs_1) + oprob = ODEProblem(rn, u0, (0.0, 1000.0), ps) + sol = solve(oprob, Tsit5()) @test sol[:X][1] == u0[:X1]^2 + ps[:op_1]*(u0[:X2] + 2*u0[:X3]) + u0[:X1]*u0[:X4]/ps[:op_2] + ps[:p] end - rxs_3 = [Reaction(k1, [X1], [X2], [1], [1]), - Reaction(0, [X2], [X3], [1], [1]), - Reaction(k2, [X3], [X4], [1], [1]), - Reaction(k3, [X4], [X5], [1], [1])] - @named rs_3 = ReactionSystem(rxs_3, t, [X1, X2, X3, X4, X5], [k1, k2, k3]) - push!(identical_networks_4, reaction_networks_weird[7] => rs_3) - - for networks in identical_networks_4 - @test isequal(get_iv(networks[1]), get_iv(networks[2])) - @test alleq(get_unknowns(networks[1]), get_unknowns(networks[2])) - @test alleq(get_ps(networks[1]), get_ps(networks[2])) - @test ModelingToolkit.get_systems(networks[1]) == - ModelingToolkit.get_systems(networks[2]) - @test length(get_eqs(networks[1])) == length(get_eqs(networks[2])) - for (e1, e2) in zip(get_eqs(networks[1]), get_eqs(networks[2])) - @test isequal(e1.rate, e2.rate) - @test isequal(e1.substrates, e2.substrates) - @test isequal(e1.products, e2.products) - @test isequal(e1.substoich, e2.substoich) - @test isequal(e1.prodstoich, e2.prodstoich) - @test isequal(e1.netstoich, e2.netstoich) - @test isequal(e1.only_use_rate, e2.only_use_rate) +# Checks that ivs are correctly found. +let + rn = @reaction_network begin + @ivs t x y + @species V1(t) V2(t,x) V3(t, y) W1(t) W2(t, y) + @observables begin + V ~ V1 + 2V2 + 3V3 + W ~ W1 + W2 end end + V,W = getfield.(observed(rn), :lhs) + @test isequal(arguments(ModelingToolkit.unwrap(V)), Any[rn.iv, rn.sivs[1], rn.sivs[2]]) + @test isequal(arguments(ModelingToolkit.unwrap(W)), Any[rn.iv, rn.sivs[2]]) end -### Tests Usage of Various Symbols ### - -# Tests that time is handled properly. +# Checks that metadata is written properly. let - time_network = @reaction_network begin - (t, k2), X1 ↔ X2 - (k3, t), X2 ↔ X3 - (t, k6), X3 ↔ X1 + rn = @reaction_network rn_observed begin + @observables (X, [description="my_description"]) ~ X1 + X2 + k, 0 --> X1 + X2 end @test ModelingToolkit.getdescription(observed(rn)[1].lhs) == "my_description" end @@ -502,62 +605,64 @@ let @test isequal(observed(rn1)[1].lhs.metadata, observed(rn2)[1].lhs.metadata) @test isequal(unknowns(rn1), unknowns(rn2)) - @test f_eval(reaction_networks_constraint[1], u, p_1, τ) ≈ f_eval(time_network, u, p_2, τ) - @test jac_eval(reaction_networks_constraint[1], u, p_1, τ) ≈ jac_eval(time_network, u, p_2, τ) - @test g_eval(reaction_networks_constraint[1], u, p_1, τ) ≈ g_eval(time_network, u, p_2, τ) + # Case with metadata. + rn3 = @reaction_network rn_observed begin + @observables (X, [description="description"]) ~ X1 + X2 + k, 0 --> X1 + X2 + end + rn4 = @reaction_network rn_observed begin + @variables X(t) [description="description"] + @observables X ~ X1 + X2 + k, 0 --> X1 + X2 end + @test isequal(observed(rn3)[1].rhs, observed(rn4)[1].rhs) + @test isequal(observed(rn3)[1].lhs.metadata, observed(rn4)[1].lhs.metadata) + @test isequal(unknowns(rn3), unknowns(rn4)) end -# Check that various symbols can be used as species/parameter names. +# Tests for interpolation into the observables option. let - @reaction_network begin - (a, A), n ⟷ N - (b, B), o ⟷ O - (c, C), p ⟷ P - (d, D), q ⟷ Q - (e, E), r ⟷ R - (f, F), s ⟷ S - (g, G), u ⟷ U - (h, H), v ⟷ V - (j, J), w ⟷ W - (k, K), x ⟷ X - (l, L), y ⟷ Y - (m, M), z ⟷ Z + # Interpolation into lhs. + @species X [description="An observable"] + rn1 = @reaction_network begin + @observables $X ~ X1 + X2 + (k1, k2), X1 <--> X2 end @test isequal(observed(rn1)[1].lhs, X) @test ModelingToolkit.getdescription(rn1.X) == "An observable" @test isspecies(rn1.X) @test length(unknowns(rn1)) == 2 - @reaction_network begin (1.0, 1.0), i ⟷ T end - - @reaction_network begin - (å, Å), ü ⟷ Ü - (ä, Ä), ñ ⟷ Ñ - (ö, Ö), æ ⟷ Æ + # Interpolation into rhs. + @parameters n [description="A parameter"] + @species S(t) + rn2 = @reaction_network begin + @observables Stot ~ $S + $n*Sn + (kB, kD), $n*S <--> Sn end + @unpack Stot, Sn, kD, kB = rn2 - @reaction_network begin - (α, Α), ν ⟷ Ν - (β, Β), ξ ⟷ Ξ - (γ, γ), ο ⟷ Ο - (δ, Δ), Π ⟷ Π - (ϵ, Ε), ρ ⟷ Ρ - (ζ, Ζ), σ ⟷ Σ - (η, Η), τ ⟷ Τ - (θ, Θ), υ ⟷ Υ - (ι, Ι), ϕ ⟷ Φ - (κ, κ), χ ⟷ Χ - (λ, Λ), ψ ↔ Ψ - (μ, Μ), ω ⟷ Ω - end + u0 = Dict([S => 5.0, Sn => 1.0]) + ps = Dict([n => 2, kB => 1.0, kD => 1.0]) + oprob = ODEProblem(rn2, u0, (0.0, 1.0), ps) + + @test issetequal(Symbolics.get_variables(observed(rn2)[1].rhs), [S, n, Sn]) + @test oprob[Stot] == u0[S] + ps[n]*u0[Sn] + @test length(unknowns(rn2)) == 2 end # Tests specific declaration of observables as species/variables. let rn = @reaction_network begin - k1, S + I --> 2I - k2, I --> R + @species X(t) + @variables Y(t) + @observables begin + X ~ X + 2X2 + Y ~ Y1 + Y2 + Z ~ X + Y + end + (kB,kD), 2X <--> X2 + (k1,k2), Y1 <--> Y2 end @test isspecies(rn.X) @@ -573,45 +678,68 @@ let k, 0 --> X1 + X2 end - rn2 = @reaction_network arrowtest begin - a1, C --> 0 - a2, 0 --> C - k1, A + B --> C - k2, C --> A + B - b1, B --> 0 + # System with observable in observable formula. + @test_throws Exception @eval @reaction_network begin + @observables begin + X ~ X1 + X2 + X2 ~ 2X + end + (p,d), 0 <--> X1 + X2 + end + + # Multiple @observables options + @test_throws Exception @eval @reaction_network begin + @observables X ~ X1 + X2 + @observables Y ~ Y1 + Y2 + k, 0 --> X1 + X2 + k, 0 --> Y1 + Y2 + end + @test_throws Exception @eval @reaction_network begin + @observables begin + X ~ X1 + X2 + end + @observables begin + X ~ 2(X1 + X2) + end + (p,d), 0 <--> X1 + X2 + end + + # Default value for compound. + @test_throws Exception @eval @reaction_network begin + @observables (X = 1.0) ~ X1 + X2 + k, 0 --> X1 + X2 end - @test rn1 == rn2 -end + # Forbidden symbols as observable names. + @test_throws Exception @eval @reaction_network begin + @observables t ~ t1 + t2 + k, 0 --> t1 + t2 + end + @test_throws Exception @eval @reaction_network begin + @observables im ~ i + m + k, 0 --> i + m + end -# Tests arrow variants in "@reaction" macro . -let - @test isequal((@reaction k, 0 --> X), (@reaction k, X <-- 0)) - @test isequal((@reaction k, 0 --> X), (@reaction k, X ⟻ 0)) - @test isequal((@reaction k, 0 --> X), (@reaction k, 0 → X)) - @test isequal((@reaction k, 0 --> X), (@reaction k, 0 ⥟ X)) -end + # Non-trivial observables expression. + @test_throws Exception @eval @reaction_network begin + @observables X - X1 ~ X2 + k, 0 --> X1 + X2 + end -# Test that symbols with special mean, or that are forbidden, are handled properly. -let - test_network = @reaction_network begin t * k, X --> ∅ end - @test length(species(test_network)) == 1 - @test length(parameters(test_network)) == 1 - - test_network = @reaction_network begin π, X --> ∅ end - @test length(species(test_network)) == 1 - @test length(parameters(test_network)) == 0 - @test reactions(test_network)[1].rate == π - - test_network = @reaction_network begin pi, X --> ∅ end - @test length(species(test_network)) == 1 - @test length(parameters(test_network)) == 0 - @test reactions(test_network)[1].rate == pi - - test_network = @reaction_network begin ℯ, X --> ∅ end - @test length(species(test_network)) == 1 - @test length(parameters(test_network)) == 0 - @test reactions(test_network)[1].rate == ℯ + # Occurrence of undeclared dependants. + @test_throws Exception @eval @reaction_network begin + @observables X ~ X1 + X2 + k, 0 --> X1 + end + + # Interpolation and explicit declaration of an observable. + @variables X(t) + @test_throws Exception @eval @reaction_network begin + @variables X(t) + @observables $X ~ X1 + X2 + (k1,k2), X1 <--> X2 + end +end ### Test `@equations` Option for Coupled CRN/Equations Models ### From bc1833d16c99fd19425d2bd6a4a712287e0b011e Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 21 May 2024 21:32:32 -0400 Subject: [PATCH 44/67] save progress --- .../serialisation_support.jl | 25 ++++--- .../serialise_fields.jl | 26 +++---- .../reactionsystem_serialisation.jl | 69 +++++++++++++++---- 3 files changed, 87 insertions(+), 33 deletions(-) diff --git a/src/reactionsystem_serialisation/serialisation_support.jl b/src/reactionsystem_serialisation/serialisation_support.jl index e466bc7835..60ed6913be 100644 --- a/src/reactionsystem_serialisation/serialisation_support.jl +++ b/src/reactionsystem_serialisation/serialisation_support.jl @@ -21,6 +21,13 @@ macro string_prepend!(input1, input2, string) return esc(:($string = $rhs)) end +# Gets the character at a specific index. +get_char(str, idx) = collect(str)[idx] +get_char_end(str, offset) = collect(str)[end - offset] +# Gets a substring (which is robust to unicode characters like η). +get_substring(str, idx1, idx2) = String(collect(str)[idx1, idx2]) +get_substring_end(str, idx1, offset) = String(collect(str)[idx1, end - offset]) + ### Field Serialisation Support Functions ### @@ -72,7 +79,7 @@ end # any calls (e.g. X(t) becomes X). E.g. a species vector [X, Y, Z] is converted to "[X, Y, Z]". function syms_2_strings(syms) strip_called_syms = [strip_call(Symbolics.unwrap(sym)) for sym in syms] - return "$(convert(Vector{Any}, strip_called_syms))"[4:end] + return get_substring("$(convert(Vector{Any}, strip_called_syms))", 4) end # Converts a vector of symbolics (e.g. the species or parameter vectors) to a string corresponding to @@ -100,10 +107,10 @@ function sym_2_declaration_string(sym; multiline_format = false) # to ensure that this is the case. if !(sym isa SymbolicUtils.BasicSymbolic{Real}) sym_type = String(Symbol(typeof(Symbolics.unwrap(sym)))) - if (sym_type[1:28] != "SymbolicUtils.BasicSymbolic{") || (sym_type[end] != '}') + if (get_substring(sym_type, 1, 28) != "SymbolicUtils.BasicSymbolic{") || (get_char_end(sym_type, 0) != '}') error("Encountered symbolic of unexpected type: $sym_type.") end - @string_append! dec_string "::" sym_type[29:end-1] + @string_append! dec_string "::" get_substring_end(sym_type, 29, -1) end # If there is a default value, adds this to the declaration. @@ -120,7 +127,7 @@ function sym_2_declaration_string(sym; multiline_format = false) for metadata in metadata_to_declare @string_append! metadata_string metadata_2_string(sym, metadata) ", " end - @string_append! dec_string metadata_string[1:end-2] "]" + @string_append! dec_string $(get_substring_end(metadata_string, 1, -2)) "]" end # Returns the declaration entry for the symbol. @@ -145,21 +152,21 @@ function x_2_string(x::Vector) for val in x @string_append! output x_2_string(val) ", " end - return output[1:end-2] * "]" + return get_substring_end(output, 1, -2) * "]" end function x_2_string(x::Tuple) output = "(" for val in x @string_append! output x_2_string(val) ", " end - return output[1:end-2] * ")" + return get_substring_end(output, 1, -2) * ")" end function x_2_string(x::Dict) output = "Dict([" for key in keys(x) @string_append! output x_2_string(key) " => " x_2_string(x[key]) ", " end - return output[1:end-2] * "])" + return get_substring_end(output, 1, -2) * "])" end function x_2_string(x::Union{Matrix, Symbolics.Arr{Any, 2}}) output = "[" @@ -167,9 +174,9 @@ function x_2_string(x::Union{Matrix, Symbolics.Arr{Any, 2}}) for i = 1:size(x)[2] @string_append! output x_2_string(x[j,i]) " " end - output = output[1:end-1] * "; " + output = get_substring_end(output, 1, -1) * "; " end - return output[1:end-2] *"]" + return get_substring_end(output, 1, -2) *"]" end diff --git a/src/reactionsystem_serialisation/serialise_fields.jl b/src/reactionsystem_serialisation/serialise_fields.jl index 59c92eac45..964e3bb617 100644 --- a/src/reactionsystem_serialisation/serialise_fields.jl +++ b/src/reactionsystem_serialisation/serialise_fields.jl @@ -87,7 +87,7 @@ function handle_us_n_ps(file_text::String, rn::ReactionSystem, annotate::Bool, t p_deps && (@string_append! us_n_ps_string "parameters, ") sp_deps && (@string_append! us_n_ps_string "species, ") var_deps && (@string_append! us_n_ps_string "variables, ") - us_n_ps_string = us_n_ps_string[1:end-2] + us_n_ps_string = get_substring_end(us_n_ps_string, 1, -2) @string_append! us_n_ps_string " depends on the declaration of other parameters, species, and/or variables.\n# These are specially handled here.\n" end @@ -117,7 +117,7 @@ function handle_us_n_ps(file_text::String, rn::ReactionSystem, annotate::Bool, t p_deps && (@string_append! us_n_ps_string "ps = " syms_2_strings(ps_all) "\n") sp_deps && (@string_append! us_n_ps_string "sps = " syms_2_strings(sps_all) "\n") var_deps && (@string_append! us_n_ps_string "vars = " syms_2_strings(vars_all) "\n") - us_n_ps_string = us_n_ps_string[1:end-1] + us_n_ps_string = get_substring_end(us_n_ps_string, 1, -1) end # If this is not a top-level system, `local ` must be added to all declarations. @@ -217,7 +217,9 @@ function get_reactions_string(rn::ReactionSystem) strip_call_dict = make_strip_call_dict(rn) # Handles the case with one reaction separately. Only effect is nicer formatting. - (length(get_rxs(rn)) == 1) && (return "rxs = [$(reaction_string(rx, strip_call_dict))]") + if length(get_rxs(rn)) == 1 + return "rxs = [$(reaction_string(get_rxs(rn)[1], strip_call_dict))]" + end # Creates the string corresponding to the code which generates the system's reactions. rxs_string = "rxs = [" @@ -226,7 +228,7 @@ function get_reactions_string(rn::ReactionSystem) end # Updates the string (including removing the last `,`) and returns it. - return rxs_string[1:end-1] * "\n]" + return get_substring_end(rxs_string, 1, -1) * "\n]" end # Creates a string that corresponds to the declaration of a single `Reaction`. @@ -251,7 +253,7 @@ function reaction_string(rx::Reaction, strip_call_dict) metadata_entry = "$(x_2_string(entry)), " @string_append! rx_string metadata_entry end - rx_string = rx_string[1:end-2] * "]" + rx_string = get_substring_end(rx_string, 1, -2) * "]" end # Returns the Reaction string. @@ -291,7 +293,7 @@ function get_equations_string(rn::ReactionSystem) end # Updates the string (including removing the last `,`) and returns it. - return eqs_string[1:end-1] * "\n]" + return get_substring_end(eqs_string, 1, -1) * "\n]" end # Creates an annotation for the system's equations. @@ -378,7 +380,7 @@ function get_continuous_events_string(rn::ReactionSystem) end # Updates the string (including removing the last `,`) and returns it. - return continuous_events_string[1:end-1] * "\n]" + return get_substring_end(continuous_events_string, 1, -1) * "\n]" end # Creates a string that corresponds to the declaration of a single continuous event. @@ -388,7 +390,7 @@ function continuous_event_string(continuous_event, strip_call_dict) for eq in continuous_event.eqs @string_append! eqs_string expression_2_string(eq; strip_call_dict) ", " end - eqs_string = eqs_string[1:end-2] * "]" + eqs_string = get_substring_end(eqs_string, 1, -2) * "]" # Creates the string corresponding to the affects. # Continuous events' `affect` field should probably be called `affects`. Likely the `s` was @@ -397,7 +399,7 @@ function continuous_event_string(continuous_event, strip_call_dict) for affect in continuous_event.affect @string_append! affects_string expression_2_string(affect; strip_call_dict) ", " end - affects_string = affects_string[1:end-2] * "]" + affects_string = get_substring_end(affects_string, 1, -2) * "]" return eqs_string * " => " * affects_string end @@ -435,7 +437,7 @@ function get_discrete_events_string(rn::ReactionSystem) end # Updates the string (including removing the last `,`) and returns it. - return discrete_events_string[1:end-1] * "\n]" + return get_substring_end(discrete_events_string, 1, -1) * "\n]" end # Creates a string that corresponds to the declaration of a single discrete event. @@ -453,7 +455,7 @@ function discrete_event_string(discrete_event, strip_call_dict) for affect in discrete_event.affects @string_append! affects_string expression_2_string(affect; strip_call_dict) ", " end - affects_string = affects_string[1:end-2] * "]" + affects_string = get_substring_end(affects_string, 1, -2) * "]" return condition_string * " => " * affects_string end @@ -504,7 +506,7 @@ function get_systems_string(rn::ReactionSystem, annotate::Bool) # Manipulates the subsystem declaration to make it nicer. subsystem_string = get_full_system_string(system, annotate, false) subsystem_string = replace(subsystem_string, "\n" => "\n\t") - subsystem_string = "let\n" * subsystem_string[7:end-6] * "end" + subsystem_string = "let\n" * get_substring_end(subsystem_string, 7, -6) * "end" @string_append! systems_string "\nsystems[$idx] = " subsystem_string end diff --git a/test/miscellaneous_tests/reactionsystem_serialisation.jl b/test/miscellaneous_tests/reactionsystem_serialisation.jl index 7059c56088..207084f507 100644 --- a/test/miscellaneous_tests/reactionsystem_serialisation.jl +++ b/test/miscellaneous_tests/reactionsystem_serialisation.jl @@ -2,6 +2,7 @@ # Fetch packages. using Catalyst +using Catalyst: get_rxs using ModelingToolkit: getdefault, getdescription, get_metadata # Creates missing getters for MTK metadata (can be removed once added to MTK). @@ -20,10 +21,10 @@ D = default_time_deriv() # Checks annotated and non-annotated files against manually written ones. let # Creates and serialises the model. - rn = @reaction_system begin + rn = @reaction_network rn begin @observables X2 ~ 2X @equations D(V) ~ 1 - V - (p,d), 0 <--> X + d, X --> 0 end save_reaction_network("test_serialisation_annotated.jl", rn; safety_check = false) save_reaction_network("test_serialisation.jl", rn; annotate = false, safety_check = false) @@ -31,10 +32,52 @@ let # Checks equivalence. file_string_annotated = read("test_serialisation_annotated.jl", String) file_string = read("test_serialisation.jl", String) - file_string_annotated_real = "" - file_string_real = "" + file_string_annotated_real = """let + + # Independent variable: + @variables t + + # Parameters: + ps = @parameters d + + # Species: + sps = @species X(t) + + # Variables: + vars = @variables V(t) + + # Reactions: + rxs = [Reaction(d, [X], nothing, [1], nothing)] + + # Equations: + eqs = [Differential(t)(V) ~ 1 - V] + + # Observables: + @variables X2(t) + observed = [X2 ~ 2X] + + # Declares ReactionSystem model: + rs = ReactionSystem([rxs; eqs], t, [sps; vars], ps; name = :rn, observed) + complete(rs) + + end""" + file_string_real = """let + + @variables t + ps = @parameters d + sps = @species X(t) + vars = @variables V(t) + rxs = [Reaction(d, [X], nothing, [1], nothing)] + eqs = [Differential(t)(V) ~ 1 - V] + @variables X2(t) + observed = [X2 ~ 2X] + + rs = ReactionSystem([rxs; eqs], t, [sps; vars], ps; name = :rn, observed) + complete(rs) + + end""" @test file_string_annotated == file_string_annotated_real - @test file == file_string_real + @test file_string == file_string_real # Deletes the files. rm("test_serialisation_annotated.jl") @@ -95,8 +138,10 @@ let end # Creates the hierarchical model. Adds metadata to both reactions and the systems. + # First reaction has `+ s + r` as that is easier than manually listing all symbolics. + # (These needs to be part of the system somehow, as they are only added through the `misc` metadata) rxs1 = [ - Reaction(a + A, [X], [], metadata = [:misc => bool_md]) + Reaction(a + A + s + r, [X], [], metadata = [:misc => bool_md]) Reaction(b + B, [Y], [], metadata = [:misc => int_md]) Reaction(c + C, [Z], [], metadata = [:misc => sym_md]) Reaction(d1 + D1, [V1], [], metadata = [:misc => str_md]) @@ -329,15 +374,15 @@ let X3 ~ 3*X end @continuous_events begin - X2 < 5.0 => [X ~ X + 1.0] - X3 > 20.0 => [X ~ X - 1.0] + [X ~ 5.0] => [X ~ X + 1.0] + [X ~ 20.0] => [X ~ X - 1.0] end - @discrete_events [5.0 => d ~ d/2] + @discrete_events 5.0 => [d ~ d/2] d, X --> 0 end # Checks that serialisation works. - save_reaction_network("serialised_rs", rs; safety_check = false) + save_reaction_network("serialised_rs.jl", rs; safety_check = false) @test isequal(rs, include("serialised_rs.jl")) rm("serialised_rs.jl") end @@ -371,7 +416,7 @@ let save_reaction_network("serialised_rs_complete.jl", rs_complete) rs_complete_loaded = include("serialised_rs_complete.jl") @test ModelingToolkit.iscomplete(rs_complete_loaded) - rn("serialised_rs_complete.jl") + rm("serialised_rs_complete.jl") # Checks for non-complete system. rs_incomplete = @network_component begin @@ -380,5 +425,5 @@ let save_reaction_network("serialised_rs_incomplete.jl", rs_incomplete) rs_incomplete_loaded = include("serialised_rs_incomplete.jl") @test !ModelingToolkit.iscomplete(rs_incomplete_loaded) - rn("serialised_rs_incomplete.jl") + rm("serialised_rs_incomplete.jl") end \ No newline at end of file From 1b2db7a7c5ae04a5a388e72e00a43fc402f6b15a Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 21 May 2024 22:02:47 -0400 Subject: [PATCH 45/67] save progress --- .../serialisation_support.jl | 10 +++++----- .../reactionsystem_serialisation.jl | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/reactionsystem_serialisation/serialisation_support.jl b/src/reactionsystem_serialisation/serialisation_support.jl index 60ed6913be..62b366ca5b 100644 --- a/src/reactionsystem_serialisation/serialisation_support.jl +++ b/src/reactionsystem_serialisation/serialisation_support.jl @@ -23,10 +23,10 @@ end # Gets the character at a specific index. get_char(str, idx) = collect(str)[idx] -get_char_end(str, offset) = collect(str)[end - offset] +get_char_end(str, offset) = collect(str)[end+offset] # Gets a substring (which is robust to unicode characters like η). -get_substring(str, idx1, idx2) = String(collect(str)[idx1, idx2]) -get_substring_end(str, idx1, offset) = String(collect(str)[idx1, end - offset]) +get_substring(str, idx1, idx2) = String(collect(str)[idx1:idx2]) +get_substring_end(str, idx1, offset) = String(collect(str)[idx1:end+offset]) ### Field Serialisation Support Functions ### @@ -79,7 +79,7 @@ end # any calls (e.g. X(t) becomes X). E.g. a species vector [X, Y, Z] is converted to "[X, Y, Z]". function syms_2_strings(syms) strip_called_syms = [strip_call(Symbolics.unwrap(sym)) for sym in syms] - return get_substring("$(convert(Vector{Any}, strip_called_syms))", 4) + return get_substring_end("$(convert(Vector{Any}, strip_called_syms))", 4, 0) end # Converts a vector of symbolics (e.g. the species or parameter vectors) to a string corresponding to @@ -127,7 +127,7 @@ function sym_2_declaration_string(sym; multiline_format = false) for metadata in metadata_to_declare @string_append! metadata_string metadata_2_string(sym, metadata) ", " end - @string_append! dec_string $(get_substring_end(metadata_string, 1, -2)) "]" + @string_append! dec_string get_substring_end(metadata_string, 1, -2) "]" end # Returns the declaration entry for the symbol. diff --git a/test/miscellaneous_tests/reactionsystem_serialisation.jl b/test/miscellaneous_tests/reactionsystem_serialisation.jl index 207084f507..10ca349a6a 100644 --- a/test/miscellaneous_tests/reactionsystem_serialisation.jl +++ b/test/miscellaneous_tests/reactionsystem_serialisation.jl @@ -1,7 +1,7 @@ ### Prepare Tests ### # Fetch packages. -using Catalyst +using Catalyst, Test using Catalyst: get_rxs using ModelingToolkit: getdefault, getdescription, get_metadata From 97196451217e2a5f588f339480772cb080cd0d28 Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 22 May 2024 13:14:45 -0400 Subject: [PATCH 46/67] up --- .../serialise_reactionsystem.jl | 8 ++++--- .../reactionsystem_serialisation.jl | 21 ++++++++----------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/reactionsystem_serialisation/serialise_reactionsystem.jl b/src/reactionsystem_serialisation/serialise_reactionsystem.jl index 1fdf48e866..20cd085d0c 100644 --- a/src/reactionsystem_serialisation/serialise_reactionsystem.jl +++ b/src/reactionsystem_serialisation/serialise_reactionsystem.jl @@ -36,9 +36,11 @@ function save_reaction_network(filename::String, rn::ReactionSystem; annotate = open(filename, "w") do file write(file, get_full_system_string(rn, annotate, true)) end - if safety_check && !isequal(rn, include(filename)) - rm(filename) - error("The serialised `ReactionSystem` is not equal to the original one. Please make a report (including the full system) at https://github.com/SciML/Catalyst.jl/issues. To disable this behaviour, please pass the `safety_check = false` argument to `save_reaction_network` (warning, this will permit the serialisation of an erroneous system).") + if safety_check + if !isequal(rn, include(joinpath(pwd(), filename))) + rm(filename) + error("The serialised `ReactionSystem` is not equal to the original one. Please make a report (including the full system) at https://github.com/SciML/Catalyst.jl/issues. To disable this behaviour, please pass the `safety_check = false` argument to `save_reaction_network` (warning, this will permit the serialisation of an erroneous system).") + end end return nothing end diff --git a/test/miscellaneous_tests/reactionsystem_serialisation.jl b/test/miscellaneous_tests/reactionsystem_serialisation.jl index 10ca349a6a..ee43d7b80d 100644 --- a/test/miscellaneous_tests/reactionsystem_serialisation.jl +++ b/test/miscellaneous_tests/reactionsystem_serialisation.jl @@ -270,7 +270,7 @@ let t_4::Float32 = p, [description="A parameter."] end @species X(t) X2_1(t) X2_2(t) X2_3(t) X2_4(t)=p [description="A species."] - @variables A(t)=p [description="A variable."] B_1(t) B_2(t) B_3(t) B_4(t) O_1(t) O_2(t) O_3(t) O_4(t) + @variables A(t)=p [description="A variable."] B_1(t) B_2(t) B_3(t) B_4(t) # Prepares all equations. eqs_1 = [ @@ -306,12 +306,6 @@ let A + 2B_4^3 ~ b_4 * X ] - # Prepares all observables. - observed_1 = [O_1 ~ X + 2*X2_1] - observed_2 = [O_2 ~ X + 2*X2_2] - observed_3 = [O_3 ~ X + 2*X2_3] - observed_4 = [O_4 ~ X + 2*X2_4] - # Prepares all events. continuous_events_1 = [(A ~ t_1) => [A ~ A + 2.0, X ~ X/2]] continuous_events_2 = [(A ~ t_2) => [A ~ A + 2.0, X ~ X/2]] @@ -339,16 +333,16 @@ let ] # Creates the systems. - @named rs_4 = ReactionSystem(eqs_4, t; observed = observed_4, continuous_events = continuous_events_4, + @named rs_4 = ReactionSystem(eqs_4, t; continuous_events = continuous_events_4, discrete_events = discrete_events_4, spatial_ivs = sivs, metadata = "System 4", systems = []) - @named rs_2 = ReactionSystem(eqs_2, t; observed = observed_2, continuous_events = continuous_events_2, + @named rs_2 = ReactionSystem(eqs_2, t; continuous_events = continuous_events_2, discrete_events = discrete_events_2, spatial_ivs = sivs, metadata = "System 2", systems = []) - @named rs_3 = ReactionSystem(eqs_3, t; observed = observed_3, continuous_events = continuous_events_3, + @named rs_3 = ReactionSystem(eqs_3, t; continuous_events = continuous_events_3, discrete_events = discrete_events_3, spatial_ivs = sivs, metadata = "System 3", systems = [rs_4]) - @named rs_1 = ReactionSystem(eqs_1, t; observed = observed_1, continuous_events = continuous_events_1, + @named rs_1 = ReactionSystem(eqs_1, t; continuous_events = continuous_events_1, discrete_events = discrete_events_1, spatial_ivs = sivs, metadata = "System 1", systems = [rs_2, rs_3]) rs = complete(rs_1) @@ -365,6 +359,9 @@ end # Tests for (slightly more) complicate system created via the DSL. # Tests for cases where the number of input is untested (i.e. multiple observables and continuous # events, but single equations and discrete events). +# Currently broken due to Symbolics doing something weird with observable variables, where these +# end up not being internal due to something internal in symbolics. I have tried tracking down the +# obscure symbolics subfields. let # Declares the model. rs = @reaction_network begin @@ -383,7 +380,7 @@ let # Checks that serialisation works. save_reaction_network("serialised_rs.jl", rs; safety_check = false) - @test isequal(rs, include("serialised_rs.jl")) + @test_broken isequal(rs, include("serialised_rs.jl")) rm("serialised_rs.jl") end From adcc75f8635a79311b58286e0755c37d6d92ec0e Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 2 Jun 2024 12:25:34 -0400 Subject: [PATCH 47/67] save progress --- .../reactionsystem_serialisation.jl | 14 +++++++------- test/runtests.jl | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/test/miscellaneous_tests/reactionsystem_serialisation.jl b/test/miscellaneous_tests/reactionsystem_serialisation.jl index ee43d7b80d..00bbcb1982 100644 --- a/test/miscellaneous_tests/reactionsystem_serialisation.jl +++ b/test/miscellaneous_tests/reactionsystem_serialisation.jl @@ -160,7 +160,7 @@ let # Loads the model and checks that it is correct. Removes the saved file save_reaction_network("serialised_rs.jl", rs; safety_check = false) - rs_loaded = include("serialised_rs.jl") + rs_loaded = include("../serialised_rs.jl") @test rs == rs_loaded rm("serialised_rs.jl") @@ -248,7 +248,7 @@ let # Creates model and checks it against serialised version. @named rs = ReactionSystem([], τ, [X, Y, Z, U, V, W, A, B, C, D, E, F, G], [a, b, c, d, e, f]) save_reaction_network("serialised_rs.jl", rs; safety_check = false) - @test rs == include("serialised_rs.jl") + @test rs == include("../serialised_rs.jl") rm("serialised_rs.jl") end @@ -349,9 +349,9 @@ let # Checks that the correct system is saved (both complete and incomplete ones). save_reaction_network("serialised_rs_incomplete.jl", rs_1; safety_check = false) - @test isequal(rs_1, include("serialised_rs_incomplete.jl")) + @test isequal(rs_1, include("../serialised_rs_incomplete.jl")) save_reaction_network("serialised_rs_complete.jl", rs; safety_check = false) - @test isequal(rs, include("serialised_rs_complete.jl")) + @test isequal(rs, include("../serialised_rs_complete.jl")) rm("serialised_rs_incomplete.jl") rm("serialised_rs_complete.jl") end @@ -380,7 +380,7 @@ let # Checks that serialisation works. save_reaction_network("serialised_rs.jl", rs; safety_check = false) - @test_broken isequal(rs, include("serialised_rs.jl")) + @test_broken isequal(rs, include("../serialised_rs.jl")) rm("serialised_rs.jl") end @@ -411,7 +411,7 @@ let (p,d), 0 <--> X end save_reaction_network("serialised_rs_complete.jl", rs_complete) - rs_complete_loaded = include("serialised_rs_complete.jl") + rs_complete_loaded = include("../serialised_rs_complete.jl") @test ModelingToolkit.iscomplete(rs_complete_loaded) rm("serialised_rs_complete.jl") @@ -420,7 +420,7 @@ let (p,d), 0 <--> X end save_reaction_network("serialised_rs_incomplete.jl", rs_incomplete) - rs_incomplete_loaded = include("serialised_rs_incomplete.jl") + rs_incomplete_loaded = include("../serialised_rs_incomplete.jl") @test !ModelingToolkit.iscomplete(rs_incomplete_loaded) rm("serialised_rs_incomplete.jl") end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 9196ca91c8..a561cc973d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,6 +10,7 @@ using SafeTestsets, Test ### Run Tests ### @time begin + @time @safetestset "ReactionSystem Serialisation Balancing" begin include("miscellaneous_tests/reactionsystem_serialisation.jl") end #if GROUP == "All" || GROUP == "ModelCreation" # Tests the `ReactionSystem` structure and its properties. @time @safetestset "Reaction Structure" begin include("reactionsystem_core/reaction.jl") end @@ -37,6 +38,7 @@ using SafeTestsets, Test @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 + @time @safetestset "ReactionSystem Serialisation Balancing" begin include("miscellaneous_tests/reactionsystem_serialisation.jl") end # Tests reaction network analysis features. @time @safetestset "Conservation Laws" begin include("network_analysis/conservation_laws.jl") end From 552eb6201aeb65b9a954a4b6b04c35358f45ab8c Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 2 Jun 2024 12:28:51 -0400 Subject: [PATCH 48/67] up --- src/reactionsystem.jl | 15 +++++++++++++++ test/reactionsystem_core/reactionsystem.jl | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/src/reactionsystem.jl b/src/reactionsystem.jl index 3be6b6c06c..da65139bec 100644 --- a/src/reactionsystem.jl +++ b/src/reactionsystem.jl @@ -207,6 +207,13 @@ end ### ReactionSystem Structure ### +# Constant storing all reaction system fields (in order). Used to check whether the `ReactionSystem` +# structure have been updated (in the `reactionsystem_uptodate` function). +const reactionsystem_fields = [:eqs, :rxs, :iv, :sivs, :unknowns, :species, :ps, :var_to_name, + :observed, :name, :systems, :defaults, :connection_type, + :networkproperties, :combinatoric_ratelaws, :continuous_events, + :discrete_events, :metadata, :complete] + """ $(TYPEDEF) @@ -1025,6 +1032,14 @@ end ### General `ReactionSystem`-specific Functions ### +# Checks if the `ReactionSystem` structure have been updated without also updating the +# `reactionsystem_fields` constant. If this is the case, returns `false`. This is used in +# certain functionalities which would break if the `ReactionSystem` structure is updated without +# also updating tehse functionalities. +function reactionsystem_uptodate() + fieldnames(ReactionSystem) == reactionsystem_fields +end + # used in the `__unpacksys` function. function __unpacksys(rn) ex = :(begin end) diff --git a/test/reactionsystem_core/reactionsystem.jl b/test/reactionsystem_core/reactionsystem.jl index 7e854efcc7..95988c0cbd 100644 --- a/test/reactionsystem_core/reactionsystem.jl +++ b/test/reactionsystem_core/reactionsystem.jl @@ -740,4 +740,12 @@ let @test nameof(ModelingToolkit.get_iv(empty_network)) == :t @test length(ModelingToolkit.get_unknowns(empty_network)) == 0 @test length(ModelingToolkit.get_ps(empty_network)) == 0 +end + +# Checks that the `reactionsystem_uptodate` function work. If it does not, the ReactionSystem +# strcuture's fields have been updated, without updating the `reactionsystem_fields` costant. If so, +# there are several places in the code where the `reactionsystem_uptodate` function is called, here +# the code might need adaptation to take the updated reaction system into account. +let + @test Catalyst.reactionsystem_uptodate() end \ No newline at end of file From 241cbd5dad60ecb36548be4c86b13d397b1f92f4 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 2 Jun 2024 12:36:16 -0400 Subject: [PATCH 49/67] start tests --- test/failed_serialisation.jl | 0 test/runtests.jl | 3 +-- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 test/failed_serialisation.jl diff --git a/test/failed_serialisation.jl b/test/failed_serialisation.jl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/runtests.jl b/test/runtests.jl index a561cc973d..6d084286c5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,7 +10,6 @@ using SafeTestsets, Test ### Run Tests ### @time begin - @time @safetestset "ReactionSystem Serialisation Balancing" begin include("miscellaneous_tests/reactionsystem_serialisation.jl") end #if GROUP == "All" || GROUP == "ModelCreation" # Tests the `ReactionSystem` structure and its properties. @time @safetestset "Reaction Structure" begin include("reactionsystem_core/reaction.jl") end @@ -38,7 +37,7 @@ using SafeTestsets, Test @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 - @time @safetestset "ReactionSystem Serialisation Balancing" begin include("miscellaneous_tests/reactionsystem_serialisation.jl") end + @time @safetestset "ReactionSystem Serialisation" begin include("miscellaneous_tests/reactionsystem_serialisation.jl") end # Tests reaction network analysis features. @time @safetestset "Conservation Laws" begin include("network_analysis/conservation_laws.jl") end From 48edfb5e4ea0e78f8cb55c12f0f118d42750201d Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 2 Jun 2024 12:47:25 -0400 Subject: [PATCH 50/67] save_reaction_network -> save_reactionsystem --- src/Catalyst.jl | 2 +- src/reactionsystem.jl | 4 ++-- .../serialise_reactionsystem.jl | 8 ++++---- .../reactionsystem_serialisation.jl | 20 +++++++++---------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Catalyst.jl b/src/Catalyst.jl index ea1a9f2c23..6521d0afb0 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -188,6 +188,6 @@ include("spatial_reaction_systems/lattice_jump_systems.jl") include("reactionsystem_serialisation/serialisation_support.jl") include("reactionsystem_serialisation/serialise_fields.jl") include("reactionsystem_serialisation/serialise_reactionsystem.jl") -export save_reaction_network +export save_reactionsystem end # module diff --git a/src/reactionsystem.jl b/src/reactionsystem.jl index da65139bec..e86d536fec 100644 --- a/src/reactionsystem.jl +++ b/src/reactionsystem.jl @@ -209,10 +209,10 @@ end # Constant storing all reaction system fields (in order). Used to check whether the `ReactionSystem` # structure have been updated (in the `reactionsystem_uptodate` function). -const reactionsystem_fields = [:eqs, :rxs, :iv, :sivs, :unknowns, :species, :ps, :var_to_name, +const reactionsystem_fields = (:eqs, :rxs, :iv, :sivs, :unknowns, :species, :ps, :var_to_name, :observed, :name, :systems, :defaults, :connection_type, :networkproperties, :combinatoric_ratelaws, :continuous_events, - :discrete_events, :metadata, :complete] + :discrete_events, :metadata, :complete) """ $(TYPEDEF) diff --git a/src/reactionsystem_serialisation/serialise_reactionsystem.jl b/src/reactionsystem_serialisation/serialise_reactionsystem.jl index 20cd085d0c..91589a436a 100644 --- a/src/reactionsystem_serialisation/serialise_reactionsystem.jl +++ b/src/reactionsystem_serialisation/serialise_reactionsystem.jl @@ -1,5 +1,5 @@ """ - save_reaction_network(filename::String, rn::ReactionSystem; annotate = true, safety_check = true) + save_reactionsystem(filename::String, rn::ReactionSystem; annotate = true, safety_check = true) Save a `ReactionSystem` model to a file. The `ReactionSystem` is saved as runnable Julia code. This can both be used to save a `ReactionSystem` model, but also to write it to a file for easy inspection. @@ -18,7 +18,7 @@ Example: rn = @reaction_network begin (p,d), 0 <--> X end -save_reaction_network("rn.jls", rn) +save_reactionsystem("rn.jls", rn) ``` The model can now be loaded using ```julia @@ -32,14 +32,14 @@ Notes: - Reaction systems with components that have units cannot currently be saved. - The `ReactionSystem` is saved using *programmatic* (not DSL) format for model creation. """ -function save_reaction_network(filename::String, rn::ReactionSystem; annotate = true, safety_check = true) +function save_reactionsystem(filename::String, rn::ReactionSystem; annotate = true, safety_check = true) open(filename, "w") do file write(file, get_full_system_string(rn, annotate, true)) end if safety_check if !isequal(rn, include(joinpath(pwd(), filename))) rm(filename) - error("The serialised `ReactionSystem` is not equal to the original one. Please make a report (including the full system) at https://github.com/SciML/Catalyst.jl/issues. To disable this behaviour, please pass the `safety_check = false` argument to `save_reaction_network` (warning, this will permit the serialisation of an erroneous system).") + error("The serialised `ReactionSystem` is not equal to the original one. Please make a report (including the full system) at https://github.com/SciML/Catalyst.jl/issues. To disable this behaviour, please pass the `safety_check = false` argument to `save_reactionsystem` (warning, this will permit the serialisation of an erroneous system).") end end return nothing diff --git a/test/miscellaneous_tests/reactionsystem_serialisation.jl b/test/miscellaneous_tests/reactionsystem_serialisation.jl index 00bbcb1982..eab88d0abf 100644 --- a/test/miscellaneous_tests/reactionsystem_serialisation.jl +++ b/test/miscellaneous_tests/reactionsystem_serialisation.jl @@ -26,8 +26,8 @@ let @equations D(V) ~ 1 - V d, X --> 0 end - save_reaction_network("test_serialisation_annotated.jl", rn; safety_check = false) - save_reaction_network("test_serialisation.jl", rn; annotate = false, safety_check = false) + save_reactionsystem("test_serialisation_annotated.jl", rn; safety_check = false) + save_reactionsystem("test_serialisation.jl", rn; annotate = false, safety_check = false) # Checks equivalence. file_string_annotated = read("test_serialisation_annotated.jl", String) @@ -159,7 +159,7 @@ let rs = complete(rs1) # Loads the model and checks that it is correct. Removes the saved file - save_reaction_network("serialised_rs.jl", rs; safety_check = false) + save_reactionsystem("serialised_rs.jl", rs; safety_check = false) rs_loaded = include("../serialised_rs.jl") @test rs == rs_loaded rm("serialised_rs.jl") @@ -247,7 +247,7 @@ let # Creates model and checks it against serialised version. @named rs = ReactionSystem([], τ, [X, Y, Z, U, V, W, A, B, C, D, E, F, G], [a, b, c, d, e, f]) - save_reaction_network("serialised_rs.jl", rs; safety_check = false) + save_reactionsystem("serialised_rs.jl", rs; safety_check = false) @test rs == include("../serialised_rs.jl") rm("serialised_rs.jl") end @@ -348,9 +348,9 @@ let rs = complete(rs_1) # Checks that the correct system is saved (both complete and incomplete ones). - save_reaction_network("serialised_rs_incomplete.jl", rs_1; safety_check = false) + save_reactionsystem("serialised_rs_incomplete.jl", rs_1; safety_check = false) @test isequal(rs_1, include("../serialised_rs_incomplete.jl")) - save_reaction_network("serialised_rs_complete.jl", rs; safety_check = false) + save_reactionsystem("serialised_rs_complete.jl", rs; safety_check = false) @test isequal(rs, include("../serialised_rs_complete.jl")) rm("serialised_rs_incomplete.jl") rm("serialised_rs_complete.jl") @@ -379,7 +379,7 @@ let end # Checks that serialisation works. - save_reaction_network("serialised_rs.jl", rs; safety_check = false) + save_reactionsystem("serialised_rs.jl", rs; safety_check = false) @test_broken isequal(rs, include("../serialised_rs.jl")) rm("serialised_rs.jl") end @@ -400,7 +400,7 @@ let @named osys = ODESystem([eq], t) @named rs = ReactionSystem(rxs, t; systems = [osys]) - @test_throws Exception save_reaction_network("failed_serialisation.jl", rs) + @test_throws Exception save_reactionsystem("failed_serialisation.jl", rs) end # Checks that completeness is recorded correctly. @@ -410,7 +410,7 @@ let rs_complete = @reaction_network begin (p,d), 0 <--> X end - save_reaction_network("serialised_rs_complete.jl", rs_complete) + save_reactionsystem("serialised_rs_complete.jl", rs_complete) rs_complete_loaded = include("../serialised_rs_complete.jl") @test ModelingToolkit.iscomplete(rs_complete_loaded) rm("serialised_rs_complete.jl") @@ -419,7 +419,7 @@ let rs_incomplete = @network_component begin (p,d), 0 <--> X end - save_reaction_network("serialised_rs_incomplete.jl", rs_incomplete) + save_reactionsystem("serialised_rs_incomplete.jl", rs_incomplete) rs_incomplete_loaded = include("../serialised_rs_incomplete.jl") @test !ModelingToolkit.iscomplete(rs_incomplete_loaded) rm("serialised_rs_incomplete.jl") From b8b5656ff5fb91c5454c31fcaddf7908d27bd7f5 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 2 Jun 2024 12:51:50 -0400 Subject: [PATCH 51/67] add reactionsystem struct check function --- src/reactionsystem.jl | 8 +++++--- .../serialise_reactionsystem.jl | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/reactionsystem.jl b/src/reactionsystem.jl index e86d536fec..17823da694 100644 --- a/src/reactionsystem.jl +++ b/src/reactionsystem.jl @@ -208,7 +208,7 @@ end ### ReactionSystem Structure ### # Constant storing all reaction system fields (in order). Used to check whether the `ReactionSystem` -# structure have been updated (in the `reactionsystem_uptodate` function). +# structure have been updated (in the `reactionsystem_uptodate_check` function). const reactionsystem_fields = (:eqs, :rxs, :iv, :sivs, :unknowns, :species, :ps, :var_to_name, :observed, :name, :systems, :defaults, :connection_type, :networkproperties, :combinatoric_ratelaws, :continuous_events, @@ -1036,8 +1036,10 @@ end # `reactionsystem_fields` constant. If this is the case, returns `false`. This is used in # certain functionalities which would break if the `ReactionSystem` structure is updated without # also updating tehse functionalities. -function reactionsystem_uptodate() - fieldnames(ReactionSystem) == reactionsystem_fields +function reactionsystem_uptodate_check() + if fieldnames(ReactionSystem) != reactionsystem_fields + @warn "The `ReactionSystem` strcuture have been modified without this being taken into account in the functionality you are attempting to use. Please report this at https://github.com/SciML/Catalyst.jl/issues. Proceed with cautioun, as there might be errors in whichever funcionality you are attempting to use". + end end # used in the `__unpacksys` function. diff --git a/src/reactionsystem_serialisation/serialise_reactionsystem.jl b/src/reactionsystem_serialisation/serialise_reactionsystem.jl index 91589a436a..3dc61c5c75 100644 --- a/src/reactionsystem_serialisation/serialise_reactionsystem.jl +++ b/src/reactionsystem_serialisation/serialise_reactionsystem.jl @@ -33,6 +33,7 @@ Notes: - The `ReactionSystem` is saved using *programmatic* (not DSL) format for model creation. """ function save_reactionsystem(filename::String, rn::ReactionSystem; annotate = true, safety_check = true) + reactionsystem_uptodate_check() open(filename, "w") do file write(file, get_full_system_string(rn, annotate, true)) end From 882f6ae7e63c18104f8443a55ef8f823424a7d39 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 2 Jun 2024 12:52:47 -0400 Subject: [PATCH 52/67] test `reactionsystem_uptodate_check` --- test/reactionsystem_core/reactionsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/reactionsystem_core/reactionsystem.jl b/test/reactionsystem_core/reactionsystem.jl index 95988c0cbd..6876026e0b 100644 --- a/test/reactionsystem_core/reactionsystem.jl +++ b/test/reactionsystem_core/reactionsystem.jl @@ -747,5 +747,5 @@ end # there are several places in the code where the `reactionsystem_uptodate` function is called, here # the code might need adaptation to take the updated reaction system into account. let - @test Catalyst.reactionsystem_uptodate() + @test_nowarn Catalyst.reactionsystem_uptodate_check() end \ No newline at end of file From 6197abf9cec3492f6d8467b5a8c2702c39312e9b Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 2 Jun 2024 12:56:16 -0400 Subject: [PATCH 53/67] fix --- src/reactionsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reactionsystem.jl b/src/reactionsystem.jl index 17823da694..edb0f9fedf 100644 --- a/src/reactionsystem.jl +++ b/src/reactionsystem.jl @@ -1038,7 +1038,7 @@ end # also updating tehse functionalities. function reactionsystem_uptodate_check() if fieldnames(ReactionSystem) != reactionsystem_fields - @warn "The `ReactionSystem` strcuture have been modified without this being taken into account in the functionality you are attempting to use. Please report this at https://github.com/SciML/Catalyst.jl/issues. Proceed with cautioun, as there might be errors in whichever funcionality you are attempting to use". + @warn "The `ReactionSystem` strcuture have been modified without this being taken into account in the functionality you are attempting to use. Please report this at https://github.com/SciML/Catalyst.jl/issues. Proceed with cautioun, as there might be errors in whichever funcionality you are attempting to use." end end From f02e90d163c498d503890eab8667fcd9e0a8968e Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 3 Jun 2024 09:56:23 -0400 Subject: [PATCH 54/67] init --- docs/Project.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/Project.toml b/docs/Project.toml index ebb086fda6..246727a23e 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -5,7 +5,6 @@ CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Catalyst = "479239e8-5488-4da2-87a7-35f2df7eef83" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DiffEqParamEstim = "1130ab10-4a5a-5621-a13d-e4788d82bd4c" -DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DynamicalSystems = "61744808-ddfa-5f27-97ff-6e42cc95d634" From c7eef72eb922e909f1d2436970f5b6bd0093cb5b Mon Sep 17 00:00:00 2001 From: Torkel Date: Wed, 17 Jan 2024 17:58:33 -0500 Subject: [PATCH 55/67] init --- .../model_file_loading_and_export.md | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 docs/src/catalyst_applications/model_file_loading_and_export.md diff --git a/docs/src/catalyst_applications/model_file_loading_and_export.md b/docs/src/catalyst_applications/model_file_loading_and_export.md new file mode 100644 index 0000000000..cabd837a7f --- /dev/null +++ b/docs/src/catalyst_applications/model_file_loading_and_export.md @@ -0,0 +1,101 @@ +# Loading Chemical Reaction Network Models from Files +Catalyst stores chemical reaction network (CRN) models in `ReactionSystem` structures. This tutorial describes how to load such `ReactionSystem`s from, and save them to, files. This can be used to save models between Julia sessions, or transfer them from one session to another. Furthermore, to facilitate the computation modelling of CRNs, several standardised file formats have been created to represent CRN models (e.g. [SBML](https://sbml.org/)). These enables e.g. CRN models to be shared between different software and programming languages. While Catalyst itself does not have the functionality for loading such files, we will here (briefly) introduce a few packages that can load files to Catalyst `ReactionSystem`s. + +## Saving Catalyst models to, and loading them from, Julia files +Catalyst provides a `save_reaction_system` function, enabling the user to save a `ReactionSystem` to a file. Here we demonstrate it by first creating a simple catalysis network +```@example file_handling_1 +using Catalyst +rn = @reaction_network begin + kB, S + E --> SE + kD, SE --> S + E + kP, SE --> P + E +end +``` +and next saving it to a file +```@example file_handling_1 +save_reaction_system("reaction_network.jl", rn) +``` +Here, `save_reaction_system`'s first argument is the path to the file where we wish to save it. The second argument is the `ReactionSystem` we wish to save. To load the file, we use Julia's `include` function: +```@example file_handling_1 +loaded_rn = include("reaction_network.jl") +``` + +!!! note + The destination file can be in a folder. E.g. `save_reaction_system("my_folder/reaction_network.jl", rn)` saves the model to the file "reaction_network.jl" in the folder "my_folder". + +`include` is used to execute the Julia code from any file. This means that `save_reaction_system` actually saves the model as executable code which re-generates the exact model which was saved (this is the reason why we use the ".jl" extension for the saved file). Indeed, if we print the file we can confirm this: +```@example file_handling_1 +println(read("reaction_network.jl", String)) +rm("reaction_network.jl") # hide +``` + +## Loading and Saving arbitrary Julia variables using Serialization.jl +Julia provide a general and lightweight interface for loading and saving Julia structures to and from files that it can be good to be aware of. It is called [Serialization.jl](https://docs.julialang.org/en/v1/stdlib/Serialization/) and provides two functions, `serialize` and `deserialize`. The first allow us to write a Julia structure to a file. E.g. if we wish to save a parameter set associated with our model, we can use +```@example file_handling_2 +using Serialization +ps = [:kB => 1.0, :kD => 0.1, :kP => 2.0] +serialize("saved_parameters.jls", ps) +``` +Here, we use the extension ".jls" (standing for **J**u**L**ia **S**erialization), however, any can be used. To load a structure, we can then use +```@example file_handling_2 +loaded_sol = deserialize("saved_parameters.jls") +``` + +## [Loading .net files usings ReactionNetworkImporters.jl](@id file_loading_rni_net) +A general-purpose format for storing CRN models are so-called .net files. These can be generated by e.g. [BioNetGen](https://bionetgen.org/). The [ReactionNetworkImporters.jl](https://github.com/SciML/ReactionNetworkImporters.jl) package enables the loading of such files to Catalyst `ReactionSystem`. Here we load a [repressilator](https://en.wikipedia.org/wiki/Repressilator) model stored in the "repressilator.net" file: +```@example file_handling_3 +using ReactionNetworkImporters +cp("../assets/model_files/repressilator.net", "repressilator.net") # hide +prn = loadrxnetwork(BNGNetwork(), "repressilator.net") +rm("repressilator.net") # hide +prn # hide +``` +Here, .net files not only contain information regarding the reaction network itself, but also the numeric values (initial conditions and parameter values) required for simulating it. Hence, `loadrxnetwork` generates a `ParsedReactionNetwork` structure, containing all this information. You can access the model as `prn.rn`, the initial conditions as `prn.u0`, and the parameter values as `prn.p`. Furthermore, these initial conditions and parameter values are also made [*default* values](@ref ref) of the model. + +A parsed reaction network can be provided directly to various problem types for simulation. E.g. here we perform an ODE simulation of our repressilator model: +```@example file_handling_3 +using Catalyst, OrdinaryDiffEq, Plots +tspan = (0.0, 1000.0) +oprob = ODEProblem(prn, tspan) +sol = solve(oprob) +plot(sol) +``` + +!!! note + It should be noted that .net files supports a wide range of potential model features, not all of which are currently supported by ReactionNetworkImporters. Hence, there might be some .net files which `loadrxnetwork` will not be able to load. + +A more detailed description of ReactionNetworkImporter's features can be found in its [documentation](https://docs.sciml.ai/ReactionNetworkImporters/stable/). + +## Loading SBML files using SBMLImporter.jl and SBMLToolkit.jl +The Systems Biology Markup Language (SBML) is the most widespread format for representing CRN models. Currently, there exists two different Julia packages, [SBMLImporter.jl](https://github.com/sebapersson/SBMLImporter.jl) and [SBMLToolkit.jl](https://github.com/SciML/SBMLToolkit.jl), both of which are able to load SBML files to Catalyst `ReactionSystem` structures. SBML is able to represent a *very* wide range of model featuresSome of these are not supported by these packages (or Catalyst). Hence, there exist SBML files (typically containing obscure model features such as events with time delays) that currently cannot be loaded into Catalyst models. + +SBMLImporter's `load_SBML` function can be used to load SBML files. Here, we load a [Brusselator](https://en.wikipedia.org/wiki/Brusselator) model stored in the "brusselator.xml" file: +```@example file_handling_4 +using SBMLImporter +cp("../assets/model_files/brusselator.xml", "brusselator.xml") # hide +prn, cbs = load_SBML("brusselator.xml") +rm("brusselator.xml") # hide +prn, cbs # hide +``` +Here, while [ReactionNetworkImporters generates a `ParsedReactionSystem` only](@ref file_loading_rni_net), SBMLImporter generates a `ParsedReactionSystem` (here stored in `prn`) and a [so-called `CallbackSet`](https://docs.sciml.ai/DiffEqDocs/stable/features/callback_functions/#CallbackSet) (here stored in `cbs`). While `prn` can be used to create various problems, when we simulate them, we must also supply `cbs`. E.g. to simulate our brusselator we use: +```@example file_handling_4 +using Catalyst, OrdinaryDiffEq, Plots +tspan = (0.0, 1000.0) +oprob = ODEProblem(prn, tspan) +sol = solve(oprob; cb=cbs) +plot(sol) +``` + +A more detailed description of SBMLImporter's features can be found in its [documentation](https://docs.sciml.ai/ReactionNetworkImporters/stable/). + +### SBMLImporter and SBMLToolkit +Above, we described how to use SBMLImporter to import SBML files. Alternatively, SBMLToolkit can be used instead. It has a slightly different syntax, which is described in its [documentation](https://github.com/SciML/SBMLToolkit.jl). A short comparison of the two packages can be found [here](https://github.com/sebapersson/SBMLImporter.jl?tab=readme-ov-file#differences-compared-to-sbmltoolkit). Generally, while they both perform well, we note that for *jump simulations* SBMLImporter is preferable (its way for internally representing reaction event enables more performant jump simulations). + +## Loading models from matrix representation using ReactionNetworkImporters.jl +While CRN models can be represented through various file formats, they can also be represented in various matrix forms. E.g. a CRN with $m$ species and $n$ reactions (and with constant rates) can be represented with either +- An $mxn$ substrate matrix (with each species's substrate stoichiometry in each reaction) and an $nxm$ product matrix (with each species's product stoichiometry in each reaction). + +Or +- An $mxn$ complex stoichiometric matrix (...) and a $2mxn$ incidence matrix (...). + +The advantage with these forms is that they offer a compact and very general way to represent a large class of CRNs. ReactionNetworkImporters have the functionality for converting matrices of these forms directly into Catalyst `ReactionSystem` models. Instructions on how to do this are available in [ReactionNetworkImporter's documentation](https://docs.sciml.ai/ReactionNetworkImporters/stable/#Loading-a-matrix-representation). \ No newline at end of file From 399374d094a415b82e6612c7df17906c98177ce2 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 2 Jun 2024 12:46:33 -0400 Subject: [PATCH 56/67] save progress --- docs/pages.jl | 1 + .../model_file_loading_and_export.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) rename docs/src/{catalyst_applications => model_creation}/model_file_loading_and_export.md (96%) diff --git a/docs/pages.jl b/docs/pages.jl index 65ea437951..2572bcb69b 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -13,6 +13,7 @@ pages = Any[ #"model_creation/constraint_equations.md", # Events. #"model_creation/parametric_stoichiometry.md",# Distributed parameters, rates, and initial conditions. + "model_creation/model_file_loading_and_export.md",# Distributed parameters, rates, and initial conditions. # Loading and writing models to files. "model_creation/model_visualisation.md", #"model_creation/network_analysis.md", diff --git a/docs/src/catalyst_applications/model_file_loading_and_export.md b/docs/src/model_creation/model_file_loading_and_export.md similarity index 96% rename from docs/src/catalyst_applications/model_file_loading_and_export.md rename to docs/src/model_creation/model_file_loading_and_export.md index cabd837a7f..b492cd9843 100644 --- a/docs/src/catalyst_applications/model_file_loading_and_export.md +++ b/docs/src/model_creation/model_file_loading_and_export.md @@ -1,5 +1,5 @@ # Loading Chemical Reaction Network Models from Files -Catalyst stores chemical reaction network (CRN) models in `ReactionSystem` structures. This tutorial describes how to load such `ReactionSystem`s from, and save them to, files. This can be used to save models between Julia sessions, or transfer them from one session to another. Furthermore, to facilitate the computation modelling of CRNs, several standardised file formats have been created to represent CRN models (e.g. [SBML](https://sbml.org/)). These enables e.g. CRN models to be shared between different software and programming languages. While Catalyst itself does not have the functionality for loading such files, we will here (briefly) introduce a few packages that can load files to Catalyst `ReactionSystem`s. +Catalyst stores chemical reaction network (CRN) models in `ReactionSystem` structures. This tutorial describes how to load such `ReactionSystem`s from, and save them to, files. This can be used to save models between Julia sessions, or transfer them from one session to another. Furthermore, to facilitate the computation modelling of CRNs, several standardised file formats have been created to represent CRN models (e.g. [SBML](https://sbml.org/)). These enables CRN models to be shared between different softwares and programming languages. While Catalyst itself does not have the functionality for loading such files, we will here (briefly) introduce a few packages that can load files to Catalyst `ReactionSystem`s. ## Saving Catalyst models to, and loading them from, Julia files Catalyst provides a `save_reaction_system` function, enabling the user to save a `ReactionSystem` to a file. Here we demonstrate it by first creating a simple catalysis network From a52ced86ec1b8abee3a7d117cfb7c0f86b3fa5bd Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 2 Jun 2024 13:27:41 -0400 Subject: [PATCH 57/67] add model files --- docs/src/assets/model_files/brusselator.xml | 161 ++++++++++++++++++ docs/src/assets/model_files/repressilator.net | 93 ++++++++++ .../model_file_loading_and_export.md | 37 ++-- 3 files changed, 275 insertions(+), 16 deletions(-) create mode 100644 docs/src/assets/model_files/brusselator.xml create mode 100644 docs/src/assets/model_files/repressilator.net diff --git a/docs/src/assets/model_files/brusselator.xml b/docs/src/assets/model_files/brusselator.xml new file mode 100644 index 0000000000..228706ff9b --- /dev/null +++ b/docs/src/assets/model_files/brusselator.xml @@ -0,0 +1,161 @@ + + + + + + + + + + + 2024-01-09T21:46:45Z + + + + + + + + + 2024-01-09T21:46:45Z + + + 2024-01-09T21:46:45Z + + + + + + + + + + v + + v + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + compartment + k1 + + + X + 2 + + Y + + + + + + + + + + + + + + + + + + + compartment + B + X + + + + + + + + + + + + + compartment + k1 + X + + + + + + + + + + + + + + + + compartment + + Constant_flux__irreversible + A + + + + + + + + \ No newline at end of file diff --git a/docs/src/assets/model_files/repressilator.net b/docs/src/assets/model_files/repressilator.net new file mode 100644 index 0000000000..769d7a71f0 --- /dev/null +++ b/docs/src/assets/model_files/repressilator.net @@ -0,0 +1,93 @@ +# Created by BioNetGen 2.3.1 +begin parameters + 1 Na 6.022e23 # Constant + 2 V 1.4e-15 # Constant + 3 c0 1e9 # Constant + 4 c1 224 # Constant + 5 c2 9 # Constant + 6 c3 0.5 # Constant + 7 c4 5e-4 # Constant + 8 c5 0.167 # Constant + 9 c6 ln(2)/120 # Constant + 10 c7 ln(2)/600 # Constant + 11 tF 1e-4 # Constant + 12 rF 1000 # Constant + 13 pF 1000 # Constant + 14 _rateLaw1 (((c0/Na)/V)*tF)/pF # ConstantExpression + 15 _rateLaw2 c1*tF # ConstantExpression + 16 _rateLaw3 (((c0/Na)/V)*tF)/pF # ConstantExpression + 17 _rateLaw4 c2*tF # ConstantExpression + 18 _rateLaw5 c3*rF # ConstantExpression + 19 _rateLaw6 c4*rF # ConstantExpression + 20 _rateLaw7 (c5/rF)*pF # ConstantExpression + 21 _rateLaw8 (((c0/Na)/V)*tF)/pF # ConstantExpression + 22 _rateLaw9 c1*tF # ConstantExpression + 23 _rateLaw10 (((c0/Na)/V)*tF)/pF # ConstantExpression + 24 _rateLaw11 c2*tF # ConstantExpression + 25 _rateLaw12 c3*rF # ConstantExpression + 26 _rateLaw13 c4*rF # ConstantExpression + 27 _rateLaw14 (c5/rF)*pF # ConstantExpression + 28 _rateLaw15 (((c0/Na)/V)*tF)/pF # ConstantExpression + 29 _rateLaw16 c1*tF # ConstantExpression + 30 _rateLaw17 (((c0/Na)/V)*tF)/pF # ConstantExpression + 31 _rateLaw18 c2*tF # ConstantExpression + 32 _rateLaw19 c3*rF # ConstantExpression + 33 _rateLaw20 c4*rF # ConstantExpression + 34 _rateLaw21 (c5/rF)*pF # ConstantExpression +end parameters +begin species + 1 Null() 1 + 2 gTetR(lac!1,lac!2).pLacI(tet!1).pLacI(tet!2) 1 + 3 gCI(tet!1,tet!2).pTetR(cI!1).pTetR(cI!2) 1 + 4 gLacI(cI!1,cI!2).pCI(lac!1).pCI(lac!2) 1 + 5 mTetR() 3163 + 6 mCI() 6819 + 7 mLacI() 129 + 8 pTetR(cI) 183453 + 9 pCI(lac) 2006198 + 10 pLacI(tet) 165670 + 11 gTetR(lac!1,lac).pLacI(tet!1) 0 + 12 gCI(tet!1,tet).pTetR(cI!1) 0 + 13 gLacI(cI!1,cI).pCI(lac!1) 0 + 14 gTetR(lac,lac) 0 + 15 gCI(tet,tet) 0 + 16 gLacI(cI,cI) 0 +end species +begin reactions + 1 2 10,11 2*_rateLaw4 #_reverse__R2 + 2 2 2,5 _rateLaw6 #_R4 + 3 5 5,8 _rateLaw7 #_R5 + 4 1,5 1 c6 #_R6 + 5 1,8 1 c7 #_R7 + 6 3 8,12 2*_rateLaw11 #_reverse__R9 + 7 3 3,6 _rateLaw13 #_R11 + 8 6 6,9 _rateLaw14 #_R12 + 9 1,6 1 c6 #_R13 + 10 1,9 1 c7 #_R14 + 11 4 9,13 2*_rateLaw18 #_reverse__R16 + 12 4 4,7 _rateLaw20 #_R18 + 13 7 7,10 _rateLaw21 #_R19 + 14 1,7 1 c6 #_R20 + 15 1,10 1 c7 #_R21 + 16 11 10,14 _rateLaw2 #_reverse__R1 + 17 10,11 2 _rateLaw3 #_R2 + 18 11 5,11 _rateLaw6 #_R4 + 19 12 8,15 _rateLaw9 #_reverse__R8 + 20 8,12 3 _rateLaw10 #_R9 + 21 12 6,12 _rateLaw13 #_R11 + 22 13 9,16 _rateLaw16 #_reverse__R15 + 23 9,13 4 _rateLaw17 #_R16 + 24 13 7,13 _rateLaw20 #_R18 + 25 10,14 11 2*_rateLaw1 #_R1 + 26 14 5,14 _rateLaw5 #_R3 + 27 8,15 12 2*_rateLaw8 #_R8 + 28 15 6,15 _rateLaw12 #_R10 + 29 9,16 13 2*_rateLaw15 #_R15 + 30 16 7,16 _rateLaw19 #_R17 +end reactions +begin groups + 1 pTetR 8 + 2 pCI 9 + 3 pLacI 10 + 4 NULL 1 +end groups \ No newline at end of file diff --git a/docs/src/model_creation/model_file_loading_and_export.md b/docs/src/model_creation/model_file_loading_and_export.md index b492cd9843..1d32293587 100644 --- a/docs/src/model_creation/model_file_loading_and_export.md +++ b/docs/src/model_creation/model_file_loading_and_export.md @@ -2,7 +2,7 @@ Catalyst stores chemical reaction network (CRN) models in `ReactionSystem` structures. This tutorial describes how to load such `ReactionSystem`s from, and save them to, files. This can be used to save models between Julia sessions, or transfer them from one session to another. Furthermore, to facilitate the computation modelling of CRNs, several standardised file formats have been created to represent CRN models (e.g. [SBML](https://sbml.org/)). These enables CRN models to be shared between different softwares and programming languages. While Catalyst itself does not have the functionality for loading such files, we will here (briefly) introduce a few packages that can load files to Catalyst `ReactionSystem`s. ## Saving Catalyst models to, and loading them from, Julia files -Catalyst provides a `save_reaction_system` function, enabling the user to save a `ReactionSystem` to a file. Here we demonstrate it by first creating a simple catalysis network +Catalyst provides a `save_reactionsystem` function, enabling the user to save a `ReactionSystem` to a file. Here we demonstrate this by first creating a [simple cross-coupling model](@ref basic_CRN_library_cc): ```@example file_handling_1 using Catalyst rn = @reaction_network begin @@ -13,21 +13,24 @@ end ``` and next saving it to a file ```@example file_handling_1 -save_reaction_system("reaction_network.jl", rn) +save_reactionsystem("cross_coupling.jl", rn) ``` -Here, `save_reaction_system`'s first argument is the path to the file where we wish to save it. The second argument is the `ReactionSystem` we wish to save. To load the file, we use Julia's `include` function: +Here, `save_reactionsystem`'s first argument is the path to the file where we wish to save it. The second argument is the `ReactionSystem` we wish to save. To load the file, we use Julia's `include` function: ```@example file_handling_1 -loaded_rn = include("reaction_network.jl") +loaded_rn = include("cross_coupling.jl") ``` !!! note - The destination file can be in a folder. E.g. `save_reaction_system("my_folder/reaction_network.jl", rn)` saves the model to the file "reaction_network.jl" in the folder "my_folder". + The destination file can be in a folder. E.g. `save_reactionsystem("my_folder/reaction_network.jl", rn)` saves the model to the file "reaction_network.jl" in the folder "my_folder". -`include` is used to execute the Julia code from any file. This means that `save_reaction_system` actually saves the model as executable code which re-generates the exact model which was saved (this is the reason why we use the ".jl" extension for the saved file). Indeed, if we print the file we can confirm this: +Here, `include` is used to execute the Julia code from any file. This means that `save_reactionsystem` actually saves the model as executable code which re-generates the exact model which was saved (this is the reason why we use the ".jl" extension for the saved file). Indeed, if we print the file we can confirm this: ```@example file_handling_1 -println(read("reaction_network.jl", String)) -rm("reaction_network.jl") # hide +read("cross_coupling.jl", String) ``` +```@example file_handling_1 +rm("cross_coupling.jl") +``` +This functionality can also be used or print a model to simplify checking its various components. ## Loading and Saving arbitrary Julia variables using Serialization.jl Julia provide a general and lightweight interface for loading and saving Julia structures to and from files that it can be good to be aware of. It is called [Serialization.jl](https://docs.julialang.org/en/v1/stdlib/Serialization/) and provides two functions, `serialize` and `deserialize`. The first allow us to write a Julia structure to a file. E.g. if we wish to save a parameter set associated with our model, we can use @@ -42,7 +45,7 @@ loaded_sol = deserialize("saved_parameters.jls") ``` ## [Loading .net files usings ReactionNetworkImporters.jl](@id file_loading_rni_net) -A general-purpose format for storing CRN models are so-called .net files. These can be generated by e.g. [BioNetGen](https://bionetgen.org/). The [ReactionNetworkImporters.jl](https://github.com/SciML/ReactionNetworkImporters.jl) package enables the loading of such files to Catalyst `ReactionSystem`. Here we load a [repressilator](https://en.wikipedia.org/wiki/Repressilator) model stored in the "repressilator.net" file: +A general-purpose format for storing CRN models are so-called .net files. These can be generated by e.g. [BioNetGen](https://bionetgen.org/). The [ReactionNetworkImporters.jl](https://github.com/SciML/ReactionNetworkImporters.jl) package enables the loading of such files to Catalyst `ReactionSystem`. Here we load a [Repressilator](@ref basic_CRN_library_repressilator) model stored in the "repressilator.net" file: ```@example file_handling_3 using ReactionNetworkImporters cp("../assets/model_files/repressilator.net", "repressilator.net") # hide @@ -50,16 +53,18 @@ prn = loadrxnetwork(BNGNetwork(), "repressilator.net") rm("repressilator.net") # hide prn # hide ``` -Here, .net files not only contain information regarding the reaction network itself, but also the numeric values (initial conditions and parameter values) required for simulating it. Hence, `loadrxnetwork` generates a `ParsedReactionNetwork` structure, containing all this information. You can access the model as `prn.rn`, the initial conditions as `prn.u0`, and the parameter values as `prn.p`. Furthermore, these initial conditions and parameter values are also made [*default* values](@ref ref) of the model. +Here, .net files not only contain information regarding the reaction network itself, but also the numeric values (initial conditions and parameter values) required for simulating it. Hence, `loadrxnetwork` generates a `ParsedReactionNetwork` structure, containing all this information. You can access the model as `prn.rn`, the initial conditions as `prn.u0`, and the parameter values as `prn.p`. Furthermore, these initial conditions and parameter values are also made [*default* values](@ref dsl_advanced_options_default_vals) of the model. -A parsed reaction network can be provided directly to various problem types for simulation. E.g. here we perform an ODE simulation of our repressilator model: +A parsed reaction network's content can then be provided to various problem types for simulation. E.g. here we perform an ODE simulation of our repressilator model: ```@example file_handling_3 using Catalyst, OrdinaryDiffEq, Plots -tspan = (0.0, 1000.0) -oprob = ODEProblem(prn, tspan) +tspan = (0.0, 100.0) +oprob = ODEProblem(prn.rn, Float64[], tspan, Float64[]) sol = solve(oprob) plot(sol) ``` +Note that, as all initial conditions and parameters have default values, we can provide empty vectors for these into our `ODEProblem`. + !!! note It should be noted that .net files supports a wide range of potential model features, not all of which are currently supported by ReactionNetworkImporters. Hence, there might be some .net files which `loadrxnetwork` will not be able to load. @@ -69,7 +74,7 @@ A more detailed description of ReactionNetworkImporter's features can be found i ## Loading SBML files using SBMLImporter.jl and SBMLToolkit.jl The Systems Biology Markup Language (SBML) is the most widespread format for representing CRN models. Currently, there exists two different Julia packages, [SBMLImporter.jl](https://github.com/sebapersson/SBMLImporter.jl) and [SBMLToolkit.jl](https://github.com/SciML/SBMLToolkit.jl), both of which are able to load SBML files to Catalyst `ReactionSystem` structures. SBML is able to represent a *very* wide range of model featuresSome of these are not supported by these packages (or Catalyst). Hence, there exist SBML files (typically containing obscure model features such as events with time delays) that currently cannot be loaded into Catalyst models. -SBMLImporter's `load_SBML` function can be used to load SBML files. Here, we load a [Brusselator](https://en.wikipedia.org/wiki/Brusselator) model stored in the "brusselator.xml" file: +SBMLImporter's `load_SBML` function can be used to load SBML files. Here, we load a [Brusselator](@ref basic_CRN_library_brusselator) model stored in the "brusselator.xml" file: ```@example file_handling_4 using SBMLImporter cp("../assets/model_files/brusselator.xml", "brusselator.xml") # hide @@ -81,8 +86,8 @@ Here, while [ReactionNetworkImporters generates a `ParsedReactionSystem` only](@ ```@example file_handling_4 using Catalyst, OrdinaryDiffEq, Plots tspan = (0.0, 1000.0) -oprob = ODEProblem(prn, tspan) -sol = solve(oprob; cb=cbs) +oprob = ODEProblem(prn.rn, Float64[], tspan, Float64[]) +sol = solve(oprob; callback = cbs) plot(sol) ``` From 8d7c79f95197d7edbacf70202ac91da135501903 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 2 Jun 2024 15:01:46 -0400 Subject: [PATCH 58/67] up --- .../assets/brusselator_sim_SBMLImporter.svg | 50 +++++++ ...essilator_sim_ReactionNetworkImporters.svg | 52 ++++++++ .../model_file_loading_and_export.md | 125 +++++++++++++----- 3 files changed, 193 insertions(+), 34 deletions(-) create mode 100644 docs/src/assets/brusselator_sim_SBMLImporter.svg create mode 100644 docs/src/assets/repressilator_sim_ReactionNetworkImporters.svg diff --git a/docs/src/assets/brusselator_sim_SBMLImporter.svg b/docs/src/assets/brusselator_sim_SBMLImporter.svg new file mode 100644 index 0000000000..ea99a21681 --- /dev/null +++ b/docs/src/assets/brusselator_sim_SBMLImporter.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/assets/repressilator_sim_ReactionNetworkImporters.svg b/docs/src/assets/repressilator_sim_ReactionNetworkImporters.svg new file mode 100644 index 0000000000..adb9b1262c --- /dev/null +++ b/docs/src/assets/repressilator_sim_ReactionNetworkImporters.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/model_creation/model_file_loading_and_export.md b/docs/src/model_creation/model_file_loading_and_export.md index 1d32293587..9812ab97f0 100644 --- a/docs/src/model_creation/model_file_loading_and_export.md +++ b/docs/src/model_creation/model_file_loading_and_export.md @@ -1,14 +1,14 @@ # Loading Chemical Reaction Network Models from Files -Catalyst stores chemical reaction network (CRN) models in `ReactionSystem` structures. This tutorial describes how to load such `ReactionSystem`s from, and save them to, files. This can be used to save models between Julia sessions, or transfer them from one session to another. Furthermore, to facilitate the computation modelling of CRNs, several standardised file formats have been created to represent CRN models (e.g. [SBML](https://sbml.org/)). These enables CRN models to be shared between different softwares and programming languages. While Catalyst itself does not have the functionality for loading such files, we will here (briefly) introduce a few packages that can load files to Catalyst `ReactionSystem`s. +Catalyst stores chemical reaction network (CRN) models in `ReactionSystem` structures. This tutorial describes how to load such `ReactionSystem`s from, and save them to, files. This can be used to save models between Julia sessions, or transfer them from one session to another. Furthermore, to facilitate the computation modelling of CRNs, several standardised file formats have been created to represent CRN models (e.g. [SBML](https://sbml.org/)). This enables CRN models to be shared between different softwares and programming languages. While Catalyst itself does not have the functionality for loading such files, we will here (briefly) introduce a few packages that can load different file types to Catalyst `ReactionSystem`s. ## Saving Catalyst models to, and loading them from, Julia files Catalyst provides a `save_reactionsystem` function, enabling the user to save a `ReactionSystem` to a file. Here we demonstrate this by first creating a [simple cross-coupling model](@ref basic_CRN_library_cc): ```@example file_handling_1 using Catalyst -rn = @reaction_network begin - kB, S + E --> SE - kD, SE --> S + E - kP, SE --> P + E +cc_system = @reaction_network begin + k₁, S₁ + C --> S₁C + k₂, S₁C + S₂ --> CP + k₃, CP --> C + P end ``` and next saving it to a file @@ -18,78 +18,101 @@ save_reactionsystem("cross_coupling.jl", rn) Here, `save_reactionsystem`'s first argument is the path to the file where we wish to save it. The second argument is the `ReactionSystem` we wish to save. To load the file, we use Julia's `include` function: ```@example file_handling_1 loaded_rn = include("cross_coupling.jl") +rm("cross_coupling.jl") # hide +loaded_rn # hide ``` !!! note - The destination file can be in a folder. E.g. `save_reactionsystem("my_folder/reaction_network.jl", rn)` saves the model to the file "reaction_network.jl" in the folder "my_folder". + The destination file can be in a folder. E.g. `save_reactionsystem("my\_folder/reaction_network.jl", rn)` saves the model to the file "reaction\_network.jl" in the folder "my_folder". -Here, `include` is used to execute the Julia code from any file. This means that `save_reactionsystem` actually saves the model as executable code which re-generates the exact model which was saved (this is the reason why we use the ".jl" extension for the saved file). Indeed, if we print the file we can confirm this: -```@example file_handling_1 -read("cross_coupling.jl", String) +Here, `include` is used to execute the Julia code from any file. This means that `save_reactionsystem` actually saves the model as executable code which re-generates the exact model which was saved (this is the reason why we use the ".jl" extension for the saved file). Indeed, we can confirm this if we check what is printed in the file: ``` -```@example file_handling_1 -rm("cross_coupling.jl") +let + +# Independent variable: +@variables t + +# Parameters: +ps = @parameters kB kD kP + +# Species: +sps = @species S(t) E(t) SE(t) P(t) + +# Reactions: +rxs = [ + Reaction(kB, [S, E], [SE], [1, 1], [1]), + Reaction(kD, [SE], [S, E], [1], [1, 1]), + Reaction(kP, [SE], [P, E], [1], [1, 1]) +] + +# Declares ReactionSystem model: +rs = ReactionSystem(rxs, t, sps, ps; name = Symbol("##ReactionSystem#12592")) +complete(rs) + +end ``` -This functionality can also be used or print a model to simplify checking its various components. +!!! note + The code that `save_reactionsystem` prints uses [programmatic modelling](@ref ref) to generate the written model. + +In addition to transferring models between Julia sessions, the `save_reactionsystem` function can also be used or print a model to a text file where you can easily inspect its components. ## Loading and Saving arbitrary Julia variables using Serialization.jl -Julia provide a general and lightweight interface for loading and saving Julia structures to and from files that it can be good to be aware of. It is called [Serialization.jl](https://docs.julialang.org/en/v1/stdlib/Serialization/) and provides two functions, `serialize` and `deserialize`. The first allow us to write a Julia structure to a file. E.g. if we wish to save a parameter set associated with our model, we can use +Julia provides a general and lightweight interface for loading and saving Julia structures to and from files that it can be good to be aware of. It is called [Serialization.jl](https://docs.julialang.org/en/v1/stdlib/Serialization/) and provides two functions, `serialize` and `deserialize`. The first allows us to write a Julia structure to a file. E.g. if we wish to save a parameter set associated with our model, we can use ```@example file_handling_2 using Serialization -ps = [:kB => 1.0, :kD => 0.1, :kP => 2.0] +ps = [:k₁ => 1.0, :k₂ => 0.1, :k₃ => 2.0] serialize("saved_parameters.jls", ps) ``` -Here, we use the extension ".jls" (standing for **J**u**L**ia **S**erialization), however, any can be used. To load a structure, we can then use +Here, we use the extension ".jls" (standing for **J**u**L**ia **S**erialization), however, any extension code can be used. To load a structure, we can then use ```@example file_handling_2 loaded_sol = deserialize("saved_parameters.jls") ``` -## [Loading .net files usings ReactionNetworkImporters.jl](@id file_loading_rni_net) -A general-purpose format for storing CRN models are so-called .net files. These can be generated by e.g. [BioNetGen](https://bionetgen.org/). The [ReactionNetworkImporters.jl](https://github.com/SciML/ReactionNetworkImporters.jl) package enables the loading of such files to Catalyst `ReactionSystem`. Here we load a [Repressilator](@ref basic_CRN_library_repressilator) model stored in the "repressilator.net" file: -```@example file_handling_3 +## [Loading .net files using ReactionNetworkImporters.jl](@id file_loading_rni_net) +A general-purpose format for storing CRN models is so-called .net files. These can be generated by e.g. [BioNetGen](https://bionetgen.org/). The [ReactionNetworkImporters.jl](https://github.com/SciML/ReactionNetworkImporters.jl) package enables the loading of such files to Catalyst `ReactionSystem`. Here we load a [Repressilator](@ref basic_CRN_library_repressilator) model stored in the "repressilator.net" file: +```julia using ReactionNetworkImporters -cp("../assets/model_files/repressilator.net", "repressilator.net") # hide prn = loadrxnetwork(BNGNetwork(), "repressilator.net") -rm("repressilator.net") # hide -prn # hide ``` Here, .net files not only contain information regarding the reaction network itself, but also the numeric values (initial conditions and parameter values) required for simulating it. Hence, `loadrxnetwork` generates a `ParsedReactionNetwork` structure, containing all this information. You can access the model as `prn.rn`, the initial conditions as `prn.u0`, and the parameter values as `prn.p`. Furthermore, these initial conditions and parameter values are also made [*default* values](@ref dsl_advanced_options_default_vals) of the model. A parsed reaction network's content can then be provided to various problem types for simulation. E.g. here we perform an ODE simulation of our repressilator model: -```@example file_handling_3 +```julia using Catalyst, OrdinaryDiffEq, Plots -tspan = (0.0, 100.0) +tspan = (0.0, 10000.0) oprob = ODEProblem(prn.rn, Float64[], tspan, Float64[]) sol = solve(oprob) -plot(sol) +plot(sol; idxs = [:mTetR, :mLacI, :mCI]) ``` +![Repressilator Simulation](../assets/repressilator_sim_ReactionNetworkImporters.svg) + Note that, as all initial conditions and parameters have default values, we can provide empty vectors for these into our `ODEProblem`. !!! note - It should be noted that .net files supports a wide range of potential model features, not all of which are currently supported by ReactionNetworkImporters. Hence, there might be some .net files which `loadrxnetwork` will not be able to load. + It should be noted that .net files support a wide range of potential model features, not all of which are currently supported by ReactionNetworkImporters. Hence, there might be some .net files which `loadrxnetwork` will not be able to load. A more detailed description of ReactionNetworkImporter's features can be found in its [documentation](https://docs.sciml.ai/ReactionNetworkImporters/stable/). ## Loading SBML files using SBMLImporter.jl and SBMLToolkit.jl -The Systems Biology Markup Language (SBML) is the most widespread format for representing CRN models. Currently, there exists two different Julia packages, [SBMLImporter.jl](https://github.com/sebapersson/SBMLImporter.jl) and [SBMLToolkit.jl](https://github.com/SciML/SBMLToolkit.jl), both of which are able to load SBML files to Catalyst `ReactionSystem` structures. SBML is able to represent a *very* wide range of model featuresSome of these are not supported by these packages (or Catalyst). Hence, there exist SBML files (typically containing obscure model features such as events with time delays) that currently cannot be loaded into Catalyst models. +The Systems Biology Markup Language (SBML) is the most widespread format for representing CRN models. Currently, there exist two different Julia packages, [SBMLImporter.jl](https://github.com/sebapersson/SBMLImporter.jl) and [SBMLToolkit.jl](https://github.com/SciML/SBMLToolkit.jl), that are able to load SBML files to Catalyst `ReactionSystem` structures. SBML is able to represent a *very* wide range of model features. Some of these are not supported by these packages (or Catalyst). Hence, there exist SBML files (typically containing obscure model features such as events with time delays) that currently cannot be loaded into Catalyst models). SBMLImporter's `load_SBML` function can be used to load SBML files. Here, we load a [Brusselator](@ref basic_CRN_library_brusselator) model stored in the "brusselator.xml" file: -```@example file_handling_4 +```julia using SBMLImporter -cp("../assets/model_files/brusselator.xml", "brusselator.xml") # hide prn, cbs = load_SBML("brusselator.xml") -rm("brusselator.xml") # hide -prn, cbs # hide ``` Here, while [ReactionNetworkImporters generates a `ParsedReactionSystem` only](@ref file_loading_rni_net), SBMLImporter generates a `ParsedReactionSystem` (here stored in `prn`) and a [so-called `CallbackSet`](https://docs.sciml.ai/DiffEqDocs/stable/features/callback_functions/#CallbackSet) (here stored in `cbs`). While `prn` can be used to create various problems, when we simulate them, we must also supply `cbs`. E.g. to simulate our brusselator we use: -```@example file_handling_4 +```julia using Catalyst, OrdinaryDiffEq, Plots -tspan = (0.0, 1000.0) -oprob = ODEProblem(prn.rn, Float64[], tspan, Float64[]) +tspan = (0.0, 50.0) +oprob = ODEProblem(prn.rn, prn.u0, tspan, prn.p) sol = solve(oprob; callback = cbs) plot(sol) ``` +![Brusselator Simulation](../assets/brusselator_sim_SBMLImporter.svg) + +Note that, while ReactionNetworkImporters adds initial condition and species values as default to the imported model, SBMLImporter does not do this. These must hence be provided to the `ODEProblem` directly. A more detailed description of SBMLImporter's features can be found in its [documentation](https://docs.sciml.ai/ReactionNetworkImporters/stable/). @@ -103,4 +126,38 @@ While CRN models can be represented through various file formats, they can also Or - An $mxn$ complex stoichiometric matrix (...) and a $2mxn$ incidence matrix (...). -The advantage with these forms is that they offer a compact and very general way to represent a large class of CRNs. ReactionNetworkImporters have the functionality for converting matrices of these forms directly into Catalyst `ReactionSystem` models. Instructions on how to do this are available in [ReactionNetworkImporter's documentation](https://docs.sciml.ai/ReactionNetworkImporters/stable/#Loading-a-matrix-representation). \ No newline at end of file +The advantage of these forms is that they offer a compact and very general way to represent a large class of CRNs. ReactionNetworkImporters have the functionality for converting matrices of these forms directly into Catalyst `ReactionSystem` models. Instructions on how to do this are available in [ReactionNetworkImporter's documentation](https://docs.sciml.ai/ReactionNetworkImporters/stable/#Loading-a-matrix-representation). + + +--- +## [Citations](@id petab_citations) +If you use any of this functionality in your research, [in addition to Catalyst](@ref catalyst_citation), please cite the paper(s) corresponding to whichever package(s) you used: +``` +@software{2022ReactionNetworkImporters, + author = {Isaacson, Samuel}, + title = {{ReactionNetworkImporters.jl}}, + howpublished = {\url{https://github.com/SciML/ReactionNetworkImporters.jl}}, + year = {2022} +} +``` +``` +@software{2024SBMLImporter, + author = {Persson, Sebastian}, + title = {{SBMLImporter.jl}}, + howpublished = {\url{https://github.com/sebapersson/SBMLImporter.jl}}, + year = {2024} +} +``` +``` +@article{LangJainRackauckas+2024, + url = {https://doi.org/10.1515/jib-2024-0003}, + title = {SBMLToolkit.jl: a Julia package for importing SBML into the SciML ecosystem}, + title = {}, + author = {Paul F. Lang and Anand Jain and Christopher Rackauckas}, + pages = {20240003}, + journal = {Journal of Integrative Bioinformatics}, + doi = {doi:10.1515/jib-2024-0003}, + year = {2024}, + lastchecked = {2024-06-02} +} +``` \ No newline at end of file From d782065cbbaec19fcb8df0120cf557911783fbce Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 3 Jun 2024 09:32:45 -0400 Subject: [PATCH 59/67] up --- .../model_creation/model_file_loading_and_export.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/src/model_creation/model_file_loading_and_export.md b/docs/src/model_creation/model_file_loading_and_export.md index 9812ab97f0..ea869a193a 100644 --- a/docs/src/model_creation/model_file_loading_and_export.md +++ b/docs/src/model_creation/model_file_loading_and_export.md @@ -95,12 +95,12 @@ Note that, as all initial conditions and parameters have default values, we can A more detailed description of ReactionNetworkImporter's features can be found in its [documentation](https://docs.sciml.ai/ReactionNetworkImporters/stable/). ## Loading SBML files using SBMLImporter.jl and SBMLToolkit.jl -The Systems Biology Markup Language (SBML) is the most widespread format for representing CRN models. Currently, there exist two different Julia packages, [SBMLImporter.jl](https://github.com/sebapersson/SBMLImporter.jl) and [SBMLToolkit.jl](https://github.com/SciML/SBMLToolkit.jl), that are able to load SBML files to Catalyst `ReactionSystem` structures. SBML is able to represent a *very* wide range of model features. Some of these are not supported by these packages (or Catalyst). Hence, there exist SBML files (typically containing obscure model features such as events with time delays) that currently cannot be loaded into Catalyst models). +The Systems Biology Markup Language (SBML) is the most widespread format for representing CRN models. Currently, there exist two different Julia packages, [SBMLImporter.jl](https://github.com/sebapersson/SBMLImporter.jl) and [SBMLToolkit.jl](https://github.com/SciML/SBMLToolkit.jl), that are able to load SBML files to Catalyst `ReactionSystem` structures. SBML is able to represent a *very* wide range of model features, with both packages supporting most features. However, there exist SBML files (typically containing obscure model features such as events with time delays) that currently cannot be loaded into Catalyst models. SBMLImporter's `load_SBML` function can be used to load SBML files. Here, we load a [Brusselator](@ref basic_CRN_library_brusselator) model stored in the "brusselator.xml" file: ```julia using SBMLImporter -prn, cbs = load_SBML("brusselator.xml") +prn, cbs = load_SBML("brusselator.xml", massaction = true) ``` Here, while [ReactionNetworkImporters generates a `ParsedReactionSystem` only](@ref file_loading_rni_net), SBMLImporter generates a `ParsedReactionSystem` (here stored in `prn`) and a [so-called `CallbackSet`](https://docs.sciml.ai/DiffEqDocs/stable/features/callback_functions/#CallbackSet) (here stored in `cbs`). While `prn` can be used to create various problems, when we simulate them, we must also supply `cbs`. E.g. to simulate our brusselator we use: ```julia @@ -114,10 +114,13 @@ plot(sol) Note that, while ReactionNetworkImporters adds initial condition and species values as default to the imported model, SBMLImporter does not do this. These must hence be provided to the `ODEProblem` directly. -A more detailed description of SBMLImporter's features can be found in its [documentation](https://docs.sciml.ai/ReactionNetworkImporters/stable/). +A more detailed description of SBMLImporter's features can be found in its [documentation](https://sebapersson.github.io/SBMLImporter.jl/stable/). + +!!! note + The `massaction = true` option informs the importer that the target model follows mass-action principles. When given, this enables SBMLImporter to make appropriate modification to the model (which are important for e.g. jump simulation performance). ### SBMLImporter and SBMLToolkit -Above, we described how to use SBMLImporter to import SBML files. Alternatively, SBMLToolkit can be used instead. It has a slightly different syntax, which is described in its [documentation](https://github.com/SciML/SBMLToolkit.jl). A short comparison of the two packages can be found [here](https://github.com/sebapersson/SBMLImporter.jl?tab=readme-ov-file#differences-compared-to-sbmltoolkit). Generally, while they both perform well, we note that for *jump simulations* SBMLImporter is preferable (its way for internally representing reaction event enables more performant jump simulations). +Above, we described how to use SBMLImporter to import SBML files. Alternatively, SBMLToolkit can be used instead. It has a slightly different syntax, which is described in its [documentation](https://github.com/SciML/SBMLToolkit.jl), and does not support as wide range of SBML features as SBMLImporter. A short comparison of the two packages can be found [here](https://github.com/sebapersson/SBMLImporter.jl?tab=readme-ov-file#differences-compared-to-sbmltoolkit). Generally, while they both perform well, we note that for *jump simulations* SBMLImporter is preferable (its way for internally representing reaction event enables more performant jump simulations). ## Loading models from matrix representation using ReactionNetworkImporters.jl While CRN models can be represented through various file formats, they can also be represented in various matrix forms. E.g. a CRN with $m$ species and $n$ reactions (and with constant rates) can be represented with either From f952cf8b7882665b0ed123a3dccd59b33c75a460 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 3 Jun 2024 09:33:50 -0400 Subject: [PATCH 60/67] spelling fix --- docs/src/model_creation/model_file_loading_and_export.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/model_creation/model_file_loading_and_export.md b/docs/src/model_creation/model_file_loading_and_export.md index ea869a193a..bcc5e4646c 100644 --- a/docs/src/model_creation/model_file_loading_and_export.md +++ b/docs/src/model_creation/model_file_loading_and_export.md @@ -120,7 +120,7 @@ A more detailed description of SBMLImporter's features can be found in its [docu The `massaction = true` option informs the importer that the target model follows mass-action principles. When given, this enables SBMLImporter to make appropriate modification to the model (which are important for e.g. jump simulation performance). ### SBMLImporter and SBMLToolkit -Above, we described how to use SBMLImporter to import SBML files. Alternatively, SBMLToolkit can be used instead. It has a slightly different syntax, which is described in its [documentation](https://github.com/SciML/SBMLToolkit.jl), and does not support as wide range of SBML features as SBMLImporter. A short comparison of the two packages can be found [here](https://github.com/sebapersson/SBMLImporter.jl?tab=readme-ov-file#differences-compared-to-sbmltoolkit). Generally, while they both perform well, we note that for *jump simulations* SBMLImporter is preferable (its way for internally representing reaction event enables more performant jump simulations). +Above, we described how to use SBMLImporter to import SBML files. Alternatively, SBMLToolkit can be used instead. It has a slightly different syntax, which is described in its [documentation](https://github.com/SciML/SBMLToolkit.jl), and does not support as wide a range of SBML features as SBMLImporter. A short comparison of the two packages can be found [here](https://github.com/sebapersson/SBMLImporter.jl?tab=readme-ov-file#differences-compared-to-sbmltoolkit). Generally, while they both perform well, we note that for *jump simulations* SBMLImporter is preferable (its way for internally representing reaction event enables more performant jump simulations). ## Loading models from matrix representation using ReactionNetworkImporters.jl While CRN models can be represented through various file formats, they can also be represented in various matrix forms. E.g. a CRN with $m$ species and $n$ reactions (and with constant rates) can be represented with either From d98b39545a0c109f9092b52c0dbbad213d0e8722 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 3 Jun 2024 09:34:26 -0400 Subject: [PATCH 61/67] spelling fix 2 --- docs/src/model_creation/model_file_loading_and_export.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/model_creation/model_file_loading_and_export.md b/docs/src/model_creation/model_file_loading_and_export.md index bcc5e4646c..52c576634d 100644 --- a/docs/src/model_creation/model_file_loading_and_export.md +++ b/docs/src/model_creation/model_file_loading_and_export.md @@ -117,7 +117,7 @@ Note that, while ReactionNetworkImporters adds initial condition and species val A more detailed description of SBMLImporter's features can be found in its [documentation](https://sebapersson.github.io/SBMLImporter.jl/stable/). !!! note - The `massaction = true` option informs the importer that the target model follows mass-action principles. When given, this enables SBMLImporter to make appropriate modification to the model (which are important for e.g. jump simulation performance). + The `massaction = true` option informs the importer that the target model follows mass-action principles. When given, this enables SBMLImporter to make appropriate modifications to the model (which are important for e.g. jump simulation performance). ### SBMLImporter and SBMLToolkit Above, we described how to use SBMLImporter to import SBML files. Alternatively, SBMLToolkit can be used instead. It has a slightly different syntax, which is described in its [documentation](https://github.com/SciML/SBMLToolkit.jl), and does not support as wide a range of SBML features as SBMLImporter. A short comparison of the two packages can be found [here](https://github.com/sebapersson/SBMLImporter.jl?tab=readme-ov-file#differences-compared-to-sbmltoolkit). Generally, while they both perform well, we note that for *jump simulations* SBMLImporter is preferable (its way for internally representing reaction event enables more performant jump simulations). From 7f2d23427e0c253c6c60fdc68bd041df51d99b2a Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 3 Jun 2024 12:04:26 -0400 Subject: [PATCH 62/67] ensure that files written in docs are properly deleted --- docs/src/model_creation/model_file_loading_and_export.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/src/model_creation/model_file_loading_and_export.md b/docs/src/model_creation/model_file_loading_and_export.md index 52c576634d..bcd86167d8 100644 --- a/docs/src/model_creation/model_file_loading_and_export.md +++ b/docs/src/model_creation/model_file_loading_and_export.md @@ -13,13 +13,13 @@ end ``` and next saving it to a file ```@example file_handling_1 -save_reactionsystem("cross_coupling.jl", rn) +save_reactionsystem("cross_coupling.jl", cc_system) ``` Here, `save_reactionsystem`'s first argument is the path to the file where we wish to save it. The second argument is the `ReactionSystem` we wish to save. To load the file, we use Julia's `include` function: ```@example file_handling_1 -loaded_rn = include("cross_coupling.jl") +cc_loaded = include("cross_coupling.jl") rm("cross_coupling.jl") # hide -loaded_rn # hide +cc_loaded # hide ``` !!! note @@ -66,6 +66,8 @@ serialize("saved_parameters.jls", ps) Here, we use the extension ".jls" (standing for **J**u**L**ia **S**erialization), however, any extension code can be used. To load a structure, we can then use ```@example file_handling_2 loaded_sol = deserialize("saved_parameters.jls") +rm("saved_parameters.jls") # hide +loaded_sol # hide ``` ## [Loading .net files using ReactionNetworkImporters.jl](@id file_loading_rni_net) From 8394755ba2e4a657d445840c32f3821f771e3268 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 3 Jun 2024 15:19:40 -0400 Subject: [PATCH 63/67] init --- docs/Project.toml | 2 +- .../steady_state_functionality/bifurcation_diagrams.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index 246727a23e..83ecae3cc6 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -37,7 +37,7 @@ Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" [compat] BenchmarkTools = "1.5" -BifurcationKit = "0.3" +BifurcationKit = "0.3.4" CairoMakie = "0.12" Catalyst = "13" DataFrames = "1.6" diff --git a/docs/src/steady_state_functionality/bifurcation_diagrams.md b/docs/src/steady_state_functionality/bifurcation_diagrams.md index cebe51dc1c..37ea73dfa2 100644 --- a/docs/src/steady_state_functionality/bifurcation_diagrams.md +++ b/docs/src/steady_state_functionality/bifurcation_diagrams.md @@ -43,7 +43,7 @@ nothing # hide Finally, we compute our bifurcation diagram using: ```@example ex1 -bif_dia = bifurcationdiagram(bprob, PALC(), 2, (args...) -> opts_br; bothside = true) +bif_dia = bifurcationdiagram(bprob, PALC(), 2, opts_br; bothside = true) nothing # hide ``` Where `PALC()` designates that we wish to use the pseudo arclength continuation method to track our solution. The third argument (`2`) designates the maximum number of recursions when branches of branches are computed (branches appear as continuation encounters certain bifurcation points). For diagrams with highly branched structures (rare for many common small chemical reaction networks) this input is important. Finally, `bothside = true` designates that we wish to perform continuation on both sides of the initial point (which is typically the case). @@ -69,7 +69,7 @@ opt_newton = NewtonPar(tol = 1e-9, max_iterations = 1000) opts_br = ContinuationPar(p_min = p_span[1], p_max = p_span[2], dsmin = 0.001, dsmax = 0.01, max_steps = 1000, newton_options = opt_newton) -bif_dia = bifurcationdiagram(bprob, PALC(), 2, (args...) -> opts_br; bothside = true) +bif_dia = bifurcationdiagram(bprob, PALC(), 2, opts_br; bothside = true) nothing # hide ``` (however, in this case these additional settings have no significant effect on the result) @@ -79,7 +79,7 @@ Let's consider the previous case, but instead compute the bifurcation diagram ov ```@example ex1 p_span = (2.0, 15.0) opts_br = ContinuationPar(p_min = p_span[1], p_max = p_span[2], max_steps = 1000) -bif_dia = bifurcationdiagram(bprob, PALC(), 2, (args...) -> opts_br; bothside = true) +bif_dia = bifurcationdiagram(bprob, PALC(), 2, opts_br; bothside = true) plot(bif_dia; xguide = "k1", yguide = "X") ``` Here, in the bistable region, we only see a single branch. The reason is that the continuation algorithm starts at our initial guess (here made at $k1 = 4.0$ for $(X,Y) = (5.0,2.0)$) and tracks the diagram from there. However, with the upper bound set at $k1=15.0$ the bifurcation diagram has a disjoint branch structure, preventing the full diagram from being computed by continuation alone. In this case it could be solved by increasing the bound from $k1=15.0$, however, this is not possible in all cases. In these cases, *deflation* can be used. This is described in the [BifurcationKit documentation](https://bifurcationkit.github.io/BifurcationKitDocs.jl/dev/tutorials/tutorials2/#Snaking-computed-with-deflation). @@ -103,7 +103,7 @@ bprob = BifurcationProblem(kinase_model, u_guess, p_start, :d; plot_var = :Xp, u p_span = (0.1, 10.0) opts_br = ContinuationPar(p_min = p_span[1], p_max = p_span[2], max_steps = 1000) -bif_dia = bifurcationdiagram(bprob, PALC(), 2, (args...) -> opts_br; bothside = true) +bif_dia = bifurcationdiagram(bprob, PALC(), 2, opts_br; bothside = true) plot(bif_dia; xguide = "d", yguide = "Xp") ``` This bifurcation diagram does not contain any interesting features (such as bifurcation points), and only shows how the steady state concentration of $Xp$ is reduced as $d$ increases. From a5dda3dfc9c308a945d5c05b8cf2a40127fdfb09 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 3 Jun 2024 20:02:16 -0400 Subject: [PATCH 64/67] init --- test/dsl/dsl_basic_model_construction.jl | 16 +- test/network_analysis/conservation_laws.jl | 183 +++++++++++++----- test/simulation_and_solving/simulate_ODEs.jl | 2 +- test/simulation_and_solving/simulate_SDEs.jl | 2 +- test/simulation_and_solving/simulate_jumps.jl | 2 +- test/test_networks.jl | 46 ++--- 6 files changed, 171 insertions(+), 80 deletions(-) diff --git a/test/dsl/dsl_basic_model_construction.jl b/test/dsl/dsl_basic_model_construction.jl index 6c3d5466c3..3d6510a8ae 100644 --- a/test/dsl/dsl_basic_model_construction.jl +++ b/test/dsl/dsl_basic_model_construction.jl @@ -71,7 +71,7 @@ let Set([:p, :k1, :k2, :k3, :k4, :k5, :k6, :d]) basic_test(reaction_networks_hill[1], 4, [:X1, :X2], [:v1, :v2, :K1, :K2, :n1, :n2, :d1, :d2]) - basic_test(reaction_networks_constraint[1], 6, [:X1, :X2, :X3], + basic_test(reaction_networks_conserved[1], 6, [:X1, :X2, :X3], [:k1, :k2, :k3, :k4, :k5, :k6]) basic_test(reaction_networks_real[1], 4, [:X, :Y], [:A, :B]) basic_test(reaction_networks_weird[1], 2, [:X], [:p, :d]) @@ -281,7 +281,7 @@ let Reaction(p + k5 * X2 * X3, nothing, [X5], nothing, [1]), Reaction(d, [X5], nothing, [1], nothing)] @named rs_2 = ReactionSystem(rxs_2, t, [X1, X2, X3, X4, X5], [k1, k2, k3, k4, p, k5, d]) - push!(identical_networks_4, reaction_networks_constraint[3] => rs_2) + push!(identical_networks_4, reaction_networks_conserved[3] => rs_2) rxs_3 = [Reaction(k1, [X1], [X2], [1], [1]), Reaction(0, [X2], [X3], [1], [1]), @@ -321,14 +321,14 @@ let for factor in [1e-2, 1e-1, 1e0, 1e1, 1e2, 1e3] τ = rand(rng) - u = rnd_u0(reaction_networks_constraint[1], rng; factor) + u = rnd_u0(reaction_networks_conserved[1], rng; factor) p_2 = rnd_ps(time_network, rng; factor) - p_1 = [p_2; reaction_networks_constraint[1].k1 => τ; - reaction_networks_constraint[1].k4 => τ; reaction_networks_constraint[1].k5 => τ] + p_1 = [p_2; reaction_networks_conserved[1].k1 => τ; + reaction_networks_conserved[1].k4 => τ; reaction_networks_conserved[1].k5 => τ] - @test f_eval(reaction_networks_constraint[1], u, p_1, τ) ≈ f_eval(time_network, u, p_2, τ) - @test jac_eval(reaction_networks_constraint[1], u, p_1, τ) ≈ jac_eval(time_network, u, p_2, τ) - @test g_eval(reaction_networks_constraint[1], u, p_1, τ) ≈ g_eval(time_network, u, p_2, τ) + @test f_eval(reaction_networks_conserved[1], u, p_1, τ) ≈ f_eval(time_network, u, p_2, τ) + @test jac_eval(reaction_networks_conserved[1], u, p_1, τ) ≈ jac_eval(time_network, u, p_2, τ) + @test g_eval(reaction_networks_conserved[1], u, p_1, τ) ≈ g_eval(time_network, u, p_2, τ) end end diff --git a/test/network_analysis/conservation_laws.jl b/test/network_analysis/conservation_laws.jl index d7fa4dc9a4..0bc563642c 100644 --- a/test/network_analysis/conservation_laws.jl +++ b/test/network_analysis/conservation_laws.jl @@ -1,7 +1,12 @@ ### Prepares Tests ### # Fetch packages. -using Catalyst, LinearAlgebra, NonlinearSolve, OrdinaryDiffEq +using Catalyst, JumpProcesses, LinearAlgebra, NonlinearSolve, OrdinaryDiffEq, SteadyStateDiffEq, StochasticDiffEq, Test + +# Sets stable rng number. +using StableRNGs +rng = StableRNG(123456) +seed = rand(rng, 1:100) # Fetch test networks. include("../test_networks.jl") @@ -42,17 +47,18 @@ end # Tests conservation law computation on large number of networks where we know which have conservation laws. let + # networks for whch we know there is no conservation laws. Cs_standard = map(conservationlaws, reaction_networks_standard) - @test all(size(C, 1) == 0 for C in Cs_standard) - Cs_hill = map(conservationlaws, reaction_networks_hill) + @test all(size(C, 1) == 0 for C in Cs_standard) @test all(size(C, 1) == 0 for C in Cs_hill) + # Networks for which there are known conservation laws (stored in `reaction_network_conslaws`). function consequiv(A, B) rank([A; B]) == rank(A) == rank(B) end - Cs_constraint = map(conservationlaws, reaction_networks_constraint) - @test all(consequiv.(Matrix{Int}.(Cs_constraint), reaction_network_constraints)) + Cs_constraint = map(conservationlaws, reaction_networks_conserved) + @test all(consequiv.(Matrix{Int}.(Cs_constraint), reaction_network_conslaws)) end # Tests additional conservation law-related functions. @@ -74,8 +80,25 @@ let @test count(isequal.(conserved_quantity, Num(0))) == 2 end +# Tests that `conservationlaws`'s caches something. +let + # Creates network with/without cached conservation laws. + rn = @reaction_network rn begin + (k1,k2), X1 <--> X2 + end + rn_cached = deepcopy(rn) + conservationlaws(rn_cached) + + # Checks that equality is correct (currently equality does not consider network property caching). + @test rn_cached == rn + @test Catalyst.get_networkproperties(rn_cached) != Catalyst.get_networkproperties(rn) +end + +### Simulation & Solving Tests ### + # Test conservation law elimination. let + # Declares the model rn = @reaction_network begin (k1, k2), A + B <--> C (m1, m2), D <--> E @@ -83,47 +106,52 @@ let b23, F2 --> F3 b31, F3 --> F1 end - osys = complete(convert(ODESystem, rn; remove_conserved = true)) - @unpack A, B, C, D, E, F1, F2, F3, k1, k2, m1, m2, b12, b23, b31 = osys + @unpack A, B, C, D, E, F1, F2, F3, k1, k2, m1, m2, b12, b23, b31 = rn + sps = species(rn) u0 = [A => 10.0, B => 10.0, C => 0.0, D => 10.0, E => 0.0, F1 => 8.0, F2 => 0.0, F3 => 0.0] p = [k1 => 1.0, k2 => 0.1, m1 => 1.0, m2 => 2.0, b12 => 1.0, b23 => 2.0, b31 => 0.1] tspan = (0.0, 20.0) - oprob = ODEProblem(osys, u0, tspan, p) - sol = solve(oprob, Tsit5(); abstol = 1e-10, reltol = 1e-10) + + # Simulates model using ODEs and checks that simulations are identical. + osys = complete(convert(ODESystem, rn; remove_conserved = true)) + oprob1 = ODEProblem(osys, u0, tspan, p) oprob2 = ODEProblem(rn, u0, tspan, p) - sol2 = solve(oprob2, Tsit5(); abstol = 1e-10, reltol = 1e-10) oprob3 = ODEProblem(rn, u0, tspan, p; remove_conserved = true) - sol3 = solve(oprob3, Tsit5(); abstol = 1e-10, reltol = 1e-10) - - tv = range(tspan[1], tspan[2], length = 101) - for s in species(rn) - @test isapprox(sol(tv, idxs = s), sol2(tv, idxs = s)) - @test isapprox(sol2(tv, idxs = s), sol2(tv, idxs = s)) - end + osol1 = solve(oprob1, Tsit5(); abstol = 1e-8, reltol = 1e-8, saveat= 0.2) + osol2 = solve(oprob2, Tsit5(); abstol = 1e-8, reltol = 1e-8, saveat= 0.2) + osol3 = solve(oprob3, Tsit5(); abstol = 1e-8, reltol = 1e-8, saveat= 0.2) + @test osol1[sps] ≈ osol2[sps] ≈ osol3[sps] + # Checks that steady states found using nonlinear solving and steady state simulations are identical. nsys = complete(convert(NonlinearSystem, rn; remove_conserved = true)) - nprob = NonlinearProblem{true}(nsys, u0, p) - nsol = solve(nprob, NewtonRaphson(); abstol = 1e-10) - nprob2 = ODEProblem(rn, u0, (0.0, 100.0 * tspan[2]), p) - nsol2 = solve(nprob2, Tsit5(); abstol = 1e-10, reltol = 1e-10) + nprob1 = NonlinearProblem{true}(nsys, u0, p) + nprob2 = NonlinearProblem(rn, u0, p) nprob3 = NonlinearProblem(rn, u0, p; remove_conserved = true) - nsol3 = solve(nprob3, NewtonRaphson(); abstol = 1e-10) - for s in species(rn) - @test isapprox(nsol[s], nsol2(tspan[2], idxs = s)) - @test isapprox(nsol2(tspan[2], idxs = s), nsol3[s]) - end - - u0 = [A => 100.0, B => 20.0, C => 5.0, D => 10.0, E => 3.0, F1 => 8.0, F2 => 2.0, + ssprob1 = SteadyStateProblem{true}(osys, u0, p) + ssprob2 = SteadyStateProblem(rn, u0, p) + ssprob3 = SteadyStateProblem(rn, u0, p; remove_conserved = true) + nsol1 = solve(nprob1, NewtonRaphson(); abstol = 1e-8) + # Nonlinear problems cannot find steady states properly without removing conserved species. + nsol3 = solve(nprob3, NewtonRaphson(); abstol = 1e-8) + sssol1 = solve(ssprob1, DynamicSS(Tsit5()); abstol = 1e-8, reltol = 1e-8) + sssol2 = solve(ssprob2, DynamicSS(Tsit5()); abstol = 1e-8, reltol = 1e-8) + sssol3 = solve(ssprob3, DynamicSS(Tsit5()); abstol = 1e-8, reltol = 1e-8) + @test nsol1[sps] ≈ nsol3[sps] ≈ sssol1[sps] ≈ sssol2[sps] ≈ sssol3[sps] + + # Creates SDEProblems using various approaches. + u0_sde = [A => 100.0, B => 20.0, C => 5.0, D => 10.0, E => 3.0, F1 => 8.0, F2 => 2.0, F3 => 20.0] ssys = complete(convert(SDESystem, rn; remove_conserved = true)) - sprob = SDEProblem(ssys, u0, tspan, p) - sprob2 = SDEProblem(rn, u0, tspan, p) - sprob3 = SDEProblem(rn, u0, tspan, p; remove_conserved = true) - ists = ModelingToolkit.get_unknowns(ssys) - sts = ModelingToolkit.get_unknowns(rn) - istsidxs = findall(in(ists), sts) - u1 = copy(sprob.u0) + sprob1 = SDEProblem(ssys, u0_sde, tspan, p) + sprob2 = SDEProblem(rn, u0_sde, tspan, p) + sprob3 = SDEProblem(rn, u0_sde, tspan, p; remove_conserved = true) + + # Checks that the SDEs f and g function evaluates to the same thing. + ind_us = ModelingToolkit.get_unknowns(ssys) + us = ModelingToolkit.get_unknowns(rn) + ind_uidxs = findall(in(ind_us), us) + u1 = copy(sprob1.u0) u2 = sprob2.u0 u3 = copy(sprob3.u0) du1 = similar(u1) @@ -132,19 +160,82 @@ let g1 = zeros(length(u1), numreactions(rn)) g2 = zeros(length(u2), numreactions(rn)) g3 = zeros(length(u3), numreactions(rn)) - sprob.f(du1, u1, sprob.p, 1.0) - sprob2.f(du2, u2, sprob2.p, 1.0) - sprob3.f(du3, u3, sprob3.p, 1.0) - @test isapprox(du1, du2[istsidxs]) - @test isapprox(du2[istsidxs], du3) - sprob.g(g1, u1, sprob.p, 1.0) - sprob2.g(g2, u2, sprob2.p, 1.0) - sprob3.g(g3, u3, sprob3.p, 1.0) - @test isapprox(g1, g2[istsidxs, :]) - @test isapprox(g2[istsidxs, :], g3) + sprob1.f(du1, sprob1.u0, sprob1.p, 1.0) + sprob2.f(du2, sprob2.u0, sprob2.p, 1.0) + sprob3.f(du3, sprob3.u0, sprob3.p, 1.0) + @test du1 ≈ du2[ind_uidxs] ≈ du3 + sprob1.g(g1, sprob1.u0, sprob1.p, 1.0) + sprob2.g(g2, sprob2.u0, sprob2.p, 1.0) + sprob3.g(g3, sprob3.u0, sprob3.p, 1.0) + @test g1 ≈ g2[ind_uidxs, :] ≈ g3 +end + +# Tests simulations for various input types (using X, rn.X, and :X forms). +# Tests that conservation laws can be generated for system with non-default parameter types. +let + # Prepares the model. + rn = @reaction_network rn begin + @parameters kB::Int64 + (kB,kD), X + Y <--> XY + end + sps = species(rn) + @unpack kB, kD, X, Y, XY = rn + + # Creates `ODEProblem`s using three types of inputs. Checks that solutions are identical. + u0_1 = [X => 2.0, Y => 3.0, XY => 4.0] + u0_2 = [rn.X => 2.0, rn.Y => 3.0, rn.XY => 4.0] + u0_3 = [:X => 2.0, :Y => 3.0, :XY => 4.0] + ps = (kB => 2, kD => 1.5) + oprob1 = ODEProblem(rn, u0_1, 10.0, ps; remove_conserved = true) + oprob2 = ODEProblem(rn, u0_2, 10.0, ps; remove_conserved = true) + oprob3 = ODEProblem(rn, u0_3, 10.0, ps; remove_conserved = true) + @test solve(oprob1)[sps] ≈ solve(oprob2)[sps] ≈ solve(oprob3)[sps] end -### ConservedParameter Metadata Tests ### +# Tests conservation laws in SDE simulation. +let + # Creates `SDEProblem`s. + rn = @reaction_network begin + (k1,k2), X1 <--> X2 + end + u0 = Dict([:X1 => 100.0, :X2 => 120.0]) + ps = [:k1 => 0.2, :k2 => 0.15] + sprob = SDEProblem(rn, u0, 10.0, ps; remove_conserved = true) + + # Checks that conservation laws hold in all simulations. + sol = solve(sprob, ImplicitEM(); seed) + @test sol[:X1] + sol[:X2] ≈ sol[rn.X1 + rn.X2] ≈ fill(u0[:X1] + u0[:X2], length(sol.t)) +end + +# Checks that the conservation law parameter's value can be changed in simulations. +let + # Prepares `ODEProblem`s. + rn = @reaction_network begin + (k1,k2), X1 <--> X2 + end + osys = complete(convert(ODESystem, rn; remove_conserved = true)) + u0 = [osys.X1 => 1.0, osys.X2 => 1.0] + ps_1 = [osys.k1 => 2.0, osys.k2 => 3.0] + ps_2 = [osys.k1 => 2.0, osys.k2 => 3.0, osys.Γ[1] => 4.0] + oprob1 = ODEProblem(osys, u0, 10.0, ps_1) + oprob2 = ODEProblem(osys, u0, 10.0, ps_2) + + # Checks that the solutions generates the correct conserved quantities. + sol1 = solve(oprob1; saveat = 1.0) + sol2 = solve(oprob2; saveat = 1.0) + @test all(sol1[osys.X1 + osys.X2] .== 2.0) + @test all(sol2[osys.X1 + osys.X2] .== 4.0) +end + +### Other Tests ### + +# Checks that `JumpSystem`s with conservation laws cannot be generated. +let + rn = @reaction_network begin + (k1,k2), X1 <--> X2 + end + @test_throws ArgumentError convert(JumpSystem, rn; remove_conserved = true) +end # Checks that `conserved` metadata is added correctly to parameters. # Checks that the `isconserved` getter function works correctly. diff --git a/test/simulation_and_solving/simulate_ODEs.jl b/test/simulation_and_solving/simulate_ODEs.jl index b706b187e3..ecb2a468ce 100644 --- a/test/simulation_and_solving/simulate_ODEs.jl +++ b/test/simulation_and_solving/simulate_ODEs.jl @@ -104,7 +104,7 @@ let du[2] = -k3 * X2 + k4 * X3 + k1 * X1 - k2 * X2 du[3] = -k5 * X3 + k6 * X1 + k3 * X2 - k4 * X3 end - push!(catalyst_networks, reaction_networks_constraint[1]) + push!(catalyst_networks, reaction_networks_conserved[1]) push!(manual_networks, real_functions_4) push!(u0_syms, [:X1, :X2, :X3]) push!(ps_syms, [:k1, :k2, :k3, :k4, :k5, :k6]) diff --git a/test/simulation_and_solving/simulate_SDEs.jl b/test/simulation_and_solving/simulate_SDEs.jl index 3e71947495..d3504a3cd6 100644 --- a/test/simulation_and_solving/simulate_SDEs.jl +++ b/test/simulation_and_solving/simulate_SDEs.jl @@ -107,7 +107,7 @@ let du[7, 5] = sqrt(k5 * X5 * X6) du[7, 6] = -sqrt(k6 * X7) end - push!(catalyst_networks, reaction_networks_constraint[9]) + push!(catalyst_networks, reaction_networks_conserved[9]) push!(manual_networks, (f = real_f_3, g = real_g_3, nrp = zeros(7, 6))) push!(u0_syms, [:X1, :X2, :X3, :X4, :X5, :X6, :X7]) push!(ps_syms, [:k1, :k2, :k3, :k4, :k5, :k6]) diff --git a/test/simulation_and_solving/simulate_jumps.jl b/test/simulation_and_solving/simulate_jumps.jl index 9c8c02db8a..370b51036e 100644 --- a/test/simulation_and_solving/simulate_jumps.jl +++ b/test/simulation_and_solving/simulate_jumps.jl @@ -117,7 +117,7 @@ let jump_3_5 = ConstantRateJump(rate_3_5, affect_3_5!) jump_3_6 = ConstantRateJump(rate_3_6, affect_3_6!) jumps_3 = (jump_3_1, jump_3_2, jump_3_3, jump_3_4, jump_3_5, jump_3_6) - push!(catalyst_networks, reaction_networks_constraint[5]) + push!(catalyst_networks, reaction_networks_conserved[5]) push!(manual_networks, jumps_3) push!(u0_syms, [:X1, :X2, :X3, :X4]) push!(ps_syms, [:k1, :k2, :k3, :k4, :k5, :k6]) diff --git a/test/test_networks.jl b/test/test_networks.jl index 45b2ea5e59..ab2afad1ad 100644 --- a/test/test_networks.jl +++ b/test/test_networks.jl @@ -3,8 +3,8 @@ # Declares the vectors which contains the various test sets. reaction_networks_standard = Vector{ReactionSystem}(undef, 10) reaction_networks_hill = Vector{ReactionSystem}(undef, 10) -reaction_networks_constraint = Vector{ReactionSystem}(undef, 10) -reaction_network_constraints = Vector{Matrix{Int}}(undef, 10) +reaction_networks_conserved = Vector{ReactionSystem}(undef, 10) +reaction_network_conslaws = Vector{Matrix{Int}}(undef, 10) reaction_networks_real = Vector{ReactionSystem}(undef, 4) reaction_networks_weird = Vector{ReactionSystem}(undef, 10) @@ -160,69 +160,69 @@ end ### Reaction networks were some linear combination concentrations remain fixed (steady state values depends on initial conditions). ### -reaction_networks_constraint[1] = @reaction_network rnc1 begin +reaction_networks_conserved[1] = @reaction_network rnc1 begin (k1, k2), X1 ↔ X2 (k3, k4), X2 ↔ X3 (k5, k6), X3 ↔ X1 end -reaction_network_constraints[1] = [1 1 1] +reaction_network_conslaws[1] = [1 1 1] -reaction_networks_constraint[2] = @reaction_network rnc2 begin +reaction_networks_conserved[2] = @reaction_network rnc2 begin (k1, k2), X1 ↔ 2X1 (k3, k4), X1 + X2 ↔ X3 (k5, k6), X3 ↔ X2 end -reaction_network_constraints[2] = [0 1 1] +reaction_network_conslaws[2] = [0 1 1] -reaction_networks_constraint[3] = @reaction_network rnc3 begin +reaction_networks_conserved[3] = @reaction_network rnc3 begin (k1, k2 * X5), X1 ↔ X2 (k3 * X5, k4), X3 ↔ X4 (p + k5 * X2 * X3, d), ∅ ↔ X5 end -reaction_network_constraints[3] = [0 0 1 1 0; 1 1 0 0 0] +reaction_network_conslaws[3] = [0 0 1 1 0; 1 1 0 0 0] -reaction_networks_constraint[4] = @reaction_network rnc4 begin +reaction_networks_conserved[4] = @reaction_network rnc4 begin (k1, k2), X1 + X2 ↔ X3 (mm(X3, v, K), d), ∅ ↔ X4 end -reaction_network_constraints[4] = [0 1 1 0; -1 1 0 0] +reaction_network_conslaws[4] = [0 1 1 0; -1 1 0 0] -reaction_networks_constraint[5] = @reaction_network rnc5 begin +reaction_networks_conserved[5] = @reaction_network rnc5 begin (k1, k2), X1 ↔ 2X2 (k3, k4), 2X2 ↔ 3X3 (k5, k6), 3X3 ↔ 4X4 end -reaction_network_constraints[5] = [12 6 4 3] +reaction_network_conslaws[5] = [12 6 4 3] -reaction_networks_constraint[6] = @reaction_network rnc6 begin +reaction_networks_conserved[6] = @reaction_network rnc6 begin mmr(X1, v1, K1), X1 → X2 mmr(X2, v2, K2), X2 → X3 mmr(X3, v3, K3), X3 → X1 end -reaction_network_constraints[6] = [1 1 1] +reaction_network_conslaws[6] = [1 1 1] -reaction_networks_constraint[7] = @reaction_network rnc7 begin +reaction_networks_conserved[7] = @reaction_network rnc7 begin (k1, k2), X1 + X2 ↔ X3 (mm(X3, v, K), d), ∅ ↔ X2 (k3, k4), X2 ↔ X4 end -reaction_network_constraints[7] = [1 0 1 0] +reaction_network_conslaws[7] = [1 0 1 0] -reaction_networks_constraint[8] = @reaction_network rnc8 begin +reaction_networks_conserved[8] = @reaction_network rnc8 begin (k1, k2), X1 + X2 ↔ X3 (mm(X3, v1, K1), mm(X4, v2, K2)), X3 ↔ X4 end -reaction_network_constraints[8] = [-1 1 0 0; 0 1 1 1] +reaction_network_conslaws[8] = [-1 1 0 0; 0 1 1 1] -reaction_networks_constraint[9] = @reaction_network rnc9 begin +reaction_networks_conserved[9] = @reaction_network rnc9 begin (k1, k2), X1 + X2 ↔ X3 (k3, k4), X3 + X4 ↔ X5 (k5, k6), X5 + X6 ↔ X7 end -reaction_network_constraints[9] = [1 0 1 0 1 0 1; -1 1 0 0 0 0 0; 0 0 0 1 1 0 1; +reaction_network_conslaws[9] = [1 0 1 0 1 0 1; -1 1 0 0 0 0 0; 0 0 0 1 1 0 1; 0 0 0 0 0 1 1] -reaction_networks_constraint[10] = @reaction_network rnc10 begin +reaction_networks_conserved[10] = @reaction_network rnc10 begin kDeg, (w, w2, w2v, v, w2v2, vP, σB, w2σB) ⟶ ∅ kDeg, vPp ⟶ phos (kBw, kDw), 2w ⟷ w2 @@ -238,7 +238,7 @@ reaction_networks_constraint[10] = @reaction_network rnc10 begin λW * v0 * ((1 + F * σB) / (K + σB)), ∅ ⟶ w λV * v0 * ((1 + F * σB) / (K + σB)), ∅ ⟶ v end; -reaction_network_constraints[10] = [0 0 0 0 0 0 0 0 1 1] +reaction_network_conslaws[10] = [0 0 0 0 0 0 0 0 1 1] ### Reaction networks that are actual models that have been used ### @@ -350,6 +350,6 @@ end ### Gathers all netowkrs in a simgle array ### reaction_networks_all = [reaction_networks_standard..., reaction_networks_hill..., - reaction_networks_constraint..., + reaction_networks_conserved..., reaction_networks_real..., reaction_networks_weird...] From c002dac4abfc22f4594eaa25115457faba016f2e Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 3 Jun 2024 20:03:17 -0400 Subject: [PATCH 65/67] up --- src/reactionsystem_conversions.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reactionsystem_conversions.jl b/src/reactionsystem_conversions.jl index 2a9688cd73..b8f5771ad2 100644 --- a/src/reactionsystem_conversions.jl +++ b/src/reactionsystem_conversions.jl @@ -653,7 +653,7 @@ function Base.convert(::Type{<:JumpSystem}, rs::ReactionSystem; name = nameof(rs spatial_convert_err(rs::ReactionSystem, JumpSystem) (remove_conserved !== nothing) && - error("Catalyst does not support removing conserved species when converting to JumpSystems.") + throw(ArgumentError("Catalyst does not support removing conserved species when converting to JumpSystems.")) flatrs = Catalyst.flatten(rs) error_if_constraints(JumpSystem, flatrs) From 5fa9ba3e8b3bcfbd62ea690b2cea05e94f4f562b Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Tue, 4 Jun 2024 10:44:57 -0400 Subject: [PATCH 66/67] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 2b90ff7b2a..b9313411b2 100644 --- a/Project.toml +++ b/Project.toml @@ -57,7 +57,7 @@ StructuralIdentifiability = "0.5.1" SymbolicUtils = "1.0.3" Symbolics = "5.27" Unitful = "1.12.4" -julia = "1.9" +julia = "1.10" [extras] BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" From 67b048ba4c8a6fab14c79b8bc384ef6acf00b1f5 Mon Sep 17 00:00:00 2001 From: Torkel Date: Fri, 31 May 2024 10:51:17 -0400 Subject: [PATCH 67/67] `0.0` to `0` --- src/reactionsystem_conversions.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reactionsystem_conversions.jl b/src/reactionsystem_conversions.jl index b8f5771ad2..6348e553a9 100644 --- a/src/reactionsystem_conversions.jl +++ b/src/reactionsystem_conversions.jl @@ -449,7 +449,7 @@ function remove_diffs(expr) return expr end end -diff_2_zero(expr) = (Symbolics.is_derivative(expr) ? 0.0 : expr) +diff_2_zero(expr) = (Symbolics.is_derivative(expr) ? 0 : expr) COMPLETENESS_ERROR = "A ReactionSystem must be complete before it can be converted to other system types. A ReactionSystem can be marked as complete using the `complete` function."