From efd44bea08a6c8d54ff6930ba36f527f6e2bf896 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 27 May 2024 19:26:40 -0400 Subject: [PATCH 01/38] save progress --- src/Catalyst.jl | 1 + src/dsl.jl | 141 ++++++++++++++++++++---------------------------- 2 files changed, 59 insertions(+), 83 deletions(-) diff --git a/src/Catalyst.jl b/src/Catalyst.jl index d80c115119..fe4388e57a 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -40,6 +40,7 @@ import ModelingToolkit: check_variables, import Base: (==), hash, size, getindex, setindex, isless, Sort.defalg, length, show import MacroTools, Graphs +using MacroTools: striplines import Graphs: DiGraph, SimpleGraph, SimpleDiGraph, vertices, edges, add_vertices!, nv, ne import DataStructures: OrderedDict, OrderedSet import Parameters: @with_kw_noshow diff --git a/src/dsl.jl b/src/dsl.jl index cbc72cdc61..35b4aa9db9 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -1,64 +1,4 @@ -""" -Macro that inputs an expression corresponding to a reaction network and outputs -a `ReactionNetwork` that can be used as input to generation of ODE, SDE, and -Jump problems. - -Most arrows accepted (both right, left, and bi-drectional arrows). Note that -while --> is a correct arrow, neither <-- nor <--> works. Using non-filled -arrows (⇐, ⟽, ⇒, ⟾, ⇔, ⟺) will disable mass kinetics and let you cutomize -reaction rates yourself. Use 0 or ∅ for degradation/creation to/from nothing. - -Example systems: - - ### Basic Usage ### - rn = @reaction_network begin # Creates a ReactionSystem. - 2.0, X + Y --> XY # This will have reaction rate corresponding to 2.0*[X][Y] - 2.0, XY ← X + Y # Identical to 2.0, X + Y --> XY - end - - ### Manipulating Reaction Rates ### - rn = @reaction_network begin - 2.0, X + Y ⟾ XY # Ignores mass kinetics. This will have reaction rate corresponding to 2.0. - 2.0X, X + Y --> XY # Reaction rate needs not be constant. This will have reaction rate corresponding to 2.0*[X]*[X]*[Y]. - XY+log(X)^2, X + Y --> XY # Reaction rate accepts quite complicated expressions. - hill(XY,2,2,2), X + Y --> XY # Reaction inis activated by XY according to a hill function. hill(x,v,K,N). - mm(XY,2,2), X + Y --> XY # Reaction inis activated by XY according to a michaelis menten function. mm(x,v,K). - end - - ### Multiple Reactions on a Single Line ### - rn = @reaction_network begin - (2.0,1.0), X + Y ↔ XY # Identical to reactions (2.0, X + Y --> XY) and (1.0, XY --> X + Y). - 2.0, (X,Y) --> 0 # This corresponds to both X and Y degrading at rate 2.0. - (2.0, 1.0), (X,Y) --> 0 # This corresponds to X and Y degrading at rates 2.0 and 1.0, respectively. - 2.0, (X1,Y1) --> (X2,Y2) # X1 and Y1 becomes X2 and Y2, respectively, at rate 2.0. - end - - ### Adding Parameters ### - kB = 2.0; kD = 1.0 - p = [kB, kD] - p = [] - rn = @reaction_network begin - (kB, kD), X + Y ↔ XY # Lets you define parameters outside on network. Parameters can be changed without recalling the network. - end - - ### Defining New Functions ### - my_hill_repression(x, v, k, n) = v*k^n/(k^n+x^n) - - # may be necessary to - # @register_symbolic my_hill_repression(x, v, k, n) - # see https://docs.sciml.ai/ModelingToolkit/stable/basics/Validation/#User-Defined-Registered-Functions-and-Types - - r = @reaction_network MyReactionType begin - my_hill_repression(x, v_x, k_x, n_x), 0 --> x - end - - ### Simulating Reaction Networks ### - probODE = ODEProblem(rn, args...; kwargs...) # Using multiple dispatch the reaction network can be used as input to create ODE, SDE and Jump problems. - probSDE = SDEProblem(rn, args...; kwargs...) - probJump = JumpProblem(prob,aggregator::Direct,rn) -""" - -### Constant Declarations ### +### Constants Declarations ### # Declare various arrow types symbols used for the empty set (also 0). const empty_set = Set{Symbol}([:∅]) @@ -114,48 +54,83 @@ end """ @reaction_network -Generates a [`ReactionSystem`](@ref dsl_description) that encodes a chemical reaction -network. +Macro for generating chemical reaction network models. Outputs a [`ReactionSystem`](@ref) structure, +which stores all information of the model. Next, it can be used as input to various simulations, or +other tools for model analysis. The `@reaction_network` macro is sometimes called the "Catalyst +DSL" (where DSL = domain-specific language), as it implements a DSL for creating chemical reaction +network models. + +The `@reaction_network` macro, and the `ReactionSystem`s it generates, are central to Catalyst +and its functionality. Catalyst is describe in more detail [in its documentation](@ref ref). The +`reaction_network` DSL, in particular, is described in more detail [here](@ref dsl_description). + +The `@reaction_network` statement is followed by a `begin ... end` block. Each line within the +block corresponds to a single reaction. Each reaction consists of: +- A rate (at which the reaction occur). +- Any number of substrates (which are consumed by the reaction). +- Any number of products (which are produced by the reaction). -See [The Reaction DSL](@ref dsl_description) documentation for details on -parameters to the macro. Examples: +Here we create a basic SIR model. It contains two reactions (infections and recovery): ```julia -# a basic SIR model, with name SIR -sir_model = @reaction_network SIR begin - c1, s + i --> 2i - c2, i --> r -end - -# a basic SIR model, with random generated name sir_model = @reaction_network begin - c1, s + i --> 2i - c2, i --> r + c1, S + I --> 2I + c2, I --> R end +``` -# an empty network with name empty -emptyrn = @reaction_network empty - -# an empty network with random generated name -emptyrn = @reaction_network +Next we create a self-activation loop. Here, a single component (`X`) activates its own production +with a Michaelis-Menten function: +```julia +sa_loop = @reaction_network begin + mm(X,v,K), 0 --> X + d, X --> 0 +end ``` +This model also contain production and degradation reactions, where `0` denotes that there are +either no substrates or not products in a reaction. + +Options: +The `@reaction_network` also accepts various options. These are inputs to the model that are not +reactions. To denote that a line contain an option (and not a reaction), the line starts with `@` +followed by the options name. E.g. an observable is declared using the `@observables` option. +Here we create a polymerisation model (where the parameter `n` denotes the number of monomers in +the polymer). We use the observable `Xtot` to track the total amount of `X` in the system. We also +bundle the forward and backwards binding reactions into a single line. +```julia +polymerisation = @reaction_network begin + @observables Xtot ~ X + n*Xn + (kB,kD), n*X <--> Xn +end +``` + +Notes: +- `ReactionSystem`s creates through `@reaction_network` are considered complete (non-complete +systems can be created through the alternative `@network_component` macro). +- `ReactionSystem`s creates through `@reaction_network`, by default, have a random name. Specific +names can be designated as a first argument (before `begin`, e.g. `rn = @reaction_network name begin ...`). +- For more information, please again consider Catalyst's documentation. -ReactionSystems generated through `@reaction_network` are complete. """ macro reaction_network(name::Symbol, ex::Expr) - :(complete($(make_reaction_system(MacroTools.striplines(ex); name = :($(QuoteNode(name))))))) + name = QuoteNode(name) + rs_expr = make_reaction_system(striplines(ex); name) + return :(complete($rs_expr)) end # Allows @reaction_network $name begin ... to interpolate variables storing a name. macro reaction_network(name::Expr, ex::Expr) - :(complete($(make_reaction_system(MacroTools.striplines(ex); name = :($(esc(name.args[1]))))))) + name = esc(name.args[1]) + rs_expr = make_reaction_system(striplines(ex); name) + return :(complete($rs_expr)) end +# Handles two disjoint cases (empty network with interpolated name, or network with no name). macro reaction_network(ex::Expr) ex = MacroTools.striplines(ex) - # no name but equations: @reaction_network begin ... end ... + # The case where no name is provided. if ex.head == :block :(complete($(make_reaction_system(ex)))) else # empty but has interpolated name: @reaction_network $name From d2985c0a20a417eba8ce0ad605f41059bd16c5cc Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 27 May 2024 21:47:20 -0400 Subject: [PATCH 02/38] save progress --- src/dsl.jl | 175 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 98 insertions(+), 77 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index 35b4aa9db9..f95996e7a2 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -113,132 +113,149 @@ names can be designated as a first argument (before `begin`, e.g. `rn = @reactio - For more information, please again consider Catalyst's documentation. """ -macro reaction_network(name::Symbol, ex::Expr) - name = QuoteNode(name) - rs_expr = make_reaction_system(striplines(ex); name) - return :(complete($rs_expr)) +macro reaction_network(name::Symbol, network_expr::Expr) + make_rs_expr(QuoteNode(name), network_expr) end -# Allows @reaction_network $name begin ... to interpolate variables storing a name. -macro reaction_network(name::Expr, ex::Expr) - name = esc(name.args[1]) - rs_expr = make_reaction_system(striplines(ex); name) - return :(complete($rs_expr)) +# The case where the name contains an interpolation. +macro reaction_network(name::Expr, network_expr::Expr) + make_rs_expr(esc(name.args[1])) end -# Handles two disjoint cases (empty network with interpolated name, or network with no name). -macro reaction_network(ex::Expr) - ex = MacroTools.striplines(ex) - - # The case where no name is provided. - if ex.head == :block - :(complete($(make_reaction_system(ex)))) - else # empty but has interpolated name: @reaction_network $name - networkname = :($(esc(ex.args[1]))) - return Expr(:block, :(@parameters t), - :(complete(ReactionSystem(Reaction[], t, [], []; name = $networkname)))) - end +# The case where nothing, or only a name, is provided. +macro reaction_network(name::Symbol = gensym(:ReactionSystem)) + make_rs_expr(QuoteNode(name)) end -# Returns a empty network (with, or without, a declared name). -macro reaction_network(name::Symbol = gensym(:ReactionSystem)) - return Expr(:block, :(@parameters t), - :(complete(ReactionSystem(Reaction[], t, [], []; name = $(QuoteNode(name)))))) +# Handles two disjoint cases. +macro reaction_network(expr::Expr) + # Case 1: The input is a name with interpolation. + (expr.head != :block) && return make_rs_expr(esc(expr.args[1])) + # Case 2: The input is a reaction network (and no name is provided). + return make_rs_expr(:(gensym(:ReactionSystem)), expr) end # Ideally, it would have been possible to combine the @reaction_network and @network_component macros. # However, this issue: https://github.com/JuliaLang/julia/issues/37691 causes problem with interpolations -# if we make the @reaction_network macro call the @network_component macro. +# if we make the @reaction_network macro call the @network_component macro. Instead, these uses the +# same input, but passes `complete = false` to `make_rs_expr`. """ @network_component -As @reaction_network, but the output system is not complete. +As the @reaction_network macro (see it for more information), but the output system +*is not* complete. """ -macro network_component(name::Symbol, ex::Expr) - make_reaction_system(MacroTools.striplines(ex); name = :($(QuoteNode(name)))) +macro network_component(name::Symbol, network_expr::Expr) + make_rs_expr(QuoteNode(name), network_expr; complete = false) end -# Allows @network_component $name begin ... to interpolate variables storing a name. -macro network_component(name::Expr, ex::Expr) - make_reaction_system(MacroTools.striplines(ex); name = :($(esc(name.args[1])))) +# The case where the name contains an interpolation. +macro network_component(name::Expr, network_expr::Expr) + make_rs_expr(esc(name.args[1]); complete = false) end -macro network_component(ex::Expr) - ex = MacroTools.striplines(ex) +# The case where nothing, or only a name, is provided. +macro network_component(name::Symbol = gensym(:ReactionSystem)) + make_rs_expr(QuoteNode(name); complete = false) +end - # no name but equations: @network_component begin ... end ... - if ex.head == :block - make_reaction_system(ex) - else # empty but has interpolated name: @network_component $name - networkname = :($(esc(ex.args[1]))) - return Expr(:block, :(@parameters t), - :(ReactionSystem(Reaction[], t, [], []; name = $networkname))) - end +# Handles two disjoint cases. +macro network_component(expr::Expr) + # Case 1: The input is a name with interpolation. + (expr.head != :block) && return make_rs_expr(esc(expr.args[1]); complete = false) + # Case 2: The input is a reaction network (and no name is provided). + return make_rs_expr(:(gensym(:ReactionSystem)), expr; complete = false) end -# Returns a empty network (with, or without, a declared name). -macro network_component(name::Symbol = gensym(:ReactionSystem)) - return Expr(:block, :(@parameters t), - :(ReactionSystem(Reaction[], t, [], []; name = $(QuoteNode(name))))) +### DSL Macros Helper Functions ### + +# For when no reaction network has been designated. Generates an empty network. +function make_rs_expr(name; complete = true) + rs_expr = :(ReactionSystem(Reaction[], t, [], []; name = $name)) + complete && (rs_expr = :(complete($rs_expr))) + return Expr(:block, :(@parameters t), rs_expr) +end + +# When both a name and a network expression is generated, dispatches thees to the internal +# `make_reaction_system` function. +function make_rs_expr(name, network_expr; complete = true) + rs_expr = make_reaction_system(striplines(network_expr), name) + complete && (rs_expr = :(complete($rs_expr))) + return rs_expr end ### Internal DSL Structures ### -# Structure containing information about one reactant in one reaction. -struct ReactantStruct +# Internal structure containing information about one reactant in one reaction. +struct ReactantInternal reactant::Union{Symbol, Expr} stoichiometry::ExprValues end -# Structure containing information about one Reaction. Contain all its substrates and products as well as its rate. Contains a specialized constructor. -struct ReactionStruct - substrates::Vector{ReactantStruct} - products::Vector{ReactantStruct} +# Internal structure containing information about one Reaction. Contain all its substrates and +# products as well as its rate and potential metadata. Uses a specialized constructor. +struct ReactionInternal + substrates::Vector{ReactantInternal} + products::Vector{ReactantInternal} rate::ExprValues metadata::Expr - function ReactionStruct(sub_line::ExprValues, prod_line::ExprValues, rate::ExprValues, + function ReactionInternal(sub_line::ExprValues, prod_line::ExprValues, rate::ExprValues, metadata_line::ExprValues) - sub = recursive_find_reactants!(sub_line, 1, Vector{ReactantStruct}(undef, 0)) - prod = recursive_find_reactants!(prod_line, 1, Vector{ReactantStruct}(undef, 0)) + subs = recursive_find_reactants!(sub_line, 1, Vector{ReactantInternal}(undef, 0)) + prods = recursive_find_reactants!(prod_line, 1, Vector{ReactantInternal}(undef, 0)) metadata = extract_metadata(metadata_line) - new(sub, prod, rate, metadata) + new(subs, prods, rate, metadata) end end # Recursive function that loops through the reaction line and finds the reactants and their -# stoichiometry. Recursion makes it able to handle weird cases like 2(X+Y+3(Z+XY)). +# stoichiometry. Recursion makes it able to handle weird cases like 2(X + Y + 3(Z + XY)). The +# reactants are stored in the `reactants` vector. As the expression tree is parsed, the +# stoichiometry is updated and new reactants added. function recursive_find_reactants!(ex::ExprValues, mult::ExprValues, - reactants::Vector{ReactantStruct}) + reactants::Vector{ReactantInternal}) + # We have reached the end of the expression tree and can finalise and return the reactants. if typeof(ex) != Expr || (ex.head == :escape) || (ex.head == :ref) + # The final bit of the expression is not a relevant reactant, no additions are required. (ex == 0 || in(ex, empty_set)) && (return reactants) + + # If the expression corresponds to a reactant on our list, increase its multiplicity. if any(ex == reactant.reactant for reactant in reactants) - idx = findall(x -> x == ex, getfield.(reactants, :reactant))[1] - reactants[idx] = ReactantStruct(ex, - processmult(+, mult, - reactants[idx].stoichiometry)) + idx = findfirst(r.reactant == ex for r in reactants) + new_mult = processmult(+, mult, reactants[idx].stoichiometry) + reactants[idx] = ReactantInternal(ex, new_mult) + + # If the expression corresponds to a new reactant, add it to the list. else - push!(reactants, ReactantStruct(ex, mult)) + push!(reactants, ReactantInternal(ex, mult)) end + + # If we have encountered a multiplication (i.e. a stoichiometry and a set of reactants). elseif ex.args[1] == :* + # The normal case (e.g. 3*X or 3*(X+Y)). Update the current multiplicity and continue. if length(ex.args) == 3 - recursive_find_reactants!(ex.args[3], processmult(*, mult, ex.args[2]), - reactants) + new_mult = processmult(*, mult, ex.args[2]) + recursive_find_reactants!(ex.args[3], new_mult, reactants) + # More complicated cases (e.g. 2*3*X). Yes, `ex.args[1:(end - 1)]` should start at 1 (not 2). else - newmult = processmult(*, mult, Expr(:call, ex.args[1:(end - 1)]...)) - recursive_find_reactants!(ex.args[end], newmult, reactants) + new_mult = processmult(*, mult, Expr(:call, ex.args[1:(end - 1)]...)) + recursive_find_reactants!(ex.args[end], new_mult, reactants) end + # If we have encountered a sum of different reactants, apply recursion on each. elseif ex.args[1] == :+ for i in 2:length(ex.args) recursive_find_reactants!(ex.args[i], mult, reactants) end else - throw("Malformed reaction, bad operator: $(ex.args[1]) found in stochiometry expression $ex.") + throw("Malformed reaction, bad operator: $(ex.args[1]) found in stoichiometry expression $ex.") end - reactants + return reactants end +# Helper function for updating the multiplicity throughout recursion (handles e.g. parametric +# stoichiometries). function processmult(op, mult, stoich) if (mult isa Number) && (stoich isa Number) op(mult, stoich) @@ -247,12 +264,16 @@ function processmult(op, mult, stoich) end end -# Finds the metadata from a metadata expresion (`[key=val, ...]`) and returns as a Vector{Pair{Symbol,ExprValues}}. +# Finds the metadata from a metadata expression (`[key=val, ...]`) and returns as a +# `Vector{Pair{Symbol,ExprValues}}``. function extract_metadata(metadata_line::Expr) metadata = :([]) for arg in metadata_line.args - (arg.head != :(=)) && error("Malformatted metadata line: $metadata_line. Each entry in the vector should contain a `=`.") - (arg.args[1] isa Symbol) || error("Malformatted metadata entry: $arg. Entries left-hand-side should be a single symbol.") + if arg.head != :(=) + error("Malformatted metadata line: $metadata_line. Each entry in the vector should contain a `=`.") + elseif !(arg.args[1] isa Symbol) + error("Malformatted metadata entry: $arg. Entries left-hand-side should be a single symbol.") + end push!(metadata.args, :($(QuoteNode(arg.args[1])) => $(arg.args[2]))) end return metadata @@ -262,7 +283,7 @@ end ### DSL Internal Master Function ### # Function for creating a ReactionSystem structure (used by the @reaction_network macro). -function make_reaction_system(ex::Expr; name = :(gensym(:ReactionSystem))) +function make_reaction_system(ex::Expr, name) # Handle interpolation of variables ex = esc_dollars!(ex) @@ -377,7 +398,7 @@ end ### DSL Reaction Reading Functions ### # Generates a vector containing a number of reaction structures, each containing the information about one reaction. -function get_reactions(exprs::Vector{Expr}, reactions = Vector{ReactionStruct}(undef, 0)) +function get_reactions(exprs::Vector{Expr}, reactions = Vector{ReactionInternal}(undef, 0)) for line in exprs # Reads core reaction information. arrow, rate, reaction, metadata = read_reaction_line(line) @@ -423,7 +444,7 @@ end # Takes a reaction line and creates reaction(s) from it and pushes those to the reaction array. # Used to create multiple reactions from, for instance, `k, (X,Y) --> 0`. -function push_reactions!(reactions::Vector{ReactionStruct}, sub_line::ExprValues, prod_line::ExprValues, +function push_reactions!(reactions::Vector{ReactionInternal}, sub_line::ExprValues, prod_line::ExprValues, rate::ExprValues, metadata::ExprValues, arrow::Symbol) # The rates, substrates, products, and metadata may be in a tupple form (e.g. `k, (X,Y) --> 0`). # This finds the length of these tuples (or 1 if not in tuple forms). Errors if lengs inconsistent. @@ -445,7 +466,7 @@ function push_reactions!(reactions::Vector{ReactionStruct}, sub_line::ExprValues error("Some reaction metadata fields where repeated: $(metadata_entries)") end - push!(reactions, ReactionStruct(get_tup_arg(sub_line, i), get_tup_arg(prod_line, i), + push!(reactions, ReactionInternal(get_tup_arg(sub_line, i), get_tup_arg(prod_line, i), get_tup_arg(rate, i), metadata_i)) end end @@ -836,7 +857,7 @@ function make_reaction(ex::Expr) end end -# Reads a single line and creates the corresponding ReactionStruct. +# Reads a single line and creates the corresponding ReactionInternal. function get_reaction(line) reaction = get_reactions([line]) if (length(reaction) != 1) From b06068f709776878dfd57117e4e609aea2efc518 Mon Sep 17 00:00:00 2001 From: Torkel Date: Mon, 27 May 2024 22:54:38 -0400 Subject: [PATCH 03/38] save progress --- src/dsl.jl | 186 +++++++++++++++++++++++++++++------------------------ 1 file changed, 101 insertions(+), 85 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index f95996e7a2..d701b18fa1 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -255,7 +255,7 @@ function recursive_find_reactants!(ex::ExprValues, mult::ExprValues, end # Helper function for updating the multiplicity throughout recursion (handles e.g. parametric -# stoichiometries). +# stoichiometries). The `op` argument is an operation (e.g. `*`, but could also e.g. be `+`). function processmult(op, mult, stoich) if (mult isa Number) && (stoich isa Number) op(mult, stoich) @@ -285,74 +285,54 @@ end # Function for creating a ReactionSystem structure (used by the @reaction_network macro). function make_reaction_system(ex::Expr, name) - # Handle interpolation of variables + # Handle interpolation of variables in the input. ex = esc_dollars!(ex) - # Read lines with reactions and options. + # Extracts the lines with reactions and lines with options. reaction_lines = Expr[x for x in ex.args if x.head == :tuple] option_lines = Expr[x for x in ex.args if x.head == :macrocall] - # Get macro options. - if length(unique(arg.args[1] for arg in option_lines)) < length(option_lines) + # Extracts the options used (throwing errors for repeated options). + if !allunique(arg.args[1] for arg in option_lines) error("Some options where given multiple times.") end - options = Dict(map(arg -> Symbol(String(arg.args[1])[2:end]) => arg, - option_lines)) - - # Reads options. - default_reaction_metadata = :([]) - check_default_noise_scaling!(default_reaction_metadata, options) + options = Dict(Symbol(String(arg.args[1])[2:end]) => arg for arg in option_lines) + + # Reads options (round 1, options which must be read before the reactions, e.g. because + # they might declare parameters/species/variables). compound_expr, compound_species = read_compound_options(options) - continuous_events_expr = read_events_option(options, :continuous_events) - discrete_events_expr = read_events_option(options, :discrete_events) - - # Parses reactions, species, and parameters. - reactions = get_reactions(reaction_lines) species_declared = [extract_syms(options, :species); compound_species] parameters_declared = extract_syms(options, :parameters) variables_declared = extract_syms(options, :variables) - - # Reads more options. vars_extracted, add_default_diff, equations = read_equations_options(options, variables_declared) - variables = vcat(variables_declared, vars_extracted) - - # handle independent variables - if haskey(options, :ivs) - ivs = Tuple(extract_syms(options, :ivs)) - ivexpr = copy(options[:ivs]) - ivexpr.args[1] = Symbol("@", "variables") - else - ivs = (DEFAULT_IV_SYM,) - ivexpr = :(@variables $(DEFAULT_IV_SYM)) - end - tiv = ivs[1] - sivs = (length(ivs) > 1) ? Expr(:vect, ivs[2:end]...) : nothing - all_ivs = (isnothing(sivs) ? [tiv] : [tiv; sivs.args]) - if haskey(options, :combinatoric_ratelaws) - combinatoric_ratelaws = options[:combinatoric_ratelaws].args[end] - else - combinatoric_ratelaws = true - end - - # Reads more options. - observed_vars, observed_eqs, obs_syms = read_observed_options(options, [species_declared; variables], all_ivs) - - declared_syms = Set(Iterators.flatten((parameters_declared, species_declared, + # Extracts all reactions. Extracts all parameters, species, and variables of the system and + # creates lists with them. + reactions = get_reactions(reaction_lines) + variables = vcat(variables_declared, vars_extracted) + syms_declared = Set(Iterators.flatten((parameters_declared, species_declared, variables))) - species_extracted, parameters_extracted = extract_species_and_parameters!(reactions, - declared_syms) + species_extracted, parameters_extracted = extract_species_and_parameters(reactions, + syms_declared) species = vcat(species_declared, species_extracted) parameters = vcat(parameters_declared, parameters_extracted) - # Create differential expression. + # Reads options (round 2, options that either can, or must, be read after the reactions). + tiv, sivs, all_ivs, ivexpr = read_ivs_option(options) + continuous_events_expr = read_events_option(options, :continuous_events) + discrete_events_expr = read_events_option(options, :discrete_events) + observed_vars, observed_eqs, obs_syms = read_observed_options(options, [species_declared; variables], all_ivs) diffexpr = create_differential_expr(options, add_default_diff, [species; parameters; variables], tiv) + default_reaction_metadata = read_default_noise_scaling_option(options) + combinatoric_ratelaws = read_combinatoric_ratelaws_option(options) # Checks for input errors. - (sum(length.([reaction_lines, option_lines])) != length(ex.args)) && + if (sum(length.([reaction_lines, option_lines])) != length(ex.args)) error("@reaction_network input contain $(length(ex.args) - sum(length.([reaction_lines,option_lines]))) malformed lines.") - any(!in(opt_in, option_keys) for opt_in in keys(options)) && + end + if any(!in(opt_in, option_keys) for opt_in in keys(options)) error("The following unsupported options were used: $(filter(opt_in->!in(opt_in,option_keys), keys(options)))") + end forbidden_symbol_check(union(species, parameters)) forbidden_variable_check(variables) unique_symbol_check(union(species, parameters, variables, ivs)) @@ -397,13 +377,17 @@ end ### DSL Reaction Reading Functions ### -# Generates a vector containing a number of reaction structures, each containing the information about one reaction. -function get_reactions(exprs::Vector{Expr}, reactions = Vector{ReactionInternal}(undef, 0)) +# Generates a vector of reaction structures, each containing the information about one reaction. +function get_reactions(exprs::Vector{Expr}) + # Declares an array to which we add all found reactions. + reactions = Vector{ReactionInternal}(undef, 0) + + # Loops through each line of reactions. Extracts and adds each lines's reactions to `reactions`. for line in exprs # Reads core reaction information. arrow, rate, reaction, metadata = read_reaction_line(line) - # Checks the type of arrow used, and creates the corresponding reaction(s). Returns them in an array. + # Checks which type of line is used, and calls `push_reactions!` on the processed line. if in(arrow, double_arrows) if typeof(rate) != Expr || rate.head != :tuple error("Error: Must provide a tuple of reaction rates when declaring a bi-directional reaction.") @@ -423,8 +407,8 @@ end # Extracts the rate, reaction, and metadata fields (the last one optional) from a reaction line. function read_reaction_line(line::Expr) - # Handles rate, reaction, and arrow. - # Special routine required for the`-->` case, which creates different expression from all other cases. + # Handles rate, reaction, and arrow. Special routine required for the`-->` case, which + # creates an expression different what the other arrows creates. rate = line.args[1] reaction = line.args[2] (reaction.head == :-->) && (reaction = Expr(:call, :→, reaction.args[1], reaction.args[2])) @@ -442,32 +426,30 @@ function read_reaction_line(line::Expr) return arrow, rate, reaction, metadata end -# Takes a reaction line and creates reaction(s) from it and pushes those to the reaction array. -# Used to create multiple reactions from, for instance, `k, (X,Y) --> 0`. -function push_reactions!(reactions::Vector{ReactionInternal}, sub_line::ExprValues, prod_line::ExprValues, +# Takes a reaction line and creates reaction(s) from it and pushes those to the reaction vector. +# Used to create multiple reactions from bundled reactions (like `k, (X,Y) --> 0`). +function push_reactions!(reactions::Vector{ReactionInternal}, subs::ExprValues, prods::ExprValues, rate::ExprValues, metadata::ExprValues, arrow::Symbol) - # The rates, substrates, products, and metadata may be in a tupple form (e.g. `k, (X,Y) --> 0`). - # This finds the length of these tuples (or 1 if not in tuple forms). Errors if lengs inconsistent. - lengs = (tup_leng(sub_line), tup_leng(prod_line), tup_leng(rate), tup_leng(metadata)) - if any(!(leng == 1 || leng == maximum(lengs)) for leng in lengs) - throw("Malformed reaction, rate=$rate, subs=$sub_line, prods=$prod_line, metadata=$metadata.") - end - - # Loops through each reaction encoded by the reaction composites. Adds the reaction to the reactions vector. - for i in 1:maximum(lengs) - # If the `only_use_rate` metadata was not provided, this has to be infered from the arrow used. + # The rates, substrates, products, and metadata may be in a tuple form (e.g. `k, (X,Y) --> 0`). + # This finds these tuples' lengths (or 1 for non-tuple forms). Inconsistent lengths yield error. + lengs = (tup_leng(subs), tup_leng(prods), tup_leng(rate), tup_leng(metadata)) + maxl = maximum(lengs) + if any(!(leng == 1 || leng == maxl) for leng in lengs) + throw("Malformed reaction, rate=$rate, subs=$subs, prods=$prods, metadata=$metadata.") + end + + # Loops through each reaction encoded by the reaction's different components. + # Creates a `ReactionInternal` representation and adds it to `reactions`. + for i in 1:maxl + # If the `only_use_rate` metadata was not provided, this must be inferred from the arrow. metadata_i = get_tup_arg(metadata, i) - if all(arg -> arg.args[1] != :only_use_rate, metadata_i.args) + if all(arg.args[1] != :only_use_rate for arg in metadata_i.args) push!(metadata_i.args, :(only_use_rate = $(in(arrow, pure_rate_arrows)))) end - # Checks that metadata fields are unqiue. - if !allunique(arg.args[1] for arg in metadata_i.args) - error("Some reaction metadata fields where repeated: $(metadata_entries)") - end - - push!(reactions, ReactionInternal(get_tup_arg(sub_line, i), get_tup_arg(prod_line, i), - get_tup_arg(rate, i), metadata_i)) + # Extracts substrates, products, and rates for the i'th reaction. + subs_i, prods_i, rate_i = get_tup_arg.([subs, prods, rate], i) + push!(reactions, ReactionInternal(subs_i, prods_i, rate_i, metadata_i)) end end @@ -480,16 +462,15 @@ function extract_syms(opts, vartype::Symbol) if haskey(opts, vartype) ex = opts[vartype] vars = Symbolics._parse_vars(vartype, Real, ex.args[3:end]) - syms = Vector{Union{Symbol, Expr}}(vars.args[end].args) + return Vector{Union{Symbol, Expr}}(vars.args[end].args) else - syms = Union{Symbol, Expr}[] + return Union{Symbol, Expr}[] end - return syms end # Function looping through all reactions, to find undeclared symbols (species or # parameters), and assign them to the right category. -function extract_species_and_parameters!(reactions, excluded_syms) +function extract_species_and_parameters(reactions, excluded_syms) species = OrderedSet{Union{Symbol, Expr}}() for reaction in reactions for reactant in Iterators.flatten((reaction.substrates, reaction.products)) @@ -509,7 +490,7 @@ function extract_species_and_parameters!(reactions, excluded_syms) collect(species), collect(parameters) end -# Function called by extract_species_and_parameters!, recursively loops through an +# Function called by extract_species_and_parameters, recursively loops through an # expression and find symbols (adding them to the push_symbols vector). function add_syms_from_expr!(push_symbols::AbstractSet, rateex::ExprValues, excluded_syms) if rateex isa Symbol @@ -590,15 +571,40 @@ end ### DSL Option Handling ### -# Checks if the `@default_noise_scaling` option is used. If so, read its input and adds it as a -# default metadata value to the `default_reaction_metadata` vector. -function check_default_noise_scaling!(default_reaction_metadata, options) +# Finds the time idenepdnet variable, and any potential spatial indepndent variables. +# Returns these (individually and combined), as well as an expression for declaring them +function read_ivs_option(options) + # Creates the independent variables expressions (depends on whether the `ivs` option was used). + if haskey(options, :ivs) + ivs = Tuple(extract_syms(options, :ivs)) + ivexpr = copy(options[:ivs]) + ivexpr.args[1] = Symbol("@", "variables") + else + ivs = (DEFAULT_IV_SYM,) + ivexpr = :(@variables $(DEFAULT_IV_SYM)) + end + + # Extracts the independet variables symbols, and returns the output. + tiv = ivs[1] + sivs = (length(ivs) > 1) ? Expr(:vect, ivs[2:end]...) : nothing + all_ivs = (isnothing(sivs) ? [tiv] : [tiv; sivs.args]) + return tiv, sivs, all_ivs, ivexpr +end + +# Returns the `default_reaction_metadata` output. Technically Catalyst's code could have been made +# more generic to account for other default reaction metadata. Practically, this will likely +# be the only relevant reaction metadata to have a default value via the DSL. If another becomes +# relevant, the code can be rewritten to take this into account. +# Checks if the `@default_noise_scaling` option is used. If so, uses it as the default value of +# the `default_noise_scaling` reaction metadata, otherwise, returns an empty vector. +function read_default_noise_scaling_option(options) if haskey(options, :default_noise_scaling) - if (length(options[:default_noise_scaling].args) != 3) # Becasue of how expressions are, 3 is used. - error("@default_noise_scaling should only have a single input, this appears not to be the case: \"$(options[:default_noise_scaling])\"") + if (length(options[:default_noise_scaling].args) != 3) + error("@default_noise_scaling should only have a single expression as its input, this appears not to be the case: \"$(options[:default_noise_scaling])\"") end - push!(default_reaction_metadata.args, :(:noise_scaling => $(options[:default_noise_scaling].args[3]))) + return :([:noise_scaling => $(options[:default_noise_scaling].args[3])]) end + return :([]) end # When compound species are declared using the "@compound begin ... end" option, get a list of the compound species, and also the expression that crates them. @@ -710,6 +716,16 @@ function create_differential_expr(options, add_default_diff, used_syms, tiv) return diffexpr end +# Reads the combinatiorial ratelaw options, which determines if a combinatorial rate law should +# be used or not. If not provides, uses the default (true). +function read_combinatoric_ratelaws_option(options) + if haskey(options, :combinatoric_ratelaws) + return options[:combinatoric_ratelaws].args[end] + else + return true + end +end + # Reads the observables options. Outputs an expression ofr creating the obervable variables, and a vector of observable equations. function read_observed_options(options, species_n_vars_declared, ivs_sorted) if haskey(options, :observables) @@ -837,7 +853,7 @@ function make_reaction(ex::Expr) # Parses reactions, species, and parameters. reaction = get_reaction(ex) - species, parameters = extract_species_and_parameters!([reaction], []) + species, parameters = extract_species_and_parameters([reaction], []) # Checks for input errors. forbidden_symbol_check(union(species, parameters)) From 5cc8f416d1f8cfdeaf699f472c27ff27bd5f3338 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 28 May 2024 14:38:16 -0400 Subject: [PATCH 04/38] save progress --- src/chemistry_functionality.jl | 2 +- src/dsl.jl | 161 ++++++++++++++++++--------------- 2 files changed, 90 insertions(+), 73 deletions(-) diff --git a/src/chemistry_functionality.jl b/src/chemistry_functionality.jl index 941038046b..5ed88bbdd2 100644 --- a/src/chemistry_functionality.jl +++ b/src/chemistry_functionality.jl @@ -85,7 +85,7 @@ function make_compound(expr) # Loops through all components, add the component and the coefficients to the corresponding vectors # Cannot extract directly using e.g. "getfield.(composition, :reactant)" because then # we get something like :([:C, :O]), rather than :([C, O]). - composition = Catalyst.recursive_find_reactants!(expr.args[3], 1, Vector{ReactantStruct}(undef, 0)) + composition = Catalyst.recursive_find_reactants!(expr.args[3], 1, Vector{ReactantInternal}(undef, 0)) components = :([]) # Becomes something like :([C, O]). coefficients = :([]) # Becomes something like :([1, 2]). for comp in composition diff --git a/src/dsl.jl b/src/dsl.jl index d701b18fa1..e95daf670c 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -300,7 +300,7 @@ function make_reaction_system(ex::Expr, name) # Reads options (round 1, options which must be read before the reactions, e.g. because # they might declare parameters/species/variables). - compound_expr, compound_species = read_compound_options(options) + compound_expr_init, compound_species = read_compound_options(options) species_declared = [extract_syms(options, :species); compound_species] parameters_declared = extract_syms(options, :parameters) variables_declared = extract_syms(options, :variables) @@ -318,10 +318,10 @@ function make_reaction_system(ex::Expr, name) parameters = vcat(parameters_declared, parameters_extracted) # Reads options (round 2, options that either can, or must, be read after the reactions). - tiv, sivs, all_ivs, ivexpr = read_ivs_option(options) + tiv, sivs, ivs, ivexpr = read_ivs_option(options) continuous_events_expr = read_events_option(options, :continuous_events) discrete_events_expr = read_events_option(options, :discrete_events) - observed_vars, observed_eqs, obs_syms = read_observed_options(options, [species_declared; variables], all_ivs) + observed_expr, observed_eqs, obs_syms = read_observed_options(options, [species_declared; variables], ivs) diffexpr = create_differential_expr(options, add_default_diff, [species; parameters; variables], tiv) default_reaction_metadata = read_default_noise_scaling_option(options) combinatoric_ratelaws = read_combinatoric_ratelaws_option(options) @@ -337,35 +337,33 @@ function make_reaction_system(ex::Expr, name) forbidden_variable_check(variables) unique_symbol_check(union(species, parameters, variables, ivs)) + if isdefined(Main, :Infiltrator) + Main.infiltrate(@__MODULE__, Base.@locals, @__FILE__, @__LINE__) + end + # Creates expressions corresponding to actual code from the internal DSL representation. - sexprs = get_sexpr(species_extracted, options; iv_symbols = ivs) - vexprs = get_sexpr(vars_extracted, options, :variables; iv_symbols = ivs) - pexprs = get_pexpr(parameters_extracted, options) - ps, pssym = scalarize_macro(!isempty(parameters), pexprs, "ps") - vars, varssym = scalarize_macro(!isempty(variables), vexprs, "vars") - sps, spssym = scalarize_macro(!isempty(species), sexprs, "specs") - comps, compssym = scalarize_macro(!isempty(compound_species), compound_expr, "comps") - rxexprs = :(Catalyst.CatalystEqType[]) - for reaction in reactions - push!(rxexprs.args, get_rxexprs(reaction)) - end - for equation in equations - push!(rxexprs.args, equation) - end + psexprs_init = get_pexpr(parameters_extracted, options) + spsexprs_init = get_usexpr(species_extracted, options; ivs) + vsexprs_init = get_usexpr(vars_extracted, options, :variables; ivs) + psexprs, psvar = scalarize_macro(!isempty(parameters), psexprs_init, "ps") + spsexprs, spsvar = scalarize_macro(!isempty(species), spsexprs_init, "specs") + vsexprs, vsvar = scalarize_macro(!isempty(variables), psexprs, "vars") + compound_expr, compsvar = scalarize_macro(!isempty(compound_species), compound_expr_init, "comps") + rxsexprs = make_rxsexprs(reactions, equations) quote - $ps + $psexprs $ivexpr - $vars - $sps - $observed_vars - $comps + $spsexprs + $vsexprs + $observed_expr + $compound_expr $diffexpr Catalyst.remake_ReactionSystem_internal( Catalyst.make_ReactionSystem_internal( - $rxexprs, $tiv, setdiff(union($spssym, $varssym, $compssym), $obs_syms), - $pssym; name = $name, spatial_ivs = $sivs, observed = $observed_eqs, + $rxsexprs, $tiv, setdiff(union($spssym, $ssvar, $compsvar), $obs_syms), + $psvar; name = $name, spatial_ivs = $sivs, observed = $observed_eqs, continuous_events = $continuous_events_expr, discrete_events = $discrete_events_expr, combinatoric_ratelaws = $combinatoric_ratelaws); @@ -509,51 +507,39 @@ end ### DSL Output Expression Builders ### -# Given the species that were extracted from the reactions, and the options dictionary, creates the @species ... expression for the macro output. -function get_sexpr(species_extracted, options, key = :species; - iv_symbols = (DEFAULT_IV_SYM,)) +# Given the extracted species (or variables) and the option dictionary, creates the +# `@species ...` (or `@variables ..`) expression which would declare these. +# If `key = :variables`, does this for variables (and not species). +function get_usexpr(us_extracted, options, key = :species; ivs = (DEFAULT_IV_SYM,)) if haskey(options, key) - sexprs = options[key] - elseif isempty(species_extracted) - sexprs = :() + usexpr = options[key] + elseif isempty(us_extracted) + usexpr = :() else - sexprs = Expr(:macrocall, Symbol("@", key), LineNumberNode(0)) + usexpr = Expr(:macrocall, Symbol("@", key), LineNumberNode(0)) + end + for u in us_extracted + u isa Symbol && push!(usexpr.args, Expr(:call, u, ivs...)) end - foreach(s -> (s isa Symbol) && push!(sexprs.args, Expr(:call, s, iv_symbols...)), - species_extracted) - sexprs + return usexpr end -# Given the parameters that were extracted from the reactions, and the options dictionary, creates the @parameters ... expression for the macro output. +# Given the parameters that were extracted from the reactions, and the options dictionary, +# creates the `@parameters ...` expression for the macro output. function get_pexpr(parameters_extracted, options) - pexprs = (haskey(options, :parameters) ? options[:parameters] : - (isempty(parameters_extracted) ? :() : :(@parameters))) - foreach(p -> push!(pexprs.args, p), parameters_extracted) - pexprs -end - -# Creates the reaction vector declaration statement. -function get_rxexprs(rxstruct) - subs_init = isempty(rxstruct.substrates) ? nothing : :([]) - subs_stoich_init = deepcopy(subs_init) - prod_init = isempty(rxstruct.products) ? nothing : :([]) - prod_stoich_init = deepcopy(prod_init) - reaction_func = :(Reaction($(recursive_expand_functions!(rxstruct.rate)), $subs_init, - $prod_init, $subs_stoich_init, $prod_stoich_init, - metadata = $(rxstruct.metadata),)) - for sub in rxstruct.substrates - push!(reaction_func.args[3].args, sub.reactant) - push!(reaction_func.args[5].args, sub.stoichiometry) - end - for prod in rxstruct.products - push!(reaction_func.args[4].args, prod.reactant) - push!(reaction_func.args[6].args, prod.stoichiometry) + if haskey(options, :parameters) + pexprs = options[:parameters] + elseif isempty(parameters_extracted) + pexprs = :() + else + pexprs = :(@parameters) end - reaction_func + foreach(p -> push!(pexprs.args, p), parameters_extracted) + return pexprs end -# takes a ModelingToolkit declaration macro like @parameters and returns an expression -# that calls the macro and then scalarizes all the symbols created into a vector of Nums +# Takes a ModelingToolkit declaration macro (like @parameters ...) and: +# Returns an expression that calls the macro and then scalarizes all the symbols created into a vector of Nums function scalarize_macro(nonempty, ex, name) namesym = gensym(name) if nonempty @@ -568,6 +554,38 @@ function scalarize_macro(nonempty, ex, name) ex, namesym end +# From the system reactions (as `ReactionInternal`s) and equations (as expressions), +# creates the expressions that evalutes to the reaction (+ equations) vector. +function make_rxsexprs(reactions, equations) + rxsexprs = :(Catalyst.CatalystEqType[]) + foreach(rx -> push!(rxsexprs.args, get_rxexpr(rx)), reactions) + foreach(eq -> push!(rxsexprs.args, eq), equations) + return rxsexprs +end + +# From a `ReactionInternal` struct, creates the expression which evaluates to the creation +# of the correponding reaction. +function get_rxexpr(rx::ReactionInternal) + # Initiates the `Reaction` expression. + rate = recursive_expand_functions!(rx.rate) + reaction_func = :(Reaction($rate, [], [], [], []; metadata = $(rx.metadata))) + if isdefined(Main, :Infiltrator) + Main.infiltrate(@__MODULE__, Base.@locals, @__FILE__, @__LINE__) + end + # Loops through all products and substrates, and adds them (and their stoichiometries) + # to the `Reaction` expression. + for sub in rx.substrates + push!(reaction_func.args[3].args, sub.reactant) + push!(reaction_func.args[5].args, sub.stoichiometry) + end + for prod in rx.products + push!(reaction_func.args[4].args, prod.reactant) + push!(reaction_func.args[6].args, prod.stoichiometry) + end + + return reaction_func +end + ### DSL Option Handling ### @@ -587,8 +605,7 @@ function read_ivs_option(options) # Extracts the independet variables symbols, and returns the output. tiv = ivs[1] sivs = (length(ivs) > 1) ? Expr(:vect, ivs[2:end]...) : nothing - all_ivs = (isnothing(sivs) ? [tiv] : [tiv; sivs.args]) - return tiv, sivs, all_ivs, ivexpr + return tiv, sivs, ivs, ivexpr end # Returns the `default_reaction_metadata` output. Technically Catalyst's code could have been made @@ -732,7 +749,7 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted) # Gets list of observable equations and prepares variable declaration expression. # (`options[:observables]` inlucdes `@observables`, `.args[3]` removes this part) observed_eqs = make_observed_eqs(options[:observables].args[3]) - observed_vars = Expr(:block, :(@variables)) + observed_expr = Expr(:block, :(@variables)) obs_syms = :([]) for (idx, obs_eq) in enumerate(observed_eqs.args) @@ -757,15 +774,15 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted) # Appends (..) to the observable (which is later replaced with the extracted ivs). # Adds the observable to the first line of the output expression (starting with `@variables`). obs_expr = insert_independent_variable(obs_eq.args[2], :(..)) - push!(observed_vars.args[1].args, obs_expr) + push!(observed_expr.args[1].args, obs_expr) - # Adds a line to the `observed_vars` expression, setting the ivs for this observable. + # Adds a line to the `observed_expr` expression, setting the ivs for this observable. # Cannot extract directly using e.g. "getfield.(dependants_structs, :reactant)" because # then we get something like :([:X1, :X2]), rather than :([X1, X2]). dep_var_expr = :(filter(!MT.isparameter, Symbolics.get_variables($(obs_eq.args[3])))) ivs_get_expr = :(unique(reduce(vcat,[arguments(MT.unwrap(dep)) for dep in $dep_var_expr]))) ivs_get_expr_sorted = :(sort($(ivs_get_expr); by = iv -> findfirst(MT.getname(iv) == ivs for ivs in $ivs_sorted))) - push!(observed_vars.args, :($obs_name = $(obs_name)($(ivs_get_expr_sorted)...))) + push!(observed_expr.args, :($obs_name = $(obs_name)($(ivs_get_expr_sorted)...))) end # In case metadata was given, this must be cleared from `observed_eqs`. @@ -778,15 +795,15 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted) is_escaped_expr(obs_eq.args[2]) || push!(obs_syms.args, obs_name) end - # If nothing was added to `observed_vars`, it has to be modified not to throw an error. - (length(observed_vars.args) == 1) && (observed_vars = :()) + # If nothing was added to `observed_expr`, it has to be modified not to throw an error. + (length(observed_expr.args) == 1) && (observed_expr = :()) else # If option is not used, return empty expression and vector. - observed_vars = :() + observed_expr = :() observed_eqs = :([]) obs_syms = :([]) end - return observed_vars, observed_eqs, obs_syms + return observed_expr, observed_eqs, obs_syms end # From the input to the @observables options, creates a vector containing one equation for each observable. @@ -859,9 +876,9 @@ function make_reaction(ex::Expr) forbidden_symbol_check(union(species, parameters)) # Creates expressions corresponding to actual code from the internal DSL representation. - sexprs = get_sexpr(species, Dict{Symbol, Expr}()) + sexprs = get_usexpr(species, Dict{Symbol, Expr}()) pexprs = get_pexpr(parameters, Dict{Symbol, Expr}()) - rxexpr = get_rxexprs(reaction) + rxexpr = get_rxexpr(reaction) iv = :(@variables $(DEFAULT_IV_SYM)) # Returns the rephrased expression. From 15273f133cb069179b180ba02ff770a3a135a038 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 28 May 2024 16:40:38 -0400 Subject: [PATCH 05/38] save progress --- src/dsl.jl | 204 +++++++++++++++++++++++----------------- src/expression_utils.jl | 47 ++++----- 2 files changed, 141 insertions(+), 110 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index e95daf670c..50ea75734e 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -337,32 +337,30 @@ function make_reaction_system(ex::Expr, name) forbidden_variable_check(variables) unique_symbol_check(union(species, parameters, variables, ivs)) - if isdefined(Main, :Infiltrator) - Main.infiltrate(@__MODULE__, Base.@locals, @__FILE__, @__LINE__) - end - # Creates expressions corresponding to actual code from the internal DSL representation. - psexprs_init = get_pexpr(parameters_extracted, options) - spsexprs_init = get_usexpr(species_extracted, options; ivs) - vsexprs_init = get_usexpr(vars_extracted, options, :variables; ivs) - psexprs, psvar = scalarize_macro(!isempty(parameters), psexprs_init, "ps") - spsexprs, spsvar = scalarize_macro(!isempty(species), spsexprs_init, "specs") - vsexprs, vsvar = scalarize_macro(!isempty(variables), psexprs, "vars") - compound_expr, compsvar = scalarize_macro(!isempty(compound_species), compound_expr_init, "comps") + psexpr_init = get_pexpr(parameters_extracted, options) + spsexpr_init = get_usexpr(species_extracted, options; ivs) + vsexpr_init = get_usexpr(vars_extracted, options, :variables; ivs) + psexpr, psvar = scalarize_macro(psexpr_init, "ps") + spsexpr, spsvar = scalarize_macro(spsexpr_init, "specs") + vsexpr, vsvar = scalarize_macro(vsexpr_init, "vars") + compound_expr, compsvar = scalarize_macro(compound_expr_init, "comps") rxsexprs = make_rxsexprs(reactions, equations) + # Assemblies the full expression that declares all required symbolic variables, and + # then the output `ReactionSystem`. quote - $psexprs + $psexpr $ivexpr - $spsexprs - $vsexprs + $spsexpr + $vsexpr $observed_expr $compound_expr $diffexpr Catalyst.remake_ReactionSystem_internal( Catalyst.make_ReactionSystem_internal( - $rxsexprs, $tiv, setdiff(union($spssym, $ssvar, $compsvar), $obs_syms), + $rxsexprs, $tiv, setdiff(union($spsvar, $vsvar, $compsvar), $obs_syms), $psvar; name = $name, spatial_ivs = $sivs, observed = $observed_eqs, continuous_events = $continuous_events_expr, discrete_events = $discrete_events_expr, @@ -457,6 +455,8 @@ end # When the user have used the @species (or @parameters) options, extract species (or # parameters) from its input. function extract_syms(opts, vartype::Symbol) + # If the corresponding option have been used, uses `Symbolics._parse_vars` to find all + # variable within it (returning them in a vector). if haskey(opts, vartype) ex = opts[vartype] vars = Symbolics._parse_vars(vartype, Real, ex.args[3:end]) @@ -469,14 +469,16 @@ end # Function looping through all reactions, to find undeclared symbols (species or # parameters), and assign them to the right category. function extract_species_and_parameters(reactions, excluded_syms) + # Loops through all reactant, extract undeclared ones as species. species = OrderedSet{Union{Symbol, Expr}}() for reaction in reactions for reactant in Iterators.flatten((reaction.substrates, reaction.products)) add_syms_from_expr!(species, reactant.reactant, excluded_syms) end end - foreach(s -> push!(excluded_syms, s), species) + + # Loops through all rates and stoichiometries, extracting used symbols as parameters. parameters = OrderedSet{Union{Symbol, Expr}}() for reaction in reactions add_syms_from_expr!(parameters, reaction.rate, excluded_syms) @@ -485,23 +487,23 @@ function extract_species_and_parameters(reactions, excluded_syms) end end - collect(species), collect(parameters) + return collect(species), collect(parameters) end # Function called by extract_species_and_parameters, recursively loops through an # expression and find symbols (adding them to the push_symbols vector). -function add_syms_from_expr!(push_symbols::AbstractSet, rateex::ExprValues, excluded_syms) - if rateex isa Symbol - if !(rateex in forbidden_symbols_skip) && !(rateex in excluded_syms) - push!(push_symbols, rateex) +function add_syms_from_expr!(push_symbols::AbstractSet, expr::ExprValues, excluded_syms) + # If we have encountered a Symbol in the recursion, we can try extracting it. + if expr isa Symbol + if !(expr in forbidden_symbols_skip) && !(expr in excluded_syms) + push!(push_symbols, expr) end - elseif rateex isa Expr + elseif expr isa Expr # note, this (correctly) skips $(...) expressions - for i in 2:length(rateex.args) - add_syms_from_expr!(push_symbols, rateex.args[i], excluded_syms) + for i in 2:length(expr.args) + add_syms_from_expr!(push_symbols, expr.args[i], excluded_syms) end end - nothing end @@ -538,20 +540,27 @@ function get_pexpr(parameters_extracted, options) return pexprs end -# Takes a ModelingToolkit declaration macro (like @parameters ...) and: -# Returns an expression that calls the macro and then scalarizes all the symbols created into a vector of Nums -function scalarize_macro(nonempty, ex, name) +# Takes a ModelingToolkit declaration macro (like @parameters ...) and return and expression: +# That calls the macro and then scalarizes all the symbols created into a vector of Nums. +# stores the created symbolic variables in a variable (which name is generated from `name`). +# It will also return the name used for the variable that stores the symbolci variables. +function scalarize_macro(expr_init, name) + # Generates a random variable name which (in generated code) will store the produced + # symbolic variables (e.g. `var"##ps#384"`). namesym = gensym(name) - if nonempty + + # If the input expression is non-emtpy, wraps it with addiional information. + if expr_init != :(()) symvec = gensym() - ex = quote - $symvec = $ex + expr = quote + $symvec = $expr_init $namesym = reduce(vcat, Symbolics.scalarize($symvec)) end else - ex = :($namesym = Num[]) + expr = :($namesym = Num[]) end - ex, namesym + + return expr, namesym end # From the system reactions (as `ReactionInternal`s) and equations (as expressions), @@ -568,22 +577,20 @@ end function get_rxexpr(rx::ReactionInternal) # Initiates the `Reaction` expression. rate = recursive_expand_functions!(rx.rate) - reaction_func = :(Reaction($rate, [], [], [], []; metadata = $(rx.metadata))) - if isdefined(Main, :Infiltrator) - Main.infiltrate(@__MODULE__, Base.@locals, @__FILE__, @__LINE__) - end + rx_constructor = :(Reaction($rate, [], [], [], []; metadata = $(rx.metadata))) + # Loops through all products and substrates, and adds them (and their stoichiometries) # to the `Reaction` expression. for sub in rx.substrates - push!(reaction_func.args[3].args, sub.reactant) - push!(reaction_func.args[5].args, sub.stoichiometry) + push!(rx_constructor.args[4].args, sub.reactant) + push!(rx_constructor.args[6].args, sub.stoichiometry) end for prod in rx.products - push!(reaction_func.args[4].args, prod.reactant) - push!(reaction_func.args[6].args, prod.stoichiometry) + push!(rx_constructor.args[5].args, prod.reactant) + push!(rx_constructor.args[7].args, prod.stoichiometry) end - return reaction_func + return rx_constructor end @@ -743,7 +750,8 @@ function read_combinatoric_ratelaws_option(options) end end -# Reads the observables options. Outputs an expression ofr creating the obervable variables, and a vector of observable equations. +# Reads the observables options. Outputs an expression for creating the obervable variables, +# a vector with the observable equations, and a vector with the observables' symbols. function read_observed_options(options, species_n_vars_declared, ivs_sorted) if haskey(options, :observables) # Gets list of observable equations and prepares variable declaration expression. @@ -753,13 +761,17 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted) obs_syms = :([]) for (idx, obs_eq) in enumerate(observed_eqs.args) - # Extract the observable, checks errors, and continues the loop if the observable has been declared. + # Extract the observable, checks for errors. obs_name, ivs, defaults, metadata = find_varinfo_in_declaration(obs_eq.args[2]) - isempty(ivs) || error("An observable ($obs_name) was given independent variable(s). These should not be given, as they are inferred automatically.") - isnothing(defaults) || error("An observable ($obs_name) was given a default value. This is forbidden.") - in(obs_name, forbidden_symbols_error) && error("A forbidden symbol ($(obs_eq.args[2])) was used as an observable name.") - - # Error checks. + if !isempty(ivs) + error("An observable ($obs_name) was given independent variable(s). These should not be given, as they are inferred automatically.") + end + if !isnothing(defaults) + error("An observable ($obs_name) was given a default value. This is forbidden.") + end + if in(obs_name, forbidden_symbols_error) + error("A forbidden symbol ($(obs_eq.args[2])) was used as an observable name.") + end if (obs_name in species_n_vars_declared) && is_escaped_expr(obs_eq.args[2]) error("An interpoalted observable have been used, which has also been explicitly delcared within the system using eitehr @species or @variables. This is not permited.") end @@ -773,15 +785,16 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted) if !((obs_name in species_n_vars_declared) || is_escaped_expr(obs_eq.args[2])) # Appends (..) to the observable (which is later replaced with the extracted ivs). # Adds the observable to the first line of the output expression (starting with `@variables`). - obs_expr = insert_independent_variable(obs_eq.args[2], :(..)) - push!(observed_expr.args[1].args, obs_expr) + obs_expr = insert_independent_variable(obs_eq.args[2], :(..)) + push!(observed_expr.args[1].args, obs_expr) # Adds a line to the `observed_expr` expression, setting the ivs for this observable. # Cannot extract directly using e.g. "getfield.(dependants_structs, :reactant)" because # then we get something like :([:X1, :X2]), rather than :([X1, X2]). dep_var_expr = :(filter(!MT.isparameter, Symbolics.get_variables($(obs_eq.args[3])))) ivs_get_expr = :(unique(reduce(vcat,[arguments(MT.unwrap(dep)) for dep in $dep_var_expr]))) - ivs_get_expr_sorted = :(sort($(ivs_get_expr); by = iv -> findfirst(MT.getname(iv) == ivs for ivs in $ivs_sorted))) + sort_func(iv) = findfirst(MT.getname(iv) == ivs for ivs in $ivs_sorted) + ivs_get_expr_sorted = :(sort($(ivs_get_expr); by = sort_func)) push!(observed_expr.args, :($obs_name = $(obs_name)($(ivs_get_expr_sorted)...))) end @@ -803,23 +816,18 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted) observed_eqs = :([]) obs_syms = :([]) end + return observed_expr, observed_eqs, obs_syms end -# From the input to the @observables options, creates a vector containing one equation for each observable. -# Checks separate cases for "@obervables O ~ ..." and "@obervables begin ... end". Other cases errors. +# From the input to the @observables options, creates a vector containing one equation for +# each observable. `option_block_form` handles if single line declaration of `@observables`, +# i.e. without a `begin ... end` block, was used. function make_observed_eqs(observables_expr) - if observables_expr.head == :call - return :([$(observables_expr)]) - elseif observables_expr.head == :block - observed_eqs = :([]) - for arg in observables_expr.args - push!(observed_eqs.args, arg) - end - return observed_eqs - else - error("Malformed observables option usage: $(observables_expr).") - end + observables_expr = option_block_form(observables_expr) + observed_eqs = :([]) + foreach(arg -> push!(observed_eqs.args, arg), observables_expr.args) + return observed_eqs end @@ -828,20 +836,41 @@ end @doc raw""" @reaction -Generates a single [`Reaction`](@ref) object. +Generates a single [`Reaction`](@ref) object using a similar syntax as the `@reaction_network` +macro (but permiting only a single reaction). A more detailed introduction to the syntax can +be found in the description of `@reaction_network`. + +The `@reaction` macro is folled by a single line consisting of three parts: +- A rate (at which the reaction occur). +- Any number of substrates (which are consumed by the reaction). +- Any number of products (which are produced by the reaction). + +The output is a reaction (just liek created using teh `Reaction` constructor). Examples: +Here we create a simple binding reaction and stroes it in the variable rx: ```julia +rx = @reaction k, X + Y --> XY +``` +The macro will automatically deduce `X`, `Y`, and `XY` to be species (as these occur as reactants) +and `k` as a parameters (as it does not occur as a reactant). + +The `@reaction` macro provides a more concise notation to the `Reaction` constructor. I.e. here +we create the same reaction using both approaches, and also confirms that they are identical. +```julia +# Creates a reaction using the `@reaction` macro. rx = @reaction k*v, A + B --> C + D -# is equivalent to +# Creates a reaction using the `Reaction` constructor. t = default_t() @parameters k v @species A(t) B(t) C(t) D(t) -rx == Reaction(k*v, [A,B], [C,D]) +rx2 = Reaction(k*v, [A, B], [C, D]) + +# Confirms that the two approaches yield identical results: +rx1 == rx2 ``` -Here `k` and `v` will be parameters and `A`, `B`, `C` and `D` will be variables. -Interpolation of existing parameters/variables also works +Interpolation of already declared symbolic variables into `@reaction` is possible: ```julia t = default_t() @parameters k b @@ -851,10 +880,8 @@ rx = @reaction b*$ex*$A, $A --> C ``` Notes: -- Any symbols arising in the rate expression that aren't interpolated are treated as -parameters. In the reaction part (`α*A + B --> C + D`), coefficients are treated as -parameters, e.g. `α`, and rightmost symbols as species, e.g. `A,B,C,D`. -- Works with any *single* arrow types supported by [`@reaction_network`](@ref). +- `@reaction` does not support bi-directional type reactions (using `<-->`) or reaction bundling +(e.g. `d, (X,Y) --> 0`). - Interpolation of Julia variables into the macro works similar to the `@reaction_network` macro. See [The Reaction DSL](@ref dsl_description) tutorial for more details. """ @@ -865,23 +892,23 @@ end # Function for creating a Reaction structure (used by the @reaction macro). function make_reaction(ex::Expr) - # Handle interpolation of variables + # Handle interpolation of variables in the input. ex = esc_dollars!(ex) - # Parses reactions, species, and parameters. + # Parses reactions. Extracts species and paraemters within it. reaction = get_reaction(ex) species, parameters = extract_species_and_parameters([reaction], []) # Checks for input errors. forbidden_symbol_check(union(species, parameters)) - # Creates expressions corresponding to actual code from the internal DSL representation. + # Creates expressions corresponding to code for declaring the parameters, species, and reaction. sexprs = get_usexpr(species, Dict{Symbol, Expr}()) pexprs = get_pexpr(parameters, Dict{Symbol, Expr}()) rxexpr = get_rxexpr(reaction) iv = :(@variables $(DEFAULT_IV_SYM)) - # Returns the rephrased expression. + # Returns a repharsed expression which generates the `Reaction`. quote $pexprs $iv @@ -896,31 +923,34 @@ function get_reaction(line) if (length(reaction) != 1) error("Malformed reaction. @reaction macro only creates a single reaction. E.g. double arrows, such as `<-->` are not supported.") end - return reaction[1] + return only(reaction) end ### Generic Expression Manipulation ### -# Recursively traverses an expression and replaces special function call like "hill(...)" with the actual corresponding expression. +# Recursively traverses an expression and replaces special function call like "hill(...)" with +# the actual corresponding expression. function recursive_expand_functions!(expr::ExprValues) (typeof(expr) != Expr) && (return expr) - foreach(i -> expr.args[i] = recursive_expand_functions!(expr.args[i]), - 1:length(expr.args)) - if expr.head == :call - !isdefined(Catalyst, expr.args[1]) && (expr.args[1] = esc(expr.args[1])) + for i in eachindex(expr.args) + expr.args[i] = recursive_expand_functions!(expr.args[i]) + end + if (expr.head == :call) && !isdefined(Catalyst, expr.args[1]) + expr.args[1] = esc(expr.args[1]) end - expr + return expr end -# Returns the length of a expression tuple, or 1 if it is not an expression tuple (probably a Symbol/Numerical). +# Returns the length of a expression tuple, or 1 if it is not an expression tuple (probably +# a Symbol/Numerical). This is used to handle bundled reaction (like `d, (X,Y) --> 0`). function tup_leng(ex::ExprValues) (typeof(ex) == Expr && ex.head == :tuple) && (return length(ex.args)) return 1 end # Gets the ith element in a expression tuple, or returns the input itself if it is not an expression tuple -# (probably a Symbol/Numerical). +# (probably a Symbol/Numerical). This is used to handle bundled reaction (like `d, (X,Y) --> 0`). function get_tup_arg(ex::ExprValues, i::Int) (tup_leng(ex) == 1) && (return ex) return ex.args[i] diff --git a/src/expression_utils.jl b/src/expression_utils.jl index f0d039759f..33c50f346a 100644 --- a/src/expression_utils.jl +++ b/src/expression_utils.jl @@ -2,16 +2,19 @@ # Function that handles variable interpolation. function esc_dollars!(ex) - if ex isa Expr - if ex.head == :$ - return esc(:($(ex.args[1]))) - else - for i in 1:length(ex.args) - ex.args[i] = esc_dollars!(ex.args[i]) - end + # If we do not have an expression: recursion has finished and we return the input. + (ex isa Expr) || (return ex) + + # If we have encountered an interpolation, perform the appropriate modification, else recur. + if ex.head == :$ + return esc(:($(ex.args[1]))) + else + for i in eachindex(ex.args) + ex.args[i] = esc_dollars!(ex.args[i]) end end - ex + + return ex end # Checks if an expression is an escaped expression (e.g. on the form `$(Expr(:escape, :Y))`) @@ -23,28 +26,26 @@ end ### Parameters/Species/Variables Symbols Correctness Checking ### # Throws an error when a forbidden symbol is used. -function forbidden_symbol_check(v) - !isempty(intersect(forbidden_symbols_error, v)) && - error("The following symbol(s) are used as species or parameters: " * - ((map(s -> "'" * string(s) * "', ", - intersect(forbidden_symbols_error, v))...)) * - "this is not permited.") - nothing +function forbidden_symbol_check(sym) + if !isempty(intersect(forbidden_symbols_error, sym)) + used_forbidden_syms = intersect(forbidden_symbols_error, sym) + error("The following symbol(s) are used as species or parameters: $used_forbidden_syms, this is not permitted.") + end end # Throws an error when a forbidden variable is used (a forbidden symbol that is not `:t`). -function forbidden_variable_check(v) - !isempty(intersect(forbidden_variables_error, v)) && - error("The following symbol(s) are used as variables: " * - ((map(s -> "'" * string(s) * "', ", - intersect(forbidden_variables_error, v))...)) * - "this is not permited.") +function forbidden_variable_check(sym) + if !isempty(intersect(forbidden_variables_error, sym)) + used_forbidden_syms = intersect(forbidden_variables_error, sym) + error("The following symbol(s) are used as variables: $used_forbidden_syms, this is not permitted.") + end end +# Checks that no symbol was sued for multiple purposes. function unique_symbol_check(syms) - allunique(syms) || + if !allunique(syms) error("Reaction network independent variables, parameters, species, and variables must all have distinct names, but a duplicate has been detected. ") - nothing + end end From b105b3e0affe1c86610e9e4c3a7003de0e26dcb9 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 28 May 2024 17:40:27 -0400 Subject: [PATCH 06/38] add additional tests --- test/dsl/dsl_basic_model_construction.jl | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/test/dsl/dsl_basic_model_construction.jl b/test/dsl/dsl_basic_model_construction.jl index 72dd01f1ab..81d3c396a2 100644 --- a/test/dsl/dsl_basic_model_construction.jl +++ b/test/dsl/dsl_basic_model_construction.jl @@ -54,6 +54,65 @@ end ## Run Tests ### +# Tests the various network constructors. Test for `@network_component` and `@network_component`. +# Tests for combinations of reactions/no reactions, no name/name/interpolated name. +let + # Declare comparison networks programmatically. + @parameters d + @species X(t) + rx = Reaction(d, [X], []) + + rs_empty = ReactionSystem([], t; name = :name) + rs = ReactionSystem([rx], t; name = :name) + rs_empty_comp = complete(rs_empty) + rs_comp = complete(rs) + + # Declare empty networks. + name_sym = :name + rs_empty_1 = @network_component + rs_empty_2 = @network_component name + rs_empty_3 = @network_component $name_sym + rs_empty_comp_1 = @reaction_network + rs_empty_comp_2 = @reaction_network name + rs_empty_comp_3 = @reaction_network $name_sym + + # Check that empty networks are correct. + isequivalent(rs_empty_1, rs_empty) + rs_empty_2 == rs_empty + rs_empty_3 == rs_empty + isequivalent(rs_empty_comp_1, rs_empty_comp) + rs_empty_comp_2 == rs_empty_comp + rs_empty_comp_3 == rs_empty_comp + + # Declare non-empty networks. + rs_1 = @network_component begin + d, X --> 0 + end + rs_2 = @network_component name begin + d, X --> 0 + end + rs_3 = @network_component $name_sym begin + d, X --> 0 + end + rs_comp_1 = @reaction_network begin + d, X --> 0 + end + rs_comp_2 = @reaction_network name begin + d, X --> 0 + end + rs_comp_3 = @reaction_network $name_sym begin + d, X --> 0 + end + + # Check that non-empty networks are correct. + isequivalent(rs_1, rs) + rs_2 == rs + rs_3 == rs + isequivalent(rs_empty_1, rs_empty) + rs_empty_2 == rs_empty + rs_empty_3 == rs_empty +end + # Test basic properties of networks. let basic_test(reaction_networks_standard[1], 10, [:X1, :X2, :X3], From 1324428c70af022005bad865a5ce43b79c9485d1 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 28 May 2024 17:41:47 -0400 Subject: [PATCH 07/38] up --- src/registered_functions.jl | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/registered_functions.jl b/src/registered_functions.jl index 70f06ac080..b7ad8f14e8 100644 --- a/src/registered_functions.jl +++ b/src/registered_functions.jl @@ -110,12 +110,13 @@ function Symbolics.derivative(::typeof(hillar), args::NTuple{5, Any}, ::Val{5}) end -### Custom CRN FUnction-related Functions ### +### Custom CRN Function-related Functions ### """ expand_registered_functions(expr) -Takes an expression, and expands registered function expressions. E.g. `mm(X,v,K)` is replaced with v*X/(X+K). Currently supported functions: `mm`, `mmr`, `hill`, `hillr`, and `hill`. +Takes an expression, and expands registered function expressions. E.g. `mm(X,v,K)` is replaced +with v*X/(X+K). Currently supported functions: `mm`, `mmr`, `hill`, `hillr`, and `hill`. """ function expand_registered_functions(expr) istree(expr) || return expr @@ -136,16 +137,20 @@ function expand_registered_functions(expr) end return expr end -# If applied to a Reaction, return a reaction with its rate modified. + +# If applied to a `Reaction`, return a reaction with its rate modified. function expand_registered_functions(rx::Reaction) Reaction(expand_registered_functions(rx.rate), rx.substrates, rx.products, rx.substoich, rx.prodstoich, rx.netstoich, rx.only_use_rate, rx.metadata) end -# If applied to a Equation, returns it with it applied to lhs and rhs + +# If applied to an `Equation`, returns it with it applied to lhs and rhs function expand_registered_functions(eq::Equation) return expand_registered_functions(eq.lhs) ~ expand_registered_functions(eq.rhs) end -# If applied to a ReactionSystem, applied function to all Reactions and other Equations, and return updated system. + +# If applied to a `ReactionSystem`, applied function to all `Reaction`s and other `Equation`s, +# and return updated system. function expand_registered_functions(rs::ReactionSystem) @set! rs.eqs = [Catalyst.expand_registered_functions(eq) for eq in get_eqs(rs)] @set! rs.rxs = [Catalyst.expand_registered_functions(rx) for rx in get_rxs(rs)] From 71c9b5ce54656e5871ce566ad51a1644e616fce9 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sun, 2 Jun 2024 11:46:40 -0400 Subject: [PATCH 08/38] fix --- src/dsl.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dsl.jl b/src/dsl.jl index 50ea75734e..555abed1b7 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -793,7 +793,7 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted) # then we get something like :([:X1, :X2]), rather than :([X1, X2]). dep_var_expr = :(filter(!MT.isparameter, Symbolics.get_variables($(obs_eq.args[3])))) ivs_get_expr = :(unique(reduce(vcat,[arguments(MT.unwrap(dep)) for dep in $dep_var_expr]))) - sort_func(iv) = findfirst(MT.getname(iv) == ivs for ivs in $ivs_sorted) + sort_func(iv) = findfirst(MT.getname(iv) == ivs for ivs in ivs_sorted) ivs_get_expr_sorted = :(sort($(ivs_get_expr); by = sort_func)) push!(observed_expr.args, :($obs_name = $(obs_name)($(ivs_get_expr_sorted)...))) end From d45f0a7b002c26eee8c9ec034b60f36bf679d340 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 11 Jun 2024 14:46:41 -0400 Subject: [PATCH 09/38] merge fixes --- src/chemistry_functionality.jl | 3 +- src/dsl.jl | 58 ++++++++++++++++++---------------- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/chemistry_functionality.jl b/src/chemistry_functionality.jl index fa78bd74d1..430c6e9d82 100644 --- a/src/chemistry_functionality.jl +++ b/src/chemistry_functionality.jl @@ -85,7 +85,8 @@ function make_compound(expr) # Loops through all components, add the component and the coefficients to the corresponding vectors # Cannot extract directly using e.g. "getfield.(composition, :reactant)" because then # we get something like :([:C, :O]), rather than :([C, O]). - composition = Catalyst.recursive_find_reactants!(expr.args[3], 1, Vector{ReactantInternal}(undef, 0)) + composition = Catalyst.recursive_find_reactants!(expr.args[3], 1, + Vector{ReactantInternal}(undef, 0)) components = :([]) # Becomes something like :([C, O]). coefficients = :([]) # Becomes something like :([1, 2]). for comp in composition diff --git a/src/dsl.jl b/src/dsl.jl index 17977f4352..ded8a854ea 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -124,7 +124,7 @@ macro reaction_network(name::Symbol = gensym(:ReactionSystem)) end # Handles two disjoint cases. -macro reaction_network(expr::Expr) +macro reaction_network(expr::Expr) # Case 1: The input is a name with interpolation. (expr.head != :block) && return make_rs_expr(esc(expr.args[1])) # Case 2: The input is a reaction network (and no name is provided). @@ -156,7 +156,7 @@ macro network_component(name::Symbol = gensym(:ReactionSystem)) end # Handles two disjoint cases. -macro network_component(expr::Expr) +macro network_component(expr::Expr) # Case 1: The input is a name with interpolation. (expr.head != :block) && return make_rs_expr(esc(expr.args[1]); complete = false) # Case 2: The input is a reaction network (and no name is provided). @@ -180,7 +180,6 @@ function make_rs_expr(name, network_expr; complete = true) return rs_expr end - ### Internal DSL Structures ### # Internal structure containing information about one reactant in one reaction. @@ -197,8 +196,8 @@ struct ReactionInternal rate::ExprValues metadata::Expr - function ReactionInternal(sub_line::ExprValues, prod_line::ExprValues, rate::ExprValues, - metadata_line::ExprValues) + function ReactionInternal(sub_line::ExprValues, prod_line::ExprValues, + rate::ExprValues, metadata_line::ExprValues) subs = recursive_find_reactants!(sub_line, 1, Vector{ReactantInternal}(undef, 0)) prods = recursive_find_reactants!(prod_line, 1, Vector{ReactantInternal}(undef, 0)) metadata = extract_metadata(metadata_line) @@ -211,7 +210,7 @@ end # reactants are stored in the `reactants` vector. As the expression tree is parsed, the # stoichiometry is updated and new reactants added. function recursive_find_reactants!(ex::ExprValues, mult::ExprValues, - reactants::Vector{ReactantInternal}) + reactants::Vector{ReactantInternal}) # We have reached the end of the expression tree and can finalise and return the reactants. if typeof(ex) != Expr || (ex.head == :escape) || (ex.head == :ref) # The final bit of the expression is not a relevant reactant, no additions are required. @@ -223,23 +222,23 @@ function recursive_find_reactants!(ex::ExprValues, mult::ExprValues, new_mult = processmult(+, mult, reactants[idx].stoichiometry) reactants[idx] = ReactantInternal(ex, new_mult) - # If the expression corresponds to a new reactant, add it to the list. + # If the expression corresponds to a new reactant, add it to the list. else push!(reactants, ReactantInternal(ex, mult)) end - # If we have encountered a multiplication (i.e. a stoichiometry and a set of reactants). + # If we have encountered a multiplication (i.e. a stoichiometry and a set of reactants). elseif ex.args[1] == :* # The normal case (e.g. 3*X or 3*(X+Y)). Update the current multiplicity and continue. if length(ex.args) == 3 new_mult = processmult(*, mult, ex.args[2]) recursive_find_reactants!(ex.args[3], new_mult, reactants) - # More complicated cases (e.g. 2*3*X). Yes, `ex.args[1:(end - 1)]` should start at 1 (not 2). + # More complicated cases (e.g. 2*3*X). Yes, `ex.args[1:(end - 1)]` should start at 1 (not 2). else new_mult = processmult(*, mult, Expr(:call, ex.args[1:(end - 1)]...)) recursive_find_reactants!(ex.args[end], new_mult, reactants) end - # If we have encountered a sum of different reactants, apply recursion on each. + # If we have encountered a sum of different reactants, apply recursion on each. elseif ex.args[1] == :+ for i in 2:length(ex.args) recursive_find_reactants!(ex.args[i], mult, reactants) @@ -289,26 +288,27 @@ function make_reaction_system(ex::Expr, name) # Extracts the options used (throwing errors for repeated options). if !allunique(arg.args[1] for arg in option_lines) - error("Some options where given multiple times.") + error("Some options where given multiple times.") end options = Dict(Symbol(String(arg.args[1])[2:end]) => arg for arg in option_lines) - + # Reads options (round 1, options which must be read before the reactions, e.g. because # they might declare parameters/species/variables). compound_expr_init, compound_species = read_compound_options(options) species_declared = [extract_syms(options, :species); compound_species] parameters_declared = extract_syms(options, :parameters) variables_declared = extract_syms(options, :variables) - vars_extracted, add_default_diff, equations = read_equations_options(options, variables_declared) + vars_extracted, add_default_diff, equations = read_equations_options(options, + variables_declared) # Extracts all reactions. Extracts all parameters, species, and variables of the system and # creates lists with them. reactions = get_reactions(reaction_lines) variables = vcat(variables_declared, vars_extracted) syms_declared = Set(Iterators.flatten((parameters_declared, species_declared, - variables))) + variables))) species_extracted, parameters_extracted = extract_species_and_parameters(reactions, - syms_declared) + syms_declared) species = vcat(species_declared, species_extracted) parameters = vcat(parameters_declared, parameters_extracted) @@ -316,8 +316,10 @@ function make_reaction_system(ex::Expr, name) tiv, sivs, ivs, ivexpr = read_ivs_option(options) continuous_events_expr = read_events_option(options, :continuous_events) discrete_events_expr = read_events_option(options, :discrete_events) - observed_expr, observed_eqs, obs_syms = read_observed_options(options, [species_declared; variables], ivs) - diffexpr = create_differential_expr(options, add_default_diff, [species; parameters; variables], tiv) + observed_expr, observed_eqs, obs_syms = read_observed_options(options, + [species_declared; variables], ivs) + diffexpr = create_differential_expr(options, add_default_diff, + [species; parameters; variables], tiv) default_reaction_metadata = read_default_noise_scaling_option(options) combinatoric_ratelaws = read_combinatoric_ratelaws_option(options) @@ -424,8 +426,8 @@ end # Takes a reaction line and creates reaction(s) from it and pushes those to the reaction vector. # Used to create multiple reactions from bundled reactions (like `k, (X,Y) --> 0`). -function push_reactions!(reactions::Vector{ReactionInternal}, subs::ExprValues, prods::ExprValues, - rate::ExprValues, metadata::ExprValues, arrow::Symbol) +function push_reactions!(reactions::Vector{ReactionInternal}, subs::ExprValues, + prods::ExprValues, rate::ExprValues, metadata::ExprValues, arrow::Symbol) # The rates, substrates, products, and metadata may be in a tuple form (e.g. `k, (X,Y) --> 0`). # This finds these tuples' lengths (or 1 for non-tuple forms). Inconsistent lengths yield error. lengs = (tup_leng(subs), tup_leng(prods), tup_leng(rate), tup_leng(metadata)) @@ -591,7 +593,6 @@ function get_rxexpr(rx::ReactionInternal) return rx_constructor end - ### DSL Option Handling ### # Finds the time idenepdnet variable, and any potential spatial indepndent variables. @@ -621,7 +622,7 @@ end # the `default_noise_scaling` reaction metadata, otherwise, returns an empty vector. function read_default_noise_scaling_option(options) if haskey(options, :default_noise_scaling) - if (length(options[:default_noise_scaling].args) != 3) + if (length(options[:default_noise_scaling].args) != 3) error("@default_noise_scaling should only have a single expression as its input, this appears not to be the case: \"$(options[:default_noise_scaling])\"") end return :([:noise_scaling => $(options[:default_noise_scaling].args[3])]) @@ -774,13 +775,13 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted) for (idx, obs_eq) in enumerate(observed_eqs.args) # Extract the observable, checks for errors. obs_name, ivs, defaults, metadata = find_varinfo_in_declaration(obs_eq.args[2]) - if !isempty(ivs) + if !isempty(ivs) error("An observable ($obs_name) was given independent variable(s). These should not be given, as they are inferred automatically.") end - if !isnothing(defaults) + if !isnothing(defaults) error("An observable ($obs_name) was given a default value. This is forbidden.") end - if in(obs_name, forbidden_symbols_error) + if in(obs_name, forbidden_symbols_error) error("A forbidden symbol ($(obs_eq.args[2])) was used as an observable name.") end if (obs_name in species_n_vars_declared) && is_escaped_expr(obs_eq.args[2]) @@ -803,11 +804,14 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted) # Adds a line to the `observed_expr` expression, setting the ivs for this observable. # Cannot extract directly using e.g. "getfield.(dependants_structs, :reactant)" because # then we get something like :([:X1, :X2]), rather than :([X1, X2]). - dep_var_expr = :(filter(!MT.isparameter, Symbolics.get_variables($(obs_eq.args[3])))) - ivs_get_expr = :(unique(reduce(vcat,[arguments(MT.unwrap(dep)) for dep in $dep_var_expr]))) + dep_var_expr = :(filter(!MT.isparameter, + Symbolics.get_variables($(obs_eq.args[3])))) + ivs_get_expr = :(unique(reduce(vcat, + [arguments(MT.unwrap(dep)) for dep in $dep_var_expr]))) sort_func(iv) = findfirst(MT.getname(iv) == ivs for ivs in ivs_sorted) ivs_get_expr_sorted = :(sort($(ivs_get_expr); by = sort_func)) - push!(observed_expr.args, :($obs_name = $(obs_name)($(ivs_get_expr_sorted)...))) + push!(observed_expr.args, + :($obs_name = $(obs_name)($(ivs_get_expr_sorted)...))) end # In case metadata was given, this must be cleared from `observed_eqs`. From cdac225d201ae2a519d59cfcfaeaa9c8eff3f019 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sat, 13 Jul 2024 19:12:08 -0400 Subject: [PATCH 10/38] merge fixes --- src/expression_utils.jl | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/expression_utils.jl b/src/expression_utils.jl index 2373d84ee4..2cc899df26 100644 --- a/src/expression_utils.jl +++ b/src/expression_utils.jl @@ -14,7 +14,7 @@ function esc_dollars!(ex) end end - return ex + ex end # Checks if an expression is an escaped expression (e.g. on the form `$(Expr(:escape, :Y))`) @@ -26,25 +26,22 @@ end # Throws an error when a forbidden symbol is used. function forbidden_symbol_check(sym) - if !isempty(intersect(forbidden_symbols_error, sym)) - used_forbidden_syms = intersect(forbidden_symbols_error, sym) - error("The following symbol(s) are used as species or parameters: $used_forbidden_syms, this is not permitted.") - end + isempty(intersect(forbidden_symbols_error, sym)) && return + used_forbidden_syms = intersect(forbidden_symbols_error, sym) + error("The following symbol(s) are used as species or parameters: $used_forbidden_syms, this is not permitted.") end # Throws an error when a forbidden variable is used (a forbidden symbol that is not `:t`). function forbidden_variable_check(sym) - if !isempty(intersect(forbidden_variables_error, sym)) - used_forbidden_syms = intersect(forbidden_variables_error, sym) - error("The following symbol(s) are used as variables: $used_forbidden_syms, this is not permitted.") - end + isempty(intersect(forbidden_variables_error, sym)) && return + used_forbidden_syms = intersect(forbidden_variables_error, sym) + error("The following symbol(s) are used as variables: $used_forbidden_syms, this is not permitted.") end # Checks that no symbol was sued for multiple purposes. function unique_symbol_check(syms) - if !allunique(syms) - error("Reaction network independent variables, parameters, species, and variables must all have distinct names, but a duplicate has been detected. ") - end + allunique(syms) && return + error("Reaction network independent variables, parameters, species, and variables must all have distinct names, but a duplicate has been detected. ") end ### Catalyst-specific Expressions Manipulation ### From ea9aae3bfed26909aa6c8e6f596d46076f7b267d Mon Sep 17 00:00:00 2001 From: Torkel Date: Sat, 13 Jul 2024 19:53:52 -0400 Subject: [PATCH 11/38] up --- src/dsl.jl | 26 ++++++++++----------- test/dsl/dsl_advanced_model_construction.jl | 2 +- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index 337593a01f..a48ab61261 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -57,18 +57,17 @@ DSL" (where DSL = domain-specific language), as it implements a DSL for creating network models. The `@reaction_network` macro, and the `ReactionSystem`s it generates, are central to Catalyst -and its functionality. Catalyst is describe in more detail [in its documentation](@ref ref). The -`reaction_network` DSL, in particular, is described in more detail [here](@ref dsl_description). +and its functionality. Catalyst is described in more detail in its documentation. The +`reaction_network` DSL in particular is described in more detail [here](@ref dsl_description). The `@reaction_network` statement is followed by a `begin ... end` block. Each line within the block corresponds to a single reaction. Each reaction consists of: -- A rate (at which the reaction occur). +- A rate (at which the reaction occurs). - Any number of substrates (which are consumed by the reaction). - Any number of products (which are produced by the reaction). - Examples: -Here we create a basic SIR model. It contains two reactions (infections and recovery): +Here we create a basic SIR model. It contains two reactions (infection and recovery): ```julia sir_model = @reaction_network begin c1, S + I --> 2I @@ -76,7 +75,7 @@ sir_model = @reaction_network begin end ``` -Next we create a self-activation loop. Here, a single component (`X`) activates its own production +Next, we create a self-activation loop. Here, a single component (`X`) activates its own production with a Michaelis-Menten function: ```julia sa_loop = @reaction_network begin @@ -84,12 +83,12 @@ sa_loop = @reaction_network begin d, X --> 0 end ``` -This model also contain production and degradation reactions, where `0` denotes that there are -either no substrates or not products in a reaction. +This model also contains production and degradation reactions, where `0` denotes that there are +either no substrates or no products in a reaction. Options: -The `@reaction_network` also accepts various options. These are inputs to the model that are not -reactions. To denote that a line contain an option (and not a reaction), the line starts with `@` +The `@reaction_network` also accepts various options. These are inputs to the model creation that are +not reactions. To denote that a line contains an option (and not a reaction), the line starts with `@` followed by the options name. E.g. an observable is declared using the `@observables` option. Here we create a polymerisation model (where the parameter `n` denotes the number of monomers in the polymer). We use the observable `Xtot` to track the total amount of `X` in the system. We also @@ -102,12 +101,11 @@ end ``` Notes: -- `ReactionSystem`s creates through `@reaction_network` are considered complete (non-complete +- `ReactionSystem`s created through `@reaction_network` are considered complete (non-complete systems can be created through the alternative `@network_component` macro). -- `ReactionSystem`s creates through `@reaction_network`, by default, have a random name. Specific +- `ReactionSystem`s created through `@reaction_network`, by default, have a random name. Specific names can be designated as a first argument (before `begin`, e.g. `rn = @reaction_network name begin ...`). - For more information, please again consider Catalyst's documentation. - """ macro reaction_network(name::Symbol, network_expr::Expr) make_rs_expr(QuoteNode(name), network_expr) @@ -115,7 +113,7 @@ end # The case where the name contains an interpolation. macro reaction_network(name::Expr, network_expr::Expr) - make_rs_expr(esc(name.args[1])) + make_rs_expr(esc(name.args[1]), network_expr) end # The case where nothing, or only a name, is provided. diff --git a/test/dsl/dsl_advanced_model_construction.jl b/test/dsl/dsl_advanced_model_construction.jl index 155dbe7fbe..9d4c0ce9bb 100644 --- a/test/dsl/dsl_advanced_model_construction.jl +++ b/test/dsl/dsl_advanced_model_construction.jl @@ -131,7 +131,7 @@ let end # Line number nodes aren't ignored so have to be manually removed Base.remove_linenums!(ex) - @test eval(Catalyst.make_reaction_system(ex)) isa ReactionSystem + @test eval(Catalyst.make_reaction_system(ex, :name)) isa ReactionSystem end # Miscellaneous interpolation tests. Unsure what they do here (not related to DSL). From 70841330f16d68bbec74851f288c35c860b1e45a Mon Sep 17 00:00:00 2001 From: Torkel Date: Sat, 13 Jul 2024 20:03:10 -0400 Subject: [PATCH 12/38] test fixes --- test/dsl/dsl_advanced_model_construction.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/dsl/dsl_advanced_model_construction.jl b/test/dsl/dsl_advanced_model_construction.jl index 9d4c0ce9bb..06068c2c9a 100644 --- a/test/dsl/dsl_advanced_model_construction.jl +++ b/test/dsl/dsl_advanced_model_construction.jl @@ -131,7 +131,7 @@ let end # Line number nodes aren't ignored so have to be manually removed Base.remove_linenums!(ex) - @test eval(Catalyst.make_reaction_system(ex, :name)) isa ReactionSystem + @test eval(Catalyst.make_reaction_system(ex, name = QuoteNode(:name))) isa ReactionSystem end # Miscellaneous interpolation tests. Unsure what they do here (not related to DSL). @@ -212,8 +212,8 @@ end # Checks that repeated metadata throws errors. let - @test_throws LoadError @eval @reaction k, 0 --> X, [md1=1.0, md1=2.0] - @test_throws LoadError @eval @reaction_network begin + @test_throws Exception @eval @reaction k, 0 --> X, [md1=1.0, md1=2.0] + @test_throws Exception @eval @reaction_network begin k, 0 --> X, [md1=1.0, md1=1.0] end end From a6b4c3ac4fed49983e3191918c51d303f7ea3920 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sat, 13 Jul 2024 20:05:27 -0400 Subject: [PATCH 13/38] format --- src/dsl.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index a48ab61261..b8e556979e 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -296,7 +296,7 @@ function make_reaction_system(ex::Expr, name) species_declared = [extract_syms(options, :species); compound_species] parameters_declared = extract_syms(options, :parameters) variables_declared = extract_syms(options, :variables) - vars_extracted, add_default_diff, equations = read_equations_options(options, + vars_extracted, add_default_diff, equations = read_equations_options(options, variables_declared) # Extracts all reactions. Extracts all parameters, species, and variables of the system and @@ -314,9 +314,9 @@ function make_reaction_system(ex::Expr, name) tiv, sivs, ivs, ivexpr = read_ivs_option(options) continuous_events_expr = read_events_option(options, :continuous_events) discrete_events_expr = read_events_option(options, :discrete_events) - observed_expr, observed_eqs, obs_syms = read_observed_options(options, + observed_expr, observed_eqs, obs_syms = read_observed_options(options, [species_declared; variables], ivs) - diffexpr = create_differential_expr(options, add_default_diff, + diffexpr = create_differential_expr(options, add_default_diff, [species; parameters; variables], tiv) default_reaction_metadata = read_default_noise_scaling_option(options) combinatoric_ratelaws = read_combinatoric_ratelaws_option(options) @@ -424,7 +424,7 @@ end # Takes a reaction line and creates reaction(s) from it and pushes those to the reaction vector. # Used to create multiple reactions from bundled reactions (like `k, (X,Y) --> 0`). -function push_reactions!(reactions::Vector{ReactionInternal}, subs::ExprValues, +function push_reactions!(reactions::Vector{ReactionInternal}, subs::ExprValues, prods::ExprValues, rate::ExprValues, metadata::ExprValues, arrow::Symbol) # The rates, substrates, products, and metadata may be in a tuple form (e.g. `k, (X,Y) --> 0`). # This finds these tuples' lengths (or 1 for non-tuple forms). Inconsistent lengths yield error. @@ -802,9 +802,9 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted) # Adds a line to the `observed_expr` expression, setting the ivs for this observable. # Cannot extract directly using e.g. "getfield.(dependants_structs, :reactant)" because # then we get something like :([:X1, :X2]), rather than :([X1, X2]). - dep_var_expr = :(filter(!MT.isparameter, + dep_var_expr = :(filter(!MT.isparameter, Symbolics.get_variables($(obs_eq.args[3])))) - ivs_get_expr = :(unique(reduce(vcat, + ivs_get_expr = :(unique(reduce(vcat, [arguments(MT.unwrap(dep)) for dep in $dep_var_expr]))) sort_func(iv) = findfirst(MT.getname(iv) == ivs for ivs in ivs_sorted) ivs_get_expr_sorted = :(sort($(ivs_get_expr); by = sort_func)) From 160668f1feb0e7ba5c2af04c0a0b256eef182d2c Mon Sep 17 00:00:00 2001 From: Torkel Date: Sat, 13 Jul 2024 21:46:02 -0400 Subject: [PATCH 14/38] up --- src/dsl.jl | 2 +- test/dsl/dsl_advanced_model_construction.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index b8e556979e..5bb85f2e2d 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -145,7 +145,7 @@ end # The case where the name contains an interpolation. macro network_component(name::Expr, network_expr::Expr) - make_rs_expr(esc(name.args[1]); complete = false) + make_rs_expr(esc(name.args[1]), network_expr; complete = false) end # The case where nothing, or only a name, is provided. diff --git a/test/dsl/dsl_advanced_model_construction.jl b/test/dsl/dsl_advanced_model_construction.jl index 06068c2c9a..ead43f303f 100644 --- a/test/dsl/dsl_advanced_model_construction.jl +++ b/test/dsl/dsl_advanced_model_construction.jl @@ -131,7 +131,7 @@ let end # Line number nodes aren't ignored so have to be manually removed Base.remove_linenums!(ex) - @test eval(Catalyst.make_reaction_system(ex, name = QuoteNode(:name))) isa ReactionSystem + @test eval(Catalyst.make_reaction_system(ex, QuoteNode(:name))) isa ReactionSystem end # Miscellaneous interpolation tests. Unsure what they do here (not related to DSL). From 9c265eec6173336fae5c94b670fbb79a788cdc92 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sat, 13 Jul 2024 22:10:49 -0400 Subject: [PATCH 15/38] observables fix --- src/dsl.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dsl.jl b/src/dsl.jl index 5bb85f2e2d..b630a93600 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -807,7 +807,7 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted) ivs_get_expr = :(unique(reduce(vcat, [arguments(MT.unwrap(dep)) for dep in $dep_var_expr]))) sort_func(iv) = findfirst(MT.getname(iv) == ivs for ivs in ivs_sorted) - ivs_get_expr_sorted = :(sort($(ivs_get_expr); by = sort_func)) + ivs_get_expr_sorted = :(sort($(ivs_get_expr); by = $sort_func)) push!(observed_expr.args, :($obs_name = $(obs_name)($(ivs_get_expr_sorted)...))) end From 34fc2c082659ab422ac521c0e60e33cb5e130794 Mon Sep 17 00:00:00 2001 From: Torkel Date: Sat, 13 Jul 2024 22:57:53 -0400 Subject: [PATCH 16/38] spatial transport reaction dsl fix --- src/spatial_reaction_systems/spatial_reactions.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spatial_reaction_systems/spatial_reactions.jl b/src/spatial_reaction_systems/spatial_reactions.jl index 204d94992a..4c8d2f05cf 100644 --- a/src/spatial_reaction_systems/spatial_reactions.jl +++ b/src/spatial_reaction_systems/spatial_reactions.jl @@ -57,7 +57,7 @@ function make_transport_reaction(rateex, species) forbidden_symbol_check(union([species], parameters)) # Creates expressions corresponding to actual code from the internal DSL representation. - sexprs = get_sexpr([species], Dict{Symbol, Expr}()) + sexprs = get_usexpr([species], Dict{Symbol, Expr}()) pexprs = get_pexpr(parameters, Dict{Symbol, Expr}()) iv = :(@variables $(DEFAULT_IV_SYM)) trxexpr = :(TransportReaction($rateex, $species)) From 37ea1d90583512d14e46ed41631eb584f46f35b5 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 16 Jul 2024 13:24:38 -0400 Subject: [PATCH 17/38] iv is now a parmaeter --- src/dsl.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index b630a93600..91e3ed2bd1 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -600,10 +600,10 @@ function read_ivs_option(options) if haskey(options, :ivs) ivs = Tuple(extract_syms(options, :ivs)) ivexpr = copy(options[:ivs]) - ivexpr.args[1] = Symbol("@", "variables") + ivexpr.args[1] = Symbol("@", "parameters") else ivs = (DEFAULT_IV_SYM,) - ivexpr = :(@variables $(DEFAULT_IV_SYM)) + ivexpr = :($(DEFAULT_IV_SYM) = default_t()) end # Extracts the independet variables symbols, and returns the output. @@ -919,7 +919,7 @@ function make_reaction(ex::Expr) sexprs = get_usexpr(species, Dict{Symbol, Expr}()) pexprs = get_pexpr(parameters, Dict{Symbol, Expr}()) rxexpr = get_rxexpr(reaction) - iv = :(@variables $(DEFAULT_IV_SYM)) + iv = :($(DEFAULT_IV_SYM) = default_t()) # Returns a repharsed expression which generates the `Reaction`. quote From 87464f71d66c1dd063341f65de0e30c050f1e18f Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 16 Jul 2024 22:08:41 -0400 Subject: [PATCH 18/38] Add tests for various erroneous declarations --- src/dsl.jl | 3 +- test/dsl/dsl_advanced_model_construction.jl | 20 ++++++- test/dsl/dsl_basic_model_construction.jl | 37 +++++++++++- test/dsl/dsl_options.jl | 62 +++++++++++++++++++-- 4 files changed, 114 insertions(+), 8 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index 91e3ed2bd1..4cc6e44734 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -330,7 +330,7 @@ function make_reaction_system(ex::Expr, name) end forbidden_symbol_check(union(species, parameters)) forbidden_variable_check(variables) - unique_symbol_check(union(species, parameters, variables, ivs)) + unique_symbol_check(vcat(species, parameters, variables, ivs)) # Creates expressions corresponding to actual code from the internal DSL representation. psexpr_init = get_pexpr(parameters_extracted, options) @@ -783,6 +783,7 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted) error("A forbidden symbol ($(obs_eq.args[2])) was used as an observable name.") end if (obs_name in species_n_vars_declared) && is_escaped_expr(obs_eq.args[2]) + println("HERE") error("An interpolated observable have been used, which has also been explicitly declared within the system using either @species or @variables. This is not permitted.") end if ((obs_name in species_n_vars_declared) || is_escaped_expr(obs_eq.args[2])) && diff --git a/test/dsl/dsl_advanced_model_construction.jl b/test/dsl/dsl_advanced_model_construction.jl index 60b2f2e960..c3b0687b32 100644 --- a/test/dsl/dsl_advanced_model_construction.jl +++ b/test/dsl/dsl_advanced_model_construction.jl @@ -267,6 +267,24 @@ let @test isequal(rn1,rn2) end +# Tests that erroneous metadata declarations yields errors. +let + # Malformed metadata/value separator. + @test_throws Exception @eval @reaction_network begin + d, X --> 0, [misc=>"Metadata should use `=`, not `=>`."] + end + + # Malformed lhs + @test_throws Exception @eval @reaction_network begin + d, X --> 0, [misc,description=>"Metadata lhs should be a single symbol."] + end + + # Malformed metadata separator. + @test_throws Exception @eval @reaction_network begin + d, X --> 0, [misc=>:misc; description="description"] + end +end + ### Other Tests ### # Test floating point stoichiometry work. @@ -344,4 +362,4 @@ let rx = Reaction(k[1]*a+k[2], [X[1], X[2]], [Y, C], [1, V[1]], [V[2] * W, B]) @named arrtest = ReactionSystem([rx], t) arrtest == rn -end +end \ No newline at end of file diff --git a/test/dsl/dsl_basic_model_construction.jl b/test/dsl/dsl_basic_model_construction.jl index 4328eb7ca8..912de9c617 100644 --- a/test/dsl/dsl_basic_model_construction.jl +++ b/test/dsl/dsl_basic_model_construction.jl @@ -463,7 +463,7 @@ let @test rn1 == rn2 end -# Tests arrow variants in `@reaction`` macro . +# 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)) @@ -496,6 +496,41 @@ let @test_throws LoadError @eval @reaction nothing, 0 --> B @test_throws LoadError @eval @reaction k, 0 --> im @test_throws LoadError @eval @reaction k, 0 --> nothing + + # Checks that non-supported arrow type usage yields error. + @test_throws Exception @eval @reaction_network begin + d, X ⇻ 0 + end end +### Error Test ### + +# Erroneous `@reaction` usage. +let + # Bi-directional reaction using the `@reaction` macro. + @test_throws Exception @eval @reaction (k1,k2), X1 <--> X2 + # Bundles reactions. + @test_throws Exception @eval @reaction k, (X1,X2) --> 0 +end + +# Tests that malformed reactions yields errors. +let + # Checks that malformed combinations of entries yields errors. + @test_throws Exception @eval @reaction_network begin + d, X --> 0, Y --> 0 + end + @test_throws Exception @eval @reaction_network begin + d, X --> 0, [misc="Ok metadata"], [description="Metadata in (erroneously) extra []."] + end + + # Checks that incorrect bundling yields error. + @test_throws Exception @eval @reaction_network begin + (k1,k2,k3), (X1,X2) --> 0 + end + + # Checks that incorrect stoichiometric expression yields an error. + @test_throws Exception @eval @reaction_network begin + k, X^Y --> XY + end +end \ No newline at end of file diff --git a/test/dsl/dsl_options.jl b/test/dsl/dsl_options.jl index 819a887427..a35dfad6a6 100644 --- a/test/dsl/dsl_options.jl +++ b/test/dsl/dsl_options.jl @@ -414,15 +414,44 @@ let spcs = (A, B1, B2, C) @test issetequal(unknowns(rn), sts) @test issetequal(species(rn), spcs) +end - @test_throws ArgumentError begin - rn = @reaction_network begin - @variables K - k, K*A --> B - end +# Tests errors in `@variables` declarations. +let + # Variable used as species in reaction. + @test_throws Exception @eval rn = @reaction_network begin + @variables K + k, K*A --> B + end + + # Tests error when disallowed name is used for variable. + @test_throws Exception @eval @reaction_network begin + @variables π(t) + end +end + + +# Tests that duplicate iv/parameter/species/variable names cannot be provided. +let + @test_throws Exception @eval @reaction_network begin + @spatial_ivs X + @species X(t) + end + @test_throws Exception @eval @reaction_network begin + @parameters X + @species X(t) + end + @test_throws Exception @eval @reaction_network begin + @species X(t) + @variables X(t) + end + @test_throws Exception @eval @reaction_network begin + @parameters X + @variables X(t) end end + ### Test Independent Variable Designations ### # Test ivs in DSL. @@ -739,6 +768,14 @@ let @observables $X ~ X1 + X2 (k1,k2), X1 <--> X2 end + + # Observable metadata provided twice. + @test_throws Exception @eval @reaction_network begin + @species X2 [description="Twice the amount of X"] + @observables (X2, [description="X times two."]) ~ 2X + d, X --> 0 + end + end @@ -916,8 +953,15 @@ let @equations X ~ p - S (P,D), 0 <--> S end + + # Differential equation using a forbidden variable (in the DSL). + @test_throws Exception @eval @reaction_network begin + @equations D(π) ~ -1 + end end +### Other DSL Option Tests ### + # test combinatoric_ratelaws DSL option let rn = @reaction_network begin @@ -951,3 +995,11 @@ let @unpack k1, A = rn3 @test isequal(rl, k1*A^2) end + +# Erroneous `@default_noise_scaling` declaration (other noise scaling tests are mostly in the SDE file). +let + # Default noise scaling with multiple entries. + @test_throws Exception @eval @reaction_network begin + @default_noise_scaling η1 η2 + end +end \ No newline at end of file From 8156eca47d9ce84ff82e902fbbb8e71b268f38e9 Mon Sep 17 00:00:00 2001 From: Torkel Date: Tue, 16 Jul 2024 22:17:49 -0400 Subject: [PATCH 19/38] update old @variable test --- test/dsl/dsl_options.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/dsl/dsl_options.jl b/test/dsl/dsl_options.jl index a35dfad6a6..b5741ef543 100644 --- a/test/dsl/dsl_options.jl +++ b/test/dsl/dsl_options.jl @@ -420,8 +420,8 @@ end let # Variable used as species in reaction. @test_throws Exception @eval rn = @reaction_network begin - @variables K - k, K*A --> B + @variables K(t) + k, K + A --> B end # Tests error when disallowed name is used for variable. From 50f8f82890281e4def27f32dcc35acffdc083785 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Thu, 16 Jan 2025 17:17:38 +0000 Subject: [PATCH 20/38] save progress --- src/chemistry_functionality.jl | 6 +- src/dsl.jl | 109 ++++++++++++++++++++------------- 2 files changed, 68 insertions(+), 47 deletions(-) diff --git a/src/chemistry_functionality.jl b/src/chemistry_functionality.jl index 5114e6455f..0576004691 100644 --- a/src/chemistry_functionality.jl +++ b/src/chemistry_functionality.jl @@ -86,9 +86,9 @@ function make_compound(expr) # Cannot extract directly using e.g. "getfield.(composition, :reactant)" because then # we get something like :([:C, :O]), rather than :([C, O]). composition = Catalyst.recursive_find_reactants!(expr.args[3], 1, - Vector{ReactantInternal}(undef, 0)) - components = :([]) # Becomes something like :([C, O]). - coefficients = :([]) # Becomes something like :([1, 2]). + Vector{DSLReactant}(undef, 0)) + components = :([]) # Becomes something like :([C, O]). + coefficients = :([]) # Becomes something like :([1, 2]). for comp in composition push!(components.args, comp.reactant) push!(coefficients.args, comp.stoichiometry) diff --git a/src/dsl.jl b/src/dsl.jl index ee30eb77a4..138d64d72a 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -47,15 +47,36 @@ end ### `@reaction_network` and `@network_component` Macros ### +""" @reaction_network + +Macro for generating chemical reaction network models (Catalyst `ReactionSystem`s). See the +[Catalyst documentation](https://catalyst.sciml.ai) for more details on the domain specific +language (DSL) that the macro implements, and for how `ReactionSystem`s can be used to generate +and simulate mathematical models of chemical systems. + +Returns: +- A Catalyst `ReactionSystem`, i.e. a symbolic model for the reaction network. The returned +system is marked `complete`. To obtain a `ReactionSystem` that is not marked complete, for +example to then use in compositional modeling, see the otherwise equivalent `@network_component` macro. + +Options: +- `@species S1(t) S2(t) ...`, defines a collection of species. +- `@variables V1(t) V2(t) ...`, defines non-species variables (for example, that evolve via a coupled ODE). +- ... - naming a network ... + +Examples: some examples illustrating various use cases, including begin/end blocks, naming, interpolation, and mixes of the options. + +""" + """ @reaction_network Macro for generating chemical reaction network models. Outputs a [`ReactionSystem`](@ref) structure, which stores all information of the model. Next, it can be used as input to various simulations, or -other tools for model analysis. The `@reaction_network` macro is sometimes called the "Catalyst +other tools for model analysis. The `@reaction_network` macro is sometimes called the "Catalyst DSL" (where DSL = domain-specific language), as it implements a DSL for creating chemical reaction network models. - + The `@reaction_network` macro, and the `ReactionSystem`s it generates, are central to Catalyst and its functionality. Catalyst is described in more detail in its documentation. The `reaction_network` DSL in particular is described in more detail [here](@ref dsl_description). @@ -83,7 +104,7 @@ sa_loop = @reaction_network begin d, X --> 0 end ``` -This model also contains production and degradation reactions, where `0` denotes that there are +This model also contains production and degradation reactions, where `0` denotes that there are either no substrates or no products in a reaction. Options: @@ -101,7 +122,7 @@ end ``` Notes: -- `ReactionSystem`s created through `@reaction_network` are considered complete (non-complete +- `ReactionSystem`s created through `@reaction_network` are considered complete (non-complete systems can be created through the alternative `@network_component` macro). - `ReactionSystem`s created through `@reaction_network`, by default, have a random name. Specific names can be designated as a first argument (before `begin`, e.g. `rn = @reaction_network name begin ...`). @@ -131,7 +152,7 @@ end # Ideally, it would have been possible to combine the @reaction_network and @network_component macros. # However, this issue: https://github.com/JuliaLang/julia/issues/37691 causes problem with interpolations -# if we make the @reaction_network macro call the @network_component macro. Instead, these uses the +# if we make the @reaction_network macro call the @network_component macro. Instead, these uses the # same input, but passes `complete = false` to `make_rs_expr`. """ @network_component @@ -181,35 +202,35 @@ end ### Internal DSL Structures ### # Internal structure containing information about one reactant in one reaction. -struct ReactantInternal +struct DSLReactant reactant::Union{Symbol, Expr} stoichiometry::ExprValues end # Internal structure containing information about one Reaction. Contain all its substrates and # products as well as its rate and potential metadata. Uses a specialized constructor. -struct ReactionInternal - substrates::Vector{ReactantInternal} - products::Vector{ReactantInternal} +struct DSLReaction + substrates::Vector{DSLReactant} + products::Vector{DSLReactant} rate::ExprValues metadata::Expr rxexpr::Expr - function ReactionInternal(sub_line::ExprValues, prod_line::ExprValues, + function DSLReaction(sub_line::ExprValues, prod_line::ExprValues, rate::ExprValues, metadata_line::ExprValues) - subs = recursive_find_reactants!(sub_line, 1, Vector{ReactantInternal}(undef, 0)) - prods = recursive_find_reactants!(prod_line, 1, Vector{ReactantInternal}(undef, 0)) + subs = recursive_find_reactants!(sub_line, 1, Vector{DSLReactant}(undef, 0)) + prods = recursive_find_reactants!(prod_line, 1, Vector{DSLReactant}(undef, 0)) metadata = extract_metadata(metadata_line) new(sub, prod, rate, metadata, rx_line) end end # Recursive function that loops through the reaction line and finds the reactants and their -# stoichiometry. Recursion makes it able to handle weird cases like 2(X + Y + 3(Z + XY)). The -# reactants are stored in the `reactants` vector. As the expression tree is parsed, the +# stoichiometry. Recursion makes it able to handle weird cases like 2(X + Y + 3(Z + XY)). The +# reactants are stored in the `reactants` vector. As the expression tree is parsed, the # stoichiometry is updated and new reactants added. function recursive_find_reactants!(ex::ExprValues, mult::ExprValues, - reactants::Vector{ReactantInternal}) + reactants::Vector{DSLReactant}) # We have reached the end of the expression tree and can finalise and return the reactants. if typeof(ex) != Expr || (ex.head == :escape) || (ex.head == :ref) # The final bit of the expression is not a relevant reactant, no additions are required. @@ -219,11 +240,11 @@ function recursive_find_reactants!(ex::ExprValues, mult::ExprValues, if any(ex == reactant.reactant for reactant in reactants) idx = findfirst(r.reactant == ex for r in reactants) new_mult = processmult(+, mult, reactants[idx].stoichiometry) - reactants[idx] = ReactantInternal(ex, new_mult) + reactants[idx] = DSLReactant(ex, new_mult) # If the expression corresponds to a new reactant, add it to the list. else - push!(reactants, ReactantInternal(ex, mult)) + push!(reactants, DSLReactant(ex, mult)) end # If we have encountered a multiplication (i.e. a stoichiometry and a set of reactants). @@ -245,7 +266,7 @@ function recursive_find_reactants!(ex::ExprValues, mult::ExprValues, else throw("Malformed reaction, bad operator: $(ex.args[1]) found in stoichiometry expression $ex.") end - return reactants + reactants end # Helper function for updating the multiplicity throughout recursion (handles e.g. parametric @@ -273,13 +294,12 @@ function extract_metadata(metadata_line::Expr) return metadata end - - +### Specialised Error for @require_declaration Option ### struct UndeclaredSymbolicError <: Exception msg::String end -function Base.showerror(io::IO, err::UndeclaredSymbolicError) +function Base.showerror(io::IO, err::UndeclaredSymbolicError) print(io, "UndeclaredSymbolicError: ") print(io, err.msg) end @@ -302,7 +322,7 @@ function make_reaction_system(ex::Expr, name) end options = Dict(Symbol(String(arg.args[1])[2:end]) => arg for arg in option_lines) - # Reads options (round 1, options which must be read before the reactions, e.g. because + # Reads options (round 1, options which must be read before the reactions, e.g. because # they might declare parameters/species/variables). compound_expr_init, compound_species = read_compound_options(options) species_declared = [extract_syms(options, :species); compound_species] @@ -334,7 +354,7 @@ function make_reaction_system(ex::Expr, name) combinatoric_ratelaws = read_combinatoric_ratelaws_option(options) # Checks for input errors. - if (sum(length.([reaction_lines, option_lines])) != length(ex.args)) + if (sum(length, [reaction_lines, option_lines]) != length(ex.args)) error("@reaction_network input contain $(length(ex.args) - sum(length.([reaction_lines,option_lines]))) malformed lines.") end if any(!in(opt_in, option_keys) for opt_in in keys(options)) @@ -387,7 +407,7 @@ end # Generates a vector of reaction structures, each containing the information about one reaction. function get_reactions(exprs::Vector{Expr}) # Declares an array to which we add all found reactions. - reactions = Vector{ReactionInternal}(undef, 0) + reactions = Vector{DSLReaction}(undef, 0) # Loops through each line of reactions. Extracts and adds each lines's reactions to `reactions`. for line in exprs @@ -396,9 +416,11 @@ function get_reactions(exprs::Vector{Expr}) # Checks which type of line is used, and calls `push_reactions!` on the processed line. if in(arrow, double_arrows) - if typeof(rate) != Expr || rate.head != :tuple + (typeof(rate) != Expr || rate.head != :tuple) && error("Error: Must provide a tuple of reaction rates when declaring a bi-directional reaction.") - end + (typeof(metadata) != Expr || metadata.head != :tuple) && + error("Error: Must provide a tuple of reaction metadata when declaring a bi-directional reaction.") + push_reactions!(reactions, reaction.args[2], reaction.args[3], rate.args[1], metadata.args[1], arrow, line) push_reactions!(reactions, reaction.args[3], reaction.args[2], @@ -418,7 +440,7 @@ end # Extracts the rate, reaction, and metadata fields (the last one optional) from a reaction line. function read_reaction_line(line::Expr) - # Handles rate, reaction, and arrow. Special routine required for the`-->` case, which + # Handles rate, reaction, and arrow. Special routine required for the`-->` case, which # creates an expression different what the other arrows creates. rate = line.args[1] reaction = line.args[2] @@ -429,7 +451,7 @@ function read_reaction_line(line::Expr) # Handles metadata. If not provided, empty metadata is created. if length(line.args) == 2 - metadata = in(arrow, double_arrows) ? :(([], [])) : :([]) + in(arrow, double_arrows) ? :(([], [])) : :([]) elseif length(line.args) == 3 metadata = line.args[3] else @@ -441,8 +463,8 @@ end # Takes a reaction line and creates reaction(s) from it and pushes those to the reaction vector. # Used to create multiple reactions from bundled reactions (like `k, (X,Y) --> 0`). -function push_reactions!(reactions::Vector{ReactionInternal}, subs::ExprValues, - prods::ExprValues, rate::ExprValues, metadata::ExprValues, arrow::Symbol) +function push_reactions!(reactions::Vector{DSLReaction}, subs::ExprValues, + prods::ExprValues, rate::ExprValues, metadata::ExprValues, arrow::Symbol, line::Expr) # The rates, substrates, products, and metadata may be in a tuple form (e.g. `k, (X,Y) --> 0`). # This finds these tuples' lengths (or 1 for non-tuple forms). Inconsistent lengths yield error. lengs = (tup_leng(subs), tup_leng(prods), tup_leng(rate), tup_leng(metadata)) @@ -451,8 +473,8 @@ function push_reactions!(reactions::Vector{ReactionInternal}, subs::ExprValues, throw("Malformed reaction, rate=$rate, subs=$subs, prods=$prods, metadata=$metadata.") end - # Loops through each reaction encoded by the reaction's different components. - # Creates a `ReactionInternal` representation and adds it to `reactions`. + # Loops through each reaction encoded by the reaction's different components. + # Creates a `DSLReaction` representation and adds it to `reactions`. for i in 1:maxl # If the `only_use_rate` metadata was not provided, this must be inferred from the arrow. metadata_i = get_tup_arg(metadata, i) @@ -461,8 +483,8 @@ function push_reactions!(reactions::Vector{ReactionInternal}, subs::ExprValues, end # Extracts substrates, products, and rates for the i'th reaction. - subs_i, prods_i, rate_i = get_tup_arg.([subs, prods, rate], i) - push!(reactions, ReactionInternal(subs_i, prods_i, rate_i, metadata_i)) + subs_i, prods_i, rate_i = get_tup_arg.((subs, prods, rate), i) + push!(reactions, DSLReaction(subs_i, prods_i, rate_i, metadata_i)) end end @@ -515,7 +537,7 @@ end # Function called by extract_species_and_parameters, recursively loops through an # expression and find symbols (adding them to the push_symbols vector). function add_syms_from_expr!(push_symbols::AbstractSet, expr::ExprValues, excluded_syms) - # If we have encountered a Symbol in the recursion, we can try extracting it. + # If we have encountered a Symbol in the recursion, we can try extracting it. if expr isa Symbol if !(expr in forbidden_symbols_skip) && !(expr in excluded_syms) push!(push_symbols, expr) @@ -530,7 +552,7 @@ end ### DSL Output Expression Builders ### -# Given the extracted species (or variables) and the option dictionary, creates the +# Given the extracted species (or variables) and the option dictionary, creates the # `@species ...` (or `@variables ..`) expression which would declare these. # If `key = :variables`, does this for variables (and not species). function get_usexpr(us_extracted, options, key = :species; ivs = (DEFAULT_IV_SYM,)) @@ -584,7 +606,7 @@ function scalarize_macro(expr_init, name) return expr, namesym end -# From the system reactions (as `ReactionInternal`s) and equations (as expressions), +# From the system reactions (as `DSLReaction`s) and equations (as expressions), # creates the expressions that evalutes to the reaction (+ equations) vector. function make_rxsexprs(reactions, equations) rxsexprs = :(Catalyst.CatalystEqType[]) @@ -593,9 +615,9 @@ function make_rxsexprs(reactions, equations) return rxsexprs end -# From a `ReactionInternal` struct, creates the expression which evaluates to the creation +# From a `DSLReaction` struct, creates the expression which evaluates to the creation # of the correponding reaction. -function get_rxexpr(rx::ReactionInternal) +function get_rxexpr(rx::DSLReaction) # Initiates the `Reaction` expression. rate = recursive_expand_functions!(rx.rate) rx_constructor = :(Reaction($rate, [], [], [], []; metadata = $(rx.metadata))) @@ -639,7 +661,7 @@ end # more generic to account for other default reaction metadata. Practically, this will likely # be the only relevant reaction metadata to have a default value via the DSL. If another becomes # relevant, the code can be rewritten to take this into account. -# Checks if the `@default_noise_scaling` option is used. If so, uses it as the default value of +# Checks if the `@default_noise_scaling` option is used. If so, uses it as the default value of # the `default_noise_scaling` reaction metadata, otherwise, returns an empty vector. function read_default_noise_scaling_option(options) if haskey(options, :default_noise_scaling) @@ -803,7 +825,7 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted; req throw(UndeclaredSymbolicError( "An undeclared variable ($obs_name) was declared as an observable in the following observable equation: \"$obs_eq\". Since the flag @require_declaration is set, all variables must be declared with the @species, @parameters, or @variables macros.")) end - isempty(ivs) || + if !isempty(ivs) error("An observable ($obs_name) was given independent variable(s). These should not be given, as they are inferred automatically.") end if !isnothing(defaults) @@ -813,8 +835,7 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted; req error("A forbidden symbol ($(obs_eq.args[2])) was used as an observable name.") end if (obs_name in species_n_vars_declared) && is_escaped_expr(obs_eq.args[2]) - println("HERE") - error("An interpolated observable have been used, which has also been explicitly declared within the system using either @species or @variables. This is not permitted.") + error("An interpolated observable have been used, which has also been ereqxplicitly declared within the system using either @species or @variables. This is not permitted.") end if ((obs_name in species_n_vars_declared) || is_escaped_expr(obs_eq.args[2])) && !isnothing(metadata) @@ -958,7 +979,7 @@ function make_reaction(ex::Expr) end end -# Reads a single line and creates the corresponding ReactionInternal. +# Reads a single line and creates the corresponding DSLReaction. function get_reaction(line) reaction = get_reactions([line]) if (length(reaction) != 1) From 4e720ee69206651e36f20e5c51e794f7fad1221d Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Thu, 16 Jan 2025 17:46:35 +0000 Subject: [PATCH 21/38] save progress --- src/dsl.jl | 81 +++++++++++++++++++++++++----------------------------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index 138d64d72a..e8d673705f 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -217,7 +217,7 @@ struct DSLReaction rxexpr::Expr function DSLReaction(sub_line::ExprValues, prod_line::ExprValues, - rate::ExprValues, metadata_line::ExprValues) + rate::ExprValues, metadata_line::ExprValues, rx_line::Expr) subs = recursive_find_reactants!(sub_line, 1, Vector{DSLReactant}(undef, 0)) prods = recursive_find_reactants!(prod_line, 1, Vector{DSLReactant}(undef, 0)) metadata = extract_metadata(metadata_line) @@ -482,9 +482,14 @@ function push_reactions!(reactions::Vector{DSLReaction}, subs::ExprValues, push!(metadata_i.args, :(only_use_rate = $(in(arrow, pure_rate_arrows)))) end + # Checks that metadata fields are unique. + if !allunique(arg.args[1] for arg in metadata_i.args) + error("Some reaction metadata fields where repeated: $(metadata_entries)") + end + # Extracts substrates, products, and rates for the i'th reaction. subs_i, prods_i, rate_i = get_tup_arg.((subs, prods, rate), i) - push!(reactions, DSLReaction(subs_i, prods_i, rate_i, metadata_i)) + push!(reactions, DSLReaction(subs_i, prods_i, rate_i, metadata_i, line)) end end @@ -505,29 +510,29 @@ function extract_syms(opts, vartype::Symbol) end # Function looping through all reactions, to find undeclared symbols (species or -# parameters), and assign them to the right category. +# parameters) and assign them to the right category. function extract_species_and_parameters(reactions, excluded_syms) # Loops through all reactant, extract undeclared ones as species. species = OrderedSet{Union{Symbol, Expr}}() for reaction in reactions for reactant in Iterators.flatten((reaction.substrates, reaction.products)) add_syms_from_expr!(species, reactant.reactant, excluded_syms) - (!isempty(species) && requiredec) && throw(UndeclaredSymbolicError( - "Unrecognized variables $(join(species, ", ")) detected in reaction expression: \"$(string(reaction.rxexpr))\". Since the flag @require_declaration is declared, all species must be explicitly declared with the @species macro.")) end + (!isempty(species) && requiredec) && + throw(UndeclaredSymbolicError("Unrecognized variables $(join(species, ", ")) detected in reaction expression: \"$(string(reaction.rxexpr))\". Since the flag @require_declaration is declared, all species must be explicitly declared with the @species macro.")) end - foreach(s -> push!(excluded_syms, s), species) + union!(excluded_syms, species) # Loops through all rates and stoichiometries, extracting used symbols as parameters. parameters = OrderedSet{Union{Symbol, Expr}}() for reaction in reactions add_syms_from_expr!(parameters, reaction.rate, excluded_syms) - (!isempty(parameters) && requiredec) && throw(UndeclaredSymbolicError( - "Unrecognized parameter $(join(parameters, ", ")) detected in rate expression: $(reaction.rate) for the following reaction expression: \"$(string(reaction.rxexpr))\". Since the flag @require_declaration is declared, all parameters must be explicitly declared with the @parameters macro.")) + (!isempty(parameters) && requiredec) && + throw(UndeclaredSymbolicError("Unrecognized parameter $(join(parameters, ", ")) detected in rate expression: $(reaction.rate) for the following reaction expression: \"$(string(reaction.rxexpr))\". Since the flag @require_declaration is declared, all parameters must be explicitly declared with the @parameters macro.")) for reactant in Iterators.flatten((reaction.substrates, reaction.products)) add_syms_from_expr!(parameters, reactant.stoichiometry, excluded_syms) - (!isempty(parameters) && requiredec) && throw(UndeclaredSymbolicError( - "Unrecognized parameters $(join(parameters, ", ")) detected in the stoichiometry for reactant $(reactant.reactant) in the following reaction expression: \"$(string(reaction.rxexpr))\". Since the flag @require_declaration is declared, all parameters must be explicitly declared with the @parameters macro.")) + (!isempty(parameters) && requiredec) && + throw(UndeclaredSymbolicError("Unrecognized parameters $(join(parameters, ", ")) detected in the stoichiometry for reactant $(reactant.reactant) in the following reaction expression: \"$(string(reaction.rxexpr))\". Since the flag @require_declaration is declared, all parameters must be explicitly declared with the @parameters macro.")) end end @@ -556,12 +561,12 @@ end # `@species ...` (or `@variables ..`) expression which would declare these. # If `key = :variables`, does this for variables (and not species). function get_usexpr(us_extracted, options, key = :species; ivs = (DEFAULT_IV_SYM,)) - if haskey(options, key) - usexpr = options[key] + usexpr = if haskey(options, key) + options[key] elseif isempty(us_extracted) - usexpr = :() + :() else - usexpr = Expr(:macrocall, Symbol("@", key), LineNumberNode(0)) + Expr(:macrocall, Symbol("@", key), LineNumberNode(0)) end for u in us_extracted u isa Symbol && push!(usexpr.args, Expr(:call, u, ivs...)) @@ -572,12 +577,12 @@ end # Given the parameters that were extracted from the reactions, and the options dictionary, # creates the `@parameters ...` expression for the macro output. function get_pexpr(parameters_extracted, options) - if haskey(options, :parameters) - pexprs = options[:parameters] + pexprs = if haskey(options, :parameters) + options[:parameters] elseif isempty(parameters_extracted) - pexprs = :() + :() else - pexprs = :(@parameters) + :(@parameters) end foreach(p -> push!(pexprs.args, p), parameters_extracted) return pexprs @@ -586,7 +591,7 @@ end # Takes a ModelingToolkit declaration macro (like @parameters ...) and return and expression: # That calls the macro and then scalarizes all the symbols created into a vector of Nums. # stores the created symbolic variables in a variable (which name is generated from `name`). -# It will also return the name used for the variable that stores the symbolci variables. +# It will also return the name used for the variable that stores the symbolic variables. function scalarize_macro(expr_init, name) # Generates a random variable name which (in generated code) will store the produced # symbolic variables (e.g. `var"##ps#384"`). @@ -638,8 +643,8 @@ end ### DSL Option Handling ### -# Finds the time idenepdnet variable, and any potential spatial indepndent variables. -# Returns these (individually and combined), as well as an expression for declaring them +# Finds the time independent variable, and any potential spatial indepndent variables. +# Returns these (individually and combined), as well as an expression for declaring them. function read_ivs_option(options) # Creates the independent variables expressions (depends on whether the `ivs` option was used). if haskey(options, :ivs) @@ -651,7 +656,7 @@ function read_ivs_option(options) ivexpr = :($(DEFAULT_IV_SYM) = default_t()) end - # Extracts the independet variables symbols, and returns the output. + # Extracts the independet variables symbols (time and spatial), and returns the output. tiv = ivs[1] sivs = (length(ivs) > 1) ? Expr(:vect, ivs[2:end]...) : nothing return tiv, sivs, ivs, ivexpr @@ -802,11 +807,8 @@ end # Reads the combinatiorial ratelaw options, which determines if a combinatorial rate law should # be used or not. If not provides, uses the default (true). function read_combinatoric_ratelaws_option(options) - if haskey(options, :combinatoric_ratelaws) - return options[:combinatoric_ratelaws].args[end] - else - return true - end + return haskey(options, :combinatoric_ratelaws) ? + options[:combinatoric_ratelaws].args[end] : true end # Reads the observables options. Outputs an expression ofr creating the observable variables, and a vector of observable equations. @@ -821,26 +823,19 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted; req for (idx, obs_eq) in enumerate(observed_eqs.args) # Extract the observable, checks for errors. obs_name, ivs, defaults, metadata = find_varinfo_in_declaration(obs_eq.args[2]) - if (requiredec && !in(obs_name, species_n_vars_declared)) - throw(UndeclaredSymbolicError( - "An undeclared variable ($obs_name) was declared as an observable in the following observable equation: \"$obs_eq\". Since the flag @require_declaration is set, all variables must be declared with the @species, @parameters, or @variables macros.")) - end - if !isempty(ivs) + + (requiredec && !in(obs_name, species_n_vars_declared)) && + throw(UndeclaredSymbolicError("An undeclared variable ($obs_name) was declared as an observable in the following observable equation: \"$obs_eq\". Since the flag @require_declaration is set, all variables must be declared with the @species, @parameters, or @variables macros.")) + isempty(ivs) || error("An observable ($obs_name) was given independent variable(s). These should not be given, as they are inferred automatically.") - end - if !isnothing(defaults) + isnothing(defaults) || error("An observable ($obs_name) was given a default value. This is forbidden.") - end - if in(obs_name, forbidden_symbols_error) + in(obs_name, forbidden_symbols_error) && error("A forbidden symbol ($(obs_eq.args[2])) was used as an observable name.") - end - if (obs_name in species_n_vars_declared) && is_escaped_expr(obs_eq.args[2]) + (obs_name in species_n_vars_declared) && is_escaped_expr(obs_eq.args[2]) && error("An interpolated observable have been used, which has also been ereqxplicitly declared within the system using either @species or @variables. This is not permitted.") - end - if ((obs_name in species_n_vars_declared) || is_escaped_expr(obs_eq.args[2])) && - !isnothing(metadata) - error("Metadata was provided to observable $obs_name in the `@observables` macro. However, the observable was also declared separately (using either @species or @variables). When this is done, metadata should instead be provided within the original @species or @variable declaration.") - end + ((obs_name in species_n_vars_declared) || is_escaped_expr(obs_eq.args[2])) && + !isnothing(metadata) && error("Metadata was provided to observable $obs_name in the `@observables` macro. However, the observable was also declared separately (using either @species or @variables). When this is done, metadata should instead be provided within the original @species or @variable declaration.") # This bits adds the observables to the @variables vector which is given as output. # For Observables that have already been declared using @species/@variables, From a1e967a2ad643a72bb3c4fc7f449bb3e320cbd36 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Fri, 17 Jan 2025 10:40:50 +0000 Subject: [PATCH 22/38] save progress --- src/dsl.jl | 82 ++++++++++----------- src/expression_utils.jl | 18 ++--- test/dsl/dsl_advanced_model_construction.jl | 23 ++---- test/dsl/dsl_options.jl | 20 ++--- 4 files changed, 66 insertions(+), 77 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index e8d673705f..24bda3d882 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -221,7 +221,7 @@ struct DSLReaction subs = recursive_find_reactants!(sub_line, 1, Vector{DSLReactant}(undef, 0)) prods = recursive_find_reactants!(prod_line, 1, Vector{DSLReactant}(undef, 0)) metadata = extract_metadata(metadata_line) - new(sub, prod, rate, metadata, rx_line) + new(subs, prods, rate, metadata, rx_line) end end @@ -232,13 +232,13 @@ end function recursive_find_reactants!(ex::ExprValues, mult::ExprValues, reactants::Vector{DSLReactant}) # We have reached the end of the expression tree and can finalise and return the reactants. - if typeof(ex) != Expr || (ex.head == :escape) || (ex.head == :ref) + if (typeof(ex) != Expr) || (ex.head == :escape) || (ex.head == :ref) # The final bit of the expression is not a relevant reactant, no additions are required. (ex == 0 || in(ex, empty_set)) && (return reactants) # If the expression corresponds to a reactant on our list, increase its multiplicity. - if any(ex == reactant.reactant for reactant in reactants) - idx = findfirst(r.reactant == ex for r in reactants) + idx = findfirst(r.reactant == ex for r in reactants) + if !isnothing(idx) new_mult = processmult(+, mult, reactants[idx].stoichiometry) reactants[idx] = DSLReactant(ex, new_mult) @@ -324,12 +324,13 @@ function make_reaction_system(ex::Expr, name) # Reads options (round 1, options which must be read before the reactions, e.g. because # they might declare parameters/species/variables). + requiredec = haskey(options, :require_declaration) compound_expr_init, compound_species = read_compound_options(options) species_declared = [extract_syms(options, :species); compound_species] parameters_declared = extract_syms(options, :parameters) variables_declared = extract_syms(options, :variables) vars_extracted, add_default_diff, equations = read_equations_options(options, - variables_declared) + variables_declared; requiredec) # Extracts all reactions. Extracts all parameters, species, and variables of the system and # creates lists with them. @@ -338,31 +339,29 @@ function make_reaction_system(ex::Expr, name) syms_declared = Set(Iterators.flatten((parameters_declared, species_declared, variables))) species_extracted, parameters_extracted = extract_species_and_parameters(reactions, - syms_declared) + syms_declared; requiredec) species = vcat(species_declared, species_extracted) parameters = vcat(parameters_declared, parameters_extracted) # Reads options (round 2, options that either can, or must, be read after the reactions). - tiv, sivs, ivs, ivexpr = read_ivs_option(options) + tiv, sivs, ivs, ivsexpr = read_ivs_option(options) continuous_events_expr = read_events_option(options, :continuous_events) discrete_events_expr = read_events_option(options, :discrete_events) - observed_expr, observed_eqs, obs_syms = read_observed_options(options, - [species_declared; variables], ivs) + obsexpr, observed_eqs, obs_syms = read_observed_options(options, + [species_declared; variables], ivs; requiredec) diffexpr = create_differential_expr(options, add_default_diff, [species; parameters; variables], tiv) default_reaction_metadata = read_default_noise_scaling_option(options) combinatoric_ratelaws = read_combinatoric_ratelaws_option(options) # Checks for input errors. - if (sum(length, [reaction_lines, option_lines]) != length(ex.args)) - error("@reaction_network input contain $(length(ex.args) - sum(length.([reaction_lines,option_lines]))) malformed lines.") - end - if any(!in(opt_in, option_keys) for opt_in in keys(options)) - error("The following unsupported options were used: $(filter(opt_in->!in(opt_in,option_keys), keys(options)))") - end forbidden_symbol_check(union(species, parameters)) forbidden_variable_check(variables) unique_symbol_check(vcat(species, parameters, variables, ivs)) + (sum(length, [reaction_lines, option_lines]) != length(ex.args)) && + error("@reaction_network input contain $(length(ex.args) - sum(length.([reaction_lines,option_lines]))) malformed lines.") + any(!in(option_keys), keys(options)) && + error("The following unsupported options were used: $(filter(opt_in->!in(opt_in,option_keys), keys(options)))") # Creates expressions corresponding to actual code from the internal DSL representation. psexpr_init = get_pexpr(parameters_extracted, options) @@ -371,35 +370,37 @@ function make_reaction_system(ex::Expr, name) psexpr, psvar = scalarize_macro(psexpr_init, "ps") spsexpr, spsvar = scalarize_macro(spsexpr_init, "specs") vsexpr, vsvar = scalarize_macro(vsexpr_init, "vars") - compound_expr, compsvar = scalarize_macro(compound_expr_init, "comps") + cmpsexpr, cmpsvar = scalarize_macro(compound_expr_init, "comps") rxsexprs = make_rxsexprs(reactions, equations) # Assemblies the full expression that declares all required symbolic variables, and # then the output `ReactionSystem`. - quote + MacroTools.flatten(striplines(quote + # Inserts the expressions which generates the `ReactionSystem` input. + $ivsexpr $psexpr - $ivexpr $spsexpr $vsexpr - $observed_expr - $compound_expr + $obsexpr + $cmpsexpr $diffexpr - sivs_vec = $sivs - rx_eq_vec = $rxexprs - vars = setdiff(union($spssym, $varssym, $compssym), $obs_syms) - obseqs = $observed_eqs - cevents = $continuous_events_expr - devents = $discrete_events_expr + # Stores each kwarg in a variable. Not necessary but useful when inspecting code for debugging. + name = $name + spatial_ivs = $sivs + rx_eq_vec = $rxsexprs + vars = setdiff(union($spsvar, $vsvar, $cmpsvar), $obs_syms) + observed = $observed_eqs + continuous_events = $continuous_events_expr + discrete_events = $discrete_events_expr + combinatoric_ratelaws = $combinatoric_ratelaws + default_reaction_metadata = $default_reaction_metadata remake_ReactionSystem_internal( - make_ReactionSystem_internal( - rx_eq_vec, $tiv, vars, $pssym; - name = $name, spatial_ivs = sivs_vec, observed = obseqs, - continuous_events = cevents, discrete_events = devents, - combinatoric_ratelaws = $combinatoric_ratelaws); - default_reaction_metadata = $default_reaction_metadata) - end + make_ReactionSystem_internal(rx_eq_vec, $tiv, vars, $psvar; name, spatial_ivs, + observed, continuous_events, discrete_events, combinatoric_ratelaws); + default_reaction_metadata) + end)) end ### DSL Reaction Reading Functions ### @@ -451,7 +452,7 @@ function read_reaction_line(line::Expr) # Handles metadata. If not provided, empty metadata is created. if length(line.args) == 2 - in(arrow, double_arrows) ? :(([], [])) : :([]) + metadata = in(arrow, double_arrows) ? :(([], [])) : :([]) elseif length(line.args) == 3 metadata = line.args[3] else @@ -511,7 +512,7 @@ end # Function looping through all reactions, to find undeclared symbols (species or # parameters) and assign them to the right category. -function extract_species_and_parameters(reactions, excluded_syms) +function extract_species_and_parameters(reactions, excluded_syms; requiredec = false) # Loops through all reactant, extract undeclared ones as species. species = OrderedSet{Union{Symbol, Expr}}() for reaction in reactions @@ -649,17 +650,17 @@ function read_ivs_option(options) # Creates the independent variables expressions (depends on whether the `ivs` option was used). if haskey(options, :ivs) ivs = Tuple(extract_syms(options, :ivs)) - ivexpr = copy(options[:ivs]) - ivexpr.args[1] = Symbol("@", "parameters") + ivsexpr = copy(options[:ivs]) + ivsexpr.args[1] = Symbol("@", "parameters") else ivs = (DEFAULT_IV_SYM,) - ivexpr = :($(DEFAULT_IV_SYM) = default_t()) + ivsexpr = :($(DEFAULT_IV_SYM) = default_t()) end # Extracts the independet variables symbols (time and spatial), and returns the output. tiv = ivs[1] sivs = (length(ivs) > 1) ? Expr(:vect, ivs[2:end]...) : nothing - return tiv, sivs, ivs, ivexpr + return tiv, sivs, ivs, ivsexpr end # Returns the `default_reaction_metadata` output. Technically Catalyst's code could have been made @@ -977,9 +978,8 @@ end # Reads a single line and creates the corresponding DSLReaction. function get_reaction(line) reaction = get_reactions([line]) - if (length(reaction) != 1) + (length(reaction) != 1) && error("Malformed reaction. @reaction macro only creates a single reaction. E.g. double arrows, such as `<-->` are not supported.") - end return only(reaction) end diff --git a/src/expression_utils.jl b/src/expression_utils.jl index 2cc899df26..c81624ea1c 100644 --- a/src/expression_utils.jl +++ b/src/expression_utils.jl @@ -5,7 +5,7 @@ function esc_dollars!(ex) # If we do not have an expression: recursion has finished and we return the input. (ex isa Expr) || (return ex) - # If we have encountered an interpolation, perform the appropriate modification, else recur. + # If we have encountered an interpolation, perform the appropriate modification, else recur. if ex.head == :$ return esc(:($(ex.args[1]))) else @@ -26,27 +26,27 @@ end # Throws an error when a forbidden symbol is used. function forbidden_symbol_check(sym) - isempty(intersect(forbidden_symbols_error, sym)) && return used_forbidden_syms = intersect(forbidden_symbols_error, sym) + isempty(used_forbidden_syms) && return error("The following symbol(s) are used as species or parameters: $used_forbidden_syms, this is not permitted.") end # Throws an error when a forbidden variable is used (a forbidden symbol that is not `:t`). function forbidden_variable_check(sym) - isempty(intersect(forbidden_variables_error, sym)) && return used_forbidden_syms = intersect(forbidden_variables_error, sym) + isempty(used_forbidden_syms) && return error("The following symbol(s) are used as variables: $used_forbidden_syms, this is not permitted.") end # Checks that no symbol was sued for multiple purposes. function unique_symbol_check(syms) - allunique(syms) && return - error("Reaction network independent variables, parameters, species, and variables must all have distinct names, but a duplicate has been detected. ") + allunique(syms)|| + error("Reaction network independent variables, parameters, species, and variables must all have distinct names, but a duplicate has been detected. ") end ### Catalyst-specific Expressions Manipulation ### -# Some options takes input on form that is either `@option ...` or `@option begin ... end`. +# Some options takes input on form that is either `@option ...` or `@option begin ... end`. # This transforms input of the latter form to the former (with only one line in the `begin ... end` block) function option_block_form(expr) (expr.head == :block) && return expr @@ -72,12 +72,12 @@ function find_varinfo_in_declaration(expr) # Case: X (expr isa Symbol) && (return expr, [], nothing, nothing) - # Case: X(t) + # Case: X(t) (expr.head == :call) && (return expr.args[1], expr.args[2:end], nothing, nothing) if expr.head == :(=) # Case: X = 1.0 (expr.args[1] isa Symbol) && (return expr.args[1], [], expr.args[2], nothing) - # Case: X(t) = 1.0 + # Case: X(t) = 1.0 (expr.args[1].head == :call) && (return expr.args[1].args[1], expr.args[1].args[2:end], expr.args[2].args[1], nothing) @@ -114,7 +114,7 @@ end # (In this example the independent variable :t was inserted). # Here, the iv is a iv_expr, which can be anything, which is inserted function insert_independent_variable(expr_in, iv_expr) - # If expr is a symbol, just attach the iv. If not we have to create a new expr and mutate it. + # If expr is a symbol, just attach the iv. If not we have to create a new expr and mutate it. # Because Symbols (a possible input) cannot be mutated, this function cannot mutate the input # (would have been easier if Expr input was guaranteed). (expr_in isa Symbol) && (return Expr(:call, expr_in, iv_expr)) diff --git a/test/dsl/dsl_advanced_model_construction.jl b/test/dsl/dsl_advanced_model_construction.jl index 6c8b38cdee..8f24aec2bb 100644 --- a/test/dsl/dsl_advanced_model_construction.jl +++ b/test/dsl/dsl_advanced_model_construction.jl @@ -123,17 +123,6 @@ let @test rn == rn2 end -# Creates a reaction network using `eval` and internal function. -let - ex = quote - (Ka, Depot --> Central) - (CL / Vc, Central --> 0) - end - # Line number nodes aren't ignored so have to be manually removed - Base.remove_linenums!(ex) - @test eval(Catalyst.make_reaction_system(ex, QuoteNode(:name))) isa ReactionSystem -end - # Miscellaneous interpolation tests. Unsure what they do here (not related to DSL). let rx = @reaction k*h, A + 2*B --> 3*C + D @@ -211,11 +200,11 @@ let end # Checks that repeated metadata throws errors. -let - @test_throws Exception @eval @reaction k, 0 --> X, [md1=1.0, md1=2.0] +let + @test_throws Exception @eval @reaction k, 0 --> X, [md1=1.0, md1=2.0] @test_throws Exception @eval @reaction_network begin - k, 0 --> X, [md1=1.0, md1=1.0] - end + k, 0 --> X, [md1=1.0, md1=1.0] + end end # Tests for nested metadata. @@ -368,11 +357,11 @@ let @species (X(t))[1:2] (k[1],k[2]), X[1] <--> X[2] end - + @parameters k[1:2] @species (X(t))[1:2] rx1 = Reaction(k[1], [X[1]], [X[2]]) rx2 = Reaction(k[2], [X[2]], [X[1]]) @named twostate = ReactionSystem([rx1, rx2], t) @test twostate == rn -end \ No newline at end of file +end diff --git a/test/dsl/dsl_options.jl b/test/dsl/dsl_options.jl index 2171d1a0a1..8143a37658 100644 --- a/test/dsl/dsl_options.jl +++ b/test/dsl/dsl_options.jl @@ -418,7 +418,7 @@ let end # Tests errors in `@variables` declarations. -let +let # Variable used as species in reaction. @test_throws Exception @eval rn = @reaction_network begin @variables K(t) @@ -813,7 +813,7 @@ let # Observable metadata provided twice. @test_throws Exception @eval @reaction_network begin @species X2 [description="Twice the amount of X"] - @observables (X2, [description="X times two."]) ~ 2X + @observables (X2, [description="X times two."]) ~ 2X d, X --> 0 end @@ -998,6 +998,14 @@ let end end +# Erroneous `@default_noise_scaling` declaration (other noise scaling tests are mostly in the SDE file). +let + # Default noise scaling with multiple entries. + @test_throws Exception @eval @reaction_network begin + @default_noise_scaling η1 η2 + end +end + ### Other DSL Option Tests ### # test combinatoric_ratelaws DSL option @@ -1174,11 +1182,3 @@ let @observables X2 ~ X1 end end - -# Erroneous `@default_noise_scaling` declaration (other noise scaling tests are mostly in the SDE file). -let - # Default noise scaling with multiple entries. - @test_throws Exception @eval @reaction_network begin - @default_noise_scaling η1 η2 - end -end \ No newline at end of file From b235784df5aee30b6a9da5aa29b71085061cb4be Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 18 Jan 2025 16:04:28 +0000 Subject: [PATCH 23/38] save progress --- src/dsl.jl | 210 +++++++++++++++++++++++++++-------------------------- 1 file changed, 106 insertions(+), 104 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index f3ccf9b428..232d32f4ba 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -312,69 +312,64 @@ function make_reaction_system(ex::Expr, name) # Handle interpolation of variables in the input. ex = esc_dollars!(ex) - # Extracts the lines with reactions and lines with options. + # Extracts the lines with reactions, the lines with options, and the options. reaction_lines = Expr[x for x in ex.args if x.head == :tuple] option_lines = Expr[x for x in ex.args if x.head == :macrocall] - - # Extracts the options used (throwing errors for repeated options). - if !allunique(arg.args[1] for arg in option_lines) - error("Some options where given multiple times.") - end options = Dict(Symbol(String(arg.args[1])[2:end]) => arg for arg in option_lines) - # Reads options (round 1, options which must be read before the reactions, e.g. because - # they might declare parameters/species/variables). + # Read options that explicitly declares some symbol. Compiles a list of all declared symbols + # and checks that there has been no double-declarations. + cmpexpr_init, cmps_declared = read_compound_options(options) + sps_declared = extract_syms(options, :species) + ps_declared = extract_syms(options, :parameters) + vs_declared = extract_syms(options, :variables) + tiv, sivs, ivs, ivsexpr = read_ivs_option(options) + diffexpr, diffs_declared = read_differentials_option(options) + syms_declared = union(cmps_declared, sps_declared, ps_declared, vs_declared, + ivs, diffs_declared) + + # Reads the reactions and equation. From these, finds inferred species, variables and parameters. requiredec = haskey(options, :require_declaration) - compound_expr_init, compound_species = read_compound_options(options) - species_declared = [extract_syms(options, :species); compound_species] - parameters_declared = extract_syms(options, :parameters) - variables_declared = extract_syms(options, :variables) - vars_extracted, add_default_diff, equations = read_equations_options(options, - variables_declared; requiredec) - - # Extracts all reactions. Extracts all parameters, species, and variables of the system and - # creates lists with them. reactions = get_reactions(reaction_lines) - variables = vcat(variables_declared, vars_extracted) - syms_declared = Set(Iterators.flatten((parameters_declared, species_declared, - variables))) - species_extracted, parameters_extracted = extract_species_and_parameters(reactions, - syms_declared; requiredec) - species = vcat(species_declared, species_extracted) - parameters = vcat(parameters_declared, parameters_extracted) - - # Reads options (round 2, options that either can, or must, be read after the reactions). - tiv, sivs, ivs, ivsexpr = read_ivs_option(options) + sps_inferred, ps_pre_inferred = extract_sps_and_ps(reactions, syms_declared; requiredec) + vs_inferred, diffs_inferred, equations = read_equations_options!(diffexpr, options, + union(syms_declared, sps_inferred), tiv; requiredec) + ps_inferred = setdiff(ps_pre_inferred, vs_inferred, diffs_inferred) + syms_inferred = union(sps_inferred, ps_inferred, vs_inferred, diffs_inferred) + sps = union(sps_declared, sps_inferred) + vs = union(vs_declared, vs_inferred) + ps = union(ps_inferred, ps_inferred) + + # Read options not related to the declaration or inference of symbols. + obsexpr, obs_eqs, obs_syms = read_observed_options(options, ivs, + union(sps_declared, vs_declared), union(syms_declared, syms_inferred); requiredec) continuous_events_expr = read_events_option(options, :continuous_events) discrete_events_expr = read_events_option(options, :discrete_events) - obsexpr, observed_eqs, obs_syms = read_observed_options(options, - [species_declared; variables], ivs; requiredec) - diffexpr = create_differential_expr(options, add_default_diff, - [species; parameters; variables], tiv) default_reaction_metadata = read_default_noise_scaling_option(options) combinatoric_ratelaws = read_combinatoric_ratelaws_option(options) - # Reads observables. - observed_vars, observed_eqs, obs_syms = read_observed_options( - options, [species_declared; variables], all_ivs; requiredec) - # Checks for input errors. - forbidden_symbol_check(union(species, parameters)) - forbidden_variable_check(variables) - unique_symbol_check(vcat(species, parameters, variables, ivs)) + forbidden_symbol_check(union(sps, ps)) + forbidden_variable_check(vs) + allunique(arg.args[1] for arg in option_lines) || + error("Some options where given multiple times.") (sum(length, [reaction_lines, option_lines]) != length(ex.args)) && error("@reaction_network input contain $(length(ex.args) - sum(length.([reaction_lines,option_lines]))) malformed lines.") any(!in(option_keys), keys(options)) && error("The following unsupported options were used: $(filter(opt_in->!in(opt_in,option_keys), keys(options)))") + if !allunique(syms_declared) + nonunique_syms = [s for s in syms_declared if count(x -> x == s, syms_declared) > 1] + error("The following symbols $(nonunique_syms) have explicitly been declared as multiple types of components (e.g. occur in at least two of the `@species`, `@parameters`, `@variables`, `@ivs`, `@compounds`, `@differentials`). This is not allowed.") + end # Creates expressions corresponding to actual code from the internal DSL representation. - psexpr_init = get_pexpr(parameters_extracted, options) - spsexpr_init = get_usexpr(species_extracted, options; ivs) - vsexpr_init = get_usexpr(vars_extracted, options, :variables; ivs) + psexpr_init = get_pexpr(ps_inferred, options) + spsexpr_init = get_usexpr(sps_inferred, options; ivs) + vsexpr_init = get_usexpr(vs_inferred, options, :variables; ivs) psexpr, psvar = scalarize_macro(psexpr_init, "ps") spsexpr, spsvar = scalarize_macro(spsexpr_init, "specs") vsexpr, vsvar = scalarize_macro(vsexpr_init, "vars") - cmpsexpr, cmpsvar = scalarize_macro(compound_expr_init, "comps") + cmpsexpr, cmpsvar = scalarize_macro(cmpexpr_init, "comps") rxsexprs = make_rxsexprs(reactions, equations) # Assemblies the full expression that declares all required symbolic variables, and @@ -394,7 +389,7 @@ function make_reaction_system(ex::Expr, name) spatial_ivs = $sivs rx_eq_vec = $rxsexprs vars = setdiff(union($spsvar, $vsvar, $cmpsvar), $obs_syms) - observed = $observed_eqs + observed = $obs_eqs continuous_events = $continuous_events_expr discrete_events = $discrete_events_expr combinatoric_ratelaws = $combinatoric_ratelaws @@ -516,7 +511,7 @@ end # Function looping through all reactions, to find undeclared symbols (species or # parameters) and assign them to the right category. -function extract_species_and_parameters(reactions, excluded_syms; requiredec = false) +function extract_sps_and_ps(reactions, excluded_syms; requiredec = false) # Loops through all reactant, extract undeclared ones as species. species = OrderedSet{Union{Symbol, Expr}}() for reaction in reactions @@ -544,7 +539,7 @@ function extract_species_and_parameters(reactions, excluded_syms; requiredec = f return collect(species), collect(parameters) end -# Function called by extract_species_and_parameters, recursively loops through an +# Function called by extract_sps_and_ps, recursively loops through an # expression and find symbols (adding them to the push_symbols vector). function add_syms_from_expr!(push_symbols::AbstractSet, expr::ExprValues, excluded_syms) # If we have encountered a Symbol in the recursion, we can try extracting it. @@ -683,19 +678,20 @@ function read_default_noise_scaling_option(options) return :([]) end -# When compound species are declared using the "@compound begin ... end" option, get a list of the compound species, and also the expression that crates them. -function read_compound_options(opts) - # If the compound option is used retrieve a list of compound species (need to be added to the reaction system's species), and the option that creates them (used to declare them as compounds at the end). - if haskey(opts, :compounds) - compound_expr = opts[:compounds] - # Find compound species names, and append the independent variable. - compound_species = [find_varinfo_in_declaration(arg.args[2])[1] - for arg in compound_expr.args[3].args] +# When compound species are declared using the "@compound begin ... end" option, get a list +# of the compound species, and also the expression that crates them. +function read_compound_options(options) + # If the compound option is used, retrieve a list of compound species and the option line + # that creates them (used to declare them as compounds at the end). + if haskey(options, :compounds) + cmpexpr_init = options[:compounds] + cmps_declared = [find_varinfo_in_declaration(arg.args[2])[1] + for arg in cmpexpr_init.args[3].args] else # If option is not used, return empty vectors and expressions. - compound_expr = :() - compound_species = Union{Symbol, Expr}[] + cmpexpr_init = :() + cmps_declared = Union{Symbol, Expr}[] end - return compound_expr, compound_species + return cmpexpr_init, cmps_declared end # Read the events (continuous or discrete) provided as options to the DSL. Returns an expression which evaluates to these. @@ -732,11 +728,10 @@ function read_events_option(options, event_type::Symbol) return events_expr end -# Reads the variables options. Outputs: -# `vars_extracted`: A vector with extracted variables (lhs in pure differential equations only). -# `dtexpr`: If a differential equation is defined, the default derivative (D ~ Differential(t)) must be defined. -# `equations`: a vector with the equations provided. -function read_equations_options(options, syms_declared, parameters_extracted; requiredec = false) +# Reads the variables options. Outputs a list of teh variables inferred from the equations, +# as well as the equation vector. If the default differential was used, updates the `diffexpr` +# expression so that this declares this as well. +function read_equations_options!(diffexpr, options, syms_unavaiable, tiv; requiredec = false) # Prepares the equations. First, extracts equations from provided option (converting to block form if required). # Next, uses MTK's `parse_equations!` function to split input into a vector with the equations. eqs_input = haskey(options, :equations) ? options[:equations].args[3] : :(begin end) @@ -748,28 +743,35 @@ function read_equations_options(options, syms_declared, parameters_extracted; re # Loops through all equations, checks for lhs of the form `D(X) ~ ...`. # When this is the case, the variable X and differential D are extracted (for automatic declaration). # Also performs simple error checks. - vars_extracted = OrderedSet{Union{Symbol, Expr}}() + vs_inferred = OrderedSet{Union{Symbol, Expr}}() add_default_diff = false for eq in equations if (eq.head != :call) || (eq.args[1] != :~) error("Malformed equation: \"$eq\". Equation's left hand and right hand sides should be separated by a \"~\".") end - # If the default differential (`D`) is used, record that it should be decalred later on. - if (:D ∉ union(syms_declared, parameters_extracted)) && find_D_call(eq) + # If the default differential (`D`) is used, record that it should be declared later on. + if (:D ∉ syms_unavaiable) && find_D_call(eq) requiredec && throw(UndeclaredSymbolicError( "Unrecognized symbol D was used as a differential in an equation: \"$eq\". Since the @require_declaration flag is set, all differentials in equations must be explicitly declared using the @differentials option.")) add_default_diff = true - push!(syms_declared, :D) + push!(syms_unavaiable, :D) end # Any undecalred symbolic variables encountered should be extracted as variables. - add_syms_from_expr!(vars_extracted, eq, syms_declared) - (!isempty(vars_extracted) && requiredec) && throw(UndeclaredSymbolicError( - "Unrecognized symbolic variables $(join(vars_extracted, ", ")) detected in equation expression: \"$(string(eq))\". Since the flag @require_declaration is declared, all symbolic variables must be explicitly declared with the @species, @variables, and @parameters options.")) + add_syms_from_expr!(vs_inferred, eq, syms_unavaiable) + (!isempty(vs_inferred) && requiredec) && throw(UndeclaredSymbolicError( + "Unrecognized symbolic variables $(join(vs_inferred, ", ")) detected in equation expression: \"$(string(eq))\". Since the flag @require_declaration is declared, all symbolic variables must be explicitly declared with the @species, @variables, and @parameters options.")) + end + + # If `D` differential is used, add it to differential expression and infered differentials list. + diffs_inferred = Union{Symbol, Expr}[] + if add_default_diff && !any(diff_dec.args[1] == :D for diff_dec in diffexpr.args) + diffs_inferred = [:D] + push!(diffexpr.args, :(D = Differential($(tiv)))) end - return collect(vars_extracted), add_default_diff, equations + return vs_inferred, diffs_inferred, equations end # Searches an expresion `expr` and returns true if it have any subexpression `D(...)` (where `...` can be anything). @@ -786,7 +788,7 @@ end # Creates an expression declaring differentials. Here, `tiv` is the time independent variables, # which is used by the default differential (if it is used). -function create_differential_expr(options, add_default_diff, used_syms, tiv) +function read_differentials_option(options) # Creates the differential expression. # If differentials was provided as options, this is used as the initial expression. # If the default differential (D(...)) was used in equations, this is added to the expression. @@ -794,25 +796,20 @@ function create_differential_expr(options, add_default_diff, used_syms, tiv) striplines(:(begin end))) diffexpr = option_block_form(diffexpr) - # Goes through all differentials, checking that they are correctly formatted and their symbol is not used elsewhere. + # Goes through all differentials, checking that they are correctly formatted. Adds their + # symbol to the list of declared differential sybols. + diffs_declared = Union{Symbol, Expr}[] for dexpr in diffexpr.args (dexpr.head != :(=)) && error("Differential declaration must have form like D = Differential(t), instead \"$(dexpr)\" was given.") (dexpr.args[1] isa Symbol) || error("Differential left-hand side must be a single symbol, instead \"$(dexpr.args[1])\" was given.") - in(dexpr.args[1], used_syms) && - error("Differential name ($(dexpr.args[1])) is also a species, variable, or parameter. This is ambiguous and not allowed.") in(dexpr.args[1], forbidden_symbols_error) && error("A forbidden symbol ($(dexpr.args[1])) was used as a differential name.") + push!(diffs_declared, dexpr.args[1]) end - # If the default differential D has been used, but not pre-declared using the @differentials - # options, add this declaration to the list of declared differentials. - if add_default_diff && !any(diff_dec.args[1] == :D for diff_dec in diffexpr.args) - push!(diffexpr.args, :(D = Differential($(tiv)))) - end - - return diffexpr + return diffexpr, diffs_declared end # Reads the combinatiorial ratelaw options, which determines if a combinatorial rate law should @@ -822,20 +819,23 @@ function read_combinatoric_ratelaws_option(options) options[:combinatoric_ratelaws].args[end] : true end -# Reads the observables options. Outputs an expression ofr creating the observable variables, and a vector of observable equations. -function read_observed_options(options, species_n_vars_declared, ivs_sorted; requiredec = false) +# Reads the observables options. Outputs an expression for creating the observable variables, +# a vector containing the observable equations, and a list of all observable symbols (this +# list contains both those declared separately or infered from the `@observables` option` input`). +function read_observed_options(options, all_ivs, us_declared, all_syms; requiredec = false) if haskey(options, :observables) # Gets list of observable equations and prepares variable declaration expression. # (`options[:observables]` includes `@observables`, `.args[3]` removes this part) - observed_eqs = make_observed_eqs(options[:observables].args[3]) - observed_expr = Expr(:block, :(@variables)) + obs_eqs = make_obs_eqs(options[:observables].args[3]) + obsexpr = Expr(:block, :(@variables)) obs_syms = :([]) - for (idx, obs_eq) in enumerate(observed_eqs.args) + for (idx, obs_eq) in enumerate(obs_eqs.args) # Extract the observable, checks for errors. obs_name, ivs, defaults, metadata = find_varinfo_in_declaration(obs_eq.args[2]) - (requiredec && !in(obs_name, species_n_vars_declared)) && + # Error checks. + (requiredec && !in(obs_name, us_declared)) && throw(UndeclaredSymbolicError("An undeclared variable ($obs_name) was declared as an observable in the following observable equation: \"$obs_eq\". Since the flag @require_declaration is set, all variables must be declared with the @species, @parameters, or @variables macros.")) isempty(ivs) || error("An observable ($obs_name) was given independent variable(s). These should not be given, as they are inferred automatically.") @@ -843,15 +843,17 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted; req error("An observable ($obs_name) was given a default value. This is forbidden.") in(obs_name, forbidden_symbols_error) && error("A forbidden symbol ($(obs_eq.args[2])) was used as an observable name.") - (obs_name in species_n_vars_declared) && is_escaped_expr(obs_eq.args[2]) && + in(obs_name, all_syms) && + error("An observable ($obs_name) uses a name that already have been already been declared or inferred as another model property.") + (obs_name in us_declared) && is_escaped_expr(obs_eq.args[2]) && error("An interpolated observable have been used, which has also been ereqxplicitly declared within the system using either @species or @variables. This is not permitted.") - ((obs_name in species_n_vars_declared) || is_escaped_expr(obs_eq.args[2])) && + ((obs_name in us_declared) || is_escaped_expr(obs_eq.args[2])) && !isnothing(metadata) && error("Metadata was provided to observable $obs_name in the `@observables` macro. However, the observable was also declared separately (using either @species or @variables). When this is done, metadata should instead be provided within the original @species or @variable declaration.") # This bits adds the observables to the @variables vector which is given as output. # For Observables that have already been declared using @species/@variables, # or are interpolated, this parts should not be carried out. - if !((obs_name in species_n_vars_declared) || is_escaped_expr(obs_eq.args[2])) + if !((obs_name in us_declared) || is_escaped_expr(obs_eq.args[2])) # Creates an expression which extracts the ivs of the species & variables the # observable depends on, and splats them out in the correct order. dep_var_expr = :(filter(!MT.isparameter, @@ -860,15 +862,15 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted; req vcat, [sorted_arguments(MT.unwrap(dep)) for dep in $dep_var_expr]))) ivs_get_expr_sorted = :(sort($(ivs_get_expr); - by = iv -> findfirst(MT.getname(iv) == ivs for ivs in $ivs_sorted))) + by = iv -> findfirst(MT.getname(iv) == ivs for ivs in $all_ivs))) obs_expr = insert_independent_variable(obs_eq.args[2], :($ivs_get_expr_sorted...)) - push!(observed_vars.args[1].args, obs_expr) + push!(obsexpr.args[1].args, obs_expr) end - # In case metadata was given, this must be cleared from `observed_eqs`. + # In case metadata was given, this must be cleared from `obs_eqs`. # For interpolated observables (I.e. $X ~ ...) this should and cannot be done. - is_escaped_expr(obs_eq.args[2]) || (observed_eqs.args[idx].args[2] = obs_name) + is_escaped_expr(obs_eq.args[2]) || (obs_eqs.args[idx].args[2] = obs_name) # Adds the observable to the list of observable names. # This is required for filtering away so these are not added to the ReactionSystem's species list. @@ -876,27 +878,27 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted; req is_escaped_expr(obs_eq.args[2]) || push!(obs_syms.args, obs_name) end - # If nothing was added to `observed_vars`, it has to be modified not to throw an error. - (striplines(observed_vars) == striplines(Expr(:block, :(@variables)))) && - (observed_vars = :()) + # If nothing was added to `obsexpr`, it has to be modified not to throw an error. + (striplines(obsexpr) == striplines(Expr(:block, :(@variables)))) && + (obsexpr = :()) else # If option is not used, return empty expression and vector. - observed_expr = :() - observed_eqs = :([]) + obsexpr = :() + obs_eqs = :([]) obs_syms = :([]) end - return observed_expr, observed_eqs, obs_syms + return obsexpr, obs_eqs, obs_syms end # From the input to the @observables options, creates a vector containing one equation for # each observable. `option_block_form` handles if single line declaration of `@observables`, # i.e. without a `begin ... end` block, was used. -function make_observed_eqs(observables_expr) +function make_obs_eqs(observables_expr) observables_expr = option_block_form(observables_expr) - observed_eqs = :([]) - foreach(arg -> push!(observed_eqs.args, arg), observables_expr.args) - return observed_eqs + obs_eqs = :([]) + foreach(arg -> push!(obs_eqs.args, arg), observables_expr.args) + return obs_eqs end ### `@reaction` Macro & its Internals ### @@ -965,7 +967,7 @@ function make_reaction(ex::Expr) # Parses reactions. Extracts species and paraemters within it. reaction = get_reaction(ex) - species, parameters = extract_species_and_parameters([reaction], []) + species, parameters = extract_sps_and_ps([reaction], []) # Checks for input errors. forbidden_symbol_check(union(species, parameters)) From 18fcbf1214cd5a4951ac71e882edad8d290fe91f Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 18 Jan 2025 18:51:57 +0000 Subject: [PATCH 24/38] prepare formatting round --- docs/src/api.md | 1 + docs/src/model_creation/dsl_advanced.md | 29 +++- src/Catalyst.jl | 5 - src/dsl.jl | 212 +++++++++--------------- src/expression_utils.jl | 7 - test/dsl/dsl_options.jl | 84 +++++----- 6 files changed, 152 insertions(+), 186 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index f78a5f70e4..df4b0f0864 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -100,6 +100,7 @@ of all options currently available. - [`continuous_events`](@ref constraint_equations_events): Allows the creation of continuous events. - [`discrete_events`](@ref constraint_equations_events): Allows the creation of discrete events. - [`combinatoric_ratelaws`](@ref faq_combinatoric_ratelaws): Takes a single option (`true` or `false`), which sets whether to use combinatorial rate laws. +- [`require_declaration`](@ref dsl_advanced_options_require_dec): Turns off all inference of parameters, species, variables, the default differential, and observables (requiring these to be explicitly declared using e.g. `@species`). ## [ModelingToolkit and Catalyst accessor functions](@id api_accessor_functions) A [`ReactionSystem`](@ref) is an instance of a diff --git a/docs/src/model_creation/dsl_advanced.md b/docs/src/model_creation/dsl_advanced.md index af806864b6..47988cd0ee 100644 --- a/docs/src/model_creation/dsl_advanced.md +++ b/docs/src/model_creation/dsl_advanced.md @@ -204,7 +204,6 @@ ModelingToolkit.getdescription(two_state_system.kA) ``` ### [Designating constant-valued/fixed species parameters](@id dsl_advanced_options_constant_species) - Catalyst enables the designation of parameters as `constantspecies`. These parameters can be used as species in reactions, however, their values are not changed by the reaction and remain constant throughout the simulation (unless changed by e.g. the [occurrence of an event](@ref constraint_equations_events). Practically, this is done by setting the parameter's `isconstantspecies` metadata to `true`. Here, we create a simple reaction where the species `X` is converted to `Xᴾ` at rate `k`. By designating `X` as a constant species parameter, we ensure that its quantity is unchanged by the occurrence of the reaction. ```@example dsl_advanced_constant_species using Catalyst # hide @@ -501,6 +500,34 @@ Catalyst.getdescription(rx) A list of all available reaction metadata can be found [in the api](@ref api_rx_metadata). +## [Declaring individual reaction using the `@reaction` macro](@id dsl_advanced_options_reaction_macro) +Catalyst exports a macro `@reaction` which can be used to generate a singular [`Reaction`](@ref) object of the same type which is stored within the [`ReactionSystem`](@ref) structure (which in turn can be generated by `@reaction_network `). In the following example, we create a simple [SIR model](@ref basic_CRN_library_sir). Next, we instead create its individual reaction components using the `@reaction` macro. Finally, we confirm that these are identical to those stored in the initial model (using the [`reactions`](@ref) function). +```@example dsl_advanced_reaction_macro +using Catalyst # hide +sir_model = @reaction_network begin + α, S + I --> 2I + β, I --> R +end +infection_rx = @reaction α, S + I --> 2I +recovery_rx = @reaction β, I --> R +issetequal(reactions(sir_model), [infection_rx, recovery_rx]) +``` + +Here, the `@reaction` macro is followed by a single line consisting of three parts: +- A rate (at which the reaction occurs). +- Any number of substrates (which are consumed by the reaction). +- Any number of products (which are produced by the reaction). +The rules of writing and interpreting this line are [identical to those for `@reaction_network`](@ref dsl_description_reactions) (however, bi-directional reactions are not permitted). + +Generally, the `@reaction` macro provides a more concise notation to the [`Reaction`](@ref) constructor. One of its primary uses is for [creating models programmatically](@ref programmatic_CRN_construction). When doing so, it can often be useful to use [*interpolation*](@ref dsl_advanced_options_symbolics_and_DSL_interpolation) of symbolic variables declared previously: +```@example dsl_advanced_reaction_macro +t = default_t() +@parameters k b +@species A(t) +ex = k*A^2 + t +rx = @reaction b*$ex*$A, $A --> C +``` + ## [Working with symbolic variables and the DSL](@id dsl_advanced_options_symbolics_and_DSL) We have previously described how Catalyst represents its models symbolically (enabling e.g. symbolic differentiation of expressions stored in models). While Catalyst utilises this for many internal operation, these symbolic representations can also be accessed and harnessed by the user. Primarily, doing so is much easier during programmatic (as opposed to DSL-based) modelling. Indeed, the section on [programmatic modelling](@ref programmatic_CRN_construction) goes into more details about symbolic representation in models, and how these can be used. It is, however, also ways to utilise these methods during DSL-based modelling. Below we briefly describe two methods for doing so. diff --git a/src/Catalyst.jl b/src/Catalyst.jl index 52daa0cec5..7c0cbbc4ee 100644 --- a/src/Catalyst.jl +++ b/src/Catalyst.jl @@ -72,11 +72,6 @@ const CONSERVED_CONSTANT_SYMBOL = :Γ const forbidden_symbols_skip = Set([:ℯ, :pi, :π, :t, :∅]) const forbidden_symbols_error = union(Set([:im, :nothing, CONSERVED_CONSTANT_SYMBOL]), forbidden_symbols_skip) -const forbidden_variables_error = let - fvars = copy(forbidden_symbols_error) - delete!(fvars, :t) - fvars -end ### Package Main ### diff --git a/src/dsl.jl b/src/dsl.jl index 232d32f4ba..c01cf6497c 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -47,45 +47,21 @@ end ### `@reaction_network` and `@network_component` Macros ### -""" @reaction_network +""" + @reaction_network Macro for generating chemical reaction network models (Catalyst `ReactionSystem`s). See the -[Catalyst documentation](https://catalyst.sciml.ai) for more details on the domain specific -language (DSL) that the macro implements, and for how `ReactionSystem`s can be used to generate -and simulate mathematical models of chemical systems. +following two section ([DSL introduction](https://docs.sciml.ai/Catalyst/stable/model_creation/dsl_basics/) +and [advantage usage](https://docs.sciml.ai/Catalyst/stable/model_creation/dsl_advanced/)) of +the Catalyst documentation for more details on the domain specific language (DSL) that the +macro implements. The macros output (a `ReactionSystem` structure) are central to Catalyst +and its functionality. How to e.g. simulate them is described in the [Catalyst documentation](https://docs.sciml.ai/Catalyst/stable/). Returns: - A Catalyst `ReactionSystem`, i.e. a symbolic model for the reaction network. The returned system is marked `complete`. To obtain a `ReactionSystem` that is not marked complete, for -example to then use in compositional modeling, see the otherwise equivalent `@network_component` macro. - -Options: -- `@species S1(t) S2(t) ...`, defines a collection of species. -- `@variables V1(t) V2(t) ...`, defines non-species variables (for example, that evolve via a coupled ODE). -- ... - naming a network ... - -Examples: some examples illustrating various use cases, including begin/end blocks, naming, interpolation, and mixes of the options. - -""" - -""" - @reaction_network - -Macro for generating chemical reaction network models. Outputs a [`ReactionSystem`](@ref) structure, -which stores all information of the model. Next, it can be used as input to various simulations, or -other tools for model analysis. The `@reaction_network` macro is sometimes called the "Catalyst -DSL" (where DSL = domain-specific language), as it implements a DSL for creating chemical reaction -network models. - -The `@reaction_network` macro, and the `ReactionSystem`s it generates, are central to Catalyst -and its functionality. Catalyst is described in more detail in its documentation. The -`reaction_network` DSL in particular is described in more detail [here](@ref dsl_description). - -The `@reaction_network` statement is followed by a `begin ... end` block. Each line within the -block corresponds to a single reaction. Each reaction consists of: -- A rate (at which the reaction occurs). -- Any number of substrates (which are consumed by the reaction). -- Any number of products (which are produced by the reaction). +example to then use in compositional modeling, see the otherwise equivalent `@network_component` +macro. Examples: Here we create a basic SIR model. It contains two reactions (infection and recovery): @@ -108,25 +84,8 @@ This model also contains production and degradation reactions, where `0` denotes either no substrates or no products in a reaction. Options: -The `@reaction_network` also accepts various options. These are inputs to the model creation that are -not reactions. To denote that a line contains an option (and not a reaction), the line starts with `@` -followed by the options name. E.g. an observable is declared using the `@observables` option. -Here we create a polymerisation model (where the parameter `n` denotes the number of monomers in -the polymer). We use the observable `Xtot` to track the total amount of `X` in the system. We also -bundle the forward and backwards binding reactions into a single line. -```julia -polymerisation = @reaction_network begin - @observables Xtot ~ X + n*Xn - (kB,kD), n*X <--> Xn -end -``` - -Notes: -- `ReactionSystem`s created through `@reaction_network` are considered complete (non-complete -systems can be created through the alternative `@network_component` macro). -- `ReactionSystem`s created through `@reaction_network`, by default, have a random name. Specific -names can be designated as a first argument (before `begin`, e.g. `rn = @reaction_network name begin ...`). -- For more information, please again consider Catalyst's documentation. +In addition to reactions, the macro also supports "option" inputs. Each option is designated +by a tag starting with a `@` followed by its input. A list of options can be found [here](https://docs.sciml.ai/Catalyst/stable/api/#api_dsl_options). """ macro reaction_network(name::Symbol, network_expr::Expr) make_rs_expr(QuoteNode(name), network_expr) @@ -191,7 +150,7 @@ function make_rs_expr(name; complete = true) return Expr(:block, :(@parameters t), rs_expr) end -# When both a name and a network expression is generated, dispatches thees to the internal +# When both a name and a network expression is generated, dispatches these to the internal # `make_reaction_system` function. function make_rs_expr(name, network_expr; complete = true) rs_expr = make_reaction_system(striplines(network_expr), name) @@ -312,10 +271,16 @@ function make_reaction_system(ex::Expr, name) # Handle interpolation of variables in the input. ex = esc_dollars!(ex) - # Extracts the lines with reactions, the lines with options, and the options. + # Extracts the lines with reactions, the lines with options, and the options. Check for input errors. reaction_lines = Expr[x for x in ex.args if x.head == :tuple] option_lines = Expr[x for x in ex.args if x.head == :macrocall] options = Dict(Symbol(String(arg.args[1])[2:end]) => arg for arg in option_lines) + allunique(arg.args[1] for arg in option_lines) || + error("Some options where given multiple times.") + (sum(length, [reaction_lines, option_lines]) != length(ex.args)) && + error("@reaction_network input contain $(length(ex.args) - sum(length.([reaction_lines,option_lines]))) malformed lines.") + any(!in(option_keys), keys(options)) && + error("The following unsupported options were used: $(filter(opt_in->!in(opt_in,option_keys), keys(options)))") # Read options that explicitly declares some symbol. Compiles a list of all declared symbols # and checks that there has been no double-declarations. @@ -324,21 +289,22 @@ function make_reaction_system(ex::Expr, name) ps_declared = extract_syms(options, :parameters) vs_declared = extract_syms(options, :variables) tiv, sivs, ivs, ivsexpr = read_ivs_option(options) - diffexpr, diffs_declared = read_differentials_option(options) - syms_declared = union(cmps_declared, sps_declared, ps_declared, vs_declared, - ivs, diffs_declared) + diffsexpr, diffs_declared = read_differentials_option(options) + syms_declared = collect(Iterators.flatten((cmps_declared, sps_declared, ps_declared, + vs_declared, ivs, diffs_declared))) + if !allunique(syms_declared) + nonunique_syms = [s for s in syms_declared if count(x -> x == s, syms_declared) > 1] + error("The following symbols $(unique(nonunique_syms)) have explicitly been declared as multiple types of components (e.g. occur in at least two of the `@species`, `@parameters`, `@variables`, `@ivs`, `@compounds`, `@differentials`). This is not allowed.") + end - # Reads the reactions and equation. From these, finds inferred species, variables and parameters. + # Reads the reactions and equation. From these, infers species, variables, and parameters. requiredec = haskey(options, :require_declaration) reactions = get_reactions(reaction_lines) sps_inferred, ps_pre_inferred = extract_sps_and_ps(reactions, syms_declared; requiredec) - vs_inferred, diffs_inferred, equations = read_equations_options!(diffexpr, options, + vs_inferred, diffs_inferred, equations = read_equations_options!(diffsexpr, options, union(syms_declared, sps_inferred), tiv; requiredec) ps_inferred = setdiff(ps_pre_inferred, vs_inferred, diffs_inferred) syms_inferred = union(sps_inferred, ps_inferred, vs_inferred, diffs_inferred) - sps = union(sps_declared, sps_inferred) - vs = union(vs_declared, vs_inferred) - ps = union(ps_inferred, ps_inferred) # Read options not related to the declaration or inference of symbols. obsexpr, obs_eqs, obs_syms = read_observed_options(options, ivs, @@ -348,29 +314,15 @@ function make_reaction_system(ex::Expr, name) default_reaction_metadata = read_default_noise_scaling_option(options) combinatoric_ratelaws = read_combinatoric_ratelaws_option(options) - # Checks for input errors. - forbidden_symbol_check(union(sps, ps)) - forbidden_variable_check(vs) - allunique(arg.args[1] for arg in option_lines) || - error("Some options where given multiple times.") - (sum(length, [reaction_lines, option_lines]) != length(ex.args)) && - error("@reaction_network input contain $(length(ex.args) - sum(length.([reaction_lines,option_lines]))) malformed lines.") - any(!in(option_keys), keys(options)) && - error("The following unsupported options were used: $(filter(opt_in->!in(opt_in,option_keys), keys(options)))") - if !allunique(syms_declared) - nonunique_syms = [s for s in syms_declared if count(x -> x == s, syms_declared) > 1] - error("The following symbols $(nonunique_syms) have explicitly been declared as multiple types of components (e.g. occur in at least two of the `@species`, `@parameters`, `@variables`, `@ivs`, `@compounds`, `@differentials`). This is not allowed.") - end - # Creates expressions corresponding to actual code from the internal DSL representation. - psexpr_init = get_pexpr(ps_inferred, options) + psexpr_init = get_psexpr(ps_inferred, options) spsexpr_init = get_usexpr(sps_inferred, options; ivs) vsexpr_init = get_usexpr(vs_inferred, options, :variables; ivs) psexpr, psvar = scalarize_macro(psexpr_init, "ps") spsexpr, spsvar = scalarize_macro(spsexpr_init, "specs") vsexpr, vsvar = scalarize_macro(vsexpr_init, "vars") cmpsexpr, cmpsvar = scalarize_macro(cmpexpr_init, "comps") - rxsexprs = make_rxsexprs(reactions, equations) + rxsexprs = get_rxexprs(reactions, equations, union(diffs_declared, diffs_inferred)) # Assemblies the full expression that declares all required symbolic variables, and # then the output `ReactionSystem`. @@ -382,9 +334,9 @@ function make_reaction_system(ex::Expr, name) $vsexpr $obsexpr $cmpsexpr - $diffexpr + $diffsexpr - # Stores each kwarg in a variable. Not necessary but useful when inspecting code for debugging. + # Stores each kwarg in a variable. Not necessary, but useful when debugging generated code. name = $name spatial_ivs = $sivs rx_eq_vec = $rxsexprs @@ -576,7 +528,7 @@ end # Given the parameters that were extracted from the reactions, and the options dictionary, # creates the `@parameters ...` expression for the macro output. -function get_pexpr(parameters_extracted, options) +function get_psexpr(parameters_extracted, options) pexprs = if haskey(options, :parameters) options[:parameters] elseif isempty(parameters_extracted) @@ -597,7 +549,7 @@ function scalarize_macro(expr_init, name) # symbolic variables (e.g. `var"##ps#384"`). namesym = gensym(name) - # If the input expression is non-emtpy, wraps it with addiional information. + # If the input expression is non-empty, wraps it with additional information. if expr_init != :(()) symvec = gensym() expr = quote @@ -612,19 +564,19 @@ function scalarize_macro(expr_init, name) end # From the system reactions (as `DSLReaction`s) and equations (as expressions), -# creates the expressions that evalutes to the reaction (+ equations) vector. -function make_rxsexprs(reactions, equations) +# creates the expressions that evaluates to the reaction (+ equations) vector. +function get_rxexprs(reactions, equations, diffsyms) rxsexprs = :(Catalyst.CatalystEqType[]) foreach(rx -> push!(rxsexprs.args, get_rxexpr(rx)), reactions) - foreach(eq -> push!(rxsexprs.args, eq), equations) + foreach(eq -> push!(rxsexprs.args, escape_equation!(eq, diffsyms)), equations) return rxsexprs end # From a `DSLReaction` struct, creates the expression which evaluates to the creation -# of the correponding reaction. +# of the corresponding reaction. function get_rxexpr(rx::DSLReaction) # Initiates the `Reaction` expression. - rate = recursive_expand_functions!(rx.rate) + rate = recursive_escape_functions!(rx.rate) rx_constructor = :(Reaction($rate, [], [], [], []; metadata = $(rx.metadata))) # Loops through all products and substrates, and adds them (and their stoichiometries) @@ -643,7 +595,7 @@ end ### DSL Option Handling ### -# Finds the time independent variable, and any potential spatial indepndent variables. +# Finds the time independent variable, and any potential spatial independent variables. # Returns these (individually and combined), as well as an expression for declaring them. function read_ivs_option(options) # Creates the independent variables expressions (depends on whether the `ivs` option was used). @@ -656,7 +608,7 @@ function read_ivs_option(options) ivsexpr = :($(DEFAULT_IV_SYM) = default_t()) end - # Extracts the independet variables symbols (time and spatial), and returns the output. + # Extracts the independent variables symbols (time and spatial), and returns the output. tiv = ivs[1] sivs = (length(ivs) > 1) ? Expr(:vect, ivs[2:end]...) : nothing return tiv, sivs, ivs, ivsexpr @@ -718,7 +670,7 @@ function read_events_option(options, event_type::Symbol) error("The condition part of continuous events (the left-hand side) must be a vector. This is not the case for: $(arg).") end if (arg isa Expr) && (arg.args[3] isa Expr) && (arg.args[3].head != :vect) - error("The affect part of all events (the righ-hand side) must be a vector. This is not the case for: $(arg).") + error("The affect part of all events (the right-hand side) must be a vector. This is not the case for: $(arg).") end # Adds the correctly formatted event to the event creation expression. @@ -729,9 +681,9 @@ function read_events_option(options, event_type::Symbol) end # Reads the variables options. Outputs a list of teh variables inferred from the equations, -# as well as the equation vector. If the default differential was used, updates the `diffexpr` +# as well as the equation vector. If the default differential was used, updates the `diffsexpr` # expression so that this declares this as well. -function read_equations_options!(diffexpr, options, syms_unavaiable, tiv; requiredec = false) +function read_equations_options!(diffsexpr, options, syms_unavailable, tiv; requiredec = false) # Prepares the equations. First, extracts equations from provided option (converting to block form if required). # Next, uses MTK's `parse_equations!` function to split input into a vector with the equations. eqs_input = haskey(options, :equations) ? options[:equations].args[3] : :(begin end) @@ -751,30 +703,30 @@ function read_equations_options!(diffexpr, options, syms_unavaiable, tiv; requir end # If the default differential (`D`) is used, record that it should be declared later on. - if (:D ∉ syms_unavaiable) && find_D_call(eq) + if (:D ∉ syms_unavailable) && find_D_call(eq) requiredec && throw(UndeclaredSymbolicError( "Unrecognized symbol D was used as a differential in an equation: \"$eq\". Since the @require_declaration flag is set, all differentials in equations must be explicitly declared using the @differentials option.")) add_default_diff = true - push!(syms_unavaiable, :D) + push!(syms_unavailable, :D) end - # Any undecalred symbolic variables encountered should be extracted as variables. - add_syms_from_expr!(vs_inferred, eq, syms_unavaiable) + # Any undeclared symbolic variables encountered should be extracted as variables. + add_syms_from_expr!(vs_inferred, eq, syms_unavailable) (!isempty(vs_inferred) && requiredec) && throw(UndeclaredSymbolicError( "Unrecognized symbolic variables $(join(vs_inferred, ", ")) detected in equation expression: \"$(string(eq))\". Since the flag @require_declaration is declared, all symbolic variables must be explicitly declared with the @species, @variables, and @parameters options.")) end - # If `D` differential is used, add it to differential expression and infered differentials list. + # If `D` differential is used, add it to differential expression and inferred differentials list. diffs_inferred = Union{Symbol, Expr}[] - if add_default_diff && !any(diff_dec.args[1] == :D for diff_dec in diffexpr.args) + if add_default_diff && !any(diff_dec.args[1] == :D for diff_dec in diffsexpr.args) diffs_inferred = [:D] - push!(diffexpr.args, :(D = Differential($(tiv)))) + push!(diffsexpr.args, :(D = Differential($(tiv)))) end return vs_inferred, diffs_inferred, equations end -# Searches an expresion `expr` and returns true if it have any subexpression `D(...)` (where `...` can be anything). +# Searches an expression `expr` and returns true if it have any subexpression `D(...)` (where `...` can be anything). # Used to determine whether the default differential D has been used in any equation provided to `@equations`. function find_D_call(expr) return if Base.isexpr(expr, :call) && expr.args[1] == :D @@ -792,14 +744,14 @@ function read_differentials_option(options) # Creates the differential expression. # If differentials was provided as options, this is used as the initial expression. # If the default differential (D(...)) was used in equations, this is added to the expression. - diffexpr = (haskey(options, :differentials) ? options[:differentials].args[3] : + diffsexpr = (haskey(options, :differentials) ? options[:differentials].args[3] : striplines(:(begin end))) - diffexpr = option_block_form(diffexpr) + diffsexpr = option_block_form(diffsexpr) # Goes through all differentials, checking that they are correctly formatted. Adds their - # symbol to the list of declared differential sybols. + # symbol to the list of declared differential symbols. diffs_declared = Union{Symbol, Expr}[] - for dexpr in diffexpr.args + for dexpr in diffsexpr.args (dexpr.head != :(=)) && error("Differential declaration must have form like D = Differential(t), instead \"$(dexpr)\" was given.") (dexpr.args[1] isa Symbol) || @@ -809,10 +761,10 @@ function read_differentials_option(options) push!(diffs_declared, dexpr.args[1]) end - return diffexpr, diffs_declared + return diffsexpr, diffs_declared end -# Reads the combinatiorial ratelaw options, which determines if a combinatorial rate law should +# Reads the combinatorial ratelaw options, which determines if a combinatorial rate law should # be used or not. If not provides, uses the default (true). function read_combinatoric_ratelaws_option(options) return haskey(options, :combinatoric_ratelaws) ? @@ -821,8 +773,9 @@ end # Reads the observables options. Outputs an expression for creating the observable variables, # a vector containing the observable equations, and a list of all observable symbols (this -# list contains both those declared separately or infered from the `@observables` option` input`). +# list contains both those declared separately or inferred from the `@observables` option` input`). function read_observed_options(options, all_ivs, us_declared, all_syms; requiredec = false) + syms_unavailable = setdiff(all_syms, us_declared) if haskey(options, :observables) # Gets list of observable equations and prepares variable declaration expression. # (`options[:observables]` includes `@observables`, `.args[3]` removes this part) @@ -843,10 +796,10 @@ function read_observed_options(options, all_ivs, us_declared, all_syms; required error("An observable ($obs_name) was given a default value. This is forbidden.") in(obs_name, forbidden_symbols_error) && error("A forbidden symbol ($(obs_eq.args[2])) was used as an observable name.") - in(obs_name, all_syms) && + in(obs_name, syms_unavailable) && error("An observable ($obs_name) uses a name that already have been already been declared or inferred as another model property.") (obs_name in us_declared) && is_escaped_expr(obs_eq.args[2]) && - error("An interpolated observable have been used, which has also been ereqxplicitly declared within the system using either @species or @variables. This is not permitted.") + error("An interpolated observable have been used, which has also been explicitly declared within the system using either @species or @variables. This is not permitted.") ((obs_name in us_declared) || is_escaped_expr(obs_eq.args[2])) && !isnothing(metadata) && error("Metadata was provided to observable $obs_name in the `@observables` macro. However, the observable was also declared separately (using either @species or @variables). When this is done, metadata should instead be provided within the original @species or @variable declaration.") @@ -904,21 +857,21 @@ end ### `@reaction` Macro & its Internals ### @doc raw""" -@reaction + @reaction -Generates a single [`Reaction`](@ref) object using a similar syntax as the `@reaction_network` -macro (but permiting only a single reaction). A more detailed introduction to the syntax can +Macro for generating a single [`Reaction`](@ref) object using a similar syntax as the `@reaction_network` +macro (but permitting only a single reaction). A more detailed introduction to the syntax can be found in the description of `@reaction_network`. -The `@reaction` macro is folled by a single line consisting of three parts: +The `@reaction` macro is followed by a single line consisting of three parts: - A rate (at which the reaction occur). - Any number of substrates (which are consumed by the reaction). - Any number of products (which are produced by the reaction). -The output is a reaction (just liek created using teh `Reaction` constructor). +The output is a reaction (just like created using teh `Reaction` constructor). Examples: -Here we create a simple binding reaction and stroes it in the variable rx: +Here we create a simple binding reaction and stores it in the variable rx: ```julia rx = @reaction k, X + Y --> XY ``` @@ -973,16 +926,16 @@ function make_reaction(ex::Expr) forbidden_symbol_check(union(species, parameters)) # Creates expressions corresponding to code for declaring the parameters, species, and reaction. - sexprs = get_usexpr(species, Dict{Symbol, Expr}()) - pexprs = get_pexpr(parameters, Dict{Symbol, Expr}()) + spexprs = get_usexpr(species, Dict{Symbol, Expr}()) + pexprs = get_psexpr(parameters, Dict{Symbol, Expr}()) rxexpr = get_rxexpr(reaction) iv = :($(DEFAULT_IV_SYM) = default_t()) - # Returns a repharsed expression which generates the `Reaction`. + # Returns a rephrased expression which generates the `Reaction`. quote $pexprs $iv - $sexprs + $spexprs $rxexpr end end @@ -997,25 +950,24 @@ end ### Generic Expression Manipulation ### -# Recursively traverses an expression and replaces special function call like "hill(...)" with -# the actual corresponding expression. -function recursive_expand_functions!(expr::ExprValues) +# Recursively traverses an expression and escapes all the user-defined functions. +# Special function calls like "hill(...)" are not expanded. +function recursive_escape_functions!(expr::ExprValues, diffsyms = []) (typeof(expr) != Expr) && (return expr) - for i in eachindex(expr.args) - expr.args[i] = recursive_expand_functions!(expr.args[i]) - end - if (expr.head == :call) && !isdefined(Catalyst, expr.args[1]) + foreach(i -> expr.args[i] = recursive_escape_functions!(expr.args[i], diffsyms), + 1:length(expr.args)) + if (expr.head == :call) && !isdefined(Catalyst, expr.args[1]) && expr.args[1] ∉ diffsyms expr.args[1] = esc(expr.args[1]) end - return expr + expr end # Returns the length of a expression tuple, or 1 if it is not an expression tuple (probably # a Symbol/Numerical). This is used to handle bundled reaction (like `d, (X,Y) --> 0`). # Recursively escape functions in the right-hand-side of an equation written using user-defined functions. Special function calls like "hill(...)" are not expanded. -function escape_equation_RHS!(eqexpr::Expr) - rhs = recursive_escape_functions!(eqexpr.args[3]) - eqexpr.args[3] = rhs +function escape_equation!(eqexpr::Expr, diffsyms) + eqexpr.args[2] = recursive_escape_functions!(eqexpr.args[2], diffsyms) + eqexpr.args[3] = recursive_escape_functions!(eqexpr.args[3], diffsyms) eqexpr end diff --git a/src/expression_utils.jl b/src/expression_utils.jl index c81624ea1c..d46042d3d5 100644 --- a/src/expression_utils.jl +++ b/src/expression_utils.jl @@ -31,13 +31,6 @@ function forbidden_symbol_check(sym) error("The following symbol(s) are used as species or parameters: $used_forbidden_syms, this is not permitted.") end -# Throws an error when a forbidden variable is used (a forbidden symbol that is not `:t`). -function forbidden_variable_check(sym) - used_forbidden_syms = intersect(forbidden_variables_error, sym) - isempty(used_forbidden_syms) && return - error("The following symbol(s) are used as variables: $used_forbidden_syms, this is not permitted.") -end - # Checks that no symbol was sued for multiple purposes. function unique_symbol_check(syms) allunique(syms)|| diff --git a/test/dsl/dsl_options.jl b/test/dsl/dsl_options.jl index 6ea68a2b77..4ec1f35a59 100644 --- a/test/dsl/dsl_options.jl +++ b/test/dsl/dsl_options.jl @@ -436,22 +436,22 @@ end # Relevant issue: https://github.com/SciML/Catalyst.jl/issues/1173 let # Species + parameter. - @test_broken false #@test_throws Exception @eval @reaction_network begin - #@species X(t) - #@parameters X - #end + @test_throws Exception @eval @reaction_network begin + @species X(t) + @parameters X + end # Species + variable. - @test_broken false #@test_throws Exception @eval @reaction_network begin - #@species X(t) - #@variables X(t) - #end + @test_throws Exception @eval @reaction_network begin + @species X(t) + @variables X(t) + end # Variable + parameter. - @test_broken false #@test_throws Exception @eval @reaction_network begin - #@variables X(t) - #@parameters X - #end + @test_throws Exception @eval @reaction_network begin + @variables X(t) + @parameters X + end # Species + differential. @test_throws Exception @eval @reaction_network begin @@ -472,31 +472,31 @@ let end # Parameter + observable (species/variable + observable is OK, as this e.g. provide additional observables information). - @test_broken false #@test_throws Exception @eval @reaction_network begin - #@species Y(t) - #@parameters X - #@observables X ~ Y - #end + @test_throws Exception @eval @reaction_network begin + @species Y(t) + @parameters X + @observables X ~ Y + end # Species + compound. - @test_broken false #@test_throws Exception @eval @reaction_network begin - #@species X(t) O(t) - #@compounds begin X(t) ~ 2O end - #end + @test_throws Exception @eval @reaction_network begin + @species X(t) O(t) + @compounds begin X(t) ~ 2O end + end # Parameter + compound. - @test_broken false #@test_throws Exception @eval @reaction_network begin - #@species O(t) - #@parameters X - #@compounds begin X(t) ~ 2O end - #end + @test_throws Exception @eval @reaction_network begin + @species O(t) + @parameters X + @compounds begin X(t) ~ 2O end + end # Variable + compound. - @test_broken false #@test_throws Exception @eval @reaction_network begin - #@species O(t) - #@variables X(t) - #@compounds begin X(t) ~ 2O end - #end + @test_throws Exception @eval @reaction_network begin + @species O(t) + @variables X(t) + @compounds begin X(t) ~ 2O end + end end ### Test Independent Variable Designations ### @@ -616,11 +616,11 @@ let @equations D(V) ~ 1 - V d, D --> 0 end - @test_broken false # @test_throws Exception @eval @reaction_network begin - #@variables D(t) - #@equations D(V) ~ 1 - V - #d, X --> 0 - #end + @test_throws Exception @eval @reaction_network begin + @variables D(t) + @equations D(V) ~ 1 - V + d, X --> 0 + end @test_throws Exception @eval @reaction_network begin @parameters D @equations D(V) ~ 1 - V @@ -682,7 +682,7 @@ let @test plot(sol; idxs=:X).series_list[1].plotattributes[:y][end] ≈ 10.0 @test plot(sol; idxs=[X, Y]).series_list[2].plotattributes[:y][end] ≈ 3.0 @test plot(sol; idxs=[rn.X, rn.Y]).series_list[2].plotattributes[:y][end] ≈ 3.0 - @test plot(sol; idxs=[:X, :Y]).series_list[2].plotattributes[:y][end] ≈ 3.0 # (https://github.com/SciML/ModelingToolkit.jl/issues/2778) + @test plot(sol; idxs=[:X, :Y]).series_list[2].plotattributes[:y][end] ≈ 3.0 end # Compares programmatic and DSL system with observables. @@ -1197,16 +1197,16 @@ end # Test whether user-defined functions are properly expanded in equations. let f(A, t) = 2*A*t + g(A) = 2*A + 2 - # Test user-defined function + # Test user-defined function (on both lhs and rhs). rn = @reaction_network begin - @equations D(A) ~ f(A, t) + @equations D(A) + g(A) ~ f(A, t) end @test length(equations(rn)) == 1 @test equations(rn)[1] isa Equation @species A(t) - @test isequal(equations(rn)[1], D(A) ~ 2*A*t) - + @test isequal(equations(rn)[1], D(A) + 2*A + 2 ~ 2*A*t) # Test whether expansion happens properly for unregistered/registered functions. hill_unregistered(A, v, K, n) = v*(A^n) / (A^n + K^n) @@ -1231,7 +1231,6 @@ let @parameters v K n @test isequal(equations(rn2r)[1], D(A) ~ hill2(A, v, K, n)) - rn3 = @reaction_network begin @species Iapp(t) @equations begin @@ -1253,7 +1252,6 @@ let rn3_sym = complete(rn3_sym) @test isequivalent(rn3, rn3_sym) - # Test more complicated expression involving both registered function and a user-defined function. g(A, K, n) = A^n + K^n rn4 = @reaction_network begin From 9230667f3fc6f690ccbea36d282bb218d4b5879f Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 18 Jan 2025 19:00:16 +0000 Subject: [PATCH 25/38] up --- src/dsl.jl | 2 +- test/dsl/dsl_advanced_model_construction.jl | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index c01cf6497c..b2327d4c71 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -577,7 +577,7 @@ end function get_rxexpr(rx::DSLReaction) # Initiates the `Reaction` expression. rate = recursive_escape_functions!(rx.rate) - rx_constructor = :(Reaction($rate, [], [], [], []; metadata = $(rx.metadata))) + rx_constructor = :(Reaction($rate, nothing, nothing; metadata = $(rx.metadata))) # Loops through all products and substrates, and adds them (and their stoichiometries) # to the `Reaction` expression. diff --git a/test/dsl/dsl_advanced_model_construction.jl b/test/dsl/dsl_advanced_model_construction.jl index 8f24aec2bb..6dd389f637 100644 --- a/test/dsl/dsl_advanced_model_construction.jl +++ b/test/dsl/dsl_advanced_model_construction.jl @@ -75,7 +75,6 @@ end ### Test Interpolation Within the DSL ### - # Declares parameters and species used across the test. @parameters α k k1 k2 @species A(t) B(t) C(t) D(t) From 78ca087191a24c673f2ef70a17475c3ea2ec4ab5 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 18 Jan 2025 19:46:09 +0000 Subject: [PATCH 26/38] multiple fixes --- src/dsl.jl | 49 ++++++++++++------------ src/expression_utils.jl | 12 ++---- test/dsl/dsl_basic_model_construction.jl | 22 +++++------ test/dsl/dsl_options.jl | 26 +++++-------- 4 files changed, 47 insertions(+), 62 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index b2327d4c71..5e4c540e4a 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -84,8 +84,9 @@ This model also contains production and degradation reactions, where `0` denotes either no substrates or no products in a reaction. Options: -In addition to reactions, the macro also supports "option" inputs. Each option is designated -by a tag starting with a `@` followed by its input. A list of options can be found [here](https://docs.sciml.ai/Catalyst/stable/api/#api_dsl_options). +In addition to reactions, the macro also supports "option" inputs (permitting e.g. the addition +of observables). Each option is designated by a tag starting with a `@` followed by its input. +A list of options can be found [here](https://docs.sciml.ai/Catalyst/stable/api/#api_dsl_options). """ macro reaction_network(name::Symbol, network_expr::Expr) make_rs_expr(QuoteNode(name), network_expr) @@ -198,8 +199,8 @@ function recursive_find_reactants!(ex::ExprValues, mult::ExprValues, # If the expression corresponds to a reactant on our list, increase its multiplicity. idx = findfirst(r.reactant == ex for r in reactants) if !isnothing(idx) - new_mult = processmult(+, mult, reactants[idx].stoichiometry) - reactants[idx] = DSLReactant(ex, new_mult) + newmult = processmult(+, mult, reactants[idx].stoichiometry) + reactants[idx] = DSLReactant(ex, newmult) # If the expression corresponds to a new reactant, add it to the list. else @@ -210,12 +211,12 @@ function recursive_find_reactants!(ex::ExprValues, mult::ExprValues, elseif ex.args[1] == :* # The normal case (e.g. 3*X or 3*(X+Y)). Update the current multiplicity and continue. if length(ex.args) == 3 - new_mult = processmult(*, mult, ex.args[2]) - recursive_find_reactants!(ex.args[3], new_mult, reactants) + newmult = processmult(*, mult, ex.args[2]) + recursive_find_reactants!(ex.args[3], newmult, reactants) # More complicated cases (e.g. 2*3*X). Yes, `ex.args[1:(end - 1)]` should start at 1 (not 2). else - new_mult = processmult(*, mult, Expr(:call, ex.args[1:(end - 1)]...)) - recursive_find_reactants!(ex.args[end], new_mult, reactants) + newmult = processmult(*, mult, Expr(:call, ex.args[1:(end - 1)]...)) + recursive_find_reactants!(ex.args[end], newmult, reactants) end # If we have encountered a sum of different reactants, apply recursion on each. elseif ex.args[1] == :+ @@ -243,11 +244,10 @@ end function extract_metadata(metadata_line::Expr) metadata = :([]) for arg in metadata_line.args - if arg.head != :(=) + (arg.head != :(=)) && error("Malformatted metadata line: $metadata_line. Each entry in the vector should contain a `=`.") - elseif !(arg.args[1] isa Symbol) + (arg.args[1] isa Symbol) || error("Malformatted metadata entry: $arg. Entries left-hand-side should be a single symbol.") - end push!(metadata.args, :($(QuoteNode(arg.args[1])) => $(arg.args[2]))) end return metadata @@ -422,7 +422,7 @@ function push_reactions!(reactions::Vector{DSLReaction}, subs::ExprValues, lengs = (tup_leng(subs), tup_leng(prods), tup_leng(rate), tup_leng(metadata)) maxl = maximum(lengs) if any(!(leng == 1 || leng == maxl) for leng in lengs) - throw("Malformed reaction, rate=$rate, subs=$subs, prods=$prods, metadata=$metadata.") + error("Malformed reaction, rate: $rate, subs: $subs, prods: $prods, metadata: $metadata.") end # Loops through each reaction encoded by the reaction's different components. @@ -452,12 +452,12 @@ end function extract_syms(opts, vartype::Symbol) # If the corresponding option have been used, uses `Symbolics._parse_vars` to find all # variable within it (returning them in a vector). - if haskey(opts, vartype) + return if haskey(opts, vartype) ex = opts[vartype] vars = Symbolics._parse_vars(vartype, Real, ex.args[3:end]) - return Vector{Union{Symbol, Expr}}(vars.args[end].args) + Vector{Union{Symbol, Expr}}(vars.args[end].args) else - return Union{Symbol, Expr}[] + Union{Symbol, Expr}[] end end @@ -593,6 +593,14 @@ function get_rxexpr(rx::DSLReaction) return rx_constructor end +# Recursively escape functions within equations of an equation written using user-defined functions. +# Does not expand special function calls like "hill(...)" and differential operators. +function escape_equation!(eqexpr::Expr, diffsyms) + eqexpr.args[2] = recursive_escape_functions!(eqexpr.args[2], diffsyms) + eqexpr.args[3] = recursive_escape_functions!(eqexpr.args[3], diffsyms) + eqexpr +end + ### DSL Option Handling ### # Finds the time independent variable, and any potential spatial independent variables. @@ -856,7 +864,7 @@ end ### `@reaction` Macro & its Internals ### -@doc raw""" +""" @reaction Macro for generating a single [`Reaction`](@ref) object using a similar syntax as the `@reaction_network` @@ -962,15 +970,6 @@ function recursive_escape_functions!(expr::ExprValues, diffsyms = []) expr end -# Returns the length of a expression tuple, or 1 if it is not an expression tuple (probably -# a Symbol/Numerical). This is used to handle bundled reaction (like `d, (X,Y) --> 0`). -# Recursively escape functions in the right-hand-side of an equation written using user-defined functions. Special function calls like "hill(...)" are not expanded. -function escape_equation!(eqexpr::Expr, diffsyms) - eqexpr.args[2] = recursive_escape_functions!(eqexpr.args[2], diffsyms) - eqexpr.args[3] = recursive_escape_functions!(eqexpr.args[3], diffsyms) - eqexpr -end - # Returns the length of a expression tuple, or 1 if it is not an expression tuple (probably a Symbol/Numerical). function tup_leng(ex::ExprValues) (typeof(ex) == Expr && ex.head == :tuple) && (return length(ex.args)) diff --git a/src/expression_utils.jl b/src/expression_utils.jl index d46042d3d5..28b1e1a71f 100644 --- a/src/expression_utils.jl +++ b/src/expression_utils.jl @@ -26,15 +26,9 @@ end # Throws an error when a forbidden symbol is used. function forbidden_symbol_check(sym) - used_forbidden_syms = intersect(forbidden_symbols_error, sym) - isempty(used_forbidden_syms) && return - error("The following symbol(s) are used as species or parameters: $used_forbidden_syms, this is not permitted.") -end - -# Checks that no symbol was sued for multiple purposes. -function unique_symbol_check(syms) - allunique(syms)|| - error("Reaction network independent variables, parameters, species, and variables must all have distinct names, but a duplicate has been detected. ") + used_forbidden_syms = + isempty(used_forbidden_syms) || + error("The following symbol(s) are used as species or parameters: $used_forbidden_syms, this is not permitted.") end ### Catalyst-specific Expressions Manipulation ### diff --git a/test/dsl/dsl_basic_model_construction.jl b/test/dsl/dsl_basic_model_construction.jl index 912de9c617..75c6fc1a15 100644 --- a/test/dsl/dsl_basic_model_construction.jl +++ b/test/dsl/dsl_basic_model_construction.jl @@ -189,7 +189,7 @@ let u0 = rnd_u0(networks[1], rng; factor) p = rnd_ps(networks[1], rng; factor) t = rand(rng) - + @test f_eval(networks[1], u0, p, t) ≈ f_eval(networks[2], u0, p, t) @test jac_eval(networks[1], u0, p, t) ≈ jac_eval(networks[2], u0, p, t) @test g_eval(networks[1], u0, p, t) ≈ g_eval(networks[2], u0, p, t) @@ -207,7 +207,7 @@ let (l3, l4), Y2 ⟷ Y3 (l5, l6), Y3 ⟷ Y4 c, Y4 → ∅ - end + end # Checks that the networks' functions evaluates equally for various randomised inputs. @unpack X1, X2, X3, X4, p, d, k1, k2, k3, k4, k5, k6 = network @@ -215,10 +215,10 @@ let u0_1 = Dict(rnd_u0(network, rng; factor)) p_1 = Dict(rnd_ps(network, rng; factor)) u0_2 = [:Y1 => u0_1[X1], :Y2 => u0_1[X2], :Y3 => u0_1[X3], :Y4 => u0_1[X4]] - p_2 = [:q => p_1[p], :c => p_1[d], :l1 => p_1[k1], :l2 => p_1[k2], :l3 => p_1[k3], + p_2 = [:q => p_1[p], :c => p_1[d], :l1 => p_1[k1], :l2 => p_1[k2], :l3 => p_1[k3], :l4 => p_1[k4], :l5 => p_1[k5], :l6 => p_1[k6]] t = rand(rng) - + @test f_eval(network, u0_1, p_1, t) ≈ f_eval(differently_written_5, u0_2, p_2, t) @test jac_eval(network, u0_1, p_1, t) ≈ jac_eval(differently_written_5, u0_2, p_2, t) @test g_eval(network, u0_1, p_1, t) ≈ g_eval(differently_written_5, u0_2, p_2, t) @@ -271,7 +271,7 @@ let u0 = rnd_u0(networks[1], rng; factor) p = rnd_ps(networks[1], rng; factor) t = rand(rng) - + @test f_eval(networks[1], u0, p, t) ≈ f_eval(networks[2], u0, p, t) @test jac_eval(networks[1], u0, p, t) ≈ jac_eval(networks[2], u0, p, t) @test g_eval(networks[1], u0, p, t) ≈ g_eval(networks[2], u0, p, t) @@ -293,7 +293,7 @@ let (sqrt(3.7), exp(1.9)), X4 ⟷ X1 + X2 end push!(identical_networks_3, reaction_networks_standard[9] => no_parameters_9) - push!(parameter_sets, [:p1 => 1.5, :p2 => 1, :p3 => 2, :d1 => 0.01, :d2 => 2.3, :d3 => 1001, + 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)]) no_parameters_10 = @reaction_network begin @@ -305,14 +305,14 @@ let 1.0, X5 ⟶ ∅ end push!(identical_networks_3, reaction_networks_standard[10] => no_parameters_10) - push!(parameter_sets, [:p => 0.01, :k1 => 3.1, :k2 => 3.2, :k3 => 0.0, :k4 => 2.1, :k5 => 901.0, + push!(parameter_sets, [:p => 0.01, :k1 => 3.1, :k2 => 3.2, :k3 => 0.0, :k4 => 2.1, :k5 => 901.0, :k6 => 63.5, :k7 => 7, :k8 => 8, :d => 1.0]) for (networks, p_1) in zip(identical_networks_3, parameter_sets) for factor in [1e-2, 1e-1, 1e0, 1e1, 1e2, 1e3] u0 = rnd_u0(networks[1], rng; factor) t = rand(rng) - + @test f_eval(networks[1], u0, p_1, t) ≈ f_eval(networks[2], u0, [], t) @test jac_eval(networks[1], u0, p_1, t) ≈ jac_eval(networks[2], u0, [], t) @test g_eval(networks[1], u0, p_1, t) ≈ g_eval(networks[2], u0, [], t) @@ -383,7 +383,7 @@ let τ = rand(rng) u = rnd_u0(reaction_networks_conserved[1], rng; factor) p_2 = rnd_ps(time_network, rng; factor) - p_1 = [p_2; reaction_networks_conserved[1].k1 => τ; + p_1 = [p_2; reaction_networks_conserved[1].k1 => τ; reaction_networks_conserved[1].k4 => τ; reaction_networks_conserved[1].k5 => τ] @test f_eval(reaction_networks_conserved[1], u, p_1, τ) ≈ f_eval(time_network, u, p_2, τ) @@ -463,7 +463,7 @@ let @test rn1 == rn2 end -# Tests arrow variants in `@reaction`` macro. +# 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)) @@ -533,4 +533,4 @@ let @test_throws Exception @eval @reaction_network begin k, X^Y --> XY end -end \ No newline at end of file +end diff --git a/test/dsl/dsl_options.jl b/test/dsl/dsl_options.jl index 4ec1f35a59..ce95ffc428 100644 --- a/test/dsl/dsl_options.jl +++ b/test/dsl/dsl_options.jl @@ -417,15 +417,8 @@ let @test issetequal(species(rn), spcs) end -# Tests errors in `@variables` declarations. +# Tests error when disallowed name is used for variable. let - # Variable used as species in reaction. - @test_throws Exception @eval rn = @reaction_network begin - @variables K(t) - k, K + A --> B - end - - # Tests error when disallowed name is used for variable. @test_throws Exception @eval @reaction_network begin @variables π(t) end @@ -433,7 +426,6 @@ end # Tests that explicitly declaring a single symbol as several things does not work. # Several of these are broken, but note sure how to test broken-ness on `@test_throws false Exception @eval`. -# Relevant issue: https://github.com/SciML/Catalyst.jl/issues/1173 let # Species + parameter. @test_throws Exception @eval @reaction_network begin @@ -1150,14 +1142,6 @@ let end end -# Erroneous `@default_noise_scaling` declaration (other noise scaling tests are mostly in the SDE file). -let - # Default noise scaling with multiple entries. - @test_throws Exception @eval @reaction_network begin - @default_noise_scaling η1 η2 - end -end - ### Other DSL Option Tests ### # test combinatoric_ratelaws DSL option @@ -1264,6 +1248,14 @@ let @test isequal(Catalyst.expand_registered_functions(equations(rn4)[1]), D(A) ~ v*(A^n)) end +# Erroneous `@default_noise_scaling` declaration (other noise scaling tests are mostly in the SDE file). +let + # Default noise scaling with multiple entries. + @test_throws Exception @eval @reaction_network begin + @default_noise_scaling η1 η2 + end +end + ### test that @no_infer properly throws errors when undeclared variables are written ### import Catalyst: UndeclaredSymbolicError From 9d62c28dcf9c1dc4126f7febcf7abbe3ab04e4e2 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 18 Jan 2025 20:03:12 +0000 Subject: [PATCH 27/38] docstring fix --- src/dsl.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index 5e4c540e4a..d6934a98e1 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -688,7 +688,7 @@ function read_events_option(options, event_type::Symbol) return events_expr end -# Reads the variables options. Outputs a list of teh variables inferred from the equations, +# Reads the variables options. Outputs a list of the variables inferred from the equations, # as well as the equation vector. If the default differential was used, updates the `diffsexpr` # expression so that this declares this as well. function read_equations_options!(diffsexpr, options, syms_unavailable, tiv; requiredec = false) @@ -876,7 +876,7 @@ The `@reaction` macro is followed by a single line consisting of three parts: - Any number of substrates (which are consumed by the reaction). - Any number of products (which are produced by the reaction). -The output is a reaction (just like created using teh `Reaction` constructor). +The output is a reaction (just like created using the `Reaction` constructor). Examples: Here we create a simple binding reaction and stores it in the variable rx: @@ -907,7 +907,7 @@ t = default_t() @parameters k b @species A(t) ex = k*A^2 + t -rx = @reaction b*$ex*$A, $A --> C +rx = @reaction b*\$ex*\$A, \$A --> C ``` Notes: From 9a928fe05649fd42d585c747d5e4f812194042bb Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 18 Jan 2025 20:10:04 +0000 Subject: [PATCH 28/38] fixes --- src/dsl.jl | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index d6934a98e1..43638ca395 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -577,7 +577,12 @@ end function get_rxexpr(rx::DSLReaction) # Initiates the `Reaction` expression. rate = recursive_escape_functions!(rx.rate) - rx_constructor = :(Reaction($rate, nothing, nothing; metadata = $(rx.metadata))) + subs_init = isempty(rx.substrates) ? nothing : :([]) + subs_stoich_init = deepcopy(subs_init) + prod_init = isempty(rx.products) ? nothing : :([]) + prod_stoich_init = deepcopy(prod_init) + rx_constructor = :(Reaction($rate, $subs_init, $subs_stoich_init, $prod_init, + $prod_stoich_init; metadata = $(rx.metadata))) # Loops through all products and substrates, and adds them (and their stoichiometries) # to the `Reaction` expression. @@ -589,7 +594,6 @@ function get_rxexpr(rx::DSLReaction) push!(rx_constructor.args[5].args, prod.reactant) push!(rx_constructor.args[7].args, prod.stoichiometry) end - return rx_constructor end @@ -731,7 +735,7 @@ function read_equations_options!(diffsexpr, options, syms_unavailable, tiv; requ push!(diffsexpr.args, :(D = Differential($(tiv)))) end - return vs_inferred, diffs_inferred, equations + return collect(vs_inferred), diffs_inferred, equations end # Searches an expression `expr` and returns true if it have any subexpression `D(...)` (where `...` can be anything). From 53a6254e2858019a7b07b2045fb1388e9715f248 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 18 Jan 2025 20:36:33 +0000 Subject: [PATCH 29/38] up --- src/dsl.jl | 32 ++++++++++++++++---------------- src/expression_utils.jl | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index 43638ca395..db759adf1c 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -53,14 +53,14 @@ end Macro for generating chemical reaction network models (Catalyst `ReactionSystem`s). See the following two section ([DSL introduction](https://docs.sciml.ai/Catalyst/stable/model_creation/dsl_basics/) and [advantage usage](https://docs.sciml.ai/Catalyst/stable/model_creation/dsl_advanced/)) of -the Catalyst documentation for more details on the domain specific language (DSL) that the -macro implements. The macros output (a `ReactionSystem` structure) are central to Catalyst -and its functionality. How to e.g. simulate them is described in the [Catalyst documentation](https://docs.sciml.ai/Catalyst/stable/). +the Catalyst documentation for more details on the domain-specific language (DSL) that the +macro implements. The macro's output (a `ReactionSystem` structure) is central to Catalyst +and its functionality. How to e.g. simulate these is described in the [Catalyst documentation](https://docs.sciml.ai/Catalyst/stable/). Returns: - A Catalyst `ReactionSystem`, i.e. a symbolic model for the reaction network. The returned system is marked `complete`. To obtain a `ReactionSystem` that is not marked complete, for -example to then use in compositional modeling, see the otherwise equivalent `@network_component` +example to then use in compositional modelling, see the otherwise equivalent `@network_component` macro. Examples: @@ -253,7 +253,7 @@ function extract_metadata(metadata_line::Expr) return metadata end -### Specialised Error for @require_declaration Option ### +### Specialised @require_declaration Option Error ### struct UndeclaredSymbolicError <: Exception msg::String end @@ -282,13 +282,13 @@ function make_reaction_system(ex::Expr, name) any(!in(option_keys), keys(options)) && error("The following unsupported options were used: $(filter(opt_in->!in(opt_in,option_keys), keys(options)))") - # Read options that explicitly declares some symbol. Compiles a list of all declared symbols - # and checks that there has been no double-declarations. - cmpexpr_init, cmps_declared = read_compound_options(options) + # Read options that explicitly declares some symbol (e.g. `@species`). Compiles a list of + # all declared symbols and checks that there has been no double-declarations. sps_declared = extract_syms(options, :species) ps_declared = extract_syms(options, :parameters) vs_declared = extract_syms(options, :variables) tiv, sivs, ivs, ivsexpr = read_ivs_option(options) + cmpexpr_init, cmps_declared = read_compound_options(options) diffsexpr, diffs_declared = read_differentials_option(options) syms_declared = collect(Iterators.flatten((cmps_declared, sps_declared, ps_declared, vs_declared, ivs, diffs_declared))) @@ -340,7 +340,7 @@ function make_reaction_system(ex::Expr, name) name = $name spatial_ivs = $sivs rx_eq_vec = $rxsexprs - vars = setdiff(union($spsvar, $vsvar, $cmpsvar), $obs_syms) + us = setdiff(union($spsvar, $vsvar, $cmpsvar), $obs_syms) observed = $obs_eqs continuous_events = $continuous_events_expr discrete_events = $discrete_events_expr @@ -348,7 +348,7 @@ function make_reaction_system(ex::Expr, name) default_reaction_metadata = $default_reaction_metadata remake_ReactionSystem_internal( - make_ReactionSystem_internal(rx_eq_vec, $tiv, vars, $psvar; name, spatial_ivs, + make_ReactionSystem_internal(rx_eq_vec, $tiv, us, $psvar; name, spatial_ivs, observed, continuous_events, discrete_events, combinatoric_ratelaws); default_reaction_metadata) end)) @@ -581,7 +581,7 @@ function get_rxexpr(rx::DSLReaction) subs_stoich_init = deepcopy(subs_init) prod_init = isempty(rx.products) ? nothing : :([]) prod_stoich_init = deepcopy(prod_init) - rx_constructor = :(Reaction($rate, $subs_init, $subs_stoich_init, $prod_init, + rx_constructor = :(Reaction($rate, $subs_init, $prod_init, $subs_stoich_init, $prod_stoich_init; metadata = $(rx.metadata))) # Loops through all products and substrates, and adds them (and their stoichiometries) @@ -876,22 +876,22 @@ macro (but permitting only a single reaction). A more detailed introduction to t be found in the description of `@reaction_network`. The `@reaction` macro is followed by a single line consisting of three parts: -- A rate (at which the reaction occur). +- A rate (at which the reaction occurs). - Any number of substrates (which are consumed by the reaction). - Any number of products (which are produced by the reaction). The output is a reaction (just like created using the `Reaction` constructor). Examples: -Here we create a simple binding reaction and stores it in the variable rx: +Here we create a simple binding reaction and store it in the variable rx: ```julia rx = @reaction k, X + Y --> XY ``` The macro will automatically deduce `X`, `Y`, and `XY` to be species (as these occur as reactants) -and `k` as a parameters (as it does not occur as a reactant). +and `k` as a parameter (as it does not occur as a reactant). The `@reaction` macro provides a more concise notation to the `Reaction` constructor. I.e. here -we create the same reaction using both approaches, and also confirms that they are identical. +we create the same reaction using both approaches, and also confirm that they are identical. ```julia # Creates a reaction using the `@reaction` macro. rx = @reaction k*v, A + B --> C + D @@ -917,7 +917,7 @@ rx = @reaction b*\$ex*\$A, \$A --> C Notes: - `@reaction` does not support bi-directional type reactions (using `<-->`) or reaction bundling (e.g. `d, (X,Y) --> 0`). -- Interpolation of Julia variables into the macro works similar to the `@reaction_network` +- Interpolation of Julia variables into the macro works similarly to the `@reaction_network` macro. See [The Reaction DSL](@ref dsl_description) tutorial for more details. """ macro reaction(ex) diff --git a/src/expression_utils.jl b/src/expression_utils.jl index 28b1e1a71f..312a678c86 100644 --- a/src/expression_utils.jl +++ b/src/expression_utils.jl @@ -25,8 +25,8 @@ end ### Parameters/Species/Variables Symbols Correctness Checking ### # Throws an error when a forbidden symbol is used. -function forbidden_symbol_check(sym) - used_forbidden_syms = +function forbidden_symbol_check(syms) + used_forbidden_syms = intersect(forbidden_symbols_error, syms) isempty(used_forbidden_syms) || error("The following symbol(s) are used as species or parameters: $used_forbidden_syms, this is not permitted.") end From 27f7add7ea7a8e44a0c2331d1503c25258eaf24a Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 18 Jan 2025 20:57:45 +0000 Subject: [PATCH 30/38] fix function expansion of differentials --- src/dsl.jl | 28 +++++++++---------- src/expression_utils.jl | 9 ------ .../spatial_reactions.jl | 6 ++-- test/runtests.jl | 13 --------- 4 files changed, 18 insertions(+), 38 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index db759adf1c..364450337e 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -305,10 +305,11 @@ function make_reaction_system(ex::Expr, name) union(syms_declared, sps_inferred), tiv; requiredec) ps_inferred = setdiff(ps_pre_inferred, vs_inferred, diffs_inferred) syms_inferred = union(sps_inferred, ps_inferred, vs_inferred, diffs_inferred) + all_syms = union(syms_declared, syms_inferred) # Read options not related to the declaration or inference of symbols. obsexpr, obs_eqs, obs_syms = read_observed_options(options, ivs, - union(sps_declared, vs_declared), union(syms_declared, syms_inferred); requiredec) + union(sps_declared, vs_declared), all_syms; requiredec) continuous_events_expr = read_events_option(options, :continuous_events) discrete_events_expr = read_events_option(options, :discrete_events) default_reaction_metadata = read_default_noise_scaling_option(options) @@ -322,7 +323,7 @@ function make_reaction_system(ex::Expr, name) spsexpr, spsvar = scalarize_macro(spsexpr_init, "specs") vsexpr, vsvar = scalarize_macro(vsexpr_init, "vars") cmpsexpr, cmpsvar = scalarize_macro(cmpexpr_init, "comps") - rxsexprs = get_rxexprs(reactions, equations, union(diffs_declared, diffs_inferred)) + rxsexprs = get_rxexprs(reactions, equations, all_syms) # Assemblies the full expression that declares all required symbolic variables, and # then the output `ReactionSystem`. @@ -565,10 +566,10 @@ end # From the system reactions (as `DSLReaction`s) and equations (as expressions), # creates the expressions that evaluates to the reaction (+ equations) vector. -function get_rxexprs(reactions, equations, diffsyms) +function get_rxexprs(reactions, equations, all_syms) rxsexprs = :(Catalyst.CatalystEqType[]) foreach(rx -> push!(rxsexprs.args, get_rxexpr(rx)), reactions) - foreach(eq -> push!(rxsexprs.args, escape_equation!(eq, diffsyms)), equations) + foreach(eq -> push!(rxsexprs.args, escape_equation!(eq, all_syms)), equations) return rxsexprs end @@ -598,10 +599,12 @@ function get_rxexpr(rx::DSLReaction) end # Recursively escape functions within equations of an equation written using user-defined functions. -# Does not expand special function calls like "hill(...)" and differential operators. -function escape_equation!(eqexpr::Expr, diffsyms) - eqexpr.args[2] = recursive_escape_functions!(eqexpr.args[2], diffsyms) - eqexpr.args[3] = recursive_escape_functions!(eqexpr.args[3], diffsyms) +# Does not escape special function calls like "hill(...)" and differential operators. Does +# also not escape stuff corresponding to e.g. species or parameters (required for good error +# for when e.g. a species is used as a differential, or for time delays in the future). +function escape_equation!(eqexpr::Expr, all_syms) + eqexpr.args[2] = recursive_escape_functions!(eqexpr.args[2], all_syms) + eqexpr.args[3] = recursive_escape_functions!(eqexpr.args[3], all_syms) eqexpr end @@ -934,9 +937,6 @@ function make_reaction(ex::Expr) reaction = get_reaction(ex) species, parameters = extract_sps_and_ps([reaction], []) - # Checks for input errors. - forbidden_symbol_check(union(species, parameters)) - # Creates expressions corresponding to code for declaring the parameters, species, and reaction. spexprs = get_usexpr(species, Dict{Symbol, Expr}()) pexprs = get_psexpr(parameters, Dict{Symbol, Expr}()) @@ -964,11 +964,11 @@ end # Recursively traverses an expression and escapes all the user-defined functions. # Special function calls like "hill(...)" are not expanded. -function recursive_escape_functions!(expr::ExprValues, diffsyms = []) +function recursive_escape_functions!(expr::ExprValues, syms_skip = []) (typeof(expr) != Expr) && (return expr) - foreach(i -> expr.args[i] = recursive_escape_functions!(expr.args[i], diffsyms), + foreach(i -> expr.args[i] = recursive_escape_functions!(expr.args[i], syms_skip), 1:length(expr.args)) - if (expr.head == :call) && !isdefined(Catalyst, expr.args[1]) && expr.args[1] ∉ diffsyms + if (expr.head == :call) && !isdefined(Catalyst, expr.args[1]) && expr.args[1] ∉ syms_skip expr.args[1] = esc(expr.args[1]) end expr diff --git a/src/expression_utils.jl b/src/expression_utils.jl index 312a678c86..f2a55ed52a 100644 --- a/src/expression_utils.jl +++ b/src/expression_utils.jl @@ -22,15 +22,6 @@ function is_escaped_expr(expr) return (expr isa Expr) && (expr.head == :escape) && (length(expr.args) == 1) end -### Parameters/Species/Variables Symbols Correctness Checking ### - -# Throws an error when a forbidden symbol is used. -function forbidden_symbol_check(syms) - used_forbidden_syms = intersect(forbidden_symbols_error, syms) - isempty(used_forbidden_syms) || - error("The following symbol(s) are used as species or parameters: $used_forbidden_syms, this is not permitted.") -end - ### Catalyst-specific Expressions Manipulation ### # Some options takes input on form that is either `@option ...` or `@option begin ... end`. diff --git a/src/spatial_reaction_systems/spatial_reactions.jl b/src/spatial_reaction_systems/spatial_reactions.jl index 3a71b297d8..a23ff56d25 100644 --- a/src/spatial_reaction_systems/spatial_reactions.jl +++ b/src/spatial_reaction_systems/spatial_reactions.jl @@ -55,11 +55,13 @@ function make_transport_reaction(rateex, species) find_parameters_in_rate!(parameters, rateex) # Checks for input errors. - forbidden_symbol_check(union([species], parameters)) + if isempty(intersect(forbidden_symbols_error, union([species], parameters))) + error("The following symbol(s) are used as species or parameters: $(intersect(forbidden_symbols_error, union([species], parameters)))), this is not permitted.") + end # Creates expressions corresponding to actual code from the internal DSL representation. sexprs = get_usexpr([species], Dict{Symbol, Expr}()) - pexprs = get_pexpr(parameters, Dict{Symbol, Expr}()) + pexprs = get_psexpr(parameters, Dict{Symbol, Expr}()) iv = :($(DEFAULT_IV_SYM) = default_t()) trxexpr = :(TransportReaction($rateex, $species)) diff --git a/test/runtests.jl b/test/runtests.jl index 29c2d0ca74..076cb7e944 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -15,19 +15,6 @@ end ### Run Tests ### @time begin if GROUP == "All" || GROUP == "Core" - # Tests the `ReactionSystem` structure and its properties. - @time @safetestset "Reaction Structure" begin include("reactionsystem_core/reaction.jl") end - @time @safetestset "ReactionSystem Structure" begin include("reactionsystem_core/reactionsystem.jl") end - @time @safetestset "Higher Order Reactions" begin include("reactionsystem_core/higher_order_reactions.jl") end - @time @safetestset "Symbolic Stoichiometry" begin include("reactionsystem_core/symbolic_stoichiometry.jl") end - @time @safetestset "Parameter Type Designation" begin include("reactionsystem_core/parameter_type_designation.jl") end - @time @safetestset "Custom CRN Functions" begin include("reactionsystem_core/custom_crn_functions.jl") end - @time @safetestset "Coupled CRN/Equation Systems" begin include("reactionsystem_core/coupled_equation_crn_systems.jl") end - @time @safetestset "Events" begin include("reactionsystem_core/events.jl") end - - # Tests model creation via the @reaction_network DSL. - @time @safetestset "DSL Basic Model Construction" begin include("dsl/dsl_basic_model_construction.jl") end - @time @safetestset "DSL Advanced Model Construction" begin include("dsl/dsl_advanced_model_construction.jl") end @time @safetestset "DSL Options" begin include("dsl/dsl_options.jl") end # Tests compositional and hierarchical modelling. From 99e54d2509faa5738648cb2cb284a9115282b9af Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 18 Jan 2025 21:06:39 +0000 Subject: [PATCH 31/38] spelling fix --- src/dsl.jl | 2 +- test/runtests.jl | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/dsl.jl b/src/dsl.jl index 364450337e..9b7a76ba41 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -933,7 +933,7 @@ function make_reaction(ex::Expr) # Handle interpolation of variables in the input. ex = esc_dollars!(ex) - # Parses reactions. Extracts species and paraemters within it. + # Parses reactions. Extracts species and parameters within it. reaction = get_reaction(ex) species, parameters = extract_sps_and_ps([reaction], []) diff --git a/test/runtests.jl b/test/runtests.jl index 076cb7e944..29c2d0ca74 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -15,6 +15,19 @@ end ### Run Tests ### @time begin if GROUP == "All" || GROUP == "Core" + # Tests the `ReactionSystem` structure and its properties. + @time @safetestset "Reaction Structure" begin include("reactionsystem_core/reaction.jl") end + @time @safetestset "ReactionSystem Structure" begin include("reactionsystem_core/reactionsystem.jl") end + @time @safetestset "Higher Order Reactions" begin include("reactionsystem_core/higher_order_reactions.jl") end + @time @safetestset "Symbolic Stoichiometry" begin include("reactionsystem_core/symbolic_stoichiometry.jl") end + @time @safetestset "Parameter Type Designation" begin include("reactionsystem_core/parameter_type_designation.jl") end + @time @safetestset "Custom CRN Functions" begin include("reactionsystem_core/custom_crn_functions.jl") end + @time @safetestset "Coupled CRN/Equation Systems" begin include("reactionsystem_core/coupled_equation_crn_systems.jl") end + @time @safetestset "Events" begin include("reactionsystem_core/events.jl") end + + # Tests model creation via the @reaction_network DSL. + @time @safetestset "DSL Basic Model Construction" begin include("dsl/dsl_basic_model_construction.jl") end + @time @safetestset "DSL Advanced Model Construction" begin include("dsl/dsl_advanced_model_construction.jl") end @time @safetestset "DSL Options" begin include("dsl/dsl_options.jl") end # Tests compositional and hierarchical modelling. From 7ec73837061ecf76d99111b738d4b6e69b8be223 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sun, 19 Jan 2025 16:17:05 +0000 Subject: [PATCH 32/38] better handling of forbidden symbols and more tests --- src/dsl.jl | 13 ++++++++----- src/expression_utils.jl | 10 ++++++++++ src/spatial_reaction_systems/spatial_reactions.jl | 5 ++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index 9b7a76ba41..6de0981594 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -472,7 +472,7 @@ function extract_sps_and_ps(reactions, excluded_syms; requiredec = false) add_syms_from_expr!(species, reactant.reactant, excluded_syms) end (!isempty(species) && requiredec) && - throw(UndeclaredSymbolicError("Unrecognized variables $(join(species, ", ")) detected in reaction expression: \"$(string(reaction.rxexpr))\". Since the flag @require_declaration is declared, all species must be explicitly declared with the @species macro.")) + throw(UndeclaredSymbolicError("Unrecognized reactant $(join(species, ", ")) detected in reaction expression: \"$(string(reaction.rxexpr))\". Since the flag @require_declaration is declared, all species must be explicitly declared with the @species option.")) end union!(excluded_syms, species) @@ -481,11 +481,11 @@ function extract_sps_and_ps(reactions, excluded_syms; requiredec = false) for reaction in reactions add_syms_from_expr!(parameters, reaction.rate, excluded_syms) (!isempty(parameters) && requiredec) && - throw(UndeclaredSymbolicError("Unrecognized parameter $(join(parameters, ", ")) detected in rate expression: $(reaction.rate) for the following reaction expression: \"$(string(reaction.rxexpr))\". Since the flag @require_declaration is declared, all parameters must be explicitly declared with the @parameters macro.")) + throw(UndeclaredSymbolicError("Unrecognized symbol $(join(parameters, ", ")) detected in rate expression: $(reaction.rate) for the following reaction expression: \"$(string(reaction.rxexpr))\". Since the flag @require_declaration is declared, all parameters must be explicitly declared with the @parameters option.")) for reactant in Iterators.flatten((reaction.substrates, reaction.products)) add_syms_from_expr!(parameters, reactant.stoichiometry, excluded_syms) (!isempty(parameters) && requiredec) && - throw(UndeclaredSymbolicError("Unrecognized parameters $(join(parameters, ", ")) detected in the stoichiometry for reactant $(reactant.reactant) in the following reaction expression: \"$(string(reaction.rxexpr))\". Since the flag @require_declaration is declared, all parameters must be explicitly declared with the @parameters macro.")) + throw(UndeclaredSymbolicError("Unrecognized symbol $(join(parameters, ", ")) detected in the stoichiometry for reactant $(reactant.reactant) in the following reaction expression: \"$(string(reaction.rxexpr))\". Since the flag @require_declaration is declared, all parameters must be explicitly declared with the @parameters option.")) end end @@ -728,7 +728,7 @@ function read_equations_options!(diffsexpr, options, syms_unavailable, tiv; requ # Any undeclared symbolic variables encountered should be extracted as variables. add_syms_from_expr!(vs_inferred, eq, syms_unavailable) (!isempty(vs_inferred) && requiredec) && throw(UndeclaredSymbolicError( - "Unrecognized symbolic variables $(join(vs_inferred, ", ")) detected in equation expression: \"$(string(eq))\". Since the flag @require_declaration is declared, all symbolic variables must be explicitly declared with the @species, @variables, and @parameters options.")) + "Unrecognized symbol $(join(vs_inferred, ", ")) detected in equation expression: \"$(string(eq))\". Since the flag @require_declaration is declared, all symbolic variables must be explicitly declared with the @species, @variables, and @parameters options.")) end # If `D` differential is used, add it to differential expression and inferred differentials list. @@ -804,7 +804,7 @@ function read_observed_options(options, all_ivs, us_declared, all_syms; required # Error checks. (requiredec && !in(obs_name, us_declared)) && - throw(UndeclaredSymbolicError("An undeclared variable ($obs_name) was declared as an observable in the following observable equation: \"$obs_eq\". Since the flag @require_declaration is set, all variables must be declared with the @species, @parameters, or @variables macros.")) + throw(UndeclaredSymbolicError("An undeclared symbol ($obs_name) was used as an observable in the following observable equation: \"$obs_eq\". Since the flag @require_declaration is set, all observables must be declared with either the @species or @variables options.")) isempty(ivs) || error("An observable ($obs_name) was given independent variable(s). These should not be given, as they are inferred automatically.") isnothing(defaults) || @@ -937,6 +937,9 @@ function make_reaction(ex::Expr) reaction = get_reaction(ex) species, parameters = extract_sps_and_ps([reaction], []) + # Checks for input errors. Needed here but not in `@reaction_network` as `ReactionSystem` perform this check but `Reaction` don't. + forbidden_symbol_check(union(species, parameters)) + # Creates expressions corresponding to code for declaring the parameters, species, and reaction. spexprs = get_usexpr(species, Dict{Symbol, Expr}()) pexprs = get_psexpr(parameters, Dict{Symbol, Expr}()) diff --git a/src/expression_utils.jl b/src/expression_utils.jl index f2a55ed52a..0036036f7d 100644 --- a/src/expression_utils.jl +++ b/src/expression_utils.jl @@ -22,6 +22,16 @@ function is_escaped_expr(expr) return (expr isa Expr) && (expr.head == :escape) && (length(expr.args) == 1) end +### Parameters/Species/Variables Symbols Correctness Checking ### + +# Throws an error when a forbidden symbol is used. +function forbidden_symbol_check(syms) + used_forbidden_syms = intersect(forbidden_symbols_error, syms) + isempty(used_forbidden_syms) || + error("The following symbol(s) are used as species or parameters: $used_forbidden_syms, this is not permitted.") +end + + ### Catalyst-specific Expressions Manipulation ### # Some options takes input on form that is either `@option ...` or `@option begin ... end`. diff --git a/src/spatial_reaction_systems/spatial_reactions.jl b/src/spatial_reaction_systems/spatial_reactions.jl index a23ff56d25..421435cf98 100644 --- a/src/spatial_reaction_systems/spatial_reactions.jl +++ b/src/spatial_reaction_systems/spatial_reactions.jl @@ -55,9 +55,8 @@ function make_transport_reaction(rateex, species) find_parameters_in_rate!(parameters, rateex) # Checks for input errors. - if isempty(intersect(forbidden_symbols_error, union([species], parameters))) - error("The following symbol(s) are used as species or parameters: $(intersect(forbidden_symbols_error, union([species], parameters)))), this is not permitted.") - end + forbidden_symbol_check(union([species], parameters)) + # Creates expressions corresponding to actual code from the internal DSL representation. sexprs = get_usexpr([species], Dict{Symbol, Expr}()) From 5f35480f247ef3807a21a9c3f49bbb7e2fd15218 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sun, 19 Jan 2025 19:13:21 +0000 Subject: [PATCH 33/38] improve option error handling, more tests --- src/dsl.jl | 20 ++-- src/expression_utils.jl | 10 ++ test/dsl/dsl_basic_model_construction.jl | 40 ++++++-- test/dsl/dsl_options.jl | 112 +++++++++++++++++++++-- 4 files changed, 161 insertions(+), 21 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index 6de0981594..ab6dcacf07 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -637,9 +637,8 @@ end # the `default_noise_scaling` reaction metadata, otherwise, returns an empty vector. function read_default_noise_scaling_option(options) if haskey(options, :default_noise_scaling) - if (length(options[:default_noise_scaling].args) != 3) - error("@default_noise_scaling should only have a single expression as its input, this appears not to be the case: \"$(options[:default_noise_scaling])\"") - end + (length(options[:default_noise_scaling].args) != 3) && + error("@default_noise_scaling should only have a single expression as its input, this appears not to be the case: \"$(options[:default_noise_scaling])\"") return :([:noise_scaling => $(options[:default_noise_scaling].args[3])]) end return :([]) @@ -649,11 +648,14 @@ end # of the compound species, and also the expression that crates them. function read_compound_options(options) # If the compound option is used, retrieve a list of compound species and the option line - # that creates them (used to declare them as compounds at the end). + # that creates them (used to declare them as compounds at the end). Due to some expression + # handling, in the case of a single compound we must change to the `@compound` macro. if haskey(options, :compounds) cmpexpr_init = options[:compounds] + cmpexpr_init.args[3] = option_block_form(get_block_option(cmpexpr_init)) cmps_declared = [find_varinfo_in_declaration(arg.args[2])[1] for arg in cmpexpr_init.args[3].args] + (length(cmps_declared) == 1) && (cmpexpr_init.args[1] = Symbol("@compound")) else # If option is not used, return empty vectors and expressions. cmpexpr_init = :() cmps_declared = Union{Symbol, Expr}[] @@ -667,7 +669,7 @@ function read_events_option(options, event_type::Symbol) if event_type ∉ [:continuous_events, :discrete_events] error("Trying to read an unsupported event type.") end - events_input = haskey(options, event_type) ? options[event_type].args[3] : + events_input = haskey(options, event_type) ? get_block_option(options[event_type]) : striplines(:(begin end)) events_input = option_block_form(events_input) @@ -701,7 +703,7 @@ end function read_equations_options!(diffsexpr, options, syms_unavailable, tiv; requiredec = false) # Prepares the equations. First, extracts equations from provided option (converting to block form if required). # Next, uses MTK's `parse_equations!` function to split input into a vector with the equations. - eqs_input = haskey(options, :equations) ? options[:equations].args[3] : :(begin end) + eqs_input = haskey(options, :equations) ? get_block_option(options[:equations]) : :(begin end) eqs_input = option_block_form(eqs_input) equations = Expr[] ModelingToolkit.parse_equations!(Expr(:block), equations, @@ -759,8 +761,8 @@ function read_differentials_option(options) # Creates the differential expression. # If differentials was provided as options, this is used as the initial expression. # If the default differential (D(...)) was used in equations, this is added to the expression. - diffsexpr = (haskey(options, :differentials) ? options[:differentials].args[3] : - striplines(:(begin end))) + diffsexpr = (haskey(options, :differentials) ? + get_block_option(options[:differentials]) : striplines(:(begin end))) diffsexpr = option_block_form(diffsexpr) # Goes through all differentials, checking that they are correctly formatted. Adds their @@ -794,7 +796,7 @@ function read_observed_options(options, all_ivs, us_declared, all_syms; required if haskey(options, :observables) # Gets list of observable equations and prepares variable declaration expression. # (`options[:observables]` includes `@observables`, `.args[3]` removes this part) - obs_eqs = make_obs_eqs(options[:observables].args[3]) + obs_eqs = make_obs_eqs(get_block_option(options[:observables])) obsexpr = Expr(:block, :(@variables)) obs_syms = :([]) diff --git a/src/expression_utils.jl b/src/expression_utils.jl index 0036036f7d..cd51be32de 100644 --- a/src/expression_utils.jl +++ b/src/expression_utils.jl @@ -34,6 +34,16 @@ end ### Catalyst-specific Expressions Manipulation ### +# Many option inputs can be on a form `@option input` or `@option begin ... end`. In both these +# cases we want to retrieve the third argument in the option expression. Further more, we wish +# to throw an error if there is more inputs (suggesting e.g. multiple inputs on a single line). +# Note that there are only some options for which we wish to make this check. +function get_block_option(expr) + (length(expr.args) > 3) && + error("An option input ($expr) is misformatted. Potentially, it has multiple inputs on a single lines, and these should be split across multiple lines using a `begin ... end` block.") + return expr.args[3] +end + # Some options takes input on form that is either `@option ...` or `@option begin ... end`. # This transforms input of the latter form to the former (with only one line in the `begin ... end` block) function option_block_form(expr) diff --git a/test/dsl/dsl_basic_model_construction.jl b/test/dsl/dsl_basic_model_construction.jl index 75c6fc1a15..a7ddb3dfad 100644 --- a/test/dsl/dsl_basic_model_construction.jl +++ b/test/dsl/dsl_basic_model_construction.jl @@ -471,8 +471,8 @@ let @test isequal((@reaction k, 0 --> X), (@reaction k, 0 ⥟ X)) end -# Test that symbols with special meanings, or that are forbidden, are handled properly. -let +# Test that symbols with special meanings 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 @@ -491,11 +491,39 @@ let @test length(species(test_network)) == 1 @test length(parameters(test_network)) == 0 @test reactions(test_network)[1].rate == ℯ +end - @test_throws LoadError @eval @reaction im, 0 --> B - @test_throws LoadError @eval @reaction nothing, 0 --> B - @test_throws LoadError @eval @reaction k, 0 --> im - @test_throws LoadError @eval @reaction k, 0 --> nothing +# Check that forbidden symbols correctly generates errors. +let + # @reaction macro, symbols that cannot be in the rate. + @test_throws Exception @eval @reaction im, 0 --> X + @test_throws Exception @eval @reaction nothing, 0 --> X + @test_throws Exception @eval @reaction Γ, 0 --> X + @test_throws Exception @eval @reaction ∅, 0 --> X + + # @reaction macro, symbols that cannot be a reactant. + @test_throws Exception @eval @reaction 1, 0 --> im + @test_throws Exception @eval @reaction 1, 0 --> nothing + @test_throws Exception @eval @reaction 1, 0 --> Γ + @test_throws Exception @eval @reaction 1, 0 --> ℯ + @test_throws Exception @eval @reaction 1, 0 --> pi + @test_throws Exception @eval @reaction 1, 0 --> π + @test_throws Exception @eval @reaction 1, 0 --> t + + # @reaction_network macro, symbols that cannot be in the rate. + @test_throws Exception @eval @reaction_network begin im, 0 --> X end + @test_throws Exception @eval @reaction_network begin nothing, 0 --> X end + @test_throws Exception @eval @reaction_network begin Γ, 0 --> X end + @test_throws Exception @eval @reaction_network begin ∅, 0 --> X end + + # @reaction_network macro, symbols that cannot be a reactant. + @test_throws Exception @eval @reaction_network begin 1, 0 --> im end + @test_throws Exception @eval @reaction_network begin 1, 0 --> nothing end + @test_throws Exception @eval @reaction_network begin 1, 0 --> Γ end + @test_throws Exception @eval @reaction_network begin 1, 0 --> ℯ end + @test_throws Exception @eval @reaction_network begin 1, 0 --> pi end + @test_throws Exception @eval @reaction_network begin 1, 0 --> π end + @test_throws Exception @eval @reaction_network begin 1, 0 --> t end # Checks that non-supported arrow type usage yields error. @test_throws Exception @eval @reaction_network begin diff --git a/test/dsl/dsl_options.jl b/test/dsl/dsl_options.jl index ce95ffc428..cf28e8de30 100644 --- a/test/dsl/dsl_options.jl +++ b/test/dsl/dsl_options.jl @@ -1144,12 +1144,112 @@ end ### Other DSL Option Tests ### +# Test that various options can be provided in block and single line form. +# Also checks that the single line form takes maximally one argument. +let + # The `@equations` option. + rn11 = @reaction_network rn1 begin + @equations D(V) ~ 1 - V + end + rn12 = @reaction_network rn1 begin + @equations begin + D(V) ~ 1 - V + end + end + @test isequal(rn11, rn12) + @test_throws Exception @eval @reaction_network begin + @equations D(V) ~ 1 - V D(W) ~ 1 - W + end + + # The `@observables` option. + rn21 = @reaction_network rn1 begin + @species X(t) + @observables X2 ~ 2X + end + rn22 = @reaction_network rn1 begin + @species X(t) + @observables begin + X2 ~ 2X + end + end + @test isequal(rn21, rn22) + @test_throws Exception @eval @reaction_network begin + @species X(t) + @observables X2 ~ 2X X3 ~ 3X + end + + # The `@compounds` option. + rn31 = @reaction_network rn1 begin + @species X(t) + @compounds X2 ~ 2X + end + rn32 = @reaction_network rn1 begin + @species X(t) + @compounds begin + X2 ~ 2X + end + end + @test isequal(rn31, rn32) + @test_throws Exception @eval @reaction_network begin + @species X(t) + @compounds X2 ~ 2X X3 ~ 3X + end + + # The `@differentials` option. + rn41 = @reaction_network rn1 begin + @differentials D = Differential(t) + end + rn42 = @reaction_network rn1 begin + @differentials begin + D = Differential(t) + end + end + @test isequal(rn41, rn42) + @test_throws Exception @eval @reaction_network begin + @differentials D = Differential(t) Δ = Differential(t) + end + + # The `@continuous_events` option. + rn51 = @reaction_network rn1 begin + @species X(t) + @continuous_events [X ~ 3.0] => [X ~ X - 1] + end + rn52 = @reaction_network rn1 begin + @species X(t) + @continuous_events begin + [X ~ 3.0] => [X ~ X - 1] + end + end + @test isequal(rn51, rn52) + @test_throws Exception @eval @reaction_network begin + @species X(t) + @continuous_events [X ~ 3.0] => [X ~ X - 1] [X ~ 1.0] => [X ~ X + 1] + end + + # The `@discrete_events` option. + rn61 = @reaction_network rn1 begin + @species X(t) + @discrete_events [X > 3.0] => [X ~ X - 1] + end + rn62 = @reaction_network rn1 begin + @species X(t) + @discrete_events begin + [X > 3.0] => [X ~ X - 1] + end + end + @test isequal(rn61, rn62) + @test_throws Exception @eval @reaction_network begin + @species X(t) + @discrete_events [X > 3.0] => [X ~ X - 1] [X < 1.0] => [X ~ X + 1] + end +end + # test combinatoric_ratelaws DSL option let rn = @reaction_network begin @combinatoric_ratelaws false (k1,k2), 2A <--> B - end + end combinatoric_ratelaw = Catalyst.get_combinatoric_ratelaws(rn) @test combinatoric_ratelaw == false rl = oderatelaw(reactions(rn)[1]; combinatoric_ratelaw) @@ -1159,7 +1259,7 @@ let rn2 = @reaction_network begin @combinatoric_ratelaws true (k1,k2), 2A <--> B - end + end combinatoric_ratelaw = Catalyst.get_combinatoric_ratelaws(rn2) @test combinatoric_ratelaw == true rl = oderatelaw(reactions(rn2)[1]; combinatoric_ratelaw) @@ -1170,7 +1270,7 @@ let rn3 = @reaction_network begin @combinatoric_ratelaws $crl (k1,k2), 2A <--> B - end + end combinatoric_ratelaw = Catalyst.get_combinatoric_ratelaws(rn3) @test combinatoric_ratelaw == crl rl = oderatelaw(reactions(rn3)[1]; combinatoric_ratelaw) @@ -1256,10 +1356,10 @@ let end end -### test that @no_infer properly throws errors when undeclared variables are written ### - -import Catalyst: UndeclaredSymbolicError +# test that @require_declaration properly throws errors when undeclared variables are written. let + import Catalyst: UndeclaredSymbolicError + # Test error when species are inferred @test_throws UndeclaredSymbolicError @macroexpand @reaction_network begin @require_declaration From 292da201c09f991001d0cacdb7f42cc2031727e7 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sun, 19 Jan 2025 22:21:08 +0000 Subject: [PATCH 34/38] minor upodates after read through --- docs/src/model_creation/dsl_advanced.md | 67 +++++---- src/dsl.jl | 174 +++++++++++------------ src/expression_utils.jl | 1 - test/dsl/dsl_basic_model_construction.jl | 66 ++++----- test/dsl/dsl_options.jl | 15 ++ 5 files changed, 174 insertions(+), 149 deletions(-) diff --git a/docs/src/model_creation/dsl_advanced.md b/docs/src/model_creation/dsl_advanced.md index 47988cd0ee..c8b4a2e9aa 100644 --- a/docs/src/model_creation/dsl_advanced.md +++ b/docs/src/model_creation/dsl_advanced.md @@ -500,34 +500,6 @@ Catalyst.getdescription(rx) A list of all available reaction metadata can be found [in the api](@ref api_rx_metadata). -## [Declaring individual reaction using the `@reaction` macro](@id dsl_advanced_options_reaction_macro) -Catalyst exports a macro `@reaction` which can be used to generate a singular [`Reaction`](@ref) object of the same type which is stored within the [`ReactionSystem`](@ref) structure (which in turn can be generated by `@reaction_network `). In the following example, we create a simple [SIR model](@ref basic_CRN_library_sir). Next, we instead create its individual reaction components using the `@reaction` macro. Finally, we confirm that these are identical to those stored in the initial model (using the [`reactions`](@ref) function). -```@example dsl_advanced_reaction_macro -using Catalyst # hide -sir_model = @reaction_network begin - α, S + I --> 2I - β, I --> R -end -infection_rx = @reaction α, S + I --> 2I -recovery_rx = @reaction β, I --> R -issetequal(reactions(sir_model), [infection_rx, recovery_rx]) -``` - -Here, the `@reaction` macro is followed by a single line consisting of three parts: -- A rate (at which the reaction occurs). -- Any number of substrates (which are consumed by the reaction). -- Any number of products (which are produced by the reaction). -The rules of writing and interpreting this line are [identical to those for `@reaction_network`](@ref dsl_description_reactions) (however, bi-directional reactions are not permitted). - -Generally, the `@reaction` macro provides a more concise notation to the [`Reaction`](@ref) constructor. One of its primary uses is for [creating models programmatically](@ref programmatic_CRN_construction). When doing so, it can often be useful to use [*interpolation*](@ref dsl_advanced_options_symbolics_and_DSL_interpolation) of symbolic variables declared previously: -```@example dsl_advanced_reaction_macro -t = default_t() -@parameters k b -@species A(t) -ex = k*A^2 + t -rx = @reaction b*$ex*$A, $A --> C -``` - ## [Working with symbolic variables and the DSL](@id dsl_advanced_options_symbolics_and_DSL) We have previously described how Catalyst represents its models symbolically (enabling e.g. symbolic differentiation of expressions stored in models). While Catalyst utilises this for many internal operation, these symbolic representations can also be accessed and harnessed by the user. Primarily, doing so is much easier during programmatic (as opposed to DSL-based) modelling. Indeed, the section on [programmatic modelling](@ref programmatic_CRN_construction) goes into more details about symbolic representation in models, and how these can be used. It is, however, also ways to utilise these methods during DSL-based modelling. Below we briefly describe two methods for doing so. @@ -602,6 +574,45 @@ nothing # hide !!! note When using interpolation, expressions like `2$spec` won't work; the multiplication symbol must be explicitly included like `2*$spec`. +## [Creating individual reaction using the `@reaction` macro](@id dsl_advanced_options_reaction_macro) +Catalyst exports a macro `@reaction`, which can be used to generate a singular [`Reaction`](@ref) object of the same type which is stored within the [`ReactionSystem`](@ref) structure (which in turn can be generated by `@reaction_network`). Generally, `@reaction` follows [identical rules to those of `@reaction_network`](@ref (@ref dsl_description_reactions)) for writing and interpreting reactions (however, bi-directional reactions are not permitted). E.g. here we create a simple dimerisation reaction: +```@example dsl_advanced_reaction_macro +using Catalyst # hide +rx_dimerisation = @reaction kD, 2X --> X2 +``` +Here, `@reaction` is followed by a single line consisting of three parts: +- A rate (at which the reaction occurs). +- Any number of substrates (which are consumed by the reaction). +- Any number of products (which are produced by the reaction). + +In the next example, we first create a simple [SIR model](@ref basic_CRN_library_sir). Next, we instead create its individual reaction components using the `@reaction` macro. Finally, we confirm that these are identical to those stored in the initial model (using the [`reactions`](@ref) function). +```@example dsl_advanced_reaction_macro +sir_model = @reaction_network begin + α, S + I --> 2I + β, I --> R +end +infection_rx = @reaction α, S + I --> 2I +recovery_rx = @reaction β, I --> R +sir_rxs = [infection_rx, recovery_rx] +issetequal(reactions(sir_model), sir_rxs) +``` +One of the primary uses of the `@reaction` macro is to provide some of the convenience of the DSL to [*programmatic modelling](@ref programmatic_CRN_construction). E.g. here we can combine our reactions to create a `ReactionSystem` directly, and also confirm that this is identical to the model created through the DSL: +```@example dsl_advanced_reaction_macro +sir_programmatic = complete(ReactionSystem(sir_rxs, default_t(); name = :sir)) +sir_programmatic == sir_model +``` + +During programmatic modelling, it can be good to keep in mind that already declared symbolic variables can be [*interpolated*](@ref dsl_advanced_options_symbolics_and_DSL_interpolation). E.g. here we create two production reactions both depending on the same Michaelis-Menten function: +```@example dsl_advanced_reaction_macro +t = default_t() +@species X(t) +@parameters v K +mm_term = Catalyst.mm(X, v, K) +rx1 = @reaction $mm_term, 0 --> P1 +rx2 = @reaction $mm_term, 0 --> P2 +nothing # hide +``` + ## [Disabling mass action for reactions](@id dsl_advanced_options_disable_ma) As [described previously](@ref math_models_in_catalyst_rre_odes), Catalyst uses *mass action kinetics* to generate ODEs from reactions. Here, each reaction generates a term for each of its reactants, which consists of the reaction's rate, substrates, and the reactant's stoichiometry. E.g. the following reaction: diff --git a/src/dsl.jl b/src/dsl.jl index ab6dcacf07..017af3344d 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -288,7 +288,7 @@ function make_reaction_system(ex::Expr, name) ps_declared = extract_syms(options, :parameters) vs_declared = extract_syms(options, :variables) tiv, sivs, ivs, ivsexpr = read_ivs_option(options) - cmpexpr_init, cmps_declared = read_compound_options(options) + cmpexpr_init, cmps_declared = read_compound_option(options) diffsexpr, diffs_declared = read_differentials_option(options) syms_declared = collect(Iterators.flatten((cmps_declared, sps_declared, ps_declared, vs_declared, ivs, diffs_declared))) @@ -301,15 +301,15 @@ function make_reaction_system(ex::Expr, name) requiredec = haskey(options, :require_declaration) reactions = get_reactions(reaction_lines) sps_inferred, ps_pre_inferred = extract_sps_and_ps(reactions, syms_declared; requiredec) - vs_inferred, diffs_inferred, equations = read_equations_options!(diffsexpr, options, + vs_inferred, diffs_inferred, equations = read_equations_option!(diffsexpr, options, union(syms_declared, sps_inferred), tiv; requiredec) ps_inferred = setdiff(ps_pre_inferred, vs_inferred, diffs_inferred) syms_inferred = union(sps_inferred, ps_inferred, vs_inferred, diffs_inferred) all_syms = union(syms_declared, syms_inferred) + obsexpr, obs_eqs, obs_syms = read_observed_option(options, ivs, + union(sps_declared, vs_declared), all_syms; requiredec) # Read options not related to the declaration or inference of symbols. - obsexpr, obs_eqs, obs_syms = read_observed_options(options, ivs, - union(sps_declared, vs_declared), all_syms; requiredec) continuous_events_expr = read_events_option(options, :continuous_events) discrete_events_expr = read_events_option(options, :discrete_events) default_reaction_metadata = read_default_noise_scaling_option(options) @@ -489,7 +489,7 @@ function extract_sps_and_ps(reactions, excluded_syms; requiredec = false) end end - return collect(species), collect(parameters) + collect(species), collect(parameters) end # Function called by extract_sps_and_ps, recursively loops through an @@ -524,7 +524,7 @@ function get_usexpr(us_extracted, options, key = :species; ivs = (DEFAULT_IV_SYM for u in us_extracted u isa Symbol && push!(usexpr.args, Expr(:call, u, ivs...)) end - return usexpr + usexpr end # Given the parameters that were extracted from the reactions, and the options dictionary, @@ -538,7 +538,7 @@ function get_psexpr(parameters_extracted, options) :(@parameters) end foreach(p -> push!(pexprs.args, p), parameters_extracted) - return pexprs + pexprs end # Takes a ModelingToolkit declaration macro (like @parameters ...) and return and expression: @@ -629,24 +629,9 @@ function read_ivs_option(options) return tiv, sivs, ivs, ivsexpr end -# Returns the `default_reaction_metadata` output. Technically Catalyst's code could have been made -# more generic to account for other default reaction metadata. Practically, this will likely -# be the only relevant reaction metadata to have a default value via the DSL. If another becomes -# relevant, the code can be rewritten to take this into account. -# Checks if the `@default_noise_scaling` option is used. If so, uses it as the default value of -# the `default_noise_scaling` reaction metadata, otherwise, returns an empty vector. -function read_default_noise_scaling_option(options) - if haskey(options, :default_noise_scaling) - (length(options[:default_noise_scaling].args) != 3) && - error("@default_noise_scaling should only have a single expression as its input, this appears not to be the case: \"$(options[:default_noise_scaling])\"") - return :([:noise_scaling => $(options[:default_noise_scaling].args[3])]) - end - return :([]) -end - # When compound species are declared using the "@compound begin ... end" option, get a list # of the compound species, and also the expression that crates them. -function read_compound_options(options) +function read_compound_option(options) # If the compound option is used, retrieve a list of compound species and the option line # that creates them (used to declare them as compounds at the end). Due to some expression # handling, in the case of a single compound we must change to the `@compound` macro. @@ -663,44 +648,36 @@ function read_compound_options(options) return cmpexpr_init, cmps_declared end -# Read the events (continuous or discrete) provided as options to the DSL. Returns an expression which evaluates to these. -function read_events_option(options, event_type::Symbol) - # Prepares the events, if required to, converts them to block form. - if event_type ∉ [:continuous_events, :discrete_events] - error("Trying to read an unsupported event type.") - end - events_input = haskey(options, event_type) ? get_block_option(options[event_type]) : - striplines(:(begin end)) - events_input = option_block_form(events_input) - - # Goes through the events, checks for errors, and adds them to the output vector. - events_expr = :([]) - for arg in events_input.args - # Formatting error checks. - # NOTE: Maybe we should move these deeper into the system (rather than the DSL), throwing errors more generally? - if (arg isa Expr) && (arg.head != :call) || (arg.args[1] != :(=>)) || - (length(arg.args) != 3) - error("Events should be on form `condition => affect`, separated by a `=>`. This appears not to be the case for: $(arg).") - end - if (arg isa Expr) && (arg.args[2] isa Expr) && (arg.args[2].head != :vect) && - (event_type == :continuous_events) - error("The condition part of continuous events (the left-hand side) must be a vector. This is not the case for: $(arg).") - end - if (arg isa Expr) && (arg.args[3] isa Expr) && (arg.args[3].head != :vect) - error("The affect part of all events (the right-hand side) must be a vector. This is not the case for: $(arg).") - end +# Creates an expression declaring differentials. Here, `tiv` is the time independent variables, +# which is used by the default differential (if it is used). +function read_differentials_option(options) + # Creates the differential expression. + # If differentials was provided as options, this is used as the initial expression. + # If the default differential (D(...)) was used in equations, this is added to the expression. + diffsexpr = (haskey(options, :differentials) ? + get_block_option(options[:differentials]) : striplines(:(begin end))) + diffsexpr = option_block_form(diffsexpr) - # Adds the correctly formatted event to the event creation expression. - push!(events_expr.args, arg) + # Goes through all differentials, checking that they are correctly formatted. Adds their + # symbol to the list of declared differential symbols. + diffs_declared = Union{Symbol, Expr}[] + for dexpr in diffsexpr.args + (dexpr.head != :(=)) && + error("Differential declaration must have form like D = Differential(t), instead \"$(dexpr)\" was given.") + (dexpr.args[1] isa Symbol) || + error("Differential left-hand side must be a single symbol, instead \"$(dexpr.args[1])\" was given.") + in(dexpr.args[1], forbidden_symbols_error) && + error("A forbidden symbol ($(dexpr.args[1])) was used as a differential name.") + push!(diffs_declared, dexpr.args[1]) end - return events_expr + return diffsexpr, diffs_declared end # Reads the variables options. Outputs a list of the variables inferred from the equations, # as well as the equation vector. If the default differential was used, updates the `diffsexpr` # expression so that this declares this as well. -function read_equations_options!(diffsexpr, options, syms_unavailable, tiv; requiredec = false) +function read_equations_option!(diffsexpr, options, syms_unavailable, tiv; requiredec = false) # Prepares the equations. First, extracts equations from provided option (converting to block form if required). # Next, uses MTK's `parse_equations!` function to split input into a vector with the equations. eqs_input = haskey(options, :equations) ? get_block_option(options[:equations]) : :(begin end) @@ -755,43 +732,10 @@ function find_D_call(expr) end end -# Creates an expression declaring differentials. Here, `tiv` is the time independent variables, -# which is used by the default differential (if it is used). -function read_differentials_option(options) - # Creates the differential expression. - # If differentials was provided as options, this is used as the initial expression. - # If the default differential (D(...)) was used in equations, this is added to the expression. - diffsexpr = (haskey(options, :differentials) ? - get_block_option(options[:differentials]) : striplines(:(begin end))) - diffsexpr = option_block_form(diffsexpr) - - # Goes through all differentials, checking that they are correctly formatted. Adds their - # symbol to the list of declared differential symbols. - diffs_declared = Union{Symbol, Expr}[] - for dexpr in diffsexpr.args - (dexpr.head != :(=)) && - error("Differential declaration must have form like D = Differential(t), instead \"$(dexpr)\" was given.") - (dexpr.args[1] isa Symbol) || - error("Differential left-hand side must be a single symbol, instead \"$(dexpr.args[1])\" was given.") - in(dexpr.args[1], forbidden_symbols_error) && - error("A forbidden symbol ($(dexpr.args[1])) was used as a differential name.") - push!(diffs_declared, dexpr.args[1]) - end - - return diffsexpr, diffs_declared -end - -# Reads the combinatorial ratelaw options, which determines if a combinatorial rate law should -# be used or not. If not provides, uses the default (true). -function read_combinatoric_ratelaws_option(options) - return haskey(options, :combinatoric_ratelaws) ? - options[:combinatoric_ratelaws].args[end] : true -end - # Reads the observables options. Outputs an expression for creating the observable variables, # a vector containing the observable equations, and a list of all observable symbols (this # list contains both those declared separately or inferred from the `@observables` option` input`). -function read_observed_options(options, all_ivs, us_declared, all_syms; requiredec = false) +function read_observed_option(options, all_ivs, us_declared, all_syms; requiredec = false) syms_unavailable = setdiff(all_syms, us_declared) if haskey(options, :observables) # Gets list of observable equations and prepares variable declaration expression. @@ -871,6 +815,62 @@ function make_obs_eqs(observables_expr) return obs_eqs end +# Read the events (continuous or discrete) provided as options to the DSL. Returns an expression which evaluates to these. +function read_events_option(options, event_type::Symbol) + # Prepares the events, if required to, converts them to block form. + if event_type ∉ [:continuous_events, :discrete_events] + error("Trying to read an unsupported event type.") + end + events_input = haskey(options, event_type) ? get_block_option(options[event_type]) : + striplines(:(begin end)) + events_input = option_block_form(events_input) + + # Goes through the events, checks for errors, and adds them to the output vector. + events_expr = :([]) + for arg in events_input.args + # Formatting error checks. + # NOTE: Maybe we should move these deeper into the system (rather than the DSL), throwing errors more generally? + if (arg isa Expr) && (arg.head != :call) || (arg.args[1] != :(=>)) || + (length(arg.args) != 3) + error("Events should be on form `condition => affect`, separated by a `=>`. This appears not to be the case for: $(arg).") + end + if (arg isa Expr) && (arg.args[2] isa Expr) && (arg.args[2].head != :vect) && + (event_type == :continuous_events) + error("The condition part of continuous events (the left-hand side) must be a vector. This is not the case for: $(arg).") + end + if (arg isa Expr) && (arg.args[3] isa Expr) && (arg.args[3].head != :vect) + error("The affect part of all events (the right-hand side) must be a vector. This is not the case for: $(arg).") + end + + # Adds the correctly formatted event to the event creation expression. + push!(events_expr.args, arg) + end + + return events_expr +end + +# Returns the `default_reaction_metadata` output. Technically Catalyst's code could have been made +# more generic to account for other default reaction metadata. Practically, this will likely +# be the only relevant reaction metadata to have a default value via the DSL. If another becomes +# relevant, the code can be rewritten to take this into account. +# Checks if the `@default_noise_scaling` option is used. If so, uses it as the default value of +# the `default_noise_scaling` reaction metadata, otherwise, returns an empty vector. +function read_default_noise_scaling_option(options) + if haskey(options, :default_noise_scaling) + (length(options[:default_noise_scaling].args) != 3) && + error("@default_noise_scaling should only have a single expression as its input, this appears not to be the case: \"$(options[:default_noise_scaling])\"") + return :([:noise_scaling => $(options[:default_noise_scaling].args[3])]) + end + return :([]) +end + +# Reads the combinatorial ratelaw options, which determines if a combinatorial rate law should +# be used or not. If not provides, uses the default (true). +function read_combinatoric_ratelaws_option(options) + return haskey(options, :combinatoric_ratelaws) ? + options[:combinatoric_ratelaws].args[end] : true +end + ### `@reaction` Macro & its Internals ### """ diff --git a/src/expression_utils.jl b/src/expression_utils.jl index cd51be32de..90239d91d7 100644 --- a/src/expression_utils.jl +++ b/src/expression_utils.jl @@ -13,7 +13,6 @@ function esc_dollars!(ex) ex.args[i] = esc_dollars!(ex.args[i]) end end - ex end diff --git a/test/dsl/dsl_basic_model_construction.jl b/test/dsl/dsl_basic_model_construction.jl index a7ddb3dfad..ae2a638ae2 100644 --- a/test/dsl/dsl_basic_model_construction.jl +++ b/test/dsl/dsl_basic_model_construction.jl @@ -493,6 +493,38 @@ let @test reactions(test_network)[1].rate == ℯ end +### Error Test ### + +# Erroneous `@reaction` usage. +let + # Bi-directional reaction using the `@reaction` macro. + @test_throws Exception @eval @reaction (k1,k2), X1 <--> X2 + + # Bundles reactions. + @test_throws Exception @eval @reaction k, (X1,X2) --> 0 +end + +# Tests that malformed reactions yields errors. +let + # Checks that malformed combinations of entries yields errors. + @test_throws Exception @eval @reaction_network begin + d, X --> 0, Y --> 0 + end + @test_throws Exception @eval @reaction_network begin + d, X --> 0, [misc="Ok metadata"], [description="Metadata in (erroneously) extra []."] + end + + # Checks that incorrect bundling yields error. + @test_throws Exception @eval @reaction_network begin + (k1,k2,k3), (X1,X2) --> 0 + end + + # Checks that incorrect stoichiometric expression yields an error. + @test_throws Exception @eval @reaction_network begin + k, X^Y --> XY + end +end + # Check that forbidden symbols correctly generates errors. let # @reaction macro, symbols that cannot be in the rate. @@ -529,36 +561,4 @@ let @test_throws Exception @eval @reaction_network begin d, X ⇻ 0 end -end - -### Error Test ### - -# Erroneous `@reaction` usage. -let - # Bi-directional reaction using the `@reaction` macro. - @test_throws Exception @eval @reaction (k1,k2), X1 <--> X2 - - # Bundles reactions. - @test_throws Exception @eval @reaction k, (X1,X2) --> 0 -end - -# Tests that malformed reactions yields errors. -let - # Checks that malformed combinations of entries yields errors. - @test_throws Exception @eval @reaction_network begin - d, X --> 0, Y --> 0 - end - @test_throws Exception @eval @reaction_network begin - d, X --> 0, [misc="Ok metadata"], [description="Metadata in (erroneously) extra []."] - end - - # Checks that incorrect bundling yields error. - @test_throws Exception @eval @reaction_network begin - (k1,k2,k3), (X1,X2) --> 0 - end - - # Checks that incorrect stoichiometric expression yields an error. - @test_throws Exception @eval @reaction_network begin - k, X^Y --> XY - end -end +end \ No newline at end of file diff --git a/test/dsl/dsl_options.jl b/test/dsl/dsl_options.jl index cf28e8de30..656836506c 100644 --- a/test/dsl/dsl_options.jl +++ b/test/dsl/dsl_options.jl @@ -422,6 +422,16 @@ let @test_throws Exception @eval @reaction_network begin @variables π(t) end + @test_throws Exception @eval @reaction_network begin + @variables Γ(t) + end + @test_throws Exception @eval @reaction_network begin + @variables ∅(t) + end + @test_throws Exception @eval @reaction_network begin + @ivs s + @variables t(s) + end end # Tests that explicitly declaring a single symbol as several things does not work. @@ -1140,6 +1150,11 @@ let @test_throws Exception @eval @reaction_network begin @equations D(π) ~ -1 end + + # Algebraic equation using a forbidden variable (in the DSL). + @test_throws Exception @eval @reaction_network begin + @equations Γ ~ 1 + 3(Γ^2 + Γ) + end end ### Other DSL Option Tests ### From 2902101a9268858e2b9c637416beae613fe16797 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Mon, 20 Jan 2025 10:31:29 +0000 Subject: [PATCH 35/38] doc fix, combinatorial_ratelaw error handling --- docs/src/model_creation/dsl_advanced.md | 2 +- src/dsl.jl | 146 ++++++++++++------------ src/expression_utils.jl | 4 +- test/dsl/dsl_options.jl | 17 +++ 4 files changed, 94 insertions(+), 75 deletions(-) diff --git a/docs/src/model_creation/dsl_advanced.md b/docs/src/model_creation/dsl_advanced.md index c8b4a2e9aa..bac449cf94 100644 --- a/docs/src/model_creation/dsl_advanced.md +++ b/docs/src/model_creation/dsl_advanced.md @@ -575,7 +575,7 @@ nothing # hide When using interpolation, expressions like `2$spec` won't work; the multiplication symbol must be explicitly included like `2*$spec`. ## [Creating individual reaction using the `@reaction` macro](@id dsl_advanced_options_reaction_macro) -Catalyst exports a macro `@reaction`, which can be used to generate a singular [`Reaction`](@ref) object of the same type which is stored within the [`ReactionSystem`](@ref) structure (which in turn can be generated by `@reaction_network`). Generally, `@reaction` follows [identical rules to those of `@reaction_network`](@ref (@ref dsl_description_reactions)) for writing and interpreting reactions (however, bi-directional reactions are not permitted). E.g. here we create a simple dimerisation reaction: +Catalyst exports a macro `@reaction`, which can be used to generate a singular [`Reaction`](@ref) object of the same type which is stored within the [`ReactionSystem`](@ref) structure (which in turn can be generated by `@reaction_network`). Generally, `@reaction` follows [identical rules to those of `@reaction_network`](@ref dsl_description_reactions) for writing and interpreting reactions (however, bi-directional reactions are not permitted). E.g. here we create a simple dimerisation reaction: ```@example dsl_advanced_reaction_macro using Catalyst # hide rx_dimerisation = @reaction kD, 2X --> X2 diff --git a/src/dsl.jl b/src/dsl.jl index 017af3344d..e27248c8ef 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -629,6 +629,21 @@ function read_ivs_option(options) return tiv, sivs, ivs, ivsexpr end +# Returns the `default_reaction_metadata` output. Technically Catalyst's code could have been made +# more generic to account for other default reaction metadata. Practically, this will likely +# be the only relevant reaction metadata to have a default value via the DSL. If another becomes +# relevant, the code can be rewritten to take this into account. +# Checks if the `@default_noise_scaling` option is used. If so, uses it as the default value of +# the `default_noise_scaling` reaction metadata, otherwise, returns an empty vector. +function read_default_noise_scaling_option(options) + if haskey(options, :default_noise_scaling) + (length(options[:default_noise_scaling].args) != 3) && + error("@default_noise_scaling should only have a single expression as its input, this appears not to be the case: \"$(options[:default_noise_scaling])\"") + return :([:noise_scaling => $(options[:default_noise_scaling].args[3])]) + end + return :([]) +end + # When compound species are declared using the "@compound begin ... end" option, get a list # of the compound species, and also the expression that crates them. function read_compound_option(options) @@ -648,30 +663,38 @@ function read_compound_option(options) return cmpexpr_init, cmps_declared end -# Creates an expression declaring differentials. Here, `tiv` is the time independent variables, -# which is used by the default differential (if it is used). -function read_differentials_option(options) - # Creates the differential expression. - # If differentials was provided as options, this is used as the initial expression. - # If the default differential (D(...)) was used in equations, this is added to the expression. - diffsexpr = (haskey(options, :differentials) ? - get_block_option(options[:differentials]) : striplines(:(begin end))) - diffsexpr = option_block_form(diffsexpr) +# Read the events (continuous or discrete) provided as options to the DSL. Returns an expression which evaluates to these. +function read_events_option(options, event_type::Symbol) + # Prepares the events, if required to, converts them to block form. + if event_type ∉ [:continuous_events, :discrete_events] + error("Trying to read an unsupported event type.") + end + events_input = haskey(options, event_type) ? get_block_option(options[event_type]) : + striplines(:(begin end)) + events_input = option_block_form(events_input) - # Goes through all differentials, checking that they are correctly formatted. Adds their - # symbol to the list of declared differential symbols. - diffs_declared = Union{Symbol, Expr}[] - for dexpr in diffsexpr.args - (dexpr.head != :(=)) && - error("Differential declaration must have form like D = Differential(t), instead \"$(dexpr)\" was given.") - (dexpr.args[1] isa Symbol) || - error("Differential left-hand side must be a single symbol, instead \"$(dexpr.args[1])\" was given.") - in(dexpr.args[1], forbidden_symbols_error) && - error("A forbidden symbol ($(dexpr.args[1])) was used as a differential name.") - push!(diffs_declared, dexpr.args[1]) + # Goes through the events, checks for errors, and adds them to the output vector. + events_expr = :([]) + for arg in events_input.args + # Formatting error checks. + # NOTE: Maybe we should move these deeper into the system (rather than the DSL), throwing errors more generally? + if (arg isa Expr) && (arg.head != :call) || (arg.args[1] != :(=>)) || + (length(arg.args) != 3) + error("Events should be on form `condition => affect`, separated by a `=>`. This appears not to be the case for: $(arg).") + end + if (arg isa Expr) && (arg.args[2] isa Expr) && (arg.args[2].head != :vect) && + (event_type == :continuous_events) + error("The condition part of continuous events (the left-hand side) must be a vector. This is not the case for: $(arg).") + end + if (arg isa Expr) && (arg.args[3] isa Expr) && (arg.args[3].head != :vect) + error("The affect part of all events (the right-hand side) must be a vector. This is not the case for: $(arg).") + end + + # Adds the correctly formatted event to the event creation expression. + push!(events_expr.args, arg) end - return diffsexpr, diffs_declared + return events_expr end # Reads the variables options. Outputs a list of the variables inferred from the equations, @@ -732,6 +755,32 @@ function find_D_call(expr) end end +# Creates an expression declaring differentials. Here, `tiv` is the time independent variables, +# which is used by the default differential (if it is used). +function read_differentials_option(options) + # Creates the differential expression. + # If differentials was provided as options, this is used as the initial expression. + # If the default differential (D(...)) was used in equations, this is added to the expression. + diffsexpr = (haskey(options, :differentials) ? + get_block_option(options[:differentials]) : striplines(:(begin end))) + diffsexpr = option_block_form(diffsexpr) + + # Goes through all differentials, checking that they are correctly formatted. Adds their + # symbol to the list of declared differential symbols. + diffs_declared = Union{Symbol, Expr}[] + for dexpr in diffsexpr.args + (dexpr.head != :(=)) && + error("Differential declaration must have form like D = Differential(t), instead \"$(dexpr)\" was given.") + (dexpr.args[1] isa Symbol) || + error("Differential left-hand side must be a single symbol, instead \"$(dexpr.args[1])\" was given.") + in(dexpr.args[1], forbidden_symbols_error) && + error("A forbidden symbol ($(dexpr.args[1])) was used as a differential name.") + push!(diffs_declared, dexpr.args[1]) + end + + return diffsexpr, diffs_declared +end + # Reads the observables options. Outputs an expression for creating the observable variables, # a vector containing the observable equations, and a list of all observable symbols (this # list contains both those declared separately or inferred from the `@observables` option` input`). @@ -761,8 +810,8 @@ function read_observed_option(options, all_ivs, us_declared, all_syms; requirede error("An observable ($obs_name) uses a name that already have been already been declared or inferred as another model property.") (obs_name in us_declared) && is_escaped_expr(obs_eq.args[2]) && error("An interpolated observable have been used, which has also been explicitly declared within the system using either @species or @variables. This is not permitted.") - ((obs_name in us_declared) || is_escaped_expr(obs_eq.args[2])) && - !isnothing(metadata) && error("Metadata was provided to observable $obs_name in the `@observables` macro. However, the observable was also declared separately (using either @species or @variables). When this is done, metadata should instead be provided within the original @species or @variable declaration.") + ((obs_name in us_declared) || is_escaped_expr(obs_eq.args[2])) && !isnothing(metadata) && + error("Metadata was provided to observable $obs_name in the `@observables` macro. However, the observable was also declared separately (using either @species or @variables). When this is done, metadata should instead be provided within the original @species or @variable declaration.") # This bits adds the observables to the @variables vector which is given as output. # For Observables that have already been declared using @species/@variables, @@ -815,60 +864,11 @@ function make_obs_eqs(observables_expr) return obs_eqs end -# Read the events (continuous or discrete) provided as options to the DSL. Returns an expression which evaluates to these. -function read_events_option(options, event_type::Symbol) - # Prepares the events, if required to, converts them to block form. - if event_type ∉ [:continuous_events, :discrete_events] - error("Trying to read an unsupported event type.") - end - events_input = haskey(options, event_type) ? get_block_option(options[event_type]) : - striplines(:(begin end)) - events_input = option_block_form(events_input) - - # Goes through the events, checks for errors, and adds them to the output vector. - events_expr = :([]) - for arg in events_input.args - # Formatting error checks. - # NOTE: Maybe we should move these deeper into the system (rather than the DSL), throwing errors more generally? - if (arg isa Expr) && (arg.head != :call) || (arg.args[1] != :(=>)) || - (length(arg.args) != 3) - error("Events should be on form `condition => affect`, separated by a `=>`. This appears not to be the case for: $(arg).") - end - if (arg isa Expr) && (arg.args[2] isa Expr) && (arg.args[2].head != :vect) && - (event_type == :continuous_events) - error("The condition part of continuous events (the left-hand side) must be a vector. This is not the case for: $(arg).") - end - if (arg isa Expr) && (arg.args[3] isa Expr) && (arg.args[3].head != :vect) - error("The affect part of all events (the right-hand side) must be a vector. This is not the case for: $(arg).") - end - - # Adds the correctly formatted event to the event creation expression. - push!(events_expr.args, arg) - end - - return events_expr -end - -# Returns the `default_reaction_metadata` output. Technically Catalyst's code could have been made -# more generic to account for other default reaction metadata. Practically, this will likely -# be the only relevant reaction metadata to have a default value via the DSL. If another becomes -# relevant, the code can be rewritten to take this into account. -# Checks if the `@default_noise_scaling` option is used. If so, uses it as the default value of -# the `default_noise_scaling` reaction metadata, otherwise, returns an empty vector. -function read_default_noise_scaling_option(options) - if haskey(options, :default_noise_scaling) - (length(options[:default_noise_scaling].args) != 3) && - error("@default_noise_scaling should only have a single expression as its input, this appears not to be the case: \"$(options[:default_noise_scaling])\"") - return :([:noise_scaling => $(options[:default_noise_scaling].args[3])]) - end - return :([]) -end - # Reads the combinatorial ratelaw options, which determines if a combinatorial rate law should # be used or not. If not provides, uses the default (true). function read_combinatoric_ratelaws_option(options) return haskey(options, :combinatoric_ratelaws) ? - options[:combinatoric_ratelaws].args[end] : true + get_block_option(options[:combinatoric_ratelaws]) : true end ### `@reaction` Macro & its Internals ### diff --git a/src/expression_utils.jl b/src/expression_utils.jl index 90239d91d7..016b56f631 100644 --- a/src/expression_utils.jl +++ b/src/expression_utils.jl @@ -39,7 +39,9 @@ end # Note that there are only some options for which we wish to make this check. function get_block_option(expr) (length(expr.args) > 3) && - error("An option input ($expr) is misformatted. Potentially, it has multiple inputs on a single lines, and these should be split across multiple lines using a `begin ... end` block.") + error("An option input ($expr) is missformatted. Potentially, it has multiple inputs on a single lines, and these should be split across multiple lines using a `begin ... end` block.") + (length(expr.args) < 3) && + error("An option input ($expr) is missformatted. It seems that it has no inputs, which is expected.") return expr.args[3] end diff --git a/test/dsl/dsl_options.jl b/test/dsl/dsl_options.jl index 656836506c..71c8c4417a 100644 --- a/test/dsl/dsl_options.jl +++ b/test/dsl/dsl_options.jl @@ -1261,6 +1261,7 @@ end # test combinatoric_ratelaws DSL option let + # Test for `@combinatoric_ratelaws false`. rn = @reaction_network begin @combinatoric_ratelaws false (k1,k2), 2A <--> B @@ -1271,6 +1272,7 @@ let @unpack k1, A = rn @test isequal(rl, k1*A^2) + # Test for `@combinatoric_ratelaws true`. rn2 = @reaction_network begin @combinatoric_ratelaws true (k1,k2), 2A <--> B @@ -1281,6 +1283,7 @@ let @unpack k1, A = rn2 @test isequal(rl, k1*A^2/2) + # Test for interpolation into `@combinatoric_ratelaws`. crl = false rn3 = @reaction_network begin @combinatoric_ratelaws $crl @@ -1291,6 +1294,20 @@ let rl = oderatelaw(reactions(rn3)[1]; combinatoric_ratelaw) @unpack k1, A = rn3 @test isequal(rl, k1*A^2) + + # Test for erroneous inputs (to few, to many, wrong type). + @test_throws Exception @eval @reaction_network begin + @combinatoric_ratelaws + d, 3X --> 0 + end + @test_throws Exception @eval @reaction_network begin + @combinatoric_ratelaws false false + d, 3X --> 0 + end + @test_throws Exception @eval @reaction_network begin + @combinatoric_ratelaws "false" + d, 3X --> 0 + end end # Test whether user-defined functions are properly expanded in equations. From 5e342fb8c43bcced25de4142be65045b11f9dde6 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Mon, 20 Jan 2025 18:28:19 +0000 Subject: [PATCH 36/38] minor ups --- src/dsl.jl | 89 ++++++++++++++++++++--------------------- src/expression_utils.jl | 6 +-- 2 files changed, 47 insertions(+), 48 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index e27248c8ef..c92ce201dd 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -422,9 +422,8 @@ function push_reactions!(reactions::Vector{DSLReaction}, subs::ExprValues, # This finds these tuples' lengths (or 1 for non-tuple forms). Inconsistent lengths yield error. lengs = (tup_leng(subs), tup_leng(prods), tup_leng(rate), tup_leng(metadata)) maxl = maximum(lengs) - if any(!(leng == 1 || leng == maxl) for leng in lengs) + any(!(leng == 1 || leng == maxl) for leng in lengs) && error("Malformed reaction, rate: $rate, subs: $subs, prods: $prods, metadata: $metadata.") - end # Loops through each reaction encoded by the reaction's different components. # Creates a `DSLReaction` representation and adds it to `reactions`. @@ -492,7 +491,7 @@ function extract_sps_and_ps(reactions, excluded_syms; requiredec = false) collect(species), collect(parameters) end -# Function called by extract_sps_and_ps, recursively loops through an +# Function called by `extract_sps_and_ps`, recursively loops through an # expression and find symbols (adding them to the push_symbols vector). function add_syms_from_expr!(push_symbols::AbstractSet, expr::ExprValues, excluded_syms) # If we have encountered a Symbol in the recursion, we can try extracting it. @@ -541,29 +540,6 @@ function get_psexpr(parameters_extracted, options) pexprs end -# Takes a ModelingToolkit declaration macro (like @parameters ...) and return and expression: -# That calls the macro and then scalarizes all the symbols created into a vector of Nums. -# stores the created symbolic variables in a variable (which name is generated from `name`). -# It will also return the name used for the variable that stores the symbolic variables. -function scalarize_macro(expr_init, name) - # Generates a random variable name which (in generated code) will store the produced - # symbolic variables (e.g. `var"##ps#384"`). - namesym = gensym(name) - - # If the input expression is non-empty, wraps it with additional information. - if expr_init != :(()) - symvec = gensym() - expr = quote - $symvec = $expr_init - $namesym = reduce(vcat, Symbolics.scalarize($symvec)) - end - else - expr = :($namesym = Num[]) - end - - return expr, namesym -end - # From the system reactions (as `DSLReaction`s) and equations (as expressions), # creates the expressions that evaluates to the reaction (+ equations) vector. function get_rxexprs(reactions, equations, all_syms) @@ -598,6 +574,29 @@ function get_rxexpr(rx::DSLReaction) return rx_constructor end +# Takes a ModelingToolkit declaration macro (like @parameters ...) and return and expression: +# That calls the macro and then scalarizes all the symbols created into a vector of Nums. +# stores the created symbolic variables in a variable (which name is generated from `name`). +# It will also return the name used for the variable that stores the symbolic variables. +function scalarize_macro(expr_init, name) + # Generates a random variable name which (in generated code) will store the produced + # symbolic variables (e.g. `var"##ps#384"`). + namesym = gensym(name) + + # If the input expression is non-empty, wraps it with additional information. + if expr_init != :(()) + symvec = gensym() + expr = quote + $symvec = $expr_init + $namesym = reduce(vcat, Symbolics.scalarize($symvec)) + end + else + expr = :($namesym = Num[]) + end + + return expr, namesym +end + # Recursively escape functions within equations of an equation written using user-defined functions. # Does not escape special function calls like "hill(...)" and differential operators. Does # also not escape stuff corresponding to e.g. species or parameters (required for good error @@ -610,25 +609,6 @@ end ### DSL Option Handling ### -# Finds the time independent variable, and any potential spatial independent variables. -# Returns these (individually and combined), as well as an expression for declaring them. -function read_ivs_option(options) - # Creates the independent variables expressions (depends on whether the `ivs` option was used). - if haskey(options, :ivs) - ivs = Tuple(extract_syms(options, :ivs)) - ivsexpr = copy(options[:ivs]) - ivsexpr.args[1] = Symbol("@", "parameters") - else - ivs = (DEFAULT_IV_SYM,) - ivsexpr = :($(DEFAULT_IV_SYM) = default_t()) - end - - # Extracts the independent variables symbols (time and spatial), and returns the output. - tiv = ivs[1] - sivs = (length(ivs) > 1) ? Expr(:vect, ivs[2:end]...) : nothing - return tiv, sivs, ivs, ivsexpr -end - # Returns the `default_reaction_metadata` output. Technically Catalyst's code could have been made # more generic to account for other default reaction metadata. Practically, this will likely # be the only relevant reaction metadata to have a default value via the DSL. If another becomes @@ -871,6 +851,25 @@ function read_combinatoric_ratelaws_option(options) get_block_option(options[:combinatoric_ratelaws]) : true end +# Finds the time independent variable, and any potential spatial independent variables. +# Returns these (individually and combined), as well as an expression for declaring them. +function read_ivs_option(options) + # Creates the independent variables expressions (depends on whether the `ivs` option was used). + if haskey(options, :ivs) + ivs = Tuple(extract_syms(options, :ivs)) + ivsexpr = copy(options[:ivs]) + ivsexpr.args[1] = Symbol("@", "parameters") + else + ivs = (DEFAULT_IV_SYM,) + ivsexpr = :($(DEFAULT_IV_SYM) = default_t()) + end + + # Extracts the independent variables symbols (time and spatial), and returns the output. + tiv = ivs[1] + sivs = (length(ivs) > 1) ? Expr(:vect, ivs[2:end]...) : nothing + return tiv, sivs, ivs, ivsexpr +end + ### `@reaction` Macro & its Internals ### """ diff --git a/src/expression_utils.jl b/src/expression_utils.jl index 016b56f631..b839844fcf 100644 --- a/src/expression_utils.jl +++ b/src/expression_utils.jl @@ -38,10 +38,10 @@ end # to throw an error if there is more inputs (suggesting e.g. multiple inputs on a single line). # Note that there are only some options for which we wish to make this check. function get_block_option(expr) - (length(expr.args) > 3) && - error("An option input ($expr) is missformatted. Potentially, it has multiple inputs on a single lines, and these should be split across multiple lines using a `begin ... end` block.") (length(expr.args) < 3) && - error("An option input ($expr) is missformatted. It seems that it has no inputs, which is expected.") + error("The $(expr.args[1]) option's input was misformatted (full declaration: `$expr`). It seems that it has no inputs, whereas some input is expected.") + (length(expr.args) > 3) && + error("The $(expr.args[1]) option's input was misformatted (full declaration: `$expr`). Potentially, it has multiple inputs on a single line, in which case these should be split across multiple lines using a `begin ... end` block.") return expr.args[3] end From 7ff3f7dbf1a350c90cb7d1a38645b18797c13bca Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Mon, 20 Jan 2025 18:52:07 +0000 Subject: [PATCH 37/38] code spell check --- src/dsl.jl | 88 +++++++++++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/src/dsl.jl b/src/dsl.jl index c92ce201dd..902984668a 100644 --- a/src/dsl.jl +++ b/src/dsl.jl @@ -110,10 +110,10 @@ macro reaction_network(expr::Expr) return make_rs_expr(:(gensym(:ReactionSystem)), expr) end -# Ideally, it would have been possible to combine the @reaction_network and @network_component macros. -# However, this issue: https://github.com/JuliaLang/julia/issues/37691 causes problem with interpolations -# if we make the @reaction_network macro call the @network_component macro. Instead, these uses the -# same input, but passes `complete = false` to `make_rs_expr`. +# Ideally, it would have been possible to combine the @reaction_network and @network_component +# macros. However, this issue: https://github.com/JuliaLang/julia/issues/37691 causes problem +# with interpolations if we make the @reaction_network macro call the @network_component macro. +# Instead, these use the same input, but pass `complete = false` to `make_rs_expr`. """ @network_component @@ -151,7 +151,7 @@ function make_rs_expr(name; complete = true) return Expr(:block, :(@parameters t), rs_expr) end -# When both a name and a network expression is generated, dispatches these to the internal +# When both a name and a network expression are generated, dispatch these to the internal # `make_reaction_system` function. function make_rs_expr(name, network_expr; complete = true) rs_expr = make_reaction_system(striplines(network_expr), name) @@ -167,7 +167,7 @@ struct DSLReactant stoichiometry::ExprValues end -# Internal structure containing information about one Reaction. Contain all its substrates and +# Internal structure containing information about one Reaction. Contains all its substrates and # products as well as its rate and potential metadata. Uses a specialized constructor. struct DSLReaction substrates::Vector{DSLReactant} @@ -188,7 +188,7 @@ end # Recursive function that loops through the reaction line and finds the reactants and their # stoichiometry. Recursion makes it able to handle weird cases like 2(X + Y + 3(Z + XY)). The # reactants are stored in the `reactants` vector. As the expression tree is parsed, the -# stoichiometry is updated and new reactants added. +# stoichiometry is updated and new reactants are added. function recursive_find_reactants!(ex::ExprValues, mult::ExprValues, reactants::Vector{DSLReactant}) # We have reached the end of the expression tree and can finalise and return the reactants. @@ -271,7 +271,7 @@ function make_reaction_system(ex::Expr, name) # Handle interpolation of variables in the input. ex = esc_dollars!(ex) - # Extracts the lines with reactions, the lines with options, and the options. Check for input errors. + # Extract the lines with reactions, the lines with options, and the options. Check for input errors. reaction_lines = Expr[x for x in ex.args if x.head == :tuple] option_lines = Expr[x for x in ex.args if x.head == :macrocall] options = Dict(Symbol(String(arg.args[1])[2:end]) => arg for arg in option_lines) @@ -282,8 +282,8 @@ function make_reaction_system(ex::Expr, name) any(!in(option_keys), keys(options)) && error("The following unsupported options were used: $(filter(opt_in->!in(opt_in,option_keys), keys(options)))") - # Read options that explicitly declares some symbol (e.g. `@species`). Compiles a list of - # all declared symbols and checks that there has been no double-declarations. + # Read options that explicitly declare some symbol (e.g. `@species`). Compiles a list of + # all declared symbols and checks that there have been no double-declarations. sps_declared = extract_syms(options, :species) ps_declared = extract_syms(options, :parameters) vs_declared = extract_syms(options, :variables) @@ -297,7 +297,7 @@ function make_reaction_system(ex::Expr, name) error("The following symbols $(unique(nonunique_syms)) have explicitly been declared as multiple types of components (e.g. occur in at least two of the `@species`, `@parameters`, `@variables`, `@ivs`, `@compounds`, `@differentials`). This is not allowed.") end - # Reads the reactions and equation. From these, infers species, variables, and parameters. + # Reads the reactions and equation. From these, infer species, variables, and parameters. requiredec = haskey(options, :require_declaration) reactions = get_reactions(reaction_lines) sps_inferred, ps_pre_inferred = extract_sps_and_ps(reactions, syms_declared; requiredec) @@ -319,16 +319,16 @@ function make_reaction_system(ex::Expr, name) psexpr_init = get_psexpr(ps_inferred, options) spsexpr_init = get_usexpr(sps_inferred, options; ivs) vsexpr_init = get_usexpr(vs_inferred, options, :variables; ivs) - psexpr, psvar = scalarize_macro(psexpr_init, "ps") - spsexpr, spsvar = scalarize_macro(spsexpr_init, "specs") - vsexpr, vsvar = scalarize_macro(vsexpr_init, "vars") - cmpsexpr, cmpsvar = scalarize_macro(cmpexpr_init, "comps") + psexpr, psvar = assign_var_to_symvar_declaration(psexpr_init, "ps") + spsexpr, spsvar = assign_var_to_symvar_declaration(spsexpr_init, "specs") + vsexpr, vsvar = assign_var_to_symvar_declaration(vsexpr_init, "vars") + cmpsexpr, cmpsvar = assign_var_to_symvar_declaration(cmpexpr_init, "comps") rxsexprs = get_rxexprs(reactions, equations, all_syms) # Assemblies the full expression that declares all required symbolic variables, and # then the output `ReactionSystem`. MacroTools.flatten(striplines(quote - # Inserts the expressions which generates the `ReactionSystem` input. + # Inserts the expressions which generate the `ReactionSystem` input. $ivsexpr $psexpr $spsexpr @@ -357,12 +357,12 @@ end ### DSL Reaction Reading Functions ### -# Generates a vector of reaction structures, each containing the information about one reaction. +# Generates a vector of reaction structures, each containing information about one reaction. function get_reactions(exprs::Vector{Expr}) # Declares an array to which we add all found reactions. reactions = Vector{DSLReaction}(undef, 0) - # Loops through each line of reactions. Extracts and adds each lines's reactions to `reactions`. + # Loops through each line of reactions. Extracts and adds each line's reactions to `reactions`. for line in exprs # Reads core reaction information. arrow, rate, reaction, metadata = read_reaction_line(line) @@ -391,10 +391,10 @@ function get_reactions(exprs::Vector{Expr}) return reactions end -# Extracts the rate, reaction, and metadata fields (the last one optional) from a reaction line. +# Extract the rate, reaction, and metadata fields (the last one optional) from a reaction line. function read_reaction_line(line::Expr) - # Handles rate, reaction, and arrow. Special routine required for the`-->` case, which - # creates an expression different what the other arrows creates. + # Handles rate, reaction, and arrow. A special routine is required for the`-->` case, + # which creates an expression different from what the other arrows create. rate = line.args[1] reaction = line.args[2] if reaction.head == :--> @@ -450,7 +450,7 @@ end # When the user have used the @species (or @parameters) options, extract species (or # parameters) from its input. function extract_syms(opts, vartype::Symbol) - # If the corresponding option have been used, uses `Symbolics._parse_vars` to find all + # If the corresponding option has been used, use `Symbolics._parse_vars` to find all # variable within it (returning them in a vector). return if haskey(opts, vartype) ex = opts[vartype] @@ -464,7 +464,7 @@ end # Function looping through all reactions, to find undeclared symbols (species or # parameters) and assign them to the right category. function extract_sps_and_ps(reactions, excluded_syms; requiredec = false) - # Loops through all reactant, extract undeclared ones as species. + # Loops through all reactants and extract undeclared ones as species. species = OrderedSet{Union{Symbol, Expr}}() for reaction in reactions for reactant in Iterators.flatten((reaction.substrates, reaction.products)) @@ -509,7 +509,7 @@ end ### DSL Output Expression Builders ### -# Given the extracted species (or variables) and the option dictionary, creates the +# Given the extracted species (or variables) and the option dictionary, create the # `@species ...` (or `@variables ..`) expression which would declare these. # If `key = :variables`, does this for variables (and not species). function get_usexpr(us_extracted, options, key = :species; ivs = (DEFAULT_IV_SYM,)) @@ -541,7 +541,7 @@ function get_psexpr(parameters_extracted, options) end # From the system reactions (as `DSLReaction`s) and equations (as expressions), -# creates the expressions that evaluates to the reaction (+ equations) vector. +# creates the expression that evaluates to the reaction (+ equations) vector. function get_rxexprs(reactions, equations, all_syms) rxsexprs = :(Catalyst.CatalystEqType[]) foreach(rx -> push!(rxsexprs.args, get_rxexpr(rx)), reactions) @@ -576,14 +576,14 @@ end # Takes a ModelingToolkit declaration macro (like @parameters ...) and return and expression: # That calls the macro and then scalarizes all the symbols created into a vector of Nums. -# stores the created symbolic variables in a variable (which name is generated from `name`). +# stores the created symbolic variables in a variable (whose name is generated from `name`). # It will also return the name used for the variable that stores the symbolic variables. -function scalarize_macro(expr_init, name) +function assign_var_to_symvar_declaration(expr_init, name) # Generates a random variable name which (in generated code) will store the produced # symbolic variables (e.g. `var"##ps#384"`). namesym = gensym(name) - # If the input expression is non-empty, wraps it with additional information. + # If the input expression is non-empty, wrap it with additional information. if expr_init != :(()) symvec = gensym() expr = quote @@ -613,7 +613,7 @@ end # more generic to account for other default reaction metadata. Practically, this will likely # be the only relevant reaction metadata to have a default value via the DSL. If another becomes # relevant, the code can be rewritten to take this into account. -# Checks if the `@default_noise_scaling` option is used. If so, uses it as the default value of +# Checks if the `@default_noise_scaling` option is used. If so, use it as the default value of # the `default_noise_scaling` reaction metadata, otherwise, returns an empty vector. function read_default_noise_scaling_option(options) if haskey(options, :default_noise_scaling) @@ -625,7 +625,7 @@ function read_default_noise_scaling_option(options) end # When compound species are declared using the "@compound begin ... end" option, get a list -# of the compound species, and also the expression that crates them. +# of the compound species, and also the expression that creates them. function read_compound_option(options) # If the compound option is used, retrieve a list of compound species and the option line # that creates them (used to declare them as compounds at the end). Due to some expression @@ -678,10 +678,10 @@ function read_events_option(options, event_type::Symbol) end # Reads the variables options. Outputs a list of the variables inferred from the equations, -# as well as the equation vector. If the default differential was used, updates the `diffsexpr` +# as well as the equation vector. If the default differential was used, update the `diffsexpr` # expression so that this declares this as well. function read_equations_option!(diffsexpr, options, syms_unavailable, tiv; requiredec = false) - # Prepares the equations. First, extracts equations from provided option (converting to block form if required). + # Prepares the equations. First, extract equations from the provided option (converting to block form if required). # Next, uses MTK's `parse_equations!` function to split input into a vector with the equations. eqs_input = haskey(options, :equations) ? get_block_option(options[:equations]) : :(begin end) eqs_input = option_block_form(eqs_input) @@ -696,7 +696,7 @@ function read_equations_option!(diffsexpr, options, syms_unavailable, tiv; requi add_default_diff = false for eq in equations if (eq.head != :call) || (eq.args[1] != :~) - error("Malformed equation: \"$eq\". Equation's left hand and right hand sides should be separated by a \"~\".") + error("Malformed equation: \"$eq\". Equation's left hand and right-hand sides should be separated by a \"~\".") end # If the default differential (`D`) is used, record that it should be declared later on. @@ -713,7 +713,7 @@ function read_equations_option!(diffsexpr, options, syms_unavailable, tiv; requi "Unrecognized symbol $(join(vs_inferred, ", ")) detected in equation expression: \"$(string(eq))\". Since the flag @require_declaration is declared, all symbolic variables must be explicitly declared with the @species, @variables, and @parameters options.")) end - # If `D` differential is used, add it to differential expression and inferred differentials list. + # If `D` differential is used, add it to the differential expression and inferred differentials list. diffs_inferred = Union{Symbol, Expr}[] if add_default_diff && !any(diff_dec.args[1] == :D for diff_dec in diffsexpr.args) diffs_inferred = [:D] @@ -723,7 +723,7 @@ function read_equations_option!(diffsexpr, options, syms_unavailable, tiv; requi return collect(vs_inferred), diffs_inferred, equations end -# Searches an expression `expr` and returns true if it have any subexpression `D(...)` (where `...` can be anything). +# Searches an expression `expr` and returns true if it has any subexpression `D(...)` (where `...` can be anything). # Used to determine whether the default differential D has been used in any equation provided to `@equations`. function find_D_call(expr) return if Base.isexpr(expr, :call) && expr.args[1] == :D @@ -739,7 +739,7 @@ end # which is used by the default differential (if it is used). function read_differentials_option(options) # Creates the differential expression. - # If differentials was provided as options, this is used as the initial expression. + # If differentials were provided as options, this is used as the initial expression. # If the default differential (D(...)) was used in equations, this is added to the expression. diffsexpr = (haskey(options, :differentials) ? get_block_option(options[:differentials]) : striplines(:(begin end))) @@ -793,7 +793,7 @@ function read_observed_option(options, all_ivs, us_declared, all_syms; requirede ((obs_name in us_declared) || is_escaped_expr(obs_eq.args[2])) && !isnothing(metadata) && error("Metadata was provided to observable $obs_name in the `@observables` macro. However, the observable was also declared separately (using either @species or @variables). When this is done, metadata should instead be provided within the original @species or @variable declaration.") - # This bits adds the observables to the @variables vector which is given as output. + # This bit adds the observables to the @variables vector which is given as output. # For Observables that have already been declared using @species/@variables, # or are interpolated, this parts should not be carried out. if !((obs_name in us_declared) || is_escaped_expr(obs_eq.args[2])) @@ -825,7 +825,7 @@ function read_observed_option(options, all_ivs, us_declared, all_syms; requirede (striplines(obsexpr) == striplines(Expr(:block, :(@variables)))) && (obsexpr = :()) else - # If option is not used, return empty expression and vector. + # If observables option is not used, return empty expression and vector. obsexpr = :() obs_eqs = :([]) obs_syms = :([]) @@ -834,7 +834,7 @@ function read_observed_option(options, all_ivs, us_declared, all_syms; requirede return obsexpr, obs_eqs, obs_syms end -# From the input to the @observables options, creates a vector containing one equation for +# From the input to the @observables options, create a vector containing one equation for # each observable. `option_block_form` handles if single line declaration of `@observables`, # i.e. without a `begin ... end` block, was used. function make_obs_eqs(observables_expr) @@ -845,7 +845,7 @@ function make_obs_eqs(observables_expr) end # Reads the combinatorial ratelaw options, which determines if a combinatorial rate law should -# be used or not. If not provides, uses the default (true). +# be used or not. If not provided, use the default (true). function read_combinatoric_ratelaws_option(options) return haskey(options, :combinatoric_ratelaws) ? get_block_option(options[:combinatoric_ratelaws]) : true @@ -938,7 +938,7 @@ function make_reaction(ex::Expr) reaction = get_reaction(ex) species, parameters = extract_sps_and_ps([reaction], []) - # Checks for input errors. Needed here but not in `@reaction_network` as `ReactionSystem` perform this check but `Reaction` don't. + # Checks for input errors. Needed here but not in `@reaction_network` as `ReactionSystem` performs this check but `Reaction` doesn't. forbidden_symbol_check(union(species, parameters)) # Creates expressions corresponding to code for declaring the parameters, species, and reaction. @@ -978,14 +978,14 @@ function recursive_escape_functions!(expr::ExprValues, syms_skip = []) expr end -# Returns the length of a expression tuple, or 1 if it is not an expression tuple (probably a Symbol/Numerical). +# Returns the length of an expression tuple, or 1 if it is not an expression tuple (probably a Symbol/Numerical). function tup_leng(ex::ExprValues) (typeof(ex) == Expr && ex.head == :tuple) && (return length(ex.args)) return 1 end -# Gets the ith element in a expression tuple, or returns the input itself if it is not an expression tuple -# (probably a Symbol/Numerical). This is used to handle bundled reaction (like `d, (X,Y) --> 0`). +# Gets the ith element in an expression tuple, or returns the input itself if it is not an expression tuple +# (probably a Symbol/Numerical). This is used to handle bundled reactions (like `d, (X,Y) --> 0`). function get_tup_arg(ex::ExprValues, i::Int) (tup_leng(ex) == 1) && (return ex) return ex.args[i] From 97cd2acd844c53b2960bf1e1cfc20e1909c7f26e Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Tue, 21 Jan 2025 10:16:48 +0000 Subject: [PATCH 38/38] Update api.md --- docs/src/api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index df4b0f0864..6efabcd08b 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -86,9 +86,9 @@ ReactionSystem ``` ## [Options for the `@reaction_network` DSL](@id api_dsl_options) -We have [previously described](@ref dsl_advanced_options) how options permits the user to supply non-reaction information to [`ReactionSystem`](@ref) created through the DSL. Here follows a list +We have [previously described](@ref dsl_advanced_options) how options permit the user to supply non-reaction information to [`ReactionSystem`](@ref) created through the DSL. Here follows a list of all options currently available. -- [`parameters`]:(@ref dsl_advanced_options_declaring_species_and_parameters) Allows the designation of a set of symbols as system parameter. +- [`parameters`](@ref dsl_advanced_options_declaring_species_and_parameters): Allows the designation of a set of symbols as system parameters. - [`species`](@ref dsl_advanced_options_declaring_species_and_parameters): Allows the designation of a set of symbols as system species. - [`variables`](@ref dsl_advanced_options_declaring_species_and_parameters): Allows the designation of a set of symbols as system non-species variables. - [`ivs`](@ref dsl_advanced_options_ivs): Allows the designation of a set of symbols as system independent variables.