1
1
# Implementing the Complete Symbolic Indexing Interface
2
2
3
+ ## Index Provider Interface
3
4
This tutorial will show how to define the entire Symbolic Indexing Interface on an
4
5
` ExampleSystem ` :
5
6
17
18
Not all the methods in the interface are required. Some only need to be implemented if a type
18
19
supports specific functionality. Consider the following struct, which needs to implement the interface:
19
20
20
- ## Mandatory methods
21
+ ### Mandatory methods
21
22
22
- ### Simple Indexing Functions
23
+ #### Simple Indexing Functions
23
24
24
25
These are the simple functions which describe how to turn symbols into indices.
25
26
@@ -84,7 +85,7 @@ function SymbolicIndexingInterface.default_values(sys::ExampleSystem)
84
85
end
85
86
```
86
87
87
- ### Observed Equation Handling
88
+ #### Observed Equation Handling
88
89
89
90
These are for handling symbolic expressions and generating equations which are not directly
90
91
in the solution vector.
131
132
In case a type does not support such observed quantities, ` is_observed ` must be
132
133
defined to always return ` false ` , and ` observed ` does not need to be implemented.
133
134
134
- ### Note about constant structure
135
+ The same process can be followed for [ ` parameter_observed ` ] ( @ref ) , with the exception
136
+ that the returned function must not have ` u ` in its signature, and must be wrapped in a
137
+ [ ` ParameterObservedFunction ` ] ( @ref ) . In-place versions can also be implemented for
138
+ ` parameter_observed ` .
139
+
140
+ #### Note about constant structure
135
141
136
142
Note that the method definitions are all assuming ` constant_structure(p) == true ` .
137
143
@@ -147,14 +153,16 @@ In case `constant_structure(p) == false`, the following methods would change:
147
153
` observed(sys::ExampleSystem, sym, i) ` where ` i ` is either the time index at which
148
154
the index of ` sym ` is required or a ` Vector ` of state symbols at the current time index.
149
155
150
- ## Optional methods
156
+ ### Optional methods
151
157
152
158
Note that ` observed ` is optional if ` is_observed ` is always ` false ` , or the type is
153
159
only responsible for identifying observed values and ` observed ` will always be called
154
160
on a type that wraps this type. An example is ` ModelingToolkit.AbstractSystem ` , which
155
161
can identify whether a value is observed, but cannot implement ` observed ` itself.
156
162
157
- Other optional methods relate to indexing functions. If a type contains the values of
163
+ ## Value Provider Interface
164
+
165
+ Other interface methods relate to indexing functions. If a type contains the values of
158
166
parameter variables, it must implement [ ` parameter_values ` ] ( @ref ) . This allows the
159
167
default definitions of [ ` getp ` ] ( @ref ) and [ ` setp ` ] ( @ref ) to work. While ` setp ` is
160
168
not typically useful for solution objects, it may be useful for integrators. Typically,
@@ -276,7 +284,7 @@ similar functionality, but is called for every parameter that is updated, instea
276
284
once. Thus, ` finalize_parameters_hook! ` is better for expensive computations that can be
277
285
performed for a bulk parameter update.
278
286
279
- # The ` ParameterIndexingProxy `
287
+ ## The ` ParameterIndexingProxy `
280
288
281
289
[ ` ParameterIndexingProxy ` ] ( @ref ) is a wrapper around another type which implements the
282
290
interface and allows using [ ` getp ` ] ( @ref ) and [ ` setp ` ] ( @ref ) to get and set parameter
@@ -305,6 +313,164 @@ integrator.ps[:b] = 3.0
305
313
setp (integrator, :b )(integrator, 3.0 ) # functionally the same as above
306
314
```
307
315
316
+ ## Parameter Timeseries
317
+
318
+ If a solution object includes modified parameter values (such as through callbacks) during the
319
+ simulation, it must implement several additional functions for correct functioning of
320
+ [ ` getu ` ] ( @ref ) and [ ` getp ` ] ( @ref ) . [ ` ParameterTimeseriesCollection ` ] ( @ref ) helps in
321
+ implementing parameter timeseries objects. The following mockup gives an example of
322
+ correct implementation of these functions and the indexing syntax they enable.
323
+
324
+ ``` @example param_timeseries
325
+ using SymbolicIndexingInterface
326
+
327
+ # First, we must implement a parameter object that knows where the parameters in
328
+ # each parameter timeseries are stored
329
+ struct MyParameterObject
330
+ p::Vector{Float64}
331
+ disc_idxs::Vector{Vector{Int}}
332
+ end
333
+
334
+ # To be able to access parameter values
335
+ SymbolicIndexingInterface.parameter_values(mpo::MyParameterObject) = mpo.p
336
+ # Update the parameter object with new values
337
+ function SymbolicIndexingInterface.with_updated_parameter_timeseries_values(mpo::MyParameterObject, args::Pair...)
338
+ for (ts_idx, val) in args
339
+ mpo.p[mpo.disc_idxs[ts_idx]] = val
340
+ end
341
+ return mpo
342
+ end
343
+
344
+ struct ExampleSolution2
345
+ sys::SymbolCache
346
+ u::Vector{Vector{Float64}}
347
+ t::Vector{Float64}
348
+ p::MyParameterObject # the parameter object. Only some parameters are timeseries params
349
+ p_ts::ParameterTimeseriesCollection
350
+ end
351
+
352
+ # Add the `:ps` property to automatically wrap in `ParameterIndexingProxy`
353
+ function Base.getproperty(fs::ExampleSolution2, s::Symbol)
354
+ s === :ps ? ParameterIndexingProxy(fs) : getfield(fs, s)
355
+ end
356
+ # Use the contained `SymbolCache` for indexing
357
+ SymbolicIndexingInterface.symbolic_container(fs::ExampleSolution2) = fs.sys
358
+ # State indexing methods
359
+ SymbolicIndexingInterface.state_values(fs::ExampleSolution2) = fs.u
360
+ SymbolicIndexingInterface.current_time(fs::ExampleSolution2) = fs.t
361
+ # By default, `parameter_values` refers to the last value
362
+ SymbolicIndexingInterface.parameter_values(fs::ExampleSolution2) = fs.p
363
+ SymbolicIndexingInterface.get_parameter_timeseries_collection(fs::ExampleSolution2) = fs.p_ts
364
+ # Mark the object as a timeseries object
365
+ SymbolicIndexingInterface.is_timeseries(::Type{ExampleSolution2}) = Timeseries()
366
+ # Mark the object as a parameter timeseries object
367
+ SymbolicIndexingInterface.is_parameter_timeseries(::Type{ExampleSolution2}) = Timeseries()
368
+ ```
369
+
370
+ We will also need a timeseries object which will store individual parameter timeseries.
371
+ ` DiffEqArray ` in ` RecursiveArrayTools.jl ` satisfies this use case, but we will implement
372
+ one manually here.
373
+
374
+ ``` @example param_timeseries
375
+ struct MyDiffEqArray
376
+ t::Vector{Float64}
377
+ u::Vector{Vector{Float64}}
378
+ end
379
+
380
+ # Must be a timeseries object, and implement `current_time` and `state_values`
381
+ SymbolicIndexingInterface.is_timeseries(::Type{MyDiffEqArray}) = Timeseries()
382
+ SymbolicIndexingInterface.current_time(a::MyDiffEqArray) = a.t
383
+ SymbolicIndexingInterface.state_values(a::MyDiffEqArray) = a.u
384
+ ```
385
+
386
+ Now we can create an example object and observe the new functionality. Note that
387
+ ` sol.ps[sym, args...] ` is identical to ` getp(sol, sym)(sol, args...) ` . In a real
388
+ application, the solution object will be populated during the solve process. We manually
389
+ construct the object here for demonstration.
390
+
391
+ ``` @example param_timeseries
392
+ sys = SymbolCache(
393
+ [:x, :y, :z], [:a, :b, :c, :d], :t;
394
+ # specify that :b, :c and :d are timeseries parameters
395
+ # :b and :c belong to the same timeseries
396
+ # :d is in a different timeseries
397
+ timeseries_parameters = Dict(
398
+ :b => ParameterTimeseriesIndex(1, 1),
399
+ :c => ParameterTimeseriesIndex(1, 2),
400
+ :d => ParameterTimeseriesIndex(2, 1),
401
+ ))
402
+ b_c_timeseries = MyDiffEqArray(
403
+ collect(0.0:0.1:1.0),
404
+ [[0.25i, 0.35i] for i in 1:11]
405
+ )
406
+ d_timeseries = MyDiffEqArray(
407
+ collect(0.0:0.2:1.0),
408
+ [[0.17i] for i in 1:6]
409
+ )
410
+ p = MyParameterObject(
411
+ # parameter values at the final time
412
+ [4.2, b_c_timeseries.u[end]..., d_timeseries.u[end]...],
413
+ [[2, 3], [4]]
414
+ )
415
+ sol = ExampleSolution2(
416
+ sys,
417
+ [i * ones(3) for i in 1:5], # u
418
+ collect(0.0:0.25:1.0), # t
419
+ p,
420
+ ParameterTimeseriesCollection([b_c_timeseries, d_timeseries], deepcopy(p))
421
+ )
422
+ sol.ps[:a] # returns the value of non-timeseries parameter
423
+ ```
424
+
425
+ ``` @example param_timeseries
426
+ sol.ps[:b] # returns the timeseries of :b
427
+ ```
428
+
429
+ ``` @example param_timeseries
430
+ sol.ps[:b, 3] # index at a specific index in the parameter timeseries
431
+ ```
432
+
433
+ ``` @example param_timeseries
434
+ sol.ps[:b, [3, 6, 8]] # index using arrays
435
+ ```
436
+
437
+ ``` @example param_timeseries
438
+ idxs = @show rand(Bool, 11) # boolean mask for indexing
439
+ sol.ps[:b, idxs]
440
+ ```
441
+
442
+ ``` @example param_timeseries
443
+ sol.ps[[:a, :b]] # returns the values at the last timestep, since :a is not timeseries
444
+ ```
445
+
446
+ ``` @example param_timeseries
447
+ # throws an error since :b and :d belong to different timeseries
448
+ try
449
+ sol.ps[[:b, :d]]
450
+ catch e
451
+ @show e
452
+ end
453
+ ```
454
+
455
+ ``` @example param_timeseries
456
+ sol.ps[:(b + c)] # observed quantities work too
457
+ ```
458
+
459
+ ``` @example param_timeseries
460
+ getu(sol, :b)(sol) # returns the values :b takes at the times in the state timeseries
461
+ ```
462
+
463
+ ``` @example param_timeseries
464
+ getu(sol, [:b, :d])(sol) # works
465
+ ```
466
+
467
+ ## Custom containers
468
+
469
+ A custom container object (such as ` ModelingToolkit.MTKParameters ` ) should implement
470
+ [ ` remake_buffer ` ] ( @ref ) to allow creating a new buffer with updated values, possibly
471
+ with different types. This is already implemented for ` AbstractArray ` s (including static
472
+ arrays).
473
+
308
474
# Implementing the ` SymbolicTypeTrait ` for a type
309
475
310
476
The ` SymbolicTypeTrait ` is used to identify values that can act as symbolic variables. It
383
549
Note the evaluation of the operation if all of the arguments are not symbolic. This is
384
550
required since ` symbolic_evaluate ` must return an evaluated value if all symbolic variables
385
551
are substituted.
386
-
387
- ## Parameter Timeseries
388
-
389
- If a solution object saves modified parameter values (such as through callbacks) during the
390
- simulation, it must implement [ ` parameter_timeseries ` ] ( @ref ) ,
391
- [ ` parameter_values_at_time ` ] ( @ref ) and [ ` parameter_values_at_state_time ` ] ( @ref ) for correct
392
- functioning of [ ` getu ` ] ( @ref ) and [ ` getp ` ] ( @ref ) . The following mockup gives an example
393
- of correct implementation of these functions and the indexing syntax they enable.
394
-
395
- ``` @example param_timeseries
396
- using SymbolicIndexingInterface
397
-
398
- struct ExampleSolution2
399
- sys::SymbolCache
400
- u::Vector{Vector{Float64}}
401
- t::Vector{Float64}
402
- p::Vector{Vector{Float64}}
403
- pt::Vector{Float64}
404
- end
405
-
406
- # Add the `:ps` property to automatically wrap in `ParameterIndexingProxy`
407
- function Base.getproperty(fs::ExampleSolution2, s::Symbol)
408
- s === :ps ? ParameterIndexingProxy(fs) : getfield(fs, s)
409
- end
410
- # Use the contained `SymbolCache` for indexing
411
- SymbolicIndexingInterface.symbolic_container(fs::ExampleSolution2) = fs.sys
412
- # By default, `parameter_values` refers to the last value
413
- SymbolicIndexingInterface.parameter_values(fs::ExampleSolution2) = fs.p[end]
414
- SymbolicIndexingInterface.parameter_values(fs::ExampleSolution2, i) = fs.p[end][i]
415
- # Index into the parameter timeseries vector
416
- function SymbolicIndexingInterface.parameter_values_at_time(fs::ExampleSolution2, t)
417
- fs.p[t]
418
- end
419
- # Find the first index in the parameter timeseries vector with a time smaller
420
- # than the time from the state timeseries, and use that to index the parameter
421
- # timeseries
422
- function SymbolicIndexingInterface.parameter_values_at_state_time(fs::ExampleSolution2, t)
423
- ptind = searchsortedfirst(fs.pt, fs.t[t]; lt = <=)
424
- fs.p[ptind - 1]
425
- end
426
- SymbolicIndexingInterface.parameter_timeseries(fs::ExampleSolution2) = fs.pt
427
- # Mark the object as a `Timeseries` object
428
- SymbolicIndexingInterface.is_timeseries(::Type{ExampleSolution2}) = Timeseries()
429
-
430
- ```
431
-
432
- Now we can create an example object and observe the new functionality. Note that
433
- ` sol.ps[sym, args...] ` is identical to ` getp(sol, sym)(sol, args...) ` .
434
-
435
- ``` @example param_timeseries
436
- sys = SymbolCache([:x, :y, :z], [:a, :b, :c], :t)
437
- sol = ExampleSolution2(
438
- sys,
439
- [i * ones(3) for i in 1:5],
440
- [0.2i for i in 1:5],
441
- [2i * ones(3) for i in 1:10],
442
- [0.1i for i in 1:10]
443
- )
444
- sol.ps[:a] # returns the value at the last timestep
445
- ```
446
-
447
- ``` @example param_timeseries
448
- sol.ps[:a, :] # use Colon to fetch the entire parameter timeseries
449
- ```
450
-
451
- ``` @example param_timeseries
452
- sol.ps[:a, 3] # index at a specific index in the parameter timeseries
453
- ```
454
-
455
- ``` @example param_timeseries
456
- sol.ps[:a, [3, 6, 8]] # index using arrays
457
- ```
458
-
459
- ``` @example param_timeseries
460
- idxs = @show rand(Bool, 10) # boolean mask for indexing
461
- sol.ps[:a, idxs]
462
- ```
463
-
464
- ## Custom containers
465
-
466
- A custom container object (such as ` ModelingToolkit.MTKParameters ` ) should implement
467
- [ ` remake_buffer ` ] ( @ref ) to allow creating a new buffer with updated values, possibly
468
- with different types. This is already implemented for ` AbstractArray ` s (including static
469
- arrays).
0 commit comments