Skip to content

Commit 22b5a69

Browse files
Merge pull request #3614 from SciML/cbtest-new
feat: implement new callback semantics
2 parents 6ea7c66 + 2fbda50 commit 22b5a69

35 files changed

+1543
-1566
lines changed

NEWS.md

+19
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
# ModelingToolkit v10 Release Notes
2+
3+
### Callbacks
4+
5+
Callback semantics have changed.
6+
7+
- There is a new `Pre` operator that is used to specify which values are before the callback.
8+
For example, the affect `A ~ A + 1` should now be written as `A ~ Pre(A) + 1`. This is
9+
**required** to be specified - `A ~ A + 1` will now be interpreted as an equation to be
10+
satisfied after the callback (and will thus error since it is unsatisfiable).
11+
12+
- All parameters that are changed by a callback must be declared as discrete parameters to
13+
the callback constructor, using the `discrete_parameters` keyword argument.
14+
15+
```julia
16+
event = SymbolicDiscreteCallback(
17+
[t == 1] => [p ~ Pre(p) + 1], discrete_parameters = [p])
18+
```
19+
120
# ModelingToolkit v9 Release Notes
221

322
### Upgrade guide

Project.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
3030
FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e"
3131
FunctionWrappersWrappers = "77dc65aa-8811-40c2-897b-53d922fa7daf"
3232
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
33+
ImplicitDiscreteSolve = "3263718b-31ed-49cf-8a0f-35a466e8af96"
3334
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
3435
JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
3536
JumpProcesses = "ccbc3e58-028d-4f4c-8cd5-9ae44345cda5"
@@ -42,6 +43,7 @@ NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3"
4243
NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec"
4344
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
4445
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
46+
OrdinaryDiffEqCore = "bbf590c4-e513-4bbe-9b18-05decba2e5d8"
4547
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
4648
RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd"
4749
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
@@ -113,6 +115,7 @@ ForwardDiff = "0.10.3"
113115
FunctionWrappers = "1.1"
114116
FunctionWrappersWrappers = "0.1"
115117
Graphs = "1.5.2"
118+
ImplicitDiscreteSolve = "0.1.2"
116119
InfiniteOpt = "0.5"
117120
InteractiveUtils = "1"
118121
JuliaFormatter = "1.0.47, 2"
@@ -139,7 +142,7 @@ RecursiveArrayTools = "3.26"
139142
Reexport = "0.2, 1"
140143
RuntimeGeneratedFunctions = "0.5.9"
141144
SCCNonlinearSolve = "1.0.0"
142-
SciMLBase = "2.84"
145+
SciMLBase = "2.89.1"
143146
SciMLStructures = "1.7"
144147
Serialization = "1"
145148
Setfield = "0.7, 0.8, 1"

docs/src/basics/Events.md

+87-21
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,67 @@ the event occurs). These can both be specified symbolically, but a more [general
2525
functional affect](@ref func_affects) representation is also allowed, as described
2626
below.
2727

