|
| 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. |
0 commit comments