Skip to content

Commit 14aab38

Browse files
docs: update docs with new parameter timeseries API, semantics
1 parent c4b6789 commit 14aab38

File tree

5 files changed

+235
-111
lines changed

5 files changed

+235
-111
lines changed

docs/src/api.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,22 @@ allvariables
3030

3131
```@docs
3232
observed
33+
parameter_observed
34+
ParameterObservedFunction
35+
```
36+
37+
#### Parameter timeseries
38+
39+
If the index provider contains parameters that change during the course of the simulation
40+
at discrete time points, it must implement the following methods to ensure correct
41+
functioning of [`getu`](@ref) and [`getp`](@ref) for value providers that save the parameter
42+
timeseries. Note that there can be multiple parameter timeseries, in case different parameters
43+
may change at different times.
44+
45+
```@docs
46+
is_timeseries_parameter
47+
timeseries_parameter_index
48+
ParameterTimeseriesIndex
3349
```
3450

3551
## Value provider interface
@@ -64,11 +80,14 @@ If a solution object saves a timeseries of parameter values that are updated dur
6480
simulation (such as by callbacks), it must implement the following methods to ensure
6581
correct functioning of [`getu`](@ref) and [`getp`](@ref).
6682

83+
Parameter timeseries support requires that the value provider store the different
84+
timeseries in a [`ParameterTimeseriesCollection`](@ref).
85+
6786
```@docs
6887
is_parameter_timeseries
69-
parameter_timeseries
70-
parameter_values_at_time
71-
parameter_values_at_state_time
88+
get_parameter_timeseries_collection
89+
ParameterTimeseriesCollection
90+
with_updated_parameter_timeseries_values
7291
```
7392

7493
### Batched Queries and Updates

docs/src/complete_sii.md

Lines changed: 173 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Implementing the Complete Symbolic Indexing Interface
22

3+
## Index Provider Interface
34
This tutorial will show how to define the entire Symbolic Indexing Interface on an
45
`ExampleSystem`:
56

@@ -17,9 +18,9 @@ end
1718
Not all the methods in the interface are required. Some only need to be implemented if a type
1819
supports 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

2425
These are the simple functions which describe how to turn symbols into indices.
2526

@@ -84,7 +85,7 @@ function SymbolicIndexingInterface.default_values(sys::ExampleSystem)
8485
end
8586
```
8687

87-
### Observed Equation Handling
88+
#### Observed Equation Handling
8889

8990
These are for handling symbolic expressions and generating equations which are not directly
9091
in the solution vector.
@@ -131,7 +132,12 @@ end
131132
In case a type does not support such observed quantities, `is_observed` must be
132133
defined 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

136142
Note 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

152158
Note that `observed` is optional if `is_observed` is always `false`, or the type is
153159
only responsible for identifying observed values and `observed` will always be called
154160
on a type that wraps this type. An example is `ModelingToolkit.AbstractSystem`, which
155161
can 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
158166
parameter variables, it must implement [`parameter_values`](@ref). This allows the
159167
default definitions of [`getp`](@ref) and [`setp`](@ref) to work. While `setp` is
160168
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
276284
once. Thus, `finalize_parameters_hook!` is better for expensive computations that can be
277285
performed 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
282290
interface and allows using [`getp`](@ref) and [`setp`](@ref) to get and set parameter
@@ -305,6 +313,164 @@ integrator.ps[:b] = 3.0
305313
setp(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

310476
The `SymbolicTypeTrait` is used to identify values that can act as symbolic variables. It
@@ -383,87 +549,3 @@ end
383549
Note the evaluation of the operation if all of the arguments are not symbolic. This is
384550
required since `symbolic_evaluate` must return an evaluated value if all symbolic variables
385551
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

Comments
 (0)