Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve documentation structure #22

Merged
merged 2 commits into from
Dec 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/pages.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

pages = [
"Home" => "index.md",
"Tutorial" => "tutorial.md",
"Usage" => "usage.md",
"Tutorials" => [
"Using the SciML Symbolic Indexing Interface" => "usage.md",
"Simple Demonstration of a Symbolic System Structure" => "simple_sii_sys.md",
"Implementing the Complete Symbolic Indexing Interface" => "complete_sii.md",
],
"Defining Solution Wrapper Fallbacks" => "solution_wrappers.md",
"API" => "api.md",
]
139 changes: 59 additions & 80 deletions docs/src/tutorial.md → docs/src/complete_sii.md
Original file line number Diff line number Diff line change
@@ -1,125 +1,76 @@
# Implementing SymbolicIndexingInterface for a type
# Implementing the Complete Symbolic Indexing Interface

Implementing the interface for a type allows it to be used by existing symbolic indexing
infrastructure. There are multiple ways to implement it, and the entire interface is
not always necessary.

## Defining a fallback

The simplest case is when the type contains an object that already implements the interface.
All its methods can simply be forwarded to that object. To do so, SymbolicIndexingInterface.jl
provides the [`symbolic_container`](@ref) method. For example,
This tutorial will show how to define the entire Symbolic Indexing Interface on an
`ExampleSystem`:

```julia
struct MySolutionWrapper{T<:SciMLBase.AbstractTimeseriesSolution}
sol::T
# other properties...
end

symbolic_container(sys::MySolutionWrapper) = sys.sol
```

`MySolutionWrapper` wraps an `AbstractTimeseriesSolution` which already implements the interface.
Since `symbolic_container` will return the wrapped solution, all method calls such as
`is_parameter(sys::MySolutionWrapper, sym)` will be forwarded to `is_parameter(sys.sol, sym)`.

In cases where some methods need to function differently than those of the wrapped type, they can be selectively
defined. For example, suppose `MySolutionWrapper` does not support observed quantities. The following
method can be defined (in addition to the one above):

```julia
is_observed(sys::MySolutionWrapper, sym) = false
```

## Defining the interface in its entirety

Not all the methods in the interface are required. Some only need to be implemented if a type
supports specific functionality. Consider the following struct, which needs to implement the interface:

```julia
struct ExampleSolution
struct ExampleSystem
state_index::Dict{Symbol,Int}
parameter_index::Dict{Symbol,Int}
independent_variable::Union{Symbol,Nothing}
# mapping from observed variable to Expr to calculate its value
observed::Dict{Symbol,Expr}
u::Vector{Vector{Float64}}
p::Vector{Float64}
t::Vector{Float64}
end
```

Not all the methods in the interface are required. Some only need to be implemented if a type
supports specific functionality. Consider the following struct, which needs to implement the interface:

## Mandatory methods

### Simple Indexing Functions

### Mandatory methods
These are the simple functions which describe how to turn symbols into indices.

