Skip to content

Commit 0e67d45

Browse files
Merge pull request #17 from SciML/as/tutorial
docs: update `getp`, `setp` docstring, add tutorial
2 parents f976ac6 + e7585e3 commit 0e67d45

File tree

3 files changed

+202
-4
lines changed

3 files changed

+202
-4
lines changed

docs/pages.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
pages = [
44
"Home" => "index.md",
55
"API" => "api.md",
6+
"Tutorial" => "tutorial.md",
67
]

docs/src/tutorial.md

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# Implementing SymbolicIndexingInterface for a type
2+
3+
Implementing the interface for a type allows it to be used by existing symbolic indexing
4+
infrastructure. There are multiple ways to implement it, and the entire interface is
5+
not always necessary.
6+
7+
## Defining a fallback
8+
9+
The simplest case is when the type contains an object that already implements the interface.
10+
All its methods can simply be forwarded to that object. To do so, SymbolicIndexingInterface.jl
11+
provides the [`symbolic_container`](@ref) method. For example,
12+
13+
```julia
14+
struct MySolutionWrapper{T<:SciMLBase.AbstractTimeseriesSolution}
15+
sol::T
16+
# other properties...
17+
end
18+
19+
symbolic_container(sys::MySolutionWrapper) = sys.sol
20+
```
21+
22+
`MySolutionWrapper` wraps an `AbstractTimeseriesSolution` which already implements the interface.
23+
Since `symbolic_container` will return the wrapped solution, all method calls such as
24+
`is_parameter(sys::MySolutionWrapper, sym)` will be forwarded to `is_parameter(sys.sol, sym)`.
25+
26+
In case some methods need to function differently than those of the wrapped type, they can selectively
27+
be defined. For example, suppose `MySolutionWrapper` does not support observed quantities. The following
28+
method can be defined (in addition to the one above):
29+
30+
```julia
31+
is_observed(sys::MySolutionWrapper, sym) = false
32+
```
33+
34+
## Defining the interface in its entirety
35+
36+
Not all of the methods in the interface are required. Some only need to be implemented if a type
37+
supports specific functionality. Consider the following struct which needs to implement the interface:
38+
39+
```julia
40+
struct ExampleSolution
41+
state_index::Dict{Symbol,Int}
42+
parameter_index::Dict{Symbol,Int}
43+
independent_variable::Union{Symbol,Nothing}
44+
u::Vector{Vector{Float64}}
45+
p::Vector{Float64}
46+
t::Vector{Float64}
47+
end
48+
```
49+
50+
51+
52+
### Mandatory methods
53+
54+
```julia
55+
function SymbolicIndexingInterface.is_variable(sys::ExampleSolution, sym)
56+
haskey(sys.state_index, sym)
57+
end
58+
59+
function SymbolicIndexingInterface.variable_index(sys::ExampleSolution, sym)
60+
get(sys.state_index, sym, nothing)
61+
end
62+
63+
function SymbolicIndexingInterface.variable_symbols(sys::ExampleSolution)
64+
collect(keys(sys.state_index))
65+
end
66+
67+
function SymbolicIndexingInterface.is_parameter(sys::ExampleSolution, sym)
68+
haskey(sys.parameter_index, sym)
69+
end
70+
71+
function SymbolicIndexingInterface.parameter_index(sys::ExampleSolution, sym)
72+
get(sys.parameter_index, sym, nothing)
73+
end
74+
75+
function SymbolicIndexingInterface.parameter_symbols(sys::ExampleSolution)
76+
collect(keys(sys.parameter_index))
77+
end
78+
79+
function SymbolicIndexingInterface.is_independent_variable(sys::ExampleSolution, sym)
80+
# note we have to check separately for `nothing`, otherwise
81+
# `is_independent_variable(p, nothing)` would return `true`.
82+
sys.independent_variable !== nothing && sym === sys.independent_variable
83+
end
84+
85+
function SymbolicIndexingInterface.independent_variable_symbols(sys::ExampleSolution)
86+
sys.independent_variable === nothing ? [] : [sys.independent_variable]
87+
end
88+
89+
# this types accepts `Expr` for observed expressions involving state/parameter
90+
# variables
91+
SymbolicIndexingInterface.is_observed(sys::ExampleSolution, sym) = sym isa Expr
92+
93+
function SymbolicIndexingInterface.observed(sys::ExampleSolution, sym::Expr)
94+
if is_time_dependent(sys)
95+
return function (u, p, t)
96+
# compute value from `sym`, leveraging `variable_index` and
97+
# `parameter_index` to turn symbols into indices
98+
end
99+
else
100+
return function (u, p)
101+
# compute value from `sym`, leveraging `variable_index` and
102+
# `parameter_index` to turn symbols into indices
103+
end
104+
end
105+
end
106+
107+
function SymbolicIndexingInterface.is_time_dependent(sys::ExampleSolution)
108+
sys.independent_variable !== nothing
109+
end
110+
111+
SymbolicIndexingInterface.constant_structure(::ExampleSolution) = true
112+
```
113+
114+
Note that the method definitions are all assuming `constant_structure(p) == true`.
115+
116+
In case `constant_structure(p) == false`, the following methods would change:
117+
- `constant_structure(::ExampleSolution) = false`
118+
- `variable_index(sys::ExampleSolution, sym)` would become
119+
`variable_index(sys::ExampleSolution, sym i)` where `i` is the time index at which
120+
the index of `sym` is required.
121+
- `variable_symbols(sys::ExampleSolution)` would become
122+
`variable_symbols(sys::ExampleSolution, i)` where `i` is the time index at which
123+
the variable symbols are required.
124+
- `observed(sys::ExampleSolution, sym)` would become
125+
`observed(sys::ExampleSolution, sym, i)` where `i` is either the time index at which
126+
the index of `sym` is required or a `Vector` of state symbols at the current time index.
127+
128+
## Optional methods
129+
130+
Note that `observed` is optional if `is_observed` is always `false`, or the type is
131+
only responsible for identifying observed values and `observed` will always be called
132+
on a type that wraps this type. An example is `ModelingToolkit.AbstractSystem`, which
133+
can identify whether a value is observed, but cannot implement `observed` itself.
134+
135+
Other optional methods relate to parameter indexing. If a type contains the values of
136+
parameter variables, it must implement [`parameter_values`](@ref). This will allow the
137+
default definitions of [`getp`](@ref) and [`setp`](@ref) to work. While `setp` is
138+
not typically useful for solution objects, it may be useful for integrators. Typically
139+
the default implementations for `getp` and `setp` will suffice and manually defining
140+
them is not necessary.
141+
142+
```julia
143+
function SymbolicIndexingInterface.parameter_values(sys::ExampleSolution)
144+
sys.p
145+
end
146+
```
147+
148+
# Implementing the `SymbolicTypeTrait` for a type
149+
150+
The `SymbolicTypeTrait` is used to identify values that can act as symbolic variables. It
151+
has three variants:
152+
- [`NotSymbolic`](@ref) for quantities that are not symbolic. This is the default for all
153+
types.
154+
- [`ScalarSymbolic`](@ref) for quantities that are symbolic, and represent a single
155+
logical value.
156+
- [`ArraySymbolic`](@ref) for quantities that are symbolic, and represent an array of
157+
values. Types implementing this trait must return an array of `ScalarSymbolic` variables
158+
of the appropriate size and dimensions when `collect`ed.
159+
160+
The trait is implemented through the [`symbolic_type`](@ref) function. Consider the following
161+
example types:
162+
163+
```julia
164+
struct MySym
165+
name::Symbol
166+
end
167+
168+
struct MySymArr{N}
169+
name::Symbol
170+
size::NTuple{N,Int}
171+
end
172+
```
173+
174+
They must implement the following functions:
175+
176+
```julia
177+
SymbolicIndexingInterface.symbolic_type(::Type{MySym}) = ScalarSymbolic()
178+
SymbolicIndexingInterface.hasname(::MySym) = true
179+
SymbolicIndexingInterface.getname(sym::MySym) = sym.name
180+
181+
SymbolicIndexingInterface.symbolic_type(::Type{<:MySymArr}) = ArraySymbolic()
182+
SymbolicIndexingInterface.hasname(::MySymArr) = true
183+
SymbolicIndexingInterface.getname(sym::MySymArr) = sym.name
184+
function Base.collect(sym::MySymArr)
185+
[
186+
MySym(Symbol(sym.name, :_, join(idxs, "_")))
187+
for idxs in Iterators.product(Base.OneTo.(sym.size)...)
188+
]
189+
end
190+
```
191+
192+
[`hasname`](@ref) is not required to always be `true` for symbolic types. For example,
193+
`Symbolics.Num` returns `false` whenever the wrapped value is a number, or an expression.

