Skip to content

Commit fadeb67

Browse files
committed
Merge branch 'master' into src___refactoring___dsl_file
2 parents a1e967a + 127c49c commit fadeb67

File tree

8 files changed

+341
-170
lines changed

8 files changed

+341
-170
lines changed

HISTORY.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@
77
(at the time the release is made). If you need a dependency version increased,
88
please open an issue and we can update it and make a new Catalyst release once
99
testing against the newer dependency version is complete.
10+
- New formula for inferring variables from equations (declared using the `@equations` options) in the DSL. The order of inference of species/variables/parameters is now:
11+
(1) Every symbol explicitly declared using `@species`, `@variables`, and `@parameters` are assigned to the correct category.
12+
(2) Every symbol used as a reaction reactant is inferred as a species.
13+
(3) Every symbol not declared in (1) or (2) that occurs in an expression provided after `@equations` is inferred as a variable.
14+
(4) Every symbol not declared in (1), (2), or (3) that occurs either as a reaction rate or stoichiometric coefficient is inferred to be a parameter.
15+
E.g. in
16+
```julia
17+
@reaction_network begin
18+
@equations V1 + S ~ V2^2
19+
(p + S + V1), S --> 0
20+
end
21+
```
22+
`S` is inferred as a species, `V1` and `V2` as variables, and `p` as a parameter. The previous special cases for the `@observables`, `@compounds`, and `@differentials` options still hold. Finally, the `@require_declaration` options (described in more detail below) can now be used to require everything to be explicitly declared.
23+
- New formula for determining whether the default differentials have been used within an `@equations` option. Now, if any expression `D(...)` is encountered (where `...` can be anything), this is inferred as usage of the default differential D. E.g. in the following equations `D` is inferred as a differential with respect to the default independent variable:
24+
```julia
25+
@reaction_network begin
26+
@equations D(V) + V ~ 1
27+
end
28+
@reaction_network begin
29+
@equations D(D(V)) ~ 1
30+
end
31+
```
32+
Please note that this cannot be used at the same time as `D` is used to represent a species, variable, or parameter (including is these are implicitly designated as such by e.g. appearing as a reaction reactant).
1033
- Array symbolics support is more consistent with ModelingToolkit v9. Parameter
1134
arrays are no longer scalarized by Catalyst, while species and variables
1235
arrays still are (as in ModelingToolkit). As such, parameter arrays should now

docs/src/inverse_problems/examples/ode_fitting_oscillation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ function optimise_p(pinit, tend)
5656
newprob = remake(prob; tspan = (0.0, tend), p = p)
5757
sol = Array(solve(newprob, Rosenbrock23(); saveat = newtimes))
5858
loss = sum(abs2, sol .- sample_vals[:, 1:size(sol,2)])
59-
return loss, sol
59+
return loss
6060
end
6161
6262
# optimize for the parameters that minimize the loss

src/dsl.jl

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,10 @@ function make_reaction_system(ex::Expr, name)
354354
default_reaction_metadata = read_default_noise_scaling_option(options)
355355
combinatoric_ratelaws = read_combinatoric_ratelaws_option(options)
356356

357+
# Reads observables.
358+
observed_vars, observed_eqs, obs_syms = read_observed_options(
359+
options, [species_declared; variables], all_ivs; requiredec)
360+
357361
# Checks for input errors.
358362
forbidden_symbol_check(union(species, parameters))
359363
forbidden_variable_check(variables)
@@ -732,7 +736,7 @@ end
732736
# `vars_extracted`: A vector with extracted variables (lhs in pure differential equations only).
733737
# `dtexpr`: If a differential equation is defined, the default derivative (D ~ Differential(t)) must be defined.
734738
# `equations`: a vector with the equations provided.
735-
function read_equations_options(options, variables_declared; requiredec = false)
739+
function read_equations_options(options, syms_declared, parameters_extracted; requiredec = false)
736740
# Prepares the equations. First, extracts equations from provided option (converting to block form if required).
737741
# Next, uses MTK's `parse_equations!` function to split input into a vector with the equations.
738742
eqs_input = haskey(options, :equations) ? options[:equations].args[3] : :(begin end)
@@ -744,34 +748,40 @@ function read_equations_options(options, variables_declared; requiredec = false)
744748
# Loops through all equations, checks for lhs of the form `D(X) ~ ...`.
745749
# When this is the case, the variable X and differential D are extracted (for automatic declaration).
746750
# Also performs simple error checks.
747-
vars_extracted = Vector{Symbol}()
751+
vars_extracted = OrderedSet{Union{Symbol, Expr}}()
748752
add_default_diff = false
749753
for eq in equations
750754
if (eq.head != :call) || (eq.args[1] != :~)
751755
error("Malformed equation: \"$eq\". Equation's left hand and right hand sides should be separated by a \"~\".")
752756
end
753757

