Skip to content

Commit d9c2368

Browse files
committed
add docs on callbacks
1 parent 269914f commit d9c2368

File tree

5 files changed

+165
-4
lines changed

5 files changed

+165
-4
lines changed

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/make.jl

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

916
# generate examples
1017
example_dir = joinpath(@__DIR__, "examples")
@@ -24,6 +31,7 @@ kwargs = (;
2431
modules=[NetworkDynamics, mtkext],
2532
linkcheck=true, # checks if external links resolve
2633
pagesonly=true,
34+
plugins=[links],
2735
pages=[
2836
"General" => "index.md",
2937
"mathematical_model.md",

docs/src/API.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,16 +127,18 @@ set_bounds!
127127
## Callbacks API
128128
### Define Callbacks
129129
```@docs
130+
ComponentCallback
130131
ContinousComponentCallback
131132
VectorContinousComponentCallback
132133
ComponentCondition
133134
ComponentAffect
134135
SymbolicView
136+
get_callbacks(::NetworkDynamics.Network)
135137
```
136138
### Attach Callbacks to Edge/VertexModels
137139
```@docs
138140
has_callback
139-
get_callbacks
141+
get_callbacks(::NetworkDynamics.ComponentModel)
140142
set_callback!
141143
add_callback!
142144
```

docs/src/callbacks.md

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,153 @@
1-
# Callbacks
1+
# Callbacks and Events (@id Callbacks)
22

3-
tbd
3+
Callback-functions are a way of handling discontinuities in differential equations.
4+
In a nutshell, the solver checks for some "condition" (i.e. a zero crossing of some variable)
5+
and calls some "affect" if the condition is fulfilled.
6+
In the affect it is safe to modify the integrator, e.g. changing some state or some parameter.
7+
8+
Since `NetworkDynamics.jl` provides nothing more than a RHS for DifferentialEquations.jl, please check
9+
[their docs on event handling](https://docs.sciml.ai/DiffEqDocs/stable/features/callback_functions/)
10+
as a general reference.
11+
12+
This page at introducing the general concepts, for a hands on example of a simulation with callbacks
13+
refer to the [Cascading Failure](@ref) example.
14+
15+
16+
## Component-based Callback functions
17+
In practice, events often act locally, meaning they only depend and act on a
18+
specific component or type of component. `NetworkDynamics.jl` provides a way of
19+
defining those callbacks on a component level and automaticially combine them into performant
20+
[`VectorContinuousCallback`](@extref SciMLBase.VectorContinuousCallback) for the whole network.
21+
22+
The main entry points are the types [`ContinousComponentCallback`](@ref) and
23+
[`VectorContinousComponentCallback`](@ref). Both of those objects combine a [`ComponentCondition`](@ref)
24+
with an [`ComponentAffect`](@ref).
25+
The "normal" `ContinousComponentCallback` has a condition which returns a single value. The corresponding affect is triggered when the return value hits zero.
26+
In contrast, the "vector" version has an in-place condition which writes `len` outputs. When any of those outputs hits zero, the affect is triggered with an additional argument `event_idx` which tells the effect which dimension encountered the zerocrossing.
27+
28+
### Defining the Callback
29+
To construct a condition function, you need to tell network dynamics which states and parameters you'd like to "observe" within the condition. Within the actual condition, those states will be made available:
30+
```julia
31+
condition = ComponentCond([:x, :y], [:p1, :p2]) do u, p, t
32+
u[:x] == u[1] # access a state or observable :x at current time
33+
p[:p2] == p[2] # access a parameter at current time
34+
return some_condition(u[:x], u[:y], ...)
35+
end
36+
```
37+
In case of a `VectorContinousComponentCallback`, the function signature looks slightly different:
38+
```julia
39+
vectorcondition = ComponentCond([:x, :y], [:p1, :p2]) do out, u, p, t
40+
out[1] = some_condition(u[...], p[...])
41+
out[2] = some_condition(u[...], p[...])
42+
return nothing
43+
end
44+
```
45+
Note that the `syms` argument (here `[:x, :y]`) can be used to reference **any**
46+
named state of the component model, this includes "ordinary" states, observed,
47+
inputs and outputs.
48+
The arguments `u` and `p` will be passed as [`SymbolicView`](@ref) objects, which mean
49+
it is possible to use the getindex syntax to acces the desired states by name.
50+
51+
The affect takes a similar form:
52+
```julia
53+
affect = ComponentAffect([:u], [:p]) do u, p, ctx
54+
t = ctx.t # extract data from context
55+
obs = NWState(ctx.integrator)[VIndex(ctx.vidx, :obs)] # extract some observed state from context
56+
println("Trigger affect at t=$t")
57+
end
58+
vectoraffect = ComponentAffect([:u], [:p]) do u, p, event_idx, ctx
59+
if event_idx == 1
60+
u[:u] = 0 # change state
61+
else
62+
u[:p] = 0 # change parameter
63+
end
64+
end
65+
```
66+
Notably, the `syms` (here `:u`) can *exclusivly* refer to "ordinary" states, since they are now writable.
67+
However the affect gets passed a `ctx` "context" object, which is a named tuple which holds additional context like the integrator object, the component model, the index of the component model, the current time and so on. Please refere to the [`ComponentAffect`](@ref) docstring for a detailed list.
68+
69+
Lastly we need to define the actuall callback object using [`ContinousComponentCallback`](@ref)/[`VectorContinousComponentCallback`](@ref):
70+
```julia
71+
ccb = ContinousComponentCallback(condition, affect; kwargs...)
72+
vccb = VectorContinousComponentCallback(condition, affect; kwargs...)
73+
```
74+
where the `kwargs` are passed to the underlying [`SciMLBase.VectorContinuousCallback`](@extref) to finetune the zerocrossing-detection.
75+
76+
77+
### Registering the Callback
78+
Once the callback is defined, we need to "attach" it to the component, for that you can use the methods [`add_callback!`](@ref) and [`set_callback!`](@ref):
79+
```julia
80+
vert = VertexModel(...)
81+
add_callback!(vert, ccb)
82+
add_callback!(vert, vccb)
83+
```
84+
85+
86+
### Extracting the Callback
87+
In order to use the callback during simulation, we need to generate a [`SciMLBase.CallbackSet`](@extref) which contains the conditions and affects of all the component based callbacks in the network. For that we use [`get_callbacks(::Network)`](@ref `get_callbacks(::NetworkDynamics.Network)`):
88+
```julia
89+
u0 = NWState(u0)
90+
cbs = get_callbacks(nw)
91+
prob = ODEProblem(nw, uflat(u0), (0,10), pflat(u0); callback=cbs)
92+
sol = solve(prob, ...)
93+
```
94+
95+
When combining the component based callbacks to a single callback, NetworkDynamics will check whether states and or parameters changed during the affect and automaticially call [`SciMLBase.auto_dt_reset!`](@extref) and [`save_parameters!`](@ref) if necessary.
96+
97+
98+
## Normal DiffEq Callbacks
99+
Besides component based callbacks, it is also possible to use "normal" DiffEq
100+
callbacks together with `NetworkDynamics.jl`.
101+
It is far more powerful but also more cumbersome compared to the component based callback functions.
102+
To access states and parameters of specific components, we havily rely on the [Symbolic Indexing](@ref) features.
103+
104+
```julia
105+
using SymbolicIndexingInterface as SII
106+
nw = Network(#= some network =#)
107+
108+
condition = let getvalue = SII.getu(nw, VIndex(1:5, :some_state))
109+
function(out, u, t, integrator)
110+
s = NWState(integrator, u, integrator.p, t)
111+
some_state = getvalue(s)
112+
out .= some_condition(some_state)
113+
end
114+
end
115+
```
116+
Please not a few important things here:
117+
- Symbolic indexing can be costly, and the condition function gets called very
118+
often. By using [`SII.getu`](@extref `SymbolicIndexingInterface.getu`) we did
119+
some of the work *before* the callback by creating the accessor function.
120+
When handling with "normal states" and parameters consider using
121+
[`SII.variable_index`](@ref `SymbolicIndexingInterface.variable_index`) and
122+
[`SII.parameter_index`](@ref `SymbolicIndexingInterface.parameter_index`) for
123+
even better access patterns.
124+
- `t` refers to the current time of the zerocrossing-detection-algorithm. This is different from `integrator.t` which refers to the current timestep in which the zerocross-detectio takes place..
125+
126+
```julia
127+
function affect!(integrator, vidx)
128+
p = NWParameter(integrator) # get symbolicially indexable parameter object
129+
p.v[vidx, :some_vertex_parameter] = 0 # change some parameter
130+
auto_dt_reset!(integrator)
131+
save_parameters!(integrator)
132+
end
133+
```
134+
The affect function is much more straight forward, as it (typically) is called far less frequent and thus less perfomance critical.
135+
136+
Once the `condition` and `affect!` is defined, you can use the [`SciMLBase.ContinuousCallback`](@extref) and [`SciMLBase.VectorContinuousCallback`](@extref) constructors to create the callback.
137+
138+
!!! note "Introducing discontinuities with adaptive timestepping"
139+
Since changes to `u` and `p` mostly introduce discontinuities in the
140+
solution, it is recommend to call [`auto_dt_reset!`](@extref
141+
`SciMLBase.auto_dt_reset!`) within the affect to restart integration with
142+
small steps afterwards.
143+
144+
!!! note "Changing Parameters and Observables"
145+
An "observable" is kind of a "virtual" state, which can be reconstructed for
146+
a given time `t`, a given state `u` and a given set of parameters `p`
147+
```math
148+
o = f(u(t), p(t), t)
149+
```
150+
To extract or plot timeseries of observed states under *time variant
151+
parameters* (i.e. parameters that are changed in a callback), those changes
152+
need to be recorded using the [`save_parameters!`](@ref) function whenever `p` is changed.
153+
When using [ComponentCallback](@ref), NetworkDynamics will automaticially check for changes in `p` and save them if necessary.

src/callbacks.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ struct ComponentCondition{C,DIM,PDIM}
5555
end
5656

5757
"""
58-
ComponentCondition(f::Function, sym, psym)
58+
ComponentAffect(f::Function, sym, psym)
5959
6060
Creates a callback condition for a [`ComponentCallback`].
6161
- `f`: The affect function. Must be a function of the form `f(u, p, [event_idx], ctx)` where `event_idx`

0 commit comments

Comments
 (0)