Skip to content

Commit 2671197

Browse files
authored
Merge pull request #749 from SciML/reaction_metadata
[v14 ready] Implement metadata for `Reactions`s
2 parents f1157a5 + d488f98 commit 2671197

File tree

9 files changed

+429
-61
lines changed

9 files changed

+429
-61
lines changed

HISTORY.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ assess_identifiability(goodwind_oscillator; measured_quantities=[:M])
1616
to assess (global) structural identifiability for all parameters and variables of the `goodwind_oscillator` model (under the presumption that we can measure `M` only).
1717
- Automatically handles conservation laws for structural identifiability problems (eliminates these internally to speed up computations).
1818
- Adds a tutorial to illustrate the use of the extension.
19+
- Enable adding metadata to individual reactions, e.g:
20+
```julia
21+
rn = @reaction_network begin
22+
@parameters η
23+
k, 2X --> X2, [noise_scaling=η]
24+
end
25+
getnoisescaling(rn)
26+
```
27+
- Changed fields of internal `Reaction` structure. `ReactionSystems`s saved using `serialize` on previous Catalyst versions cannot be loaded using this (or later) versions.
1928
- Simulation of spatial ODEs now supported. For full details, please see https://github.com/SciML/Catalyst.jl/pull/644 and upcoming documentation. Note that these methods are currently considered alpha, with the interface and approach changing even in non-breaking Catalyst releases.
2029
- LatticeReactionSystem structure represents a spatial reaction network:
2130
```julia

src/Catalyst.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export ODEProblem,
6767
SteadyStateProblem
6868

6969
# reaction_network macro
70-
const ExprValues = Union{Expr, Symbol, Float64, Int}
70+
const ExprValues = Union{Expr, Symbol, Float64, Int, Bool}
7171
include("expression_utils.jl")
7272
include("reaction_network.jl")
7373
export @reaction_network, @add_reactions, @reaction, @species

src/expression_utils.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ function tup_leng(ex::ExprValues)
44
return 1
55
end
66

7-
#Gets the ith element in a expression tuple, or returns the input itself if it is not an expression tuple (probably a Symbol/Numerical).
7+
# Gets the ith element in a expression tuple, or returns the input itself if it is not an expression tuple
8+
# (probably a Symbol/Numerical).
89
function get_tup_arg(ex::ExprValues, i::Int)
910
(tup_leng(ex) == 1) && (return ex)
1011
return ex.args[i]

src/reaction_network.jl

Lines changed: 93 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -293,16 +293,17 @@ struct ReactionStruct
293293
substrates::Vector{ReactantStruct}
294294
products::Vector{ReactantStruct}
295295
rate::ExprValues
296-
only_use_rate::Bool
296+
metadata::Expr
297297

298-
function ReactionStruct(sub_line::ExprValues, prod_line::ExprValues, rate::ExprValues,
299-
only_use_rate::Bool)
298+
function ReactionStruct(sub_line::ExprValues, prod_line::ExprValues, rate::ExprValues,
299+
metadata_line::ExprValues)
300300
sub = recursive_find_reactants!(sub_line, 1, Vector{ReactantStruct}(undef, 0))
301301
prod = recursive_find_reactants!(prod_line, 1, Vector{ReactantStruct}(undef, 0))
302-
new(sub, prod, rate, only_use_rate)
302+
metadata = extract_metadata(metadata_line)
303+
new(sub, prod, rate, metadata)
303304
end
304305
end
305-
306+
306307
### Functions rephrasing the macro input as a ReactionSystem structure. ###
307308

308309
function forbidden_variable_check(v)
@@ -445,7 +446,7 @@ function make_reaction(ex::Expr)
445446
pexprs = get_pexpr(parameters, Dict{Symbol, Expr}())
446447
rxexpr = get_rxexprs(reaction)
447448
iv = :(@variables $(DEFAULT_IV_SYM))
448-
449+
449450
# Returns the rephrased expression.
450451
quote
451452
$pexprs
@@ -565,8 +566,8 @@ function get_rxexprs(rxstruct)
565566
prod_init = isempty(rxstruct.products) ? nothing : :([])
566567
prod_stoich_init = deepcopy(prod_init)
567568
reaction_func = :(Reaction($(recursive_expand_functions!(rxstruct.rate)), $subs_init,
568-
$prod_init, $subs_stoich_init, $prod_stoich_init,
569-
only_use_rate = $(rxstruct.only_use_rate)))
569+
$prod_init, $subs_stoich_init, $prod_stoich_init,
570+
metadata = $(rxstruct.metadata),))
570571
for sub in rxstruct.substrates
571572
push!(reaction_func.args[3].args, sub.reactant)
572573
push!(reaction_func.args[5].args, sub.stoichiometry)
@@ -580,70 +581,87 @@ end
580581

581582
### Functions for extracting the reactions from a DSL expression, and putting them ReactionStruct vector. ###
582583

583-
# Reads a line and creates the corresponding ReactionStruct.
584+
# Reads a single line and creates the corresponding ReactionStruct.
584585
function get_reaction(line)
585-
(rate, r_line) = line.args
586-
(r_line.head == :-->) && (r_line = Expr(:call, :, r_line.args[1], r_line.args[2]))
587-
588-
arrow = r_line.args[1]
589-
in(arrow, double_arrows) && error("Double arrows not allowed for single reactions.")
590-
591-
only_use_rate = in(arrow, pure_rate_arrows)
592-
if in(arrow, fwd_arrows)
593-
rs = create_ReactionStruct(r_line.args[2], r_line.args[3], rate, only_use_rate)
594-
elseif in(arrow, bwd_arrows)
595-
rs = create_ReactionStruct(r_line.args[3], r_line.args[2], rate, only_use_rate)
596-
else
597-
throw("Malformed reaction, invalid arrow type used in: $(MacroTools.striplines(line))")
586+
reaction = get_reactions([line])
587+
if (length(reaction) != 1)
588+
error("Malformed reaction. @reaction macro only creates a single reaction. E.g. double arrows, such as `<-->` are not supported.")
598589
end
599-
600-
rs
590+
return reaction[1]
601591
end
592+
602593
# Generates a vector containing a number of reaction structures, each containing the information about one reaction.
603594
function get_reactions(exprs::Vector{Expr}, reactions = Vector{ReactionStruct}(undef, 0))
604595
for line in exprs
605-
(rate, r_line) = line.args
606-
(r_line.head == :-->) && (r_line = Expr(:call, :, r_line.args[1], r_line.args[2]))
596+
# Reads core reaction information.
597+
arrow, rate, reaction, metadata = read_reaction_line(line)
607598

608-
arrow = r_line.args[1]
609-
only_use_rate = in(arrow, pure_rate_arrows)
599+
# Checks the type of arrow used, and creates the corresponding reaction(s). Returns them in an array.
610600
if in(arrow, double_arrows)
611-
(typeof(rate) == Expr && rate.head == :tuple) ||
601+
if typeof(rate) != Expr || rate.head != :tuple
612602
error("Error: Must provide a tuple of reaction rates when declaring a bi-directional reaction.")
613-
push_reactions!(reactions, r_line.args[2], r_line.args[3], rate.args[1],
614-
only_use_rate)
615-
push_reactions!(reactions, r_line.args[3], r_line.args[2], rate.args[2],
616-
only_use_rate)
603+
end
604+
push_reactions!(reactions, reaction.args[2], reaction.args[3], rate.args[1], metadata.args[1], arrow)
605+
push_reactions!(reactions, reaction.args[3], reaction.args[2], rate.args[2], metadata.args[2], arrow)
617606
elseif in(arrow, fwd_arrows)
618-
push_reactions!(reactions, r_line.args[2], r_line.args[3], rate, only_use_rate)
607+
push_reactions!(reactions, reaction.args[2], reaction.args[3], rate, metadata, arrow)
619608
elseif in(arrow, bwd_arrows)
620-
push_reactions!(reactions, r_line.args[3], r_line.args[2], rate, only_use_rate)
609+
push_reactions!(reactions, reaction.args[3], reaction.args[2], rate, metadata, arrow)
621610
else
622611
throw("Malformed reaction, invalid arrow type used in: $(MacroTools.striplines(line))")
623612
end
624613
end
625-
reactions
626-
end
614+
return reactions
615+
end
616+
617+
# Extracts the rate, reaction, and metadata fields (the last one optional) from a reaction line.
618+
function read_reaction_line(line::Expr)
619+
# Handles rate, reaction, and arrow.
620+
# Special routine required for the`-->` case, which creates different expression from all other cases.
621+
rate = line.args[1]
622+
reaction = line.args[2]
623+
(reaction.head == :-->) && (reaction = Expr(:call, :, reaction.args[1], reaction.args[2]))
624+
arrow = reaction.args[1]
625+
626+
# Handles metadata. If not provided, empty metadata is created.
627+
if length(line.args) == 2
628+
metadata = in(arrow, double_arrows) ? :(([], [])) : :([])
629+
elseif length(line.args) == 3
630+
metadata = line.args[3]
631+
else
632+
error("The following reaction line: \"$line\" was malformed. It should have a form \"rate, reaction\" or a form \"rate, reaction, metadata\". It has neither.")
633+
end
627634

628-
# Creates a ReactionStruct from the information in a single line.
629-
function create_ReactionStruct(sub_line::ExprValues, prod_line::ExprValues,
630-
rate::ExprValues, only_use_rate::Bool)
631-
all(==(1), (tup_leng(sub_line), tup_leng(prod_line), tup_leng(rate))) ||
632-
error("Malformed reaction, line appears to be defining multiple reactions incorrectly: rate=$rate, subs=$sub_line, prods=$prod_line.")
633-
ReactionStruct(get_tup_arg(sub_line, 1), get_tup_arg(prod_line, 1),
634-
get_tup_arg(rate, 1), only_use_rate)
635+
return arrow, rate, reaction, metadata
635636
end
636637

637-
#Takes a reaction line and creates reactions from it and pushes those to the reaction array. Used to create multiple reactions from, for instance, 1.0, (X,Y) --> 0.
638-
function push_reactions!(reactions::Vector{ReactionStruct}, sub_line::ExprValues,
639-
prod_line::ExprValues, rate::ExprValues, only_use_rate::Bool)
640-
lengs = (tup_leng(sub_line), tup_leng(prod_line), tup_leng(rate))
641-
for i in 1:maximum(lengs)
642-
(count(lengs .== 1) + count(lengs .== maximum(lengs)) < 3) &&
643-
(throw("Malformed reaction, rate=$rate, subs=$sub_line, prods=$prod_line."))
644-
push!(reactions,
645-
ReactionStruct(get_tup_arg(sub_line, i), get_tup_arg(prod_line, i),
646-
get_tup_arg(rate, i), only_use_rate))
638+
# Takes a reaction line and creates reaction(s) from it and pushes those to the reaction array.
639+
# Used to create multiple reactions from, for instance, `k, (X,Y) --> 0`.
640+
# Handles metadata, e.g. `1.0, Z --> 0, [noisescaling=η]`.
641+
function push_reactions!(reactions::Vector{ReactionStruct}, sub_line::ExprValues, prod_line::ExprValues,
642+
rate::ExprValues, metadata::ExprValues, arrow::Symbol)
643+
# The rates, substrates, products, and metadata may be in a tupple form (e.g. `k, (X,Y) --> 0`).
644+
# This finds the length of these tuples (or 1 if not in tuple forms). Errors if lengs inconsistent.
645+
lengs = (tup_leng(sub_line), tup_leng(prod_line), tup_leng(rate), tup_leng(metadata))
646+
if any(!(leng == 1 || leng == maximum(lengs)) for leng in lengs)
647+
throw("Malformed reaction, rate=$rate, subs=$sub_line, prods=$prod_line, metadata=$metadata.")
648+
end
649+
650+
# Loops through each reaction encoded by the reaction composites. Adds the reaction to the reactions vector.
651+
for i in 1:maximum(lengs)
652+
# If the `only_use_rate` metadata was not provided, this has to be infered from the arrow used.
653+
metadata_i = get_tup_arg(metadata, i)
654+
if all(arg.args[1] != :only_use_rate for arg in metadata_i.args)
655+
push!(metadata_i.args, :(only_use_rate = $(in(arrow, pure_rate_arrows))))
656+
end
657+
658+
# Checks that metadata fields are unqiue.
659+
if !allunique(arg.args[1] for arg in metadata_i.args)
660+
error("Some reaction metadata fields where repeated: $(metadata_entries)")
661+
end
662+
663+
push!(reactions, ReactionStruct(get_tup_arg(sub_line, i), get_tup_arg(prod_line, i),
664+
get_tup_arg(rate, i), metadata_i))
647665
end
648666
end
649667

@@ -687,6 +705,17 @@ function recursive_find_reactants!(ex::ExprValues, mult::ExprValues,
687705
reactants
688706
end
689707

708+
# Finds the metadata from a metadata expresion (`[key=val, ...]`) and returns as a Vector{Pair{Symbol,ExprValues}}.
709+
function extract_metadata(metadata_line::Expr)
710+
metadata = :([])
711+
for arg in metadata_line.args
712+
(arg.head != :(=)) && error("Malformatted metadata line: $metadata_line. Each entry in the vector should contain a `=`.")
713+
(arg.args[1] isa Symbol) || error("Malformatted metadata entry: $arg. Entries left-hand-side should be a single symbol.")
714+
push!(metadata.args, :($(QuoteNode(arg.args[1])) => $(arg.args[2])))
715+
end
716+
return metadata
717+
end
718+
690719
### DSL Options Handling ###
691720
# Most options handled in previous sections, when code re-organised, these should ideally be moved to the same place.
692721

@@ -888,3 +917,13 @@ end
888917
# end
889918
# return defaults
890919
# end
920+
921+
922+
# # Creates a ReactionStruct from the information in a single line.
923+
# function create_ReactionStruct(sub_line::ExprValues, prod_line::ExprValues,
924+
# rate::ExprValues, only_use_rate::Bool)
925+
# all(==(1), (tup_leng(sub_line), tup_leng(prod_line), tup_leng(rate))) ||
926+
# error("Malformed reaction, line appears to be defining multiple reactions incorrectly: rate=$rate, subs=$sub_line, prods=$prod_line.")
927+
# ReactionStruct(get_tup_arg(sub_line, 1), get_tup_arg(prod_line, 1),
928+
# get_tup_arg(rate, 1), only_use_rate)
929+
# end

src/reactionsystem.jl

Lines changed: 113 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ struct Reaction{S, T}
116116
`true` if `rate` represents the full reaction rate law.
117117
"""
118118
only_use_rate::Bool
119+
"""
120+
Contain additional data, such whenever the reaction have a specific noise-scaling expression for
121+
the chemical Langevin equation.
122+
"""
123+
metadata::Vector{Pair{Symbol, Any}}
119124
end
120125

121126
"""
@@ -126,8 +131,8 @@ Test if a species is valid as a reactant (i.e. a species variable or a constant
126131
isvalidreactant(s) = MT.isparameter(s) ? isconstant(s) : (isspecies(s) && !isconstant(s))
127132

128133
function Reaction(rate, subs, prods, substoich, prodstoich;
129-
netstoich = nothing, only_use_rate = false,
130-
kwargs...)
134+
netstoich = nothing, metadata = Pair{Symbol, Any}[],
135+
only_use_rate = metadata_only_use_rate_check(metadata), kwargs...)
131136
(isnothing(prods) && isnothing(subs)) &&
132137
throw(ArgumentError("A reaction requires a non-nothing substrate or product vector."))
133138
(isnothing(prodstoich) && isnothing(substoich)) &&
@@ -181,7 +186,27 @@ function Reaction(rate, subs, prods, substoich, prodstoich;
181186
convert.(stoich_type, netstoich) : netstoich
182187
end
183188

184-
Reaction(value(rate), subs, prods, substoich′, prodstoich′, ns, only_use_rate)
189+
# Check that all metadata entries are unique. (cannot use `in` since some entries may be symbolics).
190+
if !allunique(entry[1] for entry in metadata)
191+
error("Repeated entries for the same metadata encountered in the following metadata set: $([entry[1] for entry in metadata]).")
192+
end
193+
194+
# Deletes potential `:only_use_rate => ` entries from the metadata.
195+
if any(:only_use_rate == entry[1] for entry in metadata)
196+
deleteat!(metadata, findfirst(:only_use_rate == entry[1] for entry in metadata))
197+
end
198+
199+
# Ensures metadata have the correct type.
200+
metadata = convert(Vector{Pair{Symbol, Any}}, metadata)
201+
202+
Reaction(value(rate), subs, prods, substoich′, prodstoich′, ns, only_use_rate, metadata)
203+
end
204+
205+
# Checks if a metadata input has an entry :only_use_rate => true
206+
function metadata_only_use_rate_check(metadata)
207+
only_use_rate_idx = findfirst(:only_use_rate == entry[1] for entry in metadata)
208+
isnothing(only_use_rate_idx) && return true
209+
return Bool(metadata[only_use_rate_idx][2])
185210
end
186211

187212
# three argument constructor assumes stoichiometric coefs are one and integers
@@ -241,7 +266,7 @@ function ModelingToolkit.namespace_equation(rx::Reaction, name; kw...)
241266
ns = similar(rx.netstoich)
242267
map!(n -> f(n[1]) => f(n[2]), ns, rx.netstoich)
243268
end
244-
Reaction(rate, subs, prods, substoich, prodstoich, netstoich, rx.only_use_rate)
269+
Reaction(rate, subs, prods, substoich, prodstoich, netstoich, rx.only_use_rate, rx.metadata)
245270
end
246271

247272
netstoich_stoichtype(::Vector{Pair{S, T}}) where {S, T} = T
@@ -836,6 +861,90 @@ function get_indep_sts(rs::ReactionSystem, remove_conserved = false)
836861
indepsts, filter(isspecies, indepsts)
837862
end
838863

864+
######################## Other accessors ##############################
865+
866+
"""
867+
getnoisescaling(reaction::Reaction)
868+
869+
Returns the noise scaling associated with a specific reaction. If the `:noise_scaling` metadata has
870+
set, returns that. Else, returns `1.0`.
871+
872+
Arguments:
873+
- `reaction`: The reaction for which we wish to retrive all metadata.
874+
875+
Example:
876+
```julia
877+
reaction = @reaction k, 0 --> X, [noise_scaling=0.0]
878+
getnoisescaling(reaction)
879+
"""
880+
function getnoisescaling(reaction::Reaction)
881+
has_metadata(reaction, :noise_scaling) && (return get_metadata(reaction, :noise_scaling))
882+
return 1.0
883+
end
884+
885+
886+
# These are currently considered internal, but can be used by public accessor functions like getnoisescaling.
887+
888+
"""
889+
get_metadata_dict(reaction::Reaction)
890+
891+
Retrives the `ImmutableDict` containing all of the metadata associated with a specific reaction.
892+
893+
Arguments:
894+
- `reaction`: The reaction for which we wish to retrive all metadata.
895+
896+
Example:
897+
```julia
898+
reaction = @reaction k, 0 --> X, [noise_scaling=0.0]
899+
get_metadata_dict(reaction)
900+
```
901+
"""
902+
function get_metadata_dict(reaction::Reaction)
903+
return reaction.metadata
904+
end
905+
906+
"""
907+
has_metadata(reaction::Reaction, md_key::Symbol)
908+
909+
Checks if a `Reaction` have a certain metadata field. If it does, returns `true` (else returns `false`).
910+
911+
Arguments:
912+
- `reaction`: The reaction for which we wish to check if a specific metadata field exist.
913+
- `md_key`: The metadata for which we wish to check existence of.
914+
915+
Example:
916+
```julia
917+
reaction = @reaction k, 0 --> X, [noise_scaling=0.0]
918+
has_metadata(reaction, :noise_scaling)
919+
```
920+
"""
921+
function has_metadata(reaction::Reaction, md_key::Symbol)
922+
return any(isequal(md_key, entry[1]) for entry in get_metadata_dict(reaction))
923+
end
924+
925+
"""
926+
get_metadata(reaction::Reaction, md_key::Symbol)
927+
928+
Retrives a certain metadata value from a `Reaction`. If the metadata does not exists, throws an error.
929+
930+
Arguments:
931+
- `reaction`: The reaction for which we wish to retrive a specific metadata value.
932+
- `md_key`: The metadata for which we wish to retrive.
933+
934+
Example:
935+
```julia
936+
reaction = @reaction k, 0 --> X, [noise_scaling=0.0]
937+
get_metadata(reaction, :noise_scaling)
938+
```
939+
"""
940+
function get_metadata(reaction::Reaction, md_key::Symbol)
941+
if !has_metadata(reaction, md_key)
942+
error("The reaction does not have a metadata field $md_key. It does have the following metadata fields: $(keys(get_metadata_dict(reaction))).")
943+
end
944+
metadata = get_metadata_dict(reaction)
945+
return metadata[findfirst(isequal(md_key, entry[1]) for entry in get_metadata_dict(reaction))][2]
946+
end
947+
839948
######################## Conversion to ODEs/SDEs/jump, etc ##############################
840949

841950
"""

0 commit comments

Comments
 (0)