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, " \n observed = [" => " \n local 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 " \n end" )
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