Skip to content

Commit 057ee44

Browse files
authored
Merge pull request #195 from JuliaDynamics/hw/callbacks
Component Callbacks
2 parents 6af2bb1 + 3a9fefc commit 057ee44

20 files changed

+1432
-52
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ version = "0.9.7"
66
[deps]
77
ArgCheck = "dce04be8-c92d-5529-be00-80e4d2c0e197"
88
Atomix = "a9b6321e-bd34-4604-b9c9-b65b8de01458"
9+
DiffEqCallbacks = "459566f4-90b8-5000-8ac3-15dfb0a30def"
910
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
1011
FastClosures = "9aa1b823-49e4-5ca5-8b0f-3971ec8bab6a"
1112
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
@@ -48,6 +49,7 @@ Adapt = "4.0.4"
4849
ArgCheck = "2.3.0"
4950
Atomix = "1"
5051
CUDA = "5.5.2"
52+
DiffEqCallbacks = "4.2.2"
5153
DocStringExtensions = "0.9.3"
5254
FastClosures = "0.3.2"
5355
ForwardDiff = "0.10.36"

docs/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
55
DiffEqCallbacks = "459566f4-90b8-5000-8ac3-15dfb0a30def"
66
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
77
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
8+
DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656"
89
DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821"
910
GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2"
1011
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"

docs/examples/cascading_failure.jl