```julia
function SymbolicIndexingInterface.is_variable(sys::ExampleSolution, sym)
function SymbolicIndexingInterface.is_variable(sys::ExampleSystem, sym)
haskey(sys.state_index, sym)
end

function SymbolicIndexingInterface.variable_index(sys::ExampleSolution, sym)
function SymbolicIndexingInterface.variable_index(sys::ExampleSystem, sym)
get(sys.state_index, sym, nothing)
end

function SymbolicIndexingInterface.variable_symbols(sys::ExampleSolution)
function SymbolicIndexingInterface.variable_symbols(sys::ExampleSystem)
collect(keys(sys.state_index))
end

function SymbolicIndexingInterface.is_parameter(sys::ExampleSolution, sym)
function SymbolicIndexingInterface.is_parameter(sys::ExampleSystem, sym)
haskey(sys.parameter_index, sym)
end

function SymbolicIndexingInterface.parameter_index(sys::ExampleSolution, sym)
function SymbolicIndexingInterface.parameter_index(sys::ExampleSystem, sym)
get(sys.parameter_index, sym, nothing)
end

function SymbolicIndexingInterface.parameter_symbols(sys::ExampleSolution)
function SymbolicIndexingInterface.parameter_symbols(sys::ExampleSystem)
collect(keys(sys.parameter_index))
end

function SymbolicIndexingInterface.is_independent_variable(sys::ExampleSolution, sym)
function SymbolicIndexingInterface.is_independent_variable(sys::ExampleSystem, sym)
# note we have to check separately for `nothing`, otherwise
# `is_independent_variable(p, nothing)` would return `true`.
sys.independent_variable !== nothing && sym === sys.independent_variable
end

function SymbolicIndexingInterface.independent_variable_symbols(sys::ExampleSolution)
function SymbolicIndexingInterface.independent_variable_symbols(sys::ExampleSystem)
sys.independent_variable === nothing ? [] : [sys.independent_variable]
end

# this type accepts `Expr` for observed expressions involving state/parameter/observed
# variables
SymbolicIndexingInterface.is_observed(sys::ExampleSolution, sym) = sym isa Expr || sym isa Symbol && haskey(sys.observed, sym)

function SymbolicIndexingInterface.observed(sys::ExampleSolution, sym::Expr)
if is_time_dependent(sys)
return function (u, p, t)
# compute value from `sym`, leveraging `variable_index` and
# `parameter_index` to turn symbols into indices
end
else
return function (u, p)
# compute value from `sym`, leveraging `variable_index` and
# `parameter_index` to turn symbols into indices
end
end
end

function SymbolicIndexingInterface.is_time_dependent(sys::ExampleSolution)
function SymbolicIndexingInterface.is_time_dependent(sys::ExampleSystem)
sys.independent_variable !== nothing
end

SymbolicIndexingInterface.constant_structure(::ExampleSolution) = true
SymbolicIndexingInterface.constant_structure(::ExampleSystem) = true

function SymbolicIndexingInterface.all_solvable_symbols(sys::ExampleSolution)
function SymbolicIndexingInterface.all_solvable_symbols(sys::ExampleSystem)
return vcat(
collect(keys(sys.state_index)),
collect(keys(sys.observed)),
)
end

function SymbolicIndexingInterface.all_symbols(sys::ExampleSolution)
function SymbolicIndexingInterface.all_symbols(sys::ExampleSystem)
return vcat(
all_solvable_symbols(sys),
collect(keys(sys.parameter_index)),
Expand All @@ -128,18 +79,45 @@ function SymbolicIndexingInterface.all_symbols(sys::ExampleSolution)
end
```

### Observed Equation Handling

These are for handling symbolic expressions and generating equations which are not directly
in the solution vector.

```julia
# this type accepts `Expr` for observed expressions involving state/parameter/observed
# variables
SymbolicIndexingInterface.is_observed(sys::ExampleSystem, sym) = sym isa Expr || sym isa Symbol && haskey(sys.observed, sym)

function SymbolicIndexingInterface.observed(sys::ExampleSystem, sym::Expr)
if is_time_dependent(sys)
return function (u, p, t)
# compute value from `sym`, leveraging `variable_index` and
# `parameter_index` to turn symbols into indices
end
else
return function (u, p)
# compute value from `sym`, leveraging `variable_index` and
# `parameter_index` to turn symbols into indices
end
end
end
```

### Note about constant structure

Note that the method definitions are all assuming `constant_structure(p) == true`.