src/parameter_indexing.jl

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ function parameter_values end
99
getp(sys, p)
1010
1111
Return a function that takes an integrator or solution of `sys`, and returns the value of
12-
the parameter `p`. Requires that the integrator or solution implement
12+
the parameter `p`. Note that `p` can be a direct numerical index or a symbolic value.
13+
Requires that the integrator or solution implement [`parameter_values`](@ref). This function
14+
typically does not need to be implemented, and has a default implementation relying on
1315
[`parameter_values`](@ref).
1416
"""
1517
function getp(sys, p)
@@ -50,9 +52,11 @@ end
5052
setp(sys, p)
5153
5254
Return a function that takes an integrator of `sys` and a value, and sets the
53-
the parameter `p` to that value. Requires that the integrator implement
54-
[`parameter_values`](@ref) and the returned collection be a mutable reference
55-
to the parameter vector in the integrator.
55+
the parameter `p` to that value. Note that `p` can be a direct numerical index or a
56+
symbolic value. Requires that the integrator implement [`parameter_values`](@ref) and the
57+
returned collection be a mutable reference to the parameter vector in the integrator. In
58+
case `parameter_values` cannot return such a mutable reference, `setp` needs to be
59+
implemented manually.
5660
"""
5761
function setp(sys, p)
5862
symtype = symbolic_type(p)

0 commit comments

Comments
 (0)