Lines changed: 108 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ This script can be dowloaded as a normal Julia script [here](@__NAME__.jl). #md
88
99
> [1] Schäfer, B., Witthaut, D., Timme, M., & Latora, V. (2018). Dynamically induced cascading failures in power grids. Nature communications, 9(1), 1-13. https://www.nature.com/articles/s41467-018-04287-5
1010
11+
This example has three subchaperts:
12+
- first we [define the network model](#define-the-model),
13+
- secondly, we implement [component based callbacks](#component-based-callbacks) and
14+
- thirdly we solve the problem using [systemwide callbacks](#system-wide-callbacks).
15+
16+
1117
The system is modeled using swing equation and active power edges. The nodes are
1218
characterized by the voltage angle `δ`, the active power on each line is symmetric
1319
and a function of the difference between source and destination angle `δ_src - δ_dst`.
@@ -22,6 +28,8 @@ using Test #hide
2228
import SymbolicIndexingInterface as SII
2329

2430
#=
31+
## Defining the Model
32+
2533
For the nodes we define the swing equation. State `v[1] = δ`, `v[2] = ω`.
2634
The swing equation has three parameters: `p = (P_ref, I, γ)` where `P_ref`
2735
is the power setpopint, `I` is the inertia and `γ` is the droop or damping coeficcient.
@@ -58,25 +66,102 @@ g = SimpleGraph([0 1 1 0 1;
5866
1 1 0 1 0;
5967
0 1 1 0 1;
6068
1 0 0 1 0])
61-
swing_network = Network(g, vertex, edge)
69+
nw = Network(g, vertex, edge; dealias=true)
6270

6371
#=
64-
For the parameters, we create the `NWParameter` object prefilled with default p values
72+
Note that we used `dealias=true` to automaticially generate separate
73+
`ComponentModels` for each vertex/edge. Doing so allows us to later
74+
set different metadata (callbacks, default values, etc.) for each vertex/edge.
75+
76+
We proceed by setting the default reference power for the nodes:
6577
=#
66-
p = NWParameter(swing_network)
67-
## vertices 1, 3 and 4 act as loads
68-
p.v[(1,3,4), :P_ref] .= -1
69-
## vertices 2 and 5 act as generators
70-
p.v[(2,5), :P_ref] .= 1.5
78+
set_default!(nw, VIndex(1, :P_ref), -1.0) # load
79+
set_default!(nw, VIndex(2, :P_ref), 1.5) # generator
80+
set_default!(nw, VIndex(3, :P_ref), -1.0) # load
81+
set_default!(nw, VIndex(4, :P_ref), -1.0) # load
82+
set_default!(nw, VIndex(5, :P_ref), 1.5) # generator
7183
nothing #hide #md
7284

7385
#=
74-
We can use `find_fixpoint` to find a valid initial condition of the network
86+
We can use `find_fixpoint` to find a valid initial condition of the network.
87+
We also use `set_defaults!` to overwirte all the default values for states and parameters
88+
with the one of the fixpoint, this means that we can allways re-extract this setpoint by
89+
using `u0 = NWState(nw)`.
90+
=#
91+
u0 = find_fixpoint(nw)
92+
set_defaults!(nw, u0)
93+
nothing #hide #md
94+
95+
#=
96+
## Component-based Callbacks
97+
98+
For the component based callback we need to define a condtion and an affect.
99+
Both functions take three inputs:
100+
- the actual function `f`
101+
- the states which to be accessed `sym`
102+
- the parameters to be accessed `psym`
103+
=#
104+
cond = ComponentCondition([:P], [:limit]) do u, p, t
105+
abs(u[:P]) - p[:limit]
106+
end
107+
affect = ComponentAffect([], [:K]) do u, p, ctx
108+
println("Line $(ctx.eidx) tripped at t=$(ctx.integrator.t)")
109+
p[:K] = 0
110+
end
111+
edge_cb = ContinousComponentCallback(cond, affect)
112+
#=
113+
To enable the callback in simulation, we need to attach them to the individual
114+
edgemodels/vertexmodels.
75115
=#
76-
u0 = find_fixpoint(swing_network, p)
116+
for i in 1:ne(g)
117+
edgemodel = nw[EIndex(i)]
118+
set_callback!(edgemodel, edge_cb)
119+
end
77120
nothing #hide #md
78121

79122
#=
123+
The system starts at a steady state.
124+
In order to see any dynamic, we need to fail a first line intentionally.
125+
For that we use a [`PresetTimeComponentCallback`](@ref), which triggers an
126+
`ComponentAffect` at a given time. We can reuse the previously defined component
127+
affect for that and just add it to line number 5 at time 1.0.
128+
=#
129+
trip_first_cb = PresetTimeComponentCallback(1.0, affect)
130+
add_callback!(nw[EIndex(5)], trip_first_cb)
131+
nothing #hide #md
132+
#=
133+
When we inspect the edge model for 5 no, we see that we've registered 2 callbacks:
134+
=#
135+
nw[EIndex(5)]
136+
137+
#=
138+
Now we can simulate the network. We use [`get_callbacks(::Network)`](@ref)
139+
to generate a callback set for the whole network which represents all of the individual
140+
component callbacks.
141+
=#
142+
u0 = NWState(nw)
143+
network_cb = get_callbacks(nw)
144+
prob = ODEProblem(nw, uflat(u0), (0, 6), pflat(u0); callback=network_cb)
145+
sol = solve(prob, Tsit5());
146+
nothing #hide #md
147+
148+
#=
149+
Lastly we plot the power flow on all lines using the [`eidxs`](@ref) function to generate the
150+
symbolic indices for the states of interest:
151+
=#
152+
plot(sol; idxs=eidxs(sol, :, :P))
153+
154+
#=
155+
## System wide Callbacks
156+
157+
The above solution relies on the `ComponentCallback` features of
158+
NetworkDyanmics. The "low-level" API would be to use `VectorContinousCallback`
159+
and `PresetTimeCallback` directly to achieve the same effect, essentially doing
160+
manually what [`get_callbacks(::Network)`](@ref) is doing for us.
161+
162+
While not necessary in this case, this method offers more flexiblity then the
163+
component based appraoch.
164+
80165
In order to implement the line failures, we need to create a `VectorContinousCallback`.
81166
In the callback, we compare the current flow on the line with the limit. If the limit is reached,
82167
the coupling `K` is set to 0.
@@ -125,8 +210,8 @@ cache accessors to the internal states.
125210
This still isn't ideal beacuse both `getlim` and `getflow` getters will create arrays
126211
within the callback. But is far better then resolving the flat state indices every time.
127212
=#
128-
condition = let getlim = SII.getp(swing_network, epidxs(swing_network, :, :limit)),
129-
getflow = SII.getu(swing_network, eidxs(swing_network, :, :P))
213+
condition = let getlim = SII.getp(nw, epidxs(nw, :, :limit)),
214+
getflow = SII.getu(nw, eidxs(nw, :, :P))
130215
function (out, u, t, integrator)
131216
## careful, u != integrator.u
132217
## therefore construct nwstate with Network info from integrator but u
@@ -143,28 +228,26 @@ We can combine affect and condition to form the callback.
143228
trip_cb = VectorContinuousCallback(condition, affect!, ne(g));
144229

145230
#=
146-
However, there is another component missing. If we look at the powerflow on the
147-
lines in the initial steady state
148-
=#
149-
u0.e[:, :P]
150-
#=
231+
Similarily to before, we need to generate a initial perturbation by failing one line
232+
using a `PresetTimeCallback`.
151233
We see that every flow is below the trip value 1.0. Therefor we need to add a distrubance
152234
to the network. We do this by manually disabeling line 5 at time 1.
153235
=#
154236
trip_first_cb = PresetTimeCallback(1.0, integrator->affect!(integrator, 5));
155237

156238
#=
157-
With those components, we can create the problem and solve it.
239+
Now we are set for solving the system again. This time we create our own callback
240+
set by combining both Callbacks manually.
158241
=#
159-
160-
prob = ODEProblem(swing_network, uflat(u0), (0,6), copy(pflat(p));
161-
callback=CallbackSet(trip_cb, trip_first_cb))
242+
u0 = NWState(nw)
243+
cbset = CallbackSet(trip_cb, trip_first_cb)
244+
prob = ODEProblem(nw, uflat(u0), (0,6), pflat(u0); callback=cbset)
162245
Main.test_execution_styles(prob) # testing all ex styles #src
163-
sol = solve(prob, Tsit5());
246+
sol2 = solve(prob, Tsit5());
164247
## we want to test the reconstruction of the observables # hide
165-
@test all(!iszero, sol(sol.t; idxs=eidxs(sol,:,:P))[begin]) # hide
166-
@test all(iszero, sol(sol.t; idxs=eidxs(sol,:,:P))[end][[1:5...,7]]) # hide
248+
@test all(!iszero, sol2(sol2.t; idxs=eidxs(sol2,:,:P))[begin]) # hide
249+
@test all(iszero, sol2(sol2.t; idxs=eidxs(sol2,:,:P))[end][[1:5...,7]]) # hide
167250
nothing #hide
168251

169-
# Through the magic of symbolic indexing we can plot the power flows on all lines:
170-
plot(sol; idxs=eidxs(sol,:,:P))
252+
# Then again we plot the solution:
253+
plot(sol2; idxs=eidxs(sol2,:,:P))

docs/make.jl

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ using NetworkDynamics
55
using SciMLBase
66
using Literate
77
using ModelingToolkit
8+
using DocumenterInterLinks
9+
10+
links = InterLinks(
11+
"DiffEq" => "https://docs.sciml.ai/DiffEqDocs/stable/",
12+
"MTK" => "https://docs.sciml.ai/ModelingToolkit/stable/",
13+
"SymbolicIndexingInterface" => "https://docs.sciml.ai/SymbolicIndexingInterface/stable/",
14+
"DiffEqCallbacks" => "https://docs.sciml.ai/DiffEqCallbacks/stable/",
15+
)
816

917
# generate examples
1018
example_dir = joinpath(@__DIR__, "examples")
@@ -17,16 +25,14 @@ for example in filter(contains(r".jl$"), readdir(example_dir, join=true))
1725
Literate.script(example, outdir; keep_comments=true)
1826
end
1927

20-
# TODO: doc on steady state solve https://docs.sciml.ai/NonlinearSolve/stable/native/steadystatediffeq/#SteadyStateDiffEq.SSRootfind
21-
# TODO: doc on parameter & state handling? -> symbolic indexing
22-
2328
mtkext = Base.get_extension(NetworkDynamics, :MTKExt)
24-
makedocs(;
29+
kwargs = (;
2530
root=joinpath(pkgdir(NetworkDynamics), "docs"),
2631
sitename="NetworkDynamics",
2732
modules=[NetworkDynamics, mtkext],
2833
linkcheck=true, # checks if external links resolve
2934
pagesonly=true,
35+
plugins=[links],
3036
pages=[
3137
"General" => "index.md",
3238
"mathematical_model.md",
@@ -35,6 +41,7 @@ makedocs(;
3541
"symbolic_indexing.md",
3642
"metadata.md",
3743
"initialization.md",
44+
"callbacks.md",
3845
"mtk_integration.md",
3946
"external_inputs.md",
4047
],
@@ -52,12 +59,30 @@ makedocs(;
5259
],
5360
draft=false,
5461
format = Documenter.HTML(ansicolor = true),
55-
warnonly=[:cross_references, :missing_docs, :docs_block],
56-
# warnonly=true,
62+
warnonly=[:missing_docs],
5763
)
64+
kwargs_warnonly = (; kwargs..., warnonly=true)
65+
66+
if haskey(ENV,"GITHUB_ACTIONS")
67+
success = true
68+
thrown_ex = nothing
69+
try
70+
makedocs(; kwargs...)
71+
catch e
72+
@info "Strict doc build failed, try again with warnonly=true"
73+
global success = false
74+
global thrown_ex = e
75+
makedocs(; kwargs_warnonly...)
76+
end
77+
78+
deploydocs(; repo="github.com/JuliaDynamics/NetworkDynamics.jl.git",
79+
devbranch="main", push_preview=true)
80+
81+
success || throw(thrown_ex)
82+
else # local build
83+
makedocs(; kwargs_warnonly...)
84+
end
5885

59-
deploydocs(; repo="github.com/JuliaDynamics/NetworkDynamics.jl.git",
60-
devbranch="main", push_preview=true)
6186

6287
# warnonly options
6388
# :autodocs_block

docs/src/API.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,35 @@ get_bounds
124124
set_bounds!
125125
```
126126

127+
## Callbacks API
128+
### Define Callbacks
129+
```@docs
130+
NetworkDynamics.ComponentCallback
131+
ContinousComponentCallback
132+
VectorContinousComponentCallback
133+
DiscreteComponentCallback
134+
PresetTimeComponentCallback
135+
ComponentCondition
136+
ComponentAffect
137+
SymbolicView
138+
get_callbacks(::NetworkDynamics.Network)
139+
```
140+
### Attach Callbacks to Edge/VertexModels
141+
```@docs
142+
has_callback
143+
get_callbacks(::NetworkDynamics.ComponentModel)
144+
set_callback!
145+
add_callback!
146+
```
147+
127148
## Initialization
128149
```@docs
129150
find_fixpoint
130151
initialize_component!
131152
init_residual
132153
get_initial_state
133154
dump_initial_state
155+
set_defaults!
134156
```
135157

136158
## Execution Types

0 commit comments

Comments
 (0)