754-
# Checks if the equation have the format D(X) ~ ... (where X is a symbol). This means that the
755-
# default differential has been used. X is added as a declared variable to the system, and
756-
# we make a note that a differential D = Differential(iv) should be made as well.
757-
lhs = eq.args[2]
758-
# if lhs: is an expression. Is a function call. The function's name is D. Calls a single symbol.
759-
if (lhs isa Expr) && (lhs.head == :call) && (lhs.args[1] == :D) &&
760-
(lhs.args[2] isa Symbol)
761-
diff_var = lhs.args[2]
762-
if in(diff_var, forbidden_symbols_error)
763-
error("A forbidden symbol ($(diff_var)) was used as an variable in this differential equation: $eq")
764-
elseif (!in(diff_var, variables_declared)) && requiredec
765-
throw(UndeclaredSymbolicError(
766-
"Unrecognized symbol $(diff_var) was used as a variable in an equation: \"$eq\". Since the @require_declaration flag is set, all variables in equations must be explicitly declared via @variables, @species, or @parameters."))
767-
else
768-
add_default_diff = true
769-
in(diff_var, variables_declared) || push!(vars_extracted, diff_var)
770-
end
758+
# If the default differential (`D`) is used, record that it should be decalred later on.
759+
if (:D union(syms_declared, parameters_extracted)) && find_D_call(eq)
760+
requiredec && throw(UndeclaredSymbolicError(
761+
"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."))
762+
add_default_diff = true
763+
push!(syms_declared, :D)
771764
end
765+
766+
# Any undecalred symbolic variables encountered should be extracted as variables.
767+
add_syms_from_expr!(vars_extracted, eq, syms_declared)
768+
(!isempty(vars_extracted) && requiredec) && throw(UndeclaredSymbolicError(
769+
"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."))
772770
end
773771

774-
return vars_extracted, add_default_diff, equations
772+
return collect(vars_extracted), add_default_diff, equations
773+
end
774+
775+
# Searches an expresion `expr` and returns true if it have any subexpression `D(...)` (where `...` can be anything).
776+
# Used to determine whether the default differential D has been used in any equation provided to `@equations`.
777+
function find_D_call(expr)
778+
return if Base.isexpr(expr, :call) && expr.args[1] == :D
779+
true
780+
elseif expr isa Expr
781+
any(find_D_call, expr.args)
782+
else
783+
false
784+
end
775785
end
776786

777787
# Creates an expression declaring differentials. Here, `tiv` is the time independent variables,

src/network_analysis.jl

Lines changed: 76 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,6 @@ function laplacianmat(rn::ReactionSystem, pmap::Dict = Dict(); sparse = false)
206206
D * K
207207
end
208208

209-
Base.zero(::Type{Union{R, Symbolics.BasicSymbolic{Real}}}) where R <: Real = zero(R)
210-
Base.one(::Type{Union{R, Symbolics.BasicSymbolic{Real}}}) where R <: Real = one(R)
211-
212209
@doc raw"""
213210
fluxmat(rn::ReactionSystem, pmap = Dict(); sparse=false)
214211
@@ -218,15 +215,20 @@ Base.one(::Type{Union{R, Symbolics.BasicSymbolic{Real}}}) where R <: Real = one(
218215
**Warning**: Unlike other Catalyst functions, the `fluxmat` function will return a `Matrix{Num}` in the symbolic case. This is to allow easier computation of the matrix decomposition of the ODEs, and to ensure that multiplying the sparse form of the matrix will work.
219216
"""
220217
function fluxmat(rn::ReactionSystem, pmap::Dict = Dict(); sparse=false)
218+
deps = Set()
219+
for (i, rx) in enumerate(reactions(rn))
220+
empty!(deps)
221+
get_variables!(deps, rx.rate, species(rn))
222+
(!isempty(deps)) && (error("Reaction $rx's rate constant depends on species $(join(deps, ", ")). `fluxmat` cannot support rate constants of this form."))
223+
end
224+
221225
rates = if isempty(pmap)
222226
reactionrates(rn)
223227
else
224228
substitutevals(rn, pmap, parameters(rn), reactionrates(rn))
225229
end
226230

227231
rcmap = reactioncomplexmap(rn)
228-
nc = length(rcmap)
229-
nr = length(rates)
230232
mtype = eltype(rates) <: Symbolics.BasicSymbolic ? Num : eltype(rates)
231233
if sparse
232234
return fluxmat(SparseMatrixCSC{mtype, Int}, rcmap, rates)
@@ -295,6 +297,10 @@ function massactionvector(rn::ReactionSystem, scmap::Dict = Dict(); combinatoric
295297
rxs = reactions(rn)
296298
sm = speciesmap(rn)
297299

300+
for rx in rxs
301+
!ismassaction(rx, rn) && error("The supplied ReactionSystem has non-mass action reaction $rx. The `massactionvector` can only be constructed for mass action networks.")
302+
end
303+
298304
specs = if isempty(scmap)
299305
species(rn)
300306
else
@@ -936,7 +942,7 @@ function isdetailedbalanced(rs::ReactionSystem, parametermap::Dict; abstol=0, re
936942
complexes, D = reactioncomplexes(rs)
937943
img = incidencematgraph(rs)
938944
undir_img = SimpleGraph(incidencematgraph(rs))
939-
K = ratematrix(rs, pmap)
945+
K = adjacencymat(rs, pmap)
940946

941947
spanning_forest = Graphs.kruskal_mst(undir_img)
942948
outofforest_edges = setdiff(collect(edges(undir_img)), spanning_forest)
@@ -993,52 +999,25 @@ function isdetailedbalanced(rs::ReactionSystem, parametermap)
993999
end
9941000

9951001
"""
996-
iscomplexbalanced(rs::ReactionSystem, parametermap)
1002+
iscomplexbalanced(rs::ReactionSystem, parametermap; sparse = false)
9971003
9981004
Constructively compute whether a network will have complex-balanced equilibrium
9991005
solutions, following the method in van der Schaft et al., [2015](https://link.springer.com/article/10.1007/s10910-015-0498-2#Sec3).
1000-
Accepts a dictionary, vector, or tuple of variable-to-value mappings, e.g. [k1 => 1.0, k2 => 2.0,...].
1001-
"""
1002-
function iscomplexbalanced(rs::ReactionSystem, parametermap::Dict)
1003-
if length(parametermap) != numparams(rs)
1004-
error("Incorrect number of parameters specified.")
1005-
end
1006-
1007-
pmap = symmap_to_varmap(rs, parametermap)
1008-
pmap = Dict(ModelingToolkit.value(k) => v for (k, v) in pmap)
10091006
1010-
sm = speciesmap(rs)
1011-
cm = reactioncomplexmap(rs)
1012-
complexes, D = reactioncomplexes(rs)
1007+
Requires the specification of values for the parameters/rate constants. Accepts a dictionary, vector, or tuple of parameter-to-value mappings, e.g. [k1 => 1.0, k2 => 2.0,...].
1008+
"""
1009+
function iscomplexbalanced(rs::ReactionSystem, parametermap::Dict; sparse = false)
10131010
rxns = reactions(rs)
1014-
nc = length(complexes)
1015-
nr = numreactions(rs)
1016-
nm = numspecies(rs)
1017-
10181011
if !all(r -> ismassaction(r, rs), rxns)
10191012
error("The supplied ReactionSystem has reactions that are not ismassaction. Testing for being complex balanced is currently only supported for pure mass action networks.")
10201013
end
10211014

1022-
rates = [substitute(rate, pmap) for rate in reactionrates(rs)]
1023-
1024-
# Construct kinetic matrix, K
1025-
K = zeros(nr, nc)
1026-
for c in 1:nc
1027-
complex = complexes[c]
1028-
for (r, dir) in cm[complex]
1029-
rxn = rxns[r]
1030-
if dir == -1
1031-
K[r, c] = rates[r]
1032-
end
1033-
end
1034-
end
1035-
1036-
L = -D * K
1037-
S = netstoichmat(rs)
1015+
D = incidencemat(rs; sparse)
1016+
S = netstoichmat(rs; sparse)
10381017

10391018
# Compute ρ using the matrix-tree theorem
10401019
g = incidencematgraph(rs)
1041-
R = ratematrix(rs, rates)
1020+
R = adjacencymat(rs, parametermap; sparse)
10421021
ρ = matrixtree(g, R)
10431022

10441023
# Determine if 1) ρ is positive and 2) D^T Ln ρ lies in the image of S^T
@@ -1069,50 +1048,81 @@ function iscomplexbalanced(rs::ReactionSystem, parametermap)
10691048
end
10701049

10711050
"""
1072-
ratematrix(rs::ReactionSystem, parametermap)
1051+
adjacencymat(rs::ReactionSystem, pmap = Dict(); sparse = false)
10731052
10741053
Given a reaction system with n complexes, outputs an n-by-n matrix where R_{ij} is the rate
10751054
constant of the reaction between complex i and complex j. Accepts a dictionary, vector, or tuple
10761055
of variable-to-value mappings, e.g. [k1 => 1.0, k2 => 2.0,...].
1056+
1057+
Equivalent to the adjacency matrix of the weighted graph whose weights are the
1058+
reaction rates.
1059+
Returns a symbolic matrix by default, but will return a numerical matrix if parameter values are specified via pmap.
1060+
1061+
The difference between this matrix and [`fluxmat`](@ref) is quite subtle. The non-zero entries to both matrices are the rate constants. However:
1062+
- In `fluxmat`, the rows represent complexes and columns represent reactions, and an entry (c, r) is non-zero if reaction r's source complex is c.
1063+
- In `adjacencymat`, the rows and columns both represent complexes, and an entry (c1, c2) is non-zero if there is a reaction c1 --> c2.
10771064
"""
1078-
function ratematrix(rs::ReactionSystem, rates::Vector{Float64})
1079-
complexes, D = reactioncomplexes(rs)
1080-
n = length(complexes)
1081-
rxns = reactions(rs)
1082-
ratematrix = zeros(n, n)
1065+
function adjacencymat(rn::ReactionSystem, pmap::Dict = Dict(); sparse = false)
1066+
deps = Set()
1067+
for (i, rx) in enumerate(reactions(rn))
1068+
empty!(deps)
1069+
get_variables!(deps, rx.rate, species(rn))
1070+
(!isempty(deps)) && (error("Reaction $rx's rate constant depends on species $(join(deps, ", ")). `adjacencymat` cannot support rate constants of this form."))
1071+
end
10831072

1084-
for r in 1:length(rxns)
1085-
rxn = rxns[r]
1086-
s = findfirst(==(-1), @view D[:, r])
1087-
p = findfirst(==(1), @view D[:, r])
1088-
ratematrix[s, p] = rates[r]
1073+
rates = if isempty(pmap)
1074+
reactionrates(rn)
1075+
else
1076+
substitutevals(rn, pmap, parameters(rn), reactionrates(rn))
1077+
end
1078+
mtype = eltype(rates) <: Symbolics.BasicSymbolic ? Num : eltype(rates)
1079+
1080+
if sparse
1081+
return adjacencymat(SparseMatrixCSC{mtype, Int}, incidencemat(rn), rates)
1082+
else
1083+
return adjacencymat(Matrix{mtype}, incidencemat(rn), rates)
10891084
end
1090-
ratematrix
10911085
end
10921086

1093-
function ratematrix(rs::ReactionSystem, parametermap::Dict)
1094-
if length(parametermap) != numparams(rs)
1095-
error("Incorrect number of parameters specified.")
1087+
function adjacencymat(::Type{SparseMatrixCSC{T, Int}}, D, rates) where T
1088+
Is = Int[]
1089+
Js = Int[]
1090+
Vs = T[]
1091+
nc = size(D, 1)
1092+
1093+
for r in 1:length(rates)
1094+
s = findfirst(==(-1), @view D[:, r])
1095+
p = findfirst(==(1), @view D[:, r])
1096+
push!(Is, s)
1097+
push!(Js, p)
1098+
push!(Vs, rates[r])
10961099
end
1100+
A = sparse(Is, Js, Vs, nc, nc)
1101+
end
10971102

1098-
pmap = symmap_to_varmap(rs, parametermap)
1099-
pmap = Dict(ModelingToolkit.value(k) => v for (k, v) in pmap)
1103+
function adjacencymat(::Type{Matrix{T}}, D, rates) where T
1104+
nc = size(D, 1)
1105+
A = zeros(T, nc, nc)
11001106

1101-
rates = [substitute(rate, pmap) for rate in reactionrates(rs)]
1102-
ratematrix(rs, rates)
1107+
for r in 1:length(rates)
1108+
s = findfirst(==(-1), @view D[:, r])
1109+
p = findfirst(==(1), @view D[:, r])
1110+
A[s, p] = rates[r]
1111+
end
1112+
A
11031113
end
11041114

1105-
function ratematrix(rs::ReactionSystem, parametermap::Vector{<:Pair})
1106-
pdict = Dict(parametermap)
1107-
ratematrix(rs, pdict)
1115+
function adjacencymat(rn::ReactionSystem, pmap::Vector{<:Pair}; sparse=false)
1116+
pdict = Dict(pmap)
1117+
adjacencymat(rn, pdict; sparse)
11081118
end
11091119

1110-
function ratematrix(rs::ReactionSystem, parametermap::Tuple)
1111-
pdict = Dict(parametermap)
1112-
ratematrix(rs, pdict)
1120+
function adjacencymat(rn::ReactionSystem, pmap::Tuple; sparse=false)
1121+
pdict = Dict(pmap)
1122+
adjacencymat(rn, pdict; sparse)
11131123
end
11141124

1115-
function ratematrix(rs::ReactionSystem, parametermap)
1125+
function adjacencymat(rn::ReactionSystem, pmap)
11161126
error("Parameter map must be a dictionary, tuple, or vector of symbol/value pairs.")
11171127
end
11181128

0 commit comments

Comments
 (0)