Skip to content

Commit

Permalink
Implement computed / observed states (#13)
Browse files Browse the repository at this point in the history
* implement computed / observed states

* switch to testing with OrdinaryDiffEqTsit5

* bump version

* test on v1.11
  • Loading branch information
MasonProtter authored Jan 15, 2025
1 parent af404d6 commit c1c25e0
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 34 deletions.
1 change: 1 addition & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jobs:
matrix:
version:
- '1.10'
- '1.11'
- 'nightly'
os:
- ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "GraphDynamics"
uuid = "bcd5d0fe-e6b7-4ef1-9848-780c183c7f4c"
version = "0.2.3"
version = "0.2.4"

[deps]
Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697"
Expand Down
50 changes: 38 additions & 12 deletions src/GraphDynamics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,17 @@ using SciMLBase:
VectorContinuousCallback,
ContinuousCallback,
DiscreteCallback,
remake
remake,
ODEFunction

using RecursiveArrayTools: ArrayPartition

using SymbolicIndexingInterface:
SymbolicIndexingInterface,
setu,
setp
setp,
getp,
observed

using Accessors:
Accessors,
Expand Down Expand Up @@ -138,6 +141,31 @@ function get_tag end
function get_params end
function get_states end

"""
computed_properties(::Subsystem{T}) :: NamedTuple{props, NTuple{N, funcs}}
Signal that a subsystem has properties which can be computed on-the-fly based on it's existing properties. In the termoinology used by ModelingToolkit.jl, these are "observed states".
This function takes in a `Subsystem` and returns a `NamedTuple` where each key is a property name that can be computed, and each value is a function that takes in the subsystem and returns a computed value.
By default, this function returns an empty NamedTuple.
Example:
```julia
struct Position end
function GraphDynamics.computed_properties(::Subsystem{Position})
(;r = (;x, y) -> √(x^2 + y^2),
θ = (;x, y) -> atan(y, x))
end
let sys = Subsystem{Position}(states=(x=1, y=2), params=(;))
sys.r == √(sys.x^2 + sys.y^2)
end
```
"""
computed_properies(s::Subsystem) = ()

"""
subsystem_differential(subsystem, input, t)
Expand Down Expand Up @@ -253,46 +281,44 @@ Base.size(m::ConnectionMatrix{N}) where {N} = (N, N)

abstract type GraphSystem end

@kwdef struct ODEGraphSystem{CM <: ConnectionMatrices, S, P, EVT, CDEP, CCEP, Ns, SNM, PNM} <: GraphSystem
@kwdef struct ODEGraphSystem{CM <: ConnectionMatrices, S, P, EVT, CDEP, CCEP, Ns, SNM, PNM, CNM} <: GraphSystem
connection_matrices::CM
states_partitioned::S
params_partitioned::P
tstops::EVT = Float64[]
composite_discrete_events_partitioned::CDEP = nothing
composite_continuous_events_partitioned::CCEP = nothing
names_partitioned::Ns
state_namemap::SNM = compute_namemap(names_partitioned, states_partitioned)
param_namemap::PNM = compute_namemap(names_partitioned, params_partitioned)
state_namemap::SNM = make_state_namemap(names_partitioned, states_partitioned)
param_namemap::PNM = make_param_namemap(names_partitioned, params_partitioned)
compu_namemap::CNM = make_compu_namemap(names_partitioned, states_partitioned, params_partitioned)
end
@kwdef struct SDEGraphSystem{CM <: ConnectionMatrices, S, P, EVT, CDEP, CCEP, Ns, SNM, PNM} <: GraphSystem
@kwdef struct SDEGraphSystem{CM <: ConnectionMatrices, S, P, EVT, CDEP, CCEP, Ns, SNM, PNM, CNM} <: GraphSystem
connection_matrices::CM
states_partitioned::S
params_partitioned::P
tstops::EVT = Float64[]
composite_discrete_events_partitioned::CDEP = nothing
composite_continuous_events_partitioned::CCEP = nothing
names_partitioned::Ns
state_namemap::SNM = compute_namemap(names_partitioned, states_partitioned)
param_namemap::PNM = compute_namemap(names_partitioned, params_partitioned)
state_namemap::SNM = make_state_namemap(names_partitioned, states_partitioned)
param_namemap::PNM = make_param_namemap(names_partitioned, params_partitioned)
compu_namemap::CNM = make_compu_namemap(names_partitioned, states_partitioned, params_partitioned)
end


#----------------------------------------------------------
# Infrastructure for subsystems
include("subsystems.jl")



#----------------------------------------------------------
# Problem generating API:
include("problems.jl")


#----------------------------------------------------------
# Symbolically indexing the solutions of graph systems
include("symbolic_indexing.jl")


#----------------------------------------------------------
# Solving graph differential equations
include("graph_solve.jl")
Expand Down
12 changes: 8 additions & 4 deletions src/subsystems.jl
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ end
function ConstructionBase.getproperties(s::Subsystem)
states = NamedTuple(get_states(s))
params = NamedTuple(get_params(s))
# TODO: should there be observed states in here? I don't want to accidentally waste CPU cycles on them
# but it might be necessary? Not sure, since they can't be `setproperty`-d
merge(states, params)
end
function ConstructionBase.setproperties(s::Subsystem{T, Eltype, States, Params}, patch::NamedTuple) where {T, Eltype, States, Params}
Expand Down Expand Up @@ -172,11 +174,8 @@ get_states(s::Subsystem) = getfield(s, :states)
get_params(s::Subsystem) = getfield(s, :params)
get_tag(::Subsystem{Name}) where {Name} = Name



get_tag(::Type{<:Subsystem{Name}}) where {Name} = Name


function Base.getproperty(s::Subsystem{<:Any, States, Params},
prop::Symbol) where {States, Params}
states = NamedTuple(get_states(s))
Expand All @@ -186,7 +185,12 @@ function Base.getproperty(s::Subsystem{<:Any, States, Params},
elseif prop keys(params)
getproperty(params, prop)
else
subsystem_prop_err(s, prop)
comp_props = computed_properies(s)
if prop keys(comp_props)
comp_props[prop](s)
else
subsystem_prop_err(s, prop)
end
end
end
@noinline subsystem_prop_err(s::Subsystem{Name}, prop) where {Name} = error(ArgumentError(
Expand Down
59 changes: 48 additions & 11 deletions src/symbolic_indexing.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,54 @@ struct ParamIndex #todo: this'll require some generalization to support weight p
prop::Symbol
end

function compute_namemap(names_partitioned, states_partitioned::Tuple{Vararg{AbstractVector{<:SubsystemStates}}})
state_namemap = OrderedDict{Symbol, StateIndex}()
function make_state_namemap(names_partitioned::NTuple{N, Vector{Symbol}},
states_partitioned::NTuple{N, AbstractVector{<:SubsystemStates}}) where {N}
namemap = OrderedDict{Symbol, StateIndex}()
for i eachindex(names_partitioned, states_partitioned)
for j eachindex(names_partitioned[i], states_partitioned[i])
for (k, name) enumerate(propertynames(states_partitioned[i][j]))
states = states_partitioned[i][j]
for (k, name) enumerate(propertynames(states))
propname = Symbol(names_partitioned[i][j], "", name)
state_namemap[propname] = StateIndex(i, j, k)
namemap[propname] = StateIndex(i, j, k)
end
end
end
state_namemap
namemap
end
function compute_namemap(names_partitioned, params_partitioned::Tuple{Vararg{AbstractVector{<:SubsystemParams}}})
param_namemap = OrderedDict{Symbol, ParamIndex}()

function make_param_namemap(names_partitioned::NTuple{N, Vector{Symbol}},
params_partitioned::NTuple{N, AbstractVector{<:SubsystemParams}}) where {N}
namemap = OrderedDict{Symbol, ParamIndex}()
for i eachindex(names_partitioned, params_partitioned)
for j eachindex(names_partitioned[i], params_partitioned[i])
for name propertynames(params_partitioned[i][j])
params = params_partitioned[i][j]
for name propertynames(params)
propname = Symbol(names_partitioned[i][j], "", name)
#TODO: this'll require some generalization to support weight params
param_namemap[propname] = ParamIndex(i, j, name)
namemap[propname] = ParamIndex(i, j, name)
end
end
end
namemap
end


function make_compu_namemap(names_partitioned::NTuple{N, Vector{Symbol}},
states_partitioned::NTuple{N, AbstractVector{<:SubsystemStates}},
params_partitioned::NTuple{N, AbstractVector{<:SubsystemParams}}) where {N}
namemap = OrderedDict{Symbol, ParamIndex}()
for i eachindex(names_partitioned, states_partitioned, params_partitioned)
for j eachindex(names_partitioned[i], states_partitioned[i], params_partitioned[i])
states = states_partitioned[i][j]
params = params_partitioned[i][j]
sys = Subsystem(states, params)
for name keys(computed_properies(sys))
propname = Symbol(names_partitioned[i][j], "", name)
namemap[propname] = ParamIndex(i, j, name)
end
end
end
param_namemap
namemap
end


Expand Down Expand Up @@ -118,7 +142,20 @@ function SymbolicIndexingInterface.is_time_dependent(sys::GraphSystem)
end

function SymbolicIndexingInterface.is_observed(sys::GraphSystem, sym)
false # TODO: support observed variables
haskey(sys.compu_namemap, sym)
end

function SymbolicIndexingInterface.observed(f::ODEFunction{a, b, F}, sym::Symbol) where {a, b, F<:GraphSystemFunction}
observed(f.f.sys, sym)
end
function SymbolicIndexingInterface.observed(sys::GraphSystem, sym)
(; tup_index, v_index, prop) = sys.compu_namemap[sym]
function gen(u, p, t)
(; params_partitioned, state_types_val) = p
states_partitioned = to_vec_o_states(u.x, state_types_val)
subsys = Subsystem(states_partitioned[tup_index][v_index], params_partitioned[tup_index][v_index])
getproperty(subsys, prop)
end
end

# function SymbolicIndexingInterface.all_solvable_symbols(sys::GraphSystem)
Expand Down
2 changes: 1 addition & 1 deletion test/Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[deps]
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
GraphDynamics = "bcd5d0fe-e6b7-4ef1-9848-780c183c7f4c"
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a"
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Expand Down
23 changes: 18 additions & 5 deletions test/particle_osc_example.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using GraphDynamics, OrdinaryDiffEq, Test, ForwardDiff
using GraphDynamics, OrdinaryDiffEqTsit5, Test, ForwardDiff

struct Particle end
function GraphDynamics.subsystem_differential(sys::Subsystem{Particle}, F, t)
Expand All @@ -7,6 +7,7 @@ function GraphDynamics.subsystem_differential(sys::Subsystem{Particle}, F, t)
dv = F/m
SubsystemStates{Particle}((;x=dx, v=dv))
end

GraphDynamics.initialize_input(::Subsystem{Particle}) = 0.0

struct Oscillator end
Expand All @@ -17,6 +18,7 @@ function GraphDynamics.subsystem_differential(sys::Subsystem{Oscillator}, F, t)
SubsystemStates{Oscillator}((;x=dx, v=dv))
end
GraphDynamics.initialize_input(::Subsystem{Oscillator}) = 0.0
GraphDynamics.computed_properies(::Subsystem{Oscillator}) = (;ω₀ = ((;m, k),) -> (k/m))

struct Spring
k::Float64
Expand All @@ -35,6 +37,13 @@ function ((;fac)::Coulomb)(a, b)
end


@testset "basic" begin
sys = Subsystem{Oscillator}(states=(;x=-2.0, v=1.0), params=(;x₀=0.0, m=30.0, k=1.0, q=1.0))
@test sys.x == -2.0
@test sys.ω₀ == (sys.k/sys.m)
end


function solve_particle_osc(;x1, x2, tspan = (0.0, 10.0), alg=Tsit5())
# put some garbage values in here for states and params, but we'll set them to reasonable values later with the
# u0map and param_map
Expand Down Expand Up @@ -89,18 +98,22 @@ end

@testset "solutions" begin
sol = solve_particle_osc(;x1=1.0, x2=-1.0)
@test sol[:particle1₊x][end] 0.580617 rtol=1e-3
@test sol[:particle2₊x][end] -1.391576 rtol=1e-3
@test sol[:osc₊x][end] -1.063306 rtol=1e-3
@test sol[:particle1₊x, end] 0.580617 rtol=1e-3
@test sol[:particle2₊x, end] -1.391576 rtol=1e-3
@test sol[:osc₊x, end] -1.063306 rtol=1e-3
k = GraphDynamics.getp(sol, :osc₊k)(sol)
m = GraphDynamics.getp(sol, :osc₊m)(sol)
@test sol[:osc₊ω₀, end] == (k/m)
end

@testset "sensitivies" begin
jac = ForwardDiff.jacobian([1.0, -1.0]) do (x1, x2)
sol = solve_particle_osc(;x1, x2)
[sol[:particle1₊x][end], sol[:particle2₊x][end], sol[:osc₊x][end]]
[sol[:particle1₊x, end], sol[:particle2₊x, end], sol[:osc₊x, end]]
end
@test jac [-0.50821 -0.740152
-0.199444 -0.906593
-0.586021 0.118173] rtol=1e-3

end

2 comments on commit c1c25e0

@MasonProtter
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/123070

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.2.4 -m "<description of version>" c1c25e076decbc51303822ab92802783d29a4e5e
git push origin v0.2.4

Please sign in to comment.