In case `constant_structure(p) == false`, the following methods would change:
- `constant_structure(::ExampleSolution) = false`
- `variable_index(sys::ExampleSolution, sym)` would become
`variable_index(sys::ExampleSolution, sym i)` where `i` is the time index at which
- `constant_structure(::ExampleSystem) = false`
- `variable_index(sys::ExampleSystem, sym)` would become
`variable_index(sys::ExampleSystem, sym i)` where `i` is the time index at which
the index of `sym` is required.
- `variable_symbols(sys::ExampleSolution)` would become
`variable_symbols(sys::ExampleSolution, i)` where `i` is the time index at which
- `variable_symbols(sys::ExampleSystem)` would become
`variable_symbols(sys::ExampleSystem, i)` where `i` is the time index at which
the variable symbols are required.
- `observed(sys::ExampleSolution, sym)` would become
`observed(sys::ExampleSolution, sym, i)` where `i` is either the time index at which
- `observed(sys::ExampleSystem, sym)` would become
`observed(sys::ExampleSystem, sym, i)` where `i` is either the time index at which
the index of `sym` is required or a `Vector` of state symbols at the current time index.

## Optional methods
Expand All @@ -157,15 +135,16 @@ the default implementations for `getp` and `setp` will suffice, and manually def
them is not necessary.

```julia
function SymbolicIndexingInterface.parameter_values(sys::ExampleSolution)
function SymbolicIndexingInterface.parameter_values(sys::ExampleSystem)
sys.p
end
```

# Implementing the `SymbolicTypeTrait` for a type
## Implementing the `SymbolicTypeTrait` for a type

The `SymbolicTypeTrait` is used to identify values that can act as symbolic variables. It
has three variants:

- [`NotSymbolic`](@ref) for quantities that are not symbolic. This is the default for all
types.
- [`ScalarSymbolic`](@ref) for quantities that are symbolic, and represent a single
Expand Down
17 changes: 15 additions & 2 deletions docs/src/index.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SymbolicIndexingInterface.jl: Arrays of Arrays and Even Deeper
# SymbolicIndexingInterface.jl: Standardized Symbolic Indexing of Julia

SymbolicIndexingInterface.jl is a set of interface functions for handling containers
of symbolic variables.
Expand All @@ -12,6 +12,19 @@ using Pkg
Pkg.add("SymbolicIndexingInterface")
```

## Introduction

The symbolic indexing interface has 2 levels:

1. The user level. At the user level, a modeler or engineer simply uses terms from a
domain-specific language (DSL) inside of SciML functionality and will receive the requested
values. For example, if a DSL defines a symbol `x`, then `sol[x]` returns the solution
value(s) for `x`.
2. The DSL system structure level. This is the structure which defines the symbolic indexing
for a given problem/solution. DSLs can tag a constructed problem/solution with this
object in order to endow the SciML tools with the ability to index symbolically according
to the definitions the DSL writer wants.

## Contributing

- Please refer to the
Expand Down Expand Up @@ -79,4 +92,4 @@ file and the
[project]($link_project)
file.
""")
```
```
6 changes: 6 additions & 0 deletions docs/src/simple_sii_sys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Simple Demonstration of a Symbolic System Structure

In this tutorial we will show how to implement a system structure type for defining the
symbolic indexing of a domain-specific language. This tutorial will show how the
`SymbolCache` type is defined to take in arrays of symbols for its independent, dependent,
and parameter variable names and uses that to define the symbolic indexing interface.
26 changes: 26 additions & 0 deletions docs/src/solution_wrappers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Defining Solution Wrapper Fallbacks

The simplest case is when the type contains an object that already implements the interface.
All its methods can simply be forwarded to that object. To do so, SymbolicIndexingInterface.jl
provides the [`symbolic_container`](@ref) method. For example,

```julia
struct MySolutionWrapper{T<:SciMLBase.AbstractTimeseriesSolution}
sol::T
# other properties...
end

symbolic_container(sys::MySolutionWrapper) = sys.sol
```

`MySolutionWrapper` wraps an `AbstractTimeseriesSolution` which already implements the interface.
Since `symbolic_container` will return the wrapped solution, all method calls such as
`is_parameter(sys::MySolutionWrapper, sym)` will be forwarded to `is_parameter(sys.sol, sym)`.

In cases where some methods need to function differently than those of the wrapped type, they can be selectively
defined. For example, suppose `MySolutionWrapper` does not support observed quantities. The following
method can be defined (in addition to the one above):

```julia
is_observed(sys::MySolutionWrapper, sym) = false
```
Loading
Loading