28+
## Symbolic Callback Semantics
29+
30+
In callbacks, there is a distinction between values of the unknowns and parameters
31+
*before* the callback, and the desired values *after* the callback. In MTK, this
32+
is provided by the `Pre` operator. For example, if we would like to add 1 to an
33+
unknown `x` in a callback, the equation would look like the following:
34+
35+
```julia
36+
x ~ Pre(x) + 1
37+
```
38+
39+
Non `Pre`-d values will be interpreted as values *after* the callback. As such,
40+
writing
41+
42+
```julia
43+
x ~ x + 1
44+
```
45+
46+
will be interpreted as an algebraic equation to be satisfied after the callback.
47+
Since this equation obviously cannot be satisfied, an error will result.
48+
49+
Callbacks must maintain the consistency of DAEs, meaning that they must satisfy
50+
all the algebraic equations of the system after their update. However, the affect
51+
equations often do not fully specify which unknowns/parameters should be modified
52+
to maintain consistency. To make this clear, MTK uses the following rules:
53+
54+
1. All unknowns are treated as modifiable by the callback. In order to enforce that an unknown `x` remains the same, one can add `x ~ Pre(x)` to the affect equations.
55+
2. All parameters are treated as un-modifiable, *unless* they are declared as `discrete_parameters` to the callback. In order to be a discrete parameter, the parameter must be time-dependent (the terminology *discretes* here means [discrete variables](@ref save_discretes)).
56+
57+
For example, consider the following system.
58+
59+
```julia
60+
@variables x(t) y(t)
61+
@parameters p(t)
62+
@mtkbuild sys = ODESystem([x * y ~ p, D(x) ~ 0], t)
63+
event = [t == 1] => [x ~ Pre(x) + 1]
64+
```
65+
66+
By default what will happen is that `x` will increase by 1, `p` will remain constant,
67+
and `y` will change in order to compensate the increase in `x`. But what if we
68+
wanted to keep `y` constant and change `p` instead? We could use the callback
69+
constructor as follows:
70+
71+
```julia
72+
event = SymbolicDiscreteCallback(
73+
[t == 1] => [x ~ Pre(x) + 1, y ~ Pre(y)], discrete_parameters = [p])
74+
```
75+
76+
This way, we enforce that `y` will remain the same, and `p` will change.
77+
78+
!!! warning
79+
80+
Symbolic affects come with the guarantee that the state after the callback
81+
will be consistent. However, when using [general functional affects](@ref func_affects)
82+
or [imperative affects](@ref imp_affects) one must be more careful. In
83+
particular, one can pass in `reinitializealg` as a keyword arg to the
84+
callback constructor to re-initialize the system. This will default to
85+
`SciMLBase.NoInit()` in the case of symbolic affects and `SciMLBase.CheckInit()`
86+
in the case of functional affects. This keyword should *not* be provided
87+
if the affect is purely symbolic.
88+
2889
## Continuous Events
2990

3091
The basic purely symbolic continuous event interface to encode *one* continuous
@@ -91,7 +152,7 @@ like this
91152
@variables x(t)=1 v(t)=0
92153
93154
root_eqs = [x ~ 0] # the event happens at the ground x(t) = 0
94-
affect = [v ~ -v] # the effect is that the velocity changes sign
155+
affect = [v ~ -Pre(v)] # the effect is that the velocity changes sign
95156
96157
@mtkbuild ball = ODESystem([D(x) ~ v
97158
D(v) ~ -9.8], t; continuous_events = root_eqs => affect) # equation => affect
@@ -110,8 +171,8 @@ Multiple events? No problem! This example models a bouncing ball in 2D that is e
110171
```@example events
111172
@variables x(t)=1 y(t)=0 vx(t)=0 vy(t)=2
112173
113-
continuous_events = [[x ~ 0] => [vx ~ -vx]
114-
[y ~ -1.5, y ~ 1.5] => [vy ~ -vy]]
174+
continuous_events = [[x ~ 0] => [vx ~ -Pre(vx)]
175+
[y ~ -1.5, y ~ 1.5] => [vy ~ -Pre(vy)]]
115176
116177
@mtkbuild ball = ODESystem(
117178
[
@@ -204,7 +265,7 @@ bb_sol = solve(bb_prob, Tsit5())
204265
plot(bb_sol)
205266
```
206267

207-
## Discrete events support
268+
## Discrete Events
208269

209270
In addition to continuous events, discrete events are also supported. The
210271
general interface to represent a collection of discrete events is
@@ -227,13 +288,13 @@ Suppose we have a population of `N(t)` cells that can grow and die, and at time
227288
`t1` we want to inject `M` more cells into the population. We can model this by
228289

