From 67b71f0ab52894ca1ccd7d7c8056660a3cd8d975 Mon Sep 17 00:00:00 2001 From: Arno Strouwen Date: Fri, 23 Feb 2024 05:32:05 +0100 Subject: [PATCH] reapply formatting --- .JuliaFormatter.toml | 2 + README.md | 17 ++- docs/src/complete_sii.md | 194 +++++++++++++++++----------------- docs/src/index.md | 34 +++--- docs/src/simple_sii_sys.md | 2 +- docs/src/solution_wrappers.md | 6 +- docs/src/usage.md | 12 ++- src/state_indexing.jl | 1 - 8 files changed, 139 insertions(+), 129 deletions(-) diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml index 580b751..959ad88 100644 --- a/.JuliaFormatter.toml +++ b/.JuliaFormatter.toml @@ -1 +1,3 @@ style = "sciml" +format_markdown = true +format_docstrings = true \ No newline at end of file diff --git a/README.md b/README.md index d32d0c0..a2a6474 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,14 @@ Pkg.add("SymbolicIndexingInterface") 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. - + 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. ## Example diff --git a/docs/src/complete_sii.md b/docs/src/complete_sii.md index e5d592a..35375cb 100644 --- a/docs/src/complete_sii.md +++ b/docs/src/complete_sii.md @@ -5,11 +5,11 @@ This tutorial will show how to define the entire Symbolic Indexing Interface on ```julia 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} + 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} end ``` @@ -24,58 +24,58 @@ These are the simple functions which describe how to turn symbols into indices. ```julia function SymbolicIndexingInterface.is_variable(sys::ExampleSystem, sym) - haskey(sys.state_index, sym) + haskey(sys.state_index, sym) end function SymbolicIndexingInterface.variable_index(sys::ExampleSystem, sym) - get(sys.state_index, sym, nothing) + get(sys.state_index, sym, nothing) end function SymbolicIndexingInterface.variable_symbols(sys::ExampleSystem) - collect(keys(sys.state_index)) + collect(keys(sys.state_index)) end function SymbolicIndexingInterface.is_parameter(sys::ExampleSystem, sym) - haskey(sys.parameter_index, sym) + haskey(sys.parameter_index, sym) end function SymbolicIndexingInterface.parameter_index(sys::ExampleSystem, sym) - get(sys.parameter_index, sym, nothing) + get(sys.parameter_index, sym, nothing) end function SymbolicIndexingInterface.parameter_symbols(sys::ExampleSystem) - collect(keys(sys.parameter_index)) + collect(keys(sys.parameter_index)) end 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 + # 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::ExampleSystem) - sys.independent_variable === nothing ? [] : [sys.independent_variable] + sys.independent_variable === nothing ? [] : [sys.independent_variable] end function SymbolicIndexingInterface.is_time_dependent(sys::ExampleSystem) - sys.independent_variable !== nothing + sys.independent_variable !== nothing end SymbolicIndexingInterface.constant_structure(::ExampleSystem) = true function SymbolicIndexingInterface.all_solvable_symbols(sys::ExampleSystem) - return vcat( - collect(keys(sys.state_index)), - collect(keys(sys.observed)), - ) + return vcat( + collect(keys(sys.state_index)), + collect(keys(sys.observed)) + ) end function SymbolicIndexingInterface.all_symbols(sys::ExampleSystem) - return vcat( - all_solvable_symbols(sys), - collect(keys(sys.parameter_index)), - sys.independent_variable === nothing ? Symbol[] : sys.independent_variable - ) + return vcat( + all_solvable_symbols(sys), + collect(keys(sys.parameter_index)), + sys.independent_variable === nothing ? Symbol[] : sys.independent_variable + ) end ``` @@ -90,36 +90,38 @@ RuntimeGeneratedFunctions.init(@__MODULE__) # 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.is_observed(sys::ExampleSystem, sym) + sym isa Expr || sym isa Symbol && haskey(sys.observed, sym) +end function SymbolicIndexingInterface.observed(sys::ExampleSystem, sym::Expr) - # generate a function with the appropriate signature - if is_time_dependent(sys) - fn_expr = :( - function gen(u, p, t) - # assign a variable for each state symbol it's value in u - $([:($var = u[$idx]) for (var, idx) in pairs(sys.state_index)]...) - # assign a variable for each parameter symbol it's value in p - $([:($var = p[$idx]) for (var, idx) in pairs(sys.parameter_index)]...) - # assign a variable for the independent variable - $(sys.independent_variable) = t - # return the value of the expression - return $sym - end - ) - else - fn_expr = :( - function gen(u, p) - # assign a variable for each state symbol it's value in u - $([:($var = u[$idx]) for (var, idx) in pairs(sys.state_index)]...) - # assign a variable for each parameter symbol it's value in p - $([:($var = p[$idx]) for (var, idx) in pairs(sys.parameter_index)]...) - # return the value of the expression - return $sym - end - ) - end - return @RuntimeGeneratedFunction(fn_expr) + # generate a function with the appropriate signature + if is_time_dependent(sys) + fn_expr = :( + function gen(u, p, t) + # assign a variable for each state symbol it's value in u + $([:($var = u[$idx]) for (var, idx) in pairs(sys.state_index)]...) + # assign a variable for each parameter symbol it's value in p + $([:($var = p[$idx]) for (var, idx) in pairs(sys.parameter_index)]...) + # assign a variable for the independent variable + $(sys.independent_variable) = t + # return the value of the expression + return $sym + end + ) + else + fn_expr = :( + function gen(u, p) + # assign a variable for each state symbol it's value in u + $([:($var = u[$idx]) for (var, idx) in pairs(sys.state_index)]...) + # assign a variable for each parameter symbol it's value in p + $([:($var = p[$idx]) for (var, idx) in pairs(sys.parameter_index)]...) + # return the value of the expression + return $sym + end + ) + end + return @RuntimeGeneratedFunction(fn_expr) end ``` @@ -131,16 +133,17 @@ defined to always return `false`, and `observed` does not need to be implemented 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(::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::ExampleSystem)` would become - `variable_symbols(sys::ExampleSystem, i)` where `i` is the time index at which - the variable symbols are required. -- `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. + + - `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::ExampleSystem)` would become + `variable_symbols(sys::ExampleSystem, i)` where `i` is the time index at which + the variable symbols are required. + - `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 @@ -158,7 +161,7 @@ them is not necessary. ```julia function SymbolicIndexingInterface.parameter_values(sys::ExampleSystem) - sys.p + sys.p end ``` @@ -174,10 +177,10 @@ Consider the following `ExampleIntegrator` ```julia mutable struct ExampleIntegrator - u::Vector{Float64} - p::Vector{Float64} - t::Float64 - sys::ExampleSystem + u::Vector{Float64} + p::Vector{Float64} + t::Float64 + sys::ExampleSystem end # define a fallback for the interface methods @@ -188,6 +191,7 @@ SymbolicIndexingInterface.current_time(sys::ExampleIntegrator) = sys.t ``` Then the following example would work: + ```julia sys = ExampleSystem(Dict(:x => 1, :y => 2, :z => 3), Dict(:a => 1, :b => 2), :t, Dict()) integrator = ExampleIntegrator([1.0, 2.0, 3.0], [4.0, 5.0], 6.0, sys) @@ -210,10 +214,10 @@ the [`Timeseries`](@ref) trait. The type would then return a timeseries from ```julia struct ExampleSolution - u::Vector{Vector{Float64}} - t::Vector{Float64} - p::Vector{Float64} - sys::ExampleSystem + u::Vector{Vector{Float64}} + t::Vector{Float64} + p::Vector{Float64} + sys::ExampleSystem end # define a fallback for the interface methods @@ -228,6 +232,7 @@ SymbolicIndexingInterface.current_time(sol::ExampleSolution) = sol.t ``` Then the following example would work: + ```julia # using the same system that the ExampleIntegrator used sol = ExampleSolution([[1.0, 2.0, 3.0], [1.5, 2.5, 3.5]], [4.0, 5.0], [6.0, 7.0], sys) @@ -257,32 +262,33 @@ follows: ```julia function SymbolicIndexingInterface.set_state!(integrator::ExampleIntegrator, val, idx) - integrator.u[idx] = val - integrator.u_modified = true + integrator.u[idx] = val + integrator.u_modified = true end ``` # The `ParameterIndexingProxy` [`ParameterIndexingProxy`](@ref) is a wrapper around another type which implements the -interface and allows using [`getp`](@ref) and [`setp`](@ref) to get and set parameter +interface and allows using [`getp`](@ref) and [`setp`](@ref) to get and set parameter values. This allows for a cleaner interface for parameter indexing. Consider the following example for `ExampleIntegrator`: ```julia function Base.getproperty(obj::ExampleIntegrator, sym::Symbol) - if sym === :ps - return ParameterIndexingProxy(obj) - else - return getfield(obj, sym) - end + if sym === :ps + return ParameterIndexingProxy(obj) + else + return getfield(obj, sym) + end end ``` This enables the following API: ```julia -integrator = ExampleIntegrator([1.0, 2.0, 3.0], [4.0, 5.0], 6.0, Dict(:x => 1, :y => 2, :z => 3), Dict(:a => 1, :b => 2), :t) +integrator = ExampleIntegrator([1.0, 2.0, 3.0], [4.0, 5.0], 6.0, + Dict(:x => 1, :y => 2, :z => 3), Dict(:a => 1, :b => 2), :t) integrator.ps[:a] # 4.0 getp(integrator, :a)(integrator) # functionally the same as above @@ -296,25 +302,25 @@ setp(integrator, :b)(integrator, 3.0) # functionally the same as above 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 - logical value. -- [`ArraySymbolic`](@ref) for quantities that are symbolic, and represent an array of - values. Types implementing this trait must return an array of `ScalarSymbolic` variables - of the appropriate size and dimensions when `collect`ed. + - [`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 + logical value. + - [`ArraySymbolic`](@ref) for quantities that are symbolic, and represent an array of + values. Types implementing this trait must return an array of `ScalarSymbolic` variables + of the appropriate size and dimensions when `collect`ed. The trait is implemented through the [`symbolic_type`](@ref) function. Consider the following example types: ```julia struct MySym - name::Symbol + name::Symbol end struct MySymArr{N} - name::Symbol - size::NTuple{N,Int} + name::Symbol + size::NTuple{N, Int} end ``` @@ -329,10 +335,8 @@ SymbolicIndexingInterface.symbolic_type(::Type{<:MySymArr}) = ArraySymbolic() SymbolicIndexingInterface.hasname(::MySymArr) = true SymbolicIndexingInterface.getname(sym::MySymArr) = sym.name function Base.collect(sym::MySymArr) - [ - MySym(Symbol(sym.name, :_, join(idxs, "_"))) - for idxs in Iterators.product(Base.OneTo.(sym.size)...) - ] + [MySym(Symbol(sym.name, :_, join(idxs, "_"))) + for idxs in Iterators.product(Base.OneTo.(sym.size)...)] end ``` diff --git a/docs/src/index.md b/docs/src/index.md index 898f592..67a9c41 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -16,25 +16,27 @@ Pkg.add("SymbolicIndexingInterface") 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. + 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 - [SciML ColPrac: Contributor's Guide on Collaborative Practices for Community Packages](https://github.com/SciML/ColPrac/blob/master/README.md) - for guidance on PRs, issues, and other matters relating to contributing to SciML. -- There are a few community forums: - - the #diffeq-bridged channel in the [Julia Slack](https://julialang.org/slack/) - - [JuliaDiffEq](https://gitter.im/JuliaDiffEq/Lobby) on Gitter - - on the [Julia Discourse forums](https://discourse.julialang.org) - - see also [SciML Community page](https://sciml.ai/community/) + - Please refer to the + [SciML ColPrac: Contributor's Guide on Collaborative Practices for Community Packages](https://github.com/SciML/ColPrac/blob/master/README.md) + for guidance on PRs, issues, and other matters relating to contributing to SciML. + + - There are a few community forums: + + + the #diffeq-bridged channel in the [Julia Slack](https://julialang.org/slack/) + + [JuliaDiffEq](https://gitter.im/JuliaDiffEq/Lobby) on Gitter + + on the [Julia Discourse forums](https://discourse.julialang.org) + + see also [SciML Community page](https://sciml.ai/community/) ## Reproducibility diff --git a/docs/src/simple_sii_sys.md b/docs/src/simple_sii_sys.md index 1a03121..4c2499e 100644 --- a/docs/src/simple_sii_sys.md +++ b/docs/src/simple_sii_sys.md @@ -56,7 +56,7 @@ sol[:y₁] ``` ```@example symbolcache -sol(1e3, idxs=:y₁) +sol(1e3, idxs = :y₁) ``` However, we did not give names to the parameters or the independent variables. They can diff --git a/docs/src/solution_wrappers.md b/docs/src/solution_wrappers.md index c6599a5..c487a8e 100644 --- a/docs/src/solution_wrappers.md +++ b/docs/src/solution_wrappers.md @@ -5,9 +5,9 @@ All its methods can simply be forwarded to that object. To do so, SymbolicIndexi provides the [`symbolic_container`](@ref) method. For example, ```julia -struct MySolutionWrapper{T<:SciMLBase.AbstractTimeseriesSolution} - sol::T - # other properties... +struct MySolutionWrapper{T <: SciMLBase.AbstractTimeseriesSolution} + sol::T + # other properties... end symbolic_container(sys::MySolutionWrapper) = sys.sol diff --git a/docs/src/usage.md b/docs/src/usage.md index db91e73..f6daf30 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -12,6 +12,7 @@ We recommend any DSL implementing the symbolic indexing interface to link to thi as a full description of the functionality. !!! note + While this tutorial focuses on demonstrating the symbolic indexing interface for ODEs, note that the same functionality works across all of the other problem types, such as optimization problems, nonlinear problems, nonlinear solutions, etc. @@ -77,19 +78,19 @@ sol[(t, w)] ``` ```@example Usage -sol(1.3, idxs=x) +sol(1.3, idxs = x) ``` ```@example Usage -sol(1.3, idxs=[x, w]) +sol(1.3, idxs = [x, w]) ``` ```@example Usage -sol(1.3, idxs=[:y, :z]) +sol(1.3, idxs = [:y, :z]) ``` ```@example Usage -plot(sol, idxs=x) +plot(sol, idxs = x) ``` If necessary, `Symbol`s can be used to refer to variables. This is only valid for @@ -118,6 +119,7 @@ sol[solvedvariables] # equivalent to sol[variable_symbols(sol)] This does not include the observed variable `w`. To include observed variables in the output, the following shorthand is used: + ```@example Usage sol[allvariables] # equivalent to sol[all_variable_symbols(sol)] ``` @@ -127,6 +129,7 @@ sol[allvariables] # equivalent to sol[all_variable_symbols(sol)] Parameters cannot be obtained using this syntax, and instead require using [`getp`](@ref) and [`setp`](@ref). !!! note + The reason why parameters use a separate syntax is to be able to ensure type stability of the `sol[x]` indexing. Without separating the parameter indexing, the return type of symbolic indexing could be anything a parameter can be, which is general is not the same @@ -178,6 +181,7 @@ parameter_values(prob) ``` !!! note + These getters and setters generate high-performance functions for the specific chosen symbols or collection of symbols. Caching the getter/setter function and reusing it on other problem/solution instances can be the key to achieving good performance. Note diff --git a/src/state_indexing.jl b/src/state_indexing.jl index 5ebc82f..4a2db43 100644 --- a/src/state_indexing.jl +++ b/src/state_indexing.jl @@ -80,7 +80,6 @@ the state value is saved. In this case, the two-argument version of the function also be implemented to efficiently return the time at timestep `i`. By default, the two- argument method calls `current_time(p)[i]` - See: [`is_timeseries`](@ref) """ function current_time end