Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add @no_infer flag for turning off species/variable/parameter inferring #1122

Merged
merged 18 commits into from
Nov 20, 2024
33 changes: 24 additions & 9 deletions src/dsl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const pure_rate_arrows = Set{Symbol}([:(=>), :(<=), :⇐, :⟽, :⇒, :⟾, :⇔
# Declares the keys used for various options.
const option_keys = (:species, :parameters, :variables, :ivs, :compounds, :observables,
:default_noise_scaling, :differentials, :equations,
:continuous_events, :discrete_events, :combinatoric_ratelaws)
:continuous_events, :discrete_events, :combinatoric_ratelaws, :no_infer)

### `@species` Macro ###

Expand Down Expand Up @@ -286,6 +286,11 @@ end
### DSL Internal Master Function ###

# Function for creating a ReactionSystem structure (used by the @reaction_network macro).
# What should no_infer do? We currently infer in:
# equations
# observables
# differentials
# make it so that we no longer do so.
vyudu marked this conversation as resolved.
Show resolved Hide resolved
function make_reaction_system(ex::Expr; name = :(gensym(:ReactionSystem)))

# Handle interpolation of variables
Expand All @@ -308,6 +313,7 @@ function make_reaction_system(ex::Expr; name = :(gensym(:ReactionSystem)))
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)
noinfer = haskey(options, :no_infer)

# Parses reactions, species, and parameters.
reactions = get_reactions(reaction_lines)
Expand All @@ -317,7 +323,7 @@ function make_reaction_system(ex::Expr; name = :(gensym(:ReactionSystem)))

# Reads equations.
vars_extracted, add_default_diff, equations = read_equations_options(
options, variables_declared)
options, variables_declared; noinfer = noinfer)
vyudu marked this conversation as resolved.
Show resolved Hide resolved
variables = vcat(variables_declared, vars_extracted)

# Handle independent variables
Expand All @@ -341,13 +347,13 @@ function make_reaction_system(ex::Expr; name = :(gensym(:ReactionSystem)))

# Reads observables.
observed_vars, observed_eqs, obs_syms = read_observed_options(
options, [species_declared; variables], all_ivs)
options, [species_declared; variables], all_ivs; noinfer = noinfer)
vyudu marked this conversation as resolved.
Show resolved Hide resolved

# Collect species and parameters, including ones inferred from the reactions.
declared_syms = Set(Iterators.flatten((parameters_declared, species_declared,
variables)))
species_extracted, parameters_extracted = extract_species_and_parameters!(
reactions, declared_syms)
reactions, declared_syms; noinfer = noinfer)
vyudu marked this conversation as resolved.
Show resolved Hide resolved

species = vcat(species_declared, species_extracted)
parameters = vcat(parameters_declared, parameters_extracted)
Expand Down Expand Up @@ -511,20 +517,23 @@ 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; noinfer = false)
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) && noinfer) && error("Unrecognized variables $(species[1]) detected in reaction expression. Since the flag @no_infer is declared, all species must be explicitly declared with the @species macro.")
end
end

foreach(s -> push!(excluded_syms, s), species)
parameters = OrderedSet{Union{Symbol, Expr}}()
for reaction in reactions
add_syms_from_expr!(parameters, reaction.rate, excluded_syms)
(!isempty(parameters) && noinfer) && error("Unrecognized parameter $(parameters[1]) detected in rate expression $(reaction.rate). Since the flag @no_infer 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) && noinfer) && error("Unrecognized parameters $(parameters[1]) detected in the stoichiometry for reactant $(reactant.reactant). Since the flag @no_infer is declared, all parameters must be explicitly declared with the @parameters macro.")
end
end

Expand Down Expand Up @@ -682,7 +691,7 @@ end
# `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, variables_declared)
function read_equations_options(options, variables_declared; noinfer = 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)
Expand Down Expand Up @@ -711,9 +720,12 @@ function read_equations_options(options, variables_declared)
diff_var = lhs.args[2]
if in(diff_var, forbidden_symbols_error)
error("A forbidden symbol ($(diff_var)) was used as an variable in this differential equation: $eq")
elseif (!in(diff_var, variables_declared)) && noinfer
error("Unrecognized symbol $(diff_var) was used as a variable in this differential equation. Since the @no_infer flag is set, all variables in equations must be explicitly declared via @variables, @species, or @parameters.")
vyudu marked this conversation as resolved.
Show resolved Hide resolved
else
add_default_diff = true
in(diff_var, variables_declared) || push!(vars_extracted, diff_var)
end
add_default_diff = true
in(diff_var, variables_declared) || push!(vars_extracted, diff_var)
end
end

Expand Down Expand Up @@ -752,7 +764,7 @@ function create_differential_expr(options, add_default_diff, used_syms, tiv)
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)
function read_observed_options(options, species_n_vars_declared, ivs_sorted; noinfer = false)
if haskey(options, :observables)
# Gets list of observable equations and prepares variable declaration expression.
# (`options[:observables]` includes `@observables`, `.args[3]` removes this part)
Expand All @@ -763,6 +775,9 @@ function read_observed_options(options, species_n_vars_declared, ivs_sorted)
for (idx, obs_eq) in enumerate(observed_eqs.args)
# Extract the observable, checks errors, and continues the loop if the observable has been declared.
obs_name, ivs, defaults, metadata = find_varinfo_in_declaration(obs_eq.args[2])
if (noinfer && !in(obs_name, species_n_vars_declared))
error("An undeclared variable ($obs_name) was declared as an observable. Since the flag @no_infer is set, all variables must be declared with the @species, @parameters, or @variables macros.")
end
isempty(ivs) ||
error("An observable ($obs_name) was given independent variable(s). These should not be given, as they are inferred automatically.")
isnothing(defaults) ||
Expand Down
69 changes: 69 additions & 0 deletions test/dsl/dsl_options.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1022,3 +1022,72 @@ let
@parameters v n
@test isequal(Catalyst.expand_registered_functions(equations(rn4)[1]), D(A) ~ v*(A^n))
end

### test that @no_infer properly throws errors when undeclared variables are written

let
# Test error when species are inferred
@test_throws LoadError @eval rn = @reaction_network begin
@no_infer
@parameters k
k, A --> B
end
@test_nowarn @eval rn = @reaction_network begin
@no_infer
@species A(t) B(t)
@parameters k
k, A --> B
end

# Test error when a parameter in rate is inferred
@test_throws LoadError @eval rn = @reaction_network begin
@no_infer
@species A(t) B(t)
@parameters k
k*n, A --> B
end
@test_nowarn @eval rn = @reaction_network begin
@no_infer
@parameters n k
@species A(t) B(t)
k*n, A --> B
end

# Test error when a parameter in stoichiometry is inferred
@test_throws LoadError @eval rn = @reaction_network begin
@no_infer
@parameters k
@species A(t) B(t)
k, n*A --> B
end
@test_nowarn @eval rn = @reaction_network begin
@no_infer
@parameters k n
@species A(t) B(t)
k, n*A --> B
end

# Test error when a variable in an equation is inferred
@test_throws LoadError @eval rn = @reaction_network begin
@no_infer
@equations D(V) ~ V^2
end
@test_nowarn @eval rn = @reaction_network begin
@no_infer
@variables V(t)
@equations D(V) ~ V^2
end

# Test error when a variable in an observable is inferred
@test_throws LoadError @eval rn = @reaction_network begin
@no_infer
@variables X1(t)
@observables X2 ~ X1
end
@test_nowarn @eval rn = @reaction_network begin
@no_infer
@variables X1(t) X2(t)
@observables X2 ~ X1
end
end

Loading