229290
```@example events
230-
@parameters M tinject α
291+
@parameters M tinject α(t)
231292
@variables N(t)
232293
Dₜ = Differential(t)
233294
eqs = [Dₜ(N) ~ α - N]
234295
235296
# at time tinject we inject M cells
236-
injection = (t == tinject) => [N ~ N + M]
297+
injection = (t == tinject) => [N ~ Pre(N) + M]
237298
238299
u0 = [N => 0.0]
239300
tspan = (0.0, 20.0)
@@ -255,7 +316,7 @@ its steady-state value (which is 100). We can encode this by modifying the event
255316
to
256317

257318
```@example events
258-
injection = ((t == tinject) & (N < 50)) => [N ~ N + M]
319+
injection = ((t == tinject) & (N < 50)) => [N ~ Pre(N) + M]
259320
260321
@mtkbuild osys = ODESystem(eqs, t, [N], [M, tinject, α]; discrete_events = injection)
261322
oprob = ODEProblem(osys, u0, tspan, p)
@@ -269,16 +330,18 @@ event time, the event condition now returns false. Here we used logical and,
269330
cannot be used within symbolic expressions.
270331

271332
Let's now also add a drug at time `tkill` that turns off production of new
272-
cells, modeled by setting `α = 0.0`
333+
cells, modeled by setting `α = 0.0`. Since this is a parameter we must explicitly
334+
set it as `discrete_parameters`.
273335

274336
```@example events
275337
@parameters tkill
276338
277339
# we reset the first event to just occur at tinject
278-
injection = (t == tinject) => [N ~ N + M]
340+
injection = (t == tinject) => [N ~ Pre(N) + M]
279341
280342
# at time tkill we turn off production of cells
281-
killing = (t == tkill) => [α ~ 0.0]
343+
killing = ModelingToolkit.SymbolicDiscreteCallback(
344+
(t == tkill) => [α ~ 0.0]; discrete_parameters = α, iv = t)
282345
283346
tspan = (0.0, 30.0)
284347
p = [α => 100.0, tinject => 10.0, M => 50, tkill => 20.0]
@@ -298,16 +361,17 @@ A preset-time event is triggered at specific set times, which can be
298361
passed in a vector like
299362

300363
```julia
301-
discrete_events = [[1.0, 4.0] => [v ~ -v]]
364+
discrete_events = [[1.0, 4.0] => [v ~ -Pre(v)]]
302365
```
303366

304367
This will change the sign of `v` *only* at `t = 1.0` and `t = 4.0`.
305368

306369
As such, our last example with treatment and killing could instead be modeled by
307370

308371
```@example events
309-
injection = [10.0] => [N ~ N + M]
310-
killing = [20.0] => [α ~ 0.0]
372+
injection = [10.0] => [N ~ Pre(N) + M]
373+
killing = ModelingToolkit.SymbolicDiscreteCallback(
374+
[20.0] => [α ~ 0.0], discrete_parameters = α, iv = t)
311375
312376
p = [α => 100.0, M => 50]
313377
@mtkbuild osys = ODESystem(eqs, t, [N], [α, M];
@@ -325,7 +389,7 @@ specify a periodic interval, pass the interval as the condition for the event.
325389
For example,
326390

327391
```julia
328-
discrete_events = [1.0 => [v ~ -v]]
392+
discrete_events = [1.0 => [v ~ -Pre(v)]]
329393
```
330394

331395
will change the sign of `v` at `t = 1.0`, `2.0`, ...
@@ -334,10 +398,10 @@ Finally, we note that to specify an event at precisely one time, say 2.0 below,
334398
one must still use a vector
335399

336400
```julia
337-
discrete_events = [[2.0] => [v ~ -v]]
401+
discrete_events = [[2.0] => [v ~ -Pre(v)]]
338402
```
339403

340-
## Saving discrete values
404+
## [Saving discrete values](@id save_discretes)
341405

342406
Time-dependent parameters which are updated in callbacks are termed as discrete variables.
343407
ModelingToolkit enables automatically saving the timeseries of these discrete variables,
@@ -348,8 +412,10 @@ example:
348412
@variables x(t)
349413
@parameters c(t)
350414
415+
ev = ModelingToolkit.SymbolicDiscreteCallback(
416+
1.0 => [c ~ Pre(c) + 1], discrete_parameters = c, iv = t)
351417
@mtkbuild sys = ODESystem(
352-
D(x) ~ c * cos(x), t, [x], [c]; discrete_events = [1.0 => [c ~ c + 1]])
418+
D(x) ~ c * cos(x), t, [x], [c]; discrete_events = [ev])
353419
354420
prob = ODEProblem(sys, [x => 0.0], (0.0, 2pi), [c => 1.0])
355421
sol = solve(prob, Tsit5())
@@ -362,15 +428,15 @@ The solution object can also be interpolated with the discrete variables
362428
sol([1.0, 2.0], idxs = [c, c * cos(x)])
363429
```
364430

365-
Note that only time-dependent parameters will be saved. If we repeat the above example with
366-
this change:
431+
Note that only time-dependent parameters that are explicitly passed as `discrete_parameters`
432+
will be saved. If we repeat the above example with `c` not a `discrete_parameter`:
367433

368434
```@example events
369435
@variables x(t)
370-
@parameters c
436+
@parameters c(t)
371437
372438
@mtkbuild sys = ODESystem(
373-
D(x) ~ c * cos(x), t, [x], [c]; discrete_events = [1.0 => [c ~ c + 1]])
439+
D(x) ~ c * cos(x), t, [x], [c]; discrete_events = [1.0 => [c ~ Pre(c) + 1]])
374440
375441
prob = ODEProblem(sys, [x => 0.0], (0.0, 2pi), [c => 1.0])
376442
sol = solve(prob, Tsit5())

docs/src/basics/MTKLanguage.md

+7-3
Original file line numberDiff line numberDiff line change
@@ -203,14 +203,15 @@ getdefault(model_c3.model_a.k_array[2])
203203

204204
- Defining continuous events as described [here](https://docs.sciml.ai/ModelingToolkit/stable/basics/Events/#Continuous-Events).
205205
- If this block is not defined in the model, no continuous events will be added.
206+
- Discrete parameters and other keyword arguments should be specified in a vector, as seen below.
206207

207208
```@example mtkmodel-example
208209
using ModelingToolkit
209210
using ModelingToolkit: t
210211
211212
@mtkmodel M begin
212213
@parameters begin
213-
k
214+
k(t)
214215
end
215216
@variables begin
216217
x(t)
@@ -223,6 +224,7 @@ using ModelingToolkit: t
223224
@continuous_events begin
224225
[x ~ 1.5] => [x ~ 5, y ~ 5]
225226
[t ~ 4] => [x ~ 10]
227+
[t ~ 5] => [k ~ 3], [discrete_parameters = k]
226228
end
227229
end
228230
```
@@ -231,13 +233,14 @@ end
231233

232234
- Defining discrete events as described [here](https://docs.sciml.ai/ModelingToolkit/stable/basics/Events/#Discrete-events-support).
233235
- If this block is not defined in the model, no discrete events will be added.
236+
- Discrete parameters and other keyword arguments should be specified in a vector, as seen below.
234237

235238
```@example mtkmodel-example
236239
using ModelingToolkit
237240
238241
@mtkmodel M begin
239242
@parameters begin
240-
k
243+
k(t)
241244
end
242245
@variables begin
243246
x(t)
@@ -248,7 +251,8 @@ using ModelingToolkit
248251
D(y) ~ -k
249252
end
250253
@discrete_events begin
251-
(t == 1.5) => [x ~ x + 5, y ~ 5]
254+
(t == 1.5) => [x ~ Pre(x) + 5, y ~ 5]
255+
(t == 2.5) => [k ~ Pre(k) * 2], [discrete_parameters = k]
252256
end
253257
end
254258
```

docs/src/systems/DiscreteSystem.md

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ DiscreteSystem
1212
- `get_unknowns(sys)` or `unknowns(sys)`: The set of unknowns in the discrete system.
1313
- `get_ps(sys)` or `parameters(sys)`: The parameters of the discrete system.
1414
- `get_iv(sys)`: The independent variable of the discrete system
15-
- `discrete_events(sys)`: The set of discrete events in the discrete system.
1615

1716
## Transformations
1817

docs/src/systems/ImplicitDiscreteSystem.md

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ ImplicitDiscreteSystem
1212
- `get_unknowns(sys)` or `unknowns(sys)`: The set of unknowns in the implicit discrete system.
1313
- `get_ps(sys)` or `parameters(sys)`: The parameters of the implicit discrete system.
1414
- `get_iv(sys)`: The independent variable of the implicit discrete system
15-
- `discrete_events(sys)`: The set of discrete events in the implicit discrete system.
1615

1716
## Transformations
1817

docs/src/tutorials/fmi.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ we will create a model from a CoSimulation FMU.
9494
```@example fmi
9595
fmu = loadFMU("SpringPendulum1D", "Dymola", "2023x", "3.0"; type = :CS)
9696
@named inner = ModelingToolkit.FMIComponent(
97-
Val(3); fmu, communication_step_size = 0.001, type = :CS)
97+
Val(3); fmu, communication_step_size = 0.001, type = :CS,
98+
reinitializealg = BrownFullBasicInit())
9899
```
99100

100101
This FMU has fewer equations, partly due to missing aliasing variables and partly due to being a CS FMU.
@@ -170,7 +171,8 @@ end
170171
`a` and `b` are inputs, `c` is a state, and `out` and `out2` are outputs of the component.
171172

172173
```@repl fmi
173-
@named adder = ModelingToolkit.FMIComponent(Val(2); fmu, type = :ME);
174+
@named adder = ModelingToolkit.FMIComponent(
175+
Val(2); fmu, type = :ME, reinitializealg = BrownFullBasicInit());
174176
isinput(adder.a)
175177
isinput(adder.b)
176178
isoutput(adder.out)

ext/MTKFMIExt.jl

+3-4
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ with the name `namespace__variable`.
9393
- `name`: The name of the system.
9494
"""
9595
function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6,
96-
communication_step_size = nothing, reinitializealg = SciMLBase.NoInit(), type, name) where {Ver}
96+
communication_step_size = nothing, reinitializealg = nothing, type, name) where {Ver}
9797
if Ver != 2 && Ver != 3
9898
throw(ArgumentError("FMI Version must be `2` or `3`"))
9999
end
@@ -238,7 +238,7 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6,
238238
finalize_affect = MTK.FunctionalAffect(fmiFinalize!, [], [wrapper], [])
239239
step_affect = MTK.FunctionalAffect(Returns(nothing), [], [], [])
240240
instance_management_callback = MTK.SymbolicDiscreteCallback(
241-
(t != t - 1), step_affect; finalize = finalize_affect, reinitializealg = reinitializealg)
241+
(t == t - 1), step_affect; finalize = finalize_affect, reinitializealg = SciMLBase.NoInit())
242242

243243
push!(params, wrapper)
244244
append!(observed, der_observed)
@@ -279,8 +279,7 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6,
279279
fmiCSStep!; observed = cb_observed, modified = cb_modified, ctx = _functor)
280280
instance_management_callback = MTK.SymbolicDiscreteCallback(
281281
communication_step_size, step_affect; initialize = initialize_affect,
282-
finalize = finalize_affect, reinitializealg = reinitializealg
283-
)
282+
finalize = finalize_affect, reinitializealg)
284283

285284
# guarded in case there are no outputs/states and the variable is `[]`.
286285
symbolic_type(__mtk_internal_o) == NotSymbolic() || push!(params, __mtk_internal_o)

0 commit comments

Comments
 (0)