Skip to content

Commit 28f104c

Browse files
authored
Merge pull request #857 from SciML/save_rs_to_file
Serialise `ReactionSystem` to file.
2 parents 457082b + 6197abf commit 28f104c

File tree

9 files changed

+1460
-0
lines changed

9 files changed

+1460
-0
lines changed

src/Catalyst.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,12 @@ include("spatial_reaction_systems/utility.jl")
182182
include("spatial_reaction_systems/spatial_ODE_systems.jl")
183183
include("spatial_reaction_systems/lattice_jump_systems.jl")
184184

185+
186+
### ReactionSystem Serialisation ###
187+
# Has to be at the end (because it uses records of all metadata declared by Catalyst).
188+
include("reactionsystem_serialisation/serialisation_support.jl")
189+
include("reactionsystem_serialisation/serialise_fields.jl")
190+
include("reactionsystem_serialisation/serialise_reactionsystem.jl")
191+
export save_reactionsystem
192+
185193
end # module

src/reactionsystem.jl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,13 @@ end
207207

208208
### ReactionSystem Structure ###
209209

210+
# Constant storing all reaction system fields (in order). Used to check whether the `ReactionSystem`
211+
# structure have been updated (in the `reactionsystem_uptodate_check` function).
212+
const reactionsystem_fields = (:eqs, :rxs, :iv, :sivs, :unknowns, :species, :ps, :var_to_name,
213+
:observed, :name, :systems, :defaults, :connection_type,
214+
:networkproperties, :combinatoric_ratelaws, :continuous_events,
215+
:discrete_events, :metadata, :complete)
216+
210217
"""
211218
$(TYPEDEF)
212219
@@ -1025,6 +1032,16 @@ end
10251032

10261033
### General `ReactionSystem`-specific Functions ###
10271034

1035+
# Checks if the `ReactionSystem` structure have been updated without also updating the
1036+
# `reactionsystem_fields` constant. If this is the case, returns `false`. This is used in
1037+
# certain functionalities which would break if the `ReactionSystem` structure is updated without
1038+
# also updating tehse functionalities.
1039+
function reactionsystem_uptodate_check()
1040+
if fieldnames(ReactionSystem) != reactionsystem_fields
1041+
@warn "The `ReactionSystem` strcuture have been modified without this being taken into account in the functionality you are attempting to use. Please report this at https://github.com/SciML/Catalyst.jl/issues. Proceed with cautioun, as there might be errors in whichever funcionality you are attempting to use."
1042+
end
1043+
end
1044+
10281045
# used in the `__unpacksys` function.
10291046
function __unpacksys(rn)
10301047
ex = :(begin end)
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
## String Handling ###
2+
3+
# Appends stuff to a string.
4+
# E.g `@string_append! str_base str1 str2` becomes `str_base = str_base * str1 * str2`.
5+
macro string_append!(string, inputs...)
6+
rhs = :($string * $(inputs[1]))
7+
for input in inputs[2:end]
8+
push!(rhs.args, input)
9+
end
10+
return esc(:($string = $rhs))
11+
end
12+
13+
# Prepends stuff to a string. Can only take 1 or 2 inputs.
14+
# E.g `@string_prepend! str1 str_base` becomes `str_base = str1 * str_base`.
15+
macro string_prepend!(input, string)
16+
rhs = :($input * $string)
17+
return esc(:($string = $rhs))
18+
end
19+
macro string_prepend!(input1, input2, string)
20+
rhs = :($input1 * $input2 * $string)
21+
return esc(:($string = $rhs))
22+
end
23+
24+
# Gets the character at a specific index.
25+
get_char(str, idx) = collect(str)[idx]
26+
get_char_end(str, offset) = collect(str)[end+offset]
27+
# Gets a substring (which is robust to unicode characters like η).
28+
get_substring(str, idx1, idx2) = String(collect(str)[idx1:idx2])
29+
get_substring_end(str, idx1, offset) = String(collect(str)[idx1:end+offset])
30+
31+
32+
### Field Serialisation Support Functions ###
33+
34+
# Function which handles the addition of a single component to the file string.
35+
function push_field(file_text::String, rn::ReactionSystem, annotate::Bool, top_level::Bool, comp_funcs::Tuple)
36+
has_component, get_comp_string, get_comp_annotation = comp_funcs
37+
has_component(rn) || (return (file_text, false))
38+
39+
# Prepares the text creating the field. For non-top level systems, adds `local `. Observables
40+
# must be handled differently (as the declaration is not at the beginning of the code for these).
41+
# The independent variables is not declared as a variable, and also should not have a `1ocal `.
42+
write_string = get_comp_string(rn)
43+
if !(top_level || comp_funcs == IV_FS)
44+
if comp_funcs == OBSERVED_FS
45+
write_string = replace(write_string, "\nobserved = [" => "\nlocal observed = [")
46+
else
47+
@string_prepend! "local " write_string
48+
end
49+
end
50+
@string_prepend! "\n" write_string
51+
52+
# Adds (potential) annotation. Returns the expanded file text, and a Bool that this field was added.
53+
annotate && (@string_prepend! "\n\n# " get_comp_annotation(rn) write_string)
54+
return (file_text * write_string, true)
55+
end
56+
57+
# Generic function for creating an string for an unsupported argument.
58+
function get_unsupported_comp_string(component::String)
59+
@warn "Writing ReactionSystem models with $(component) is currently not supported. This field is not written to the file."
60+
return ""
61+
end
62+
63+
# Generic function for creating the annotation string for an unsupported argument.
64+
function get_unsupported_comp_annotation(component::String)
65+
return "$(component): (OBS: Currently not supported, and hence empty)"
66+
end
67+
68+
69+
### String Conversion ###
70+
71+
# Converts a numeric expression (e.g. p*X + 2Y) to a string (e.g. "p*X + 2Y"). Also ensures that for
72+
# any variables (e.g. X(t)) the call part is stripped, and only variable name (e.g. X) is written.
73+
function expression_2_string(expr; strip_call_dict = make_strip_call_dict(Symbolics.get_variables(expr)))
74+
strip_called_expr = substitute(expr, strip_call_dict)
75+
return repr(strip_called_expr)
76+
end
77+
78+
# Converts a vector of symbolics (e.g. the species or parameter vectors) to a string vector. Strips
79+
# any calls (e.g. X(t) becomes X). E.g. a species vector [X, Y, Z] is converted to "[X, Y, Z]".
80+
function syms_2_strings(syms)
81+
strip_called_syms = [strip_call(Symbolics.unwrap(sym)) for sym in syms]
82+
return get_substring_end("$(convert(Vector{Any}, strip_called_syms))", 4, 0)
83+
end
84+
85+
# Converts a vector of symbolics (e.g. the species or parameter vectors) to a string corresponding to
86+
# the code required to declare them (potential @parameters or @species commands must still be added).
87+
# The `multiline_format` option formats it with a `begin ... end` block and declarations on separate lines.
88+
function syms_2_declaration_string(syms; multiline_format = false)
89+
decs_string = (multiline_format ? " begin" : "")
90+
for sym in syms
91+
delimiter = (multiline_format ? "\n\t" : " ")
92+
@string_append! decs_string delimiter sym_2_declaration_string(sym; multiline_format)
93+
end
94+
multiline_format && (@string_append! decs_string "\nend")
95+
return decs_string
96+
end
97+
98+
# Converts a symbolic (e.g. a species or parameter) to a string corresponding to how it would be declared
99+
# in code. Takes default values and metadata into account. Example output "p=2.0 [bounds=(0.0, 1.0)]".
100+
# The `multiline_format` option formats the string as if it is part of a `begin .. end` block.
101+
function sym_2_declaration_string(sym; multiline_format = false)
102+
# Creates the basic symbol. The `"$(sym)"` ensures that we get e.g. "X(t)" and not "X".
103+
dec_string = "$(sym)"
104+
105+
# If the symbol have a non-default type, appends the declaration of this.
106+
# Assumes that the type is on the form `SymbolicUtils.BasicSymbolic{X}`. Contain error checks
107+
# to ensure that this is the case.
108+
if !(sym isa SymbolicUtils.BasicSymbolic{Real})
109+
sym_type = String(Symbol(typeof(Symbolics.unwrap(sym))))
110+
if (get_substring(sym_type, 1, 28) != "SymbolicUtils.BasicSymbolic{") || (get_char_end(sym_type, 0) != '}')
111+
error("Encountered symbolic of unexpected type: $sym_type.")
112+
end
113+
@string_append! dec_string "::" get_substring_end(sym_type, 29, -1)
114+
end
115+
116+
# If there is a default value, adds this to the declaration.
117+
if ModelingToolkit.hasdefault(sym)
118+
def_val = x_2_string(ModelingToolkit.getdefault(sym))
119+
separator = (multiline_format ? " = " : "=")
120+
@string_append! dec_string separator "$(def_val)"
121+
end
122+
123+
# Adds any metadata to the declaration.
124+
metadata_to_declare = get_metadata_to_declare(sym)
125+
if !isempty(metadata_to_declare)
126+
metadata_string = (multiline_format ? ", [" : " [")
127+
for metadata in metadata_to_declare
128+
@string_append! metadata_string metadata_2_string(sym, metadata) ", "
129+
end
130+
@string_append! dec_string get_substring_end(metadata_string, 1, -2) "]"
131+
end
132+
133+
# Returns the declaration entry for the symbol.
134+
return dec_string
135+
end
136+
137+
# Converts a generic value to a String. Handles each type of value separately. Unsupported values might
138+
# not necessarily generate valid code, and hence throw errors. Primarily used to write default values
139+
# and metadata values (which hopefully almost exclusively) has simple, supported, types. Ideally,
140+
# more supported types can be added here.
141+
x_2_string(x::Num) = expression_2_string(x)
142+
x_2_string(x::SymbolicUtils.BasicSymbolic{<:Real}) = expression_2_string(x)
143+
x_2_string(x::Bool) = string(x)
144+
x_2_string(x::String) = "\"$x\""
145+
x_2_string(x::Char) = "\'$x\'"
146+
x_2_string(x::Symbol) = ":$x"
147+
x_2_string(x::Number) = string(x)
148+
x_2_string(x::Pair) = "$(x_2_string(x[1])) => $(x_2_string(x[2]))"
149+
x_2_string(x::Nothing) = "nothing"
150+
function x_2_string(x::Vector)
151+
output = "["
152+
for val in x
153+
@string_append! output x_2_string(val) ", "
154+
end
155+
return get_substring_end(output, 1, -2) * "]"
156+
end
157+
function x_2_string(x::Tuple)
158+
output = "("
159+
for val in x
160+
@string_append! output x_2_string(val) ", "
161+
end
162+
return get_substring_end(output, 1, -2) * ")"
163+
end
164+
function x_2_string(x::Dict)
165+
output = "Dict(["
166+
for key in keys(x)
167+
@string_append! output x_2_string(key) " => " x_2_string(x[key]) ", "
168+
end
169+
return get_substring_end(output, 1, -2) * "])"
170+
end
171+
function x_2_string(x::Union{Matrix, Symbolics.Arr{Any, 2}})
172+
output = "["
173+
for j = 1:size(x)[1]
174+
for i = 1:size(x)[2]
175+
@string_append! output x_2_string(x[j,i]) " "
176+
end
177+
output = get_substring_end(output, 1, -1) * "; "
178+
end
179+
return get_substring_end(output, 1, -2) *"]"
180+
end
181+
182+
183+
x_2_string(x) = error("Tried to write an unsupported value ($(x)) of an unsupported type ($(typeof(x))) to a string.")
184+
185+
186+
### Symbolics Metadata Handling ###
187+
188+
# For a Symbolic, retrieve all metadata that needs to be added to its declaration. Certain metadata
189+
# (such as default values and whether a variable is a species or not) are skipped (these are stored
190+
# in the `SKIPPED_METADATA` constant).
191+
# Because it is impossible to retrieve the keyword used to declare individual metadata from the
192+
# metadata entry, these must be stored manually (in `RECOGNISED_METADATA`). If one of these are
193+
# encountered, a warning is thrown and it is skipped (we could also throw an error). I have asked
194+
# Shashi, and he claims there is not alternative (general) solution.
195+
function get_metadata_to_declare(sym)
196+
metadata_keys = collect(keys(sym.metadata))
197+
metadata_keys = filter(mdk -> !(mdk in SKIPPED_METADATA), metadata_keys)
198+
if any(!(mdk in keys(RECOGNISED_METADATA)) for mdk in metadata_keys)
199+
@warn "The following unrecognised metadata entries: $(setdiff(metadata_keys, keys(RECOGNISED_METADATA))) are not recognised for species/variable/parameter $sym. If you raise an issue at https://github.com/SciML/Catalyst.jl, we can add support for this metadata type."
200+
metadata_keys = filter(mdk -> in(mdk, keys(RECOGNISED_METADATA)), metadata_keys)
201+
end
202+
return metadata_keys
203+
end
204+
205+
# Converts a given metadata into the string used to declare it.
206+
function metadata_2_string(sym, metadata)
207+
return RECOGNISED_METADATA[metadata] * " = " * x_2_string(sym.metadata[metadata])
208+
end
209+
210+
# List of all recognised metadata (we should add as many as possible), and th keyword used to declare
211+
# them in code.
212+
const RECOGNISED_METADATA = Dict([
213+
Catalyst.ParameterConstantSpecies => "isconstantspecies"
214+
Catalyst.VariableBCSpecies => "isbcspecies"
215+
Catalyst.VariableSpecies => "isspecies"
216+
Catalyst.EdgeParameter => "edgeparameter"
217+
Catalyst.CompoundSpecies => "iscompound"
218+
Catalyst.CompoundComponents => "components"
219+
Catalyst.CompoundCoefficients => "coefficients"
220+
221+
ModelingToolkit.VariableDescription => "description"
222+
ModelingToolkit.VariableBounds => "bounds"
223+
ModelingToolkit.VariableUnit => "unit"
224+
ModelingToolkit.VariableConnectType => "connect"
225+
ModelingToolkit.VariableNoiseType => "noise"
226+
ModelingToolkit.VariableInput => "input"
227+
ModelingToolkit.VariableOutput => "output"
228+
ModelingToolkit.VariableIrreducible => "irreducible"
229+
ModelingToolkit.VariableStatePriority => "state_priority"
230+
ModelingToolkit.VariableMisc => "misc"
231+
ModelingToolkit.TimeDomain => "timedomain"
232+
])
233+
234+
# List of metadata that does not need to be explicitly declared to be added (or which is handled separately).
235+
const SKIPPED_METADATA = [ModelingToolkit.MTKVariableTypeCtx, Symbolics.VariableSource,
236+
Symbolics.VariableDefaultValue, Catalyst.VariableSpecies]
237+
238+
239+
### Generic Expression Handling ###
240+
241+
# Potentially strips the call for a symbolics. E.g. X(t) becomes X (but p remains p). This is used
242+
# when variables are written to files, as in code they are used without the call part.
243+
function strip_call(sym)
244+
return istree(sym) ? Sym{Real}(Symbolics.getname(sym)) : sym
245+
end
246+
247+
# For an vector of symbolics, creates a dictionary taking each symbolics to each call-stripped form.
248+
function make_strip_call_dict(syms)
249+
return Dict([sym => strip_call(Symbolics.unwrap(sym)) for sym in syms])
250+
end
251+
function make_strip_call_dict(rn::ReactionSystem)
252+
return make_strip_call_dict(get_unknowns(rn))
253+
end
254+
255+
256+
### Handle Parameters/Species/Variables Declaration Dependencies ###
257+
258+
# Gets a vector with the symbolics a symbolic depends on (currently only considers defaults).
259+
function get_dep_syms(sym)
260+
ModelingToolkit.hasdefault(sym) || return []
261+
return Symbolics.get_variables(ModelingToolkit.getdefault(sym))
262+
end
263+
264+
# Checks if a symbolic depends on an symbolics in a vector being declared.
265+
# Because Symbolics has to utilise `isequal`, the `isdisjoint` function cannot be used.
266+
function depends_on(sym, syms)
267+
dep_syms = get_dep_syms(sym)
268+
for s1 in dep_syms
269+
for s2 in syms
270+
isequal(s1, s2) && return true
271+
end
272+
end
273+
return false
274+
end
275+
276+
# For a set of remaining parameters/species/variables (remaining_syms), return this split into
277+
# two sets:
278+
# One with those that does not depend on any sym in `all_remaining_syms`.
279+
# One with those that does depend on at least one sym in `all_remaining_syms`.
280+
# The first set is returned. Next `remaining_syms` is updated to be the second set.
281+
function dependency_split!(remaining_syms, all_remaining_syms)
282+
writable_syms = filter(sym -> !depends_on(sym, all_remaining_syms), remaining_syms)
283+
filter!(sym -> depends_on(sym, all_remaining_syms), remaining_syms)
284+
return writable_syms
285+
end
286+
287+
288+
### Other Functions ###
289+
290+
291+
# Checks if a symbolic's declaration is "complicated". The declaration is considered complicated
292+
# if it have metadata, default value, or type designation that must be declared.
293+
function complicated_declaration(sym)
294+
isempty(get_metadata_to_declare(sym)) || (return true)
295+
ModelingToolkit.hasdefault(sym) && (return true)
296+
(sym isa SymbolicUtils.BasicSymbolic{Real}) || (return true)
297+
return false
298+
end

0 commit comments

Comments
 (0)