11# Implementing the Complete Symbolic Indexing Interface
22
3+ ## Index Provider Interface
34This tutorial will show how to define the entire Symbolic Indexing Interface on an
45` ExampleSystem ` :
56
1718Not all the methods in the interface are required. Some only need to be implemented if a type
1819supports specific functionality. Consider the following struct, which needs to implement the interface:
1920
20- ## Mandatory methods
21+ ### Mandatory methods
2122
22- ### Simple Indexing Functions
23+ #### Simple Indexing Functions
2324
2425These are the simple functions which describe how to turn symbols into indices.
2526
@@ -84,7 +85,7 @@ function SymbolicIndexingInterface.default_values(sys::ExampleSystem)
8485end
8586```
8687
87- ### Observed Equation Handling
88+ #### Observed Equation Handling
8889
8990These are for handling symbolic expressions and generating equations which are not directly
9091in the solution vector.
131132In case a type does not support such observed quantities, ` is_observed ` must be
132133defined to always return ` false ` , and ` observed ` does not need to be implemented.
133134
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
135141
136142Note that the method definitions are all assuming ` constant_structure(p) == true ` .
137143
@@ -147,14 +153,16 @@ In case `constant_structure(p) == false`, the following methods would change:
147153 ` observed(sys::ExampleSystem, sym, i) ` where ` i ` is either the time index at which
148154 the index of ` sym ` is required or a ` Vector ` of state symbols at the current time index.
149155
150- ## Optional methods
156+ ### Optional methods
151157
152158Note that ` observed ` is optional if ` is_observed ` is always ` false ` , or the type is
153159only responsible for identifying observed values and ` observed ` will always be called
154160on a type that wraps this type. An example is ` ModelingToolkit.AbstractSystem ` , which
155161can identify whether a value is observed, but cannot implement ` observed ` itself.
156162
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
158166parameter variables, it must implement [ ` parameter_values ` ] ( @ref ) . This allows the
159167default definitions of [ ` getp ` ] ( @ref ) and [ ` setp ` ] ( @ref ) to work. While ` setp ` is
160168not 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
276284once. Thus, ` finalize_parameters_hook! ` is better for expensive computations that can be
277285performed for a bulk parameter update.
278286
279- # The ` ParameterIndexingProxy `
287+ ## The ` ParameterIndexingProxy `
280288
281289[ ` ParameterIndexingProxy ` ] ( @ref ) is a wrapper around another type which implements the
282290interface and allows using [ ` getp ` ] ( @ref ) and [ ` setp ` ] ( @ref ) to get and set parameter
@@ -305,6 +313,164 @@ integrator.ps[:b] = 3.0
305313setp (integrator, :b )(integrator, 3.0 ) # functionally the same as above
306314```
307315
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+
308474# Implementing the ` SymbolicTypeTrait ` for a type
309475
310476The ` SymbolicTypeTrait ` is used to identify values that can act as symbolic variables. It
383549Note the evaluation of the operation if all of the arguments are not symbolic. This is
384550required since ` symbolic_evaluate ` must return an evaluated value if all symbolic variables
385551are 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