Skip to content

Commit 0cb4893

Browse files
committed
Merge remote-tracking branch 'origin/master' into MTK
2 parents 9226ad6 + ba842c2 commit 0cb4893

33 files changed

+2534
-793
lines changed

Project.toml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "ModelingToolkit"
22
uuid = "961ee093-0014-501f-94e3-6117800e7a78"
33
authors = ["Yingbo Ma <[email protected]>", "Chris Rackauckas <[email protected]> and contributors"]
4-
version = "9.54.0"
4+
version = "9.58.0"
55

66
[deps]
77
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
@@ -44,6 +44,7 @@ PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
4444
RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd"
4545
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
4646
RuntimeGeneratedFunctions = "7e49a35a-f44a-4d26-94aa-eba1b4ca6b47"
47+
SCCNonlinearSolve = "9dfe8606-65a1-4bb3-9748-cb89d1561431"
4748
SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462"
4849
SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226"
4950
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
@@ -121,13 +122,15 @@ NonlinearSolve = "3.14, 4"
121122
OffsetArrays = "1"
122123
OrderedCollections = "1"
123124
OrdinaryDiffEq = "6.82.0"
124-
OrdinaryDiffEqCore = "1.7.0"
125+
OrdinaryDiffEqCore = "1.13.0"
126+
OrdinaryDiffEqNonlinearSolve = "1.3.0"
125127
PrecompileTools = "1"
126128
REPL = "1"
127129
RecursiveArrayTools = "3.26"
128130
Reexport = "0.2, 1"
129131
RuntimeGeneratedFunctions = "0.5.9"
130-
SciMLBase = "2.57.1"
132+
SCCNonlinearSolve = "1.0.0"
133+
SciMLBase = "2.66"
131134
SciMLStructures = "1.0"
132135
Serialization = "1"
133136
Setfield = "0.7, 0.8, 1"
@@ -162,6 +165,7 @@ OptimizationMOI = "fd9f6733-72f4-499f-8506-86b2bdd0dea1"
162165
OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e"
163166
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
164167
OrdinaryDiffEqCore = "bbf590c4-e513-4bbe-9b18-05decba2e5d8"
168+
OrdinaryDiffEqNonlinearSolve = "127b3ac7-2247-4354-8eb6-78cf4e7c58e8"
165169
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
166170
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
167171
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
@@ -176,4 +180,4 @@ Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4"
176180
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
177181

178182
[targets]
179-
test = ["AmplNLWriter", "BenchmarkTools", "BoundaryValueDiffEq", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET"]
183+
test = ["AmplNLWriter", "BenchmarkTools", "BoundaryValueDiffEq", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET", "OrdinaryDiffEqNonlinearSolve"]

docs/Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e"
1515
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
1616
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
1717
SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226"
18+
Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46"
1819
StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0"
1920
SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5"
2021
SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b"
@@ -37,6 +38,7 @@ OptimizationOptimJL = "0.1, 0.4"
3738
OrdinaryDiffEq = "6.31"
3839
Plots = "1.36"
3940
SciMLStructures = "1.1"
41+
Setfield = "1"
4042
StochasticDiffEq = "6"
4143
SymbolicIndexingInterface = "0.3.1"
4244
SymbolicUtils = "3"

docs/src/basics/Events.md

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,3 +378,218 @@ sol.ps[c] # sol[c] will error, since `c` is not a timeseries value
378378
```
379379

380380
It can be seen that the timeseries for `c` is not saved.
381+
382+
## [(Experimental) Imperative affects](@id imp_affects)
383+
384+
The `ImperativeAffect` can be used as an alternative to the aforementioned functional affect form. Note
385+
that `ImperativeAffect` is still experimental; to emphasize this, we do not export it and it should be
386+
included as `ModelingToolkit.ImperativeAffect`. `ImperativeAffect` aims to simplify the manipulation of
387+
system state.
388+
389+
We will use two examples to describe `ImperativeAffect`: a simple heater and a quadrature encoder.
390+
These examples will also demonstrate advanced usage of `ModelingToolkit.SymbolicContinuousCallback`,
391+
the low-level interface of the tuple form converts into that allows control over the SciMLBase-level
392+
event that is generated for a continuous event.
393+
394+
### [Heater](@id heater_events)
395+
396+
Bang-bang control of a heater connected to a leaky plant requires hysteresis in order to prevent rapid control oscillation.
397+
398+
```@example events
399+
@variables temp(t)
400+
params = @parameters furnace_on_threshold=0.5 furnace_off_threshold=0.7 furnace_power=1.0 leakage=0.1 furnace_on(t)::Bool=false
401+
eqs = [
402+
D(temp) ~ furnace_on * furnace_power - temp^2 * leakage
403+
]
404+
```
405+
406+
Our plant is simple. We have a heater that's turned on and off by the time-indexed parameter `furnace_on`
407+
which adds `furnace_power` forcing to the system when enabled. We then leak heat proportional to `leakage`
408+
as a function of the square of the current temperature.
409+
410+
We need a controller with hysteresis to control the plant. We wish the furnace to turn on when the temperature
411+
is below `furnace_on_threshold` and off when above `furnace_off_threshold`, while maintaining its current state
412+
in between. To do this, we create two continuous callbacks:
413+
414+
```@example events
415+
using Setfield
416+
furnace_disable = ModelingToolkit.SymbolicContinuousCallback(
417+
[temp ~ furnace_off_threshold],
418+
ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i
419+
@set! x.furnace_on = false
420+
end)
421+
furnace_enable = ModelingToolkit.SymbolicContinuousCallback(
422+
[temp ~ furnace_on_threshold],
423+
ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i
424+
@set! x.furnace_on = true
425+
end)
426+
```
427+
428+
We're using the explicit form of `SymbolicContinuousCallback` here, though
429+
so far we aren't using anything that's not possible with the implicit interface.
430+
You can also write
431+
432+
```julia
433+
[temp ~ furnace_off_threshold] => ModelingToolkit.ImperativeAffect(modified = (;
434+
furnace_on)) do x, o, i, c
435+
@set! x.furnace_on = false
436+
end
437+
```
438+
439+
and it would work the same.
440+
441+
The `ImperativeAffect` is the larger change in this example. `ImperativeAffect` has the constructor signature
442+
443+
```julia
444+
ImperativeAffect(f::Function; modified::NamedTuple, observed::NamedTuple, ctx)
445+
```
446+
447+
that accepts the function to call, a named tuple of both the names of and symbolic values representing
448+
values in the system to be modified, a named tuple of the values that are merely observed (that is, used from
449+
the system but not modified), and a context that's passed to the affect function.
450+
451+
In our example, each event merely changes whether the furnace is on or off. Accordingly, we pass a `modified` tuple
452+
`(; furnace_on)` (creating a `NamedTuple` equivalent to `(furnace_on = furnace_on)`). `ImperativeAffect` will then
453+
evaluate this before calling our function to fill out all of the numerical values, then apply them back to the system
454+
once our affect function returns. Furthermore, it will check that it is possible to do this assignment.
455+
456+
The function given to `ImperativeAffect` needs to have the signature:
457+
458+
```julia
459+
f(modified::NamedTuple, observed::NamedTuple, ctx, integrator)::NamedTuple
460+
```
461+
462+
The function `f` will be called with `observed` and `modified` `NamedTuple`s that are derived from their respective `NamedTuple` definitions.
463+
In our example, if `furnace_on` is `false`, then the value of the `x` that's passed in as `modified` will be `(furnace_on = false)`.
464+
The modified values should be passed out in the same format: to set `furnace_on` to `true` we need to return a tuple `(furnace_on = true)`.
465+
The examples does this with Setfield, recreating the result tuple before returning it; the returned tuple may optionally be missing values as
466+
well, in which case those values will not be written back to the problem.
467+
468+
Accordingly, we can now interpret the `ImperativeAffect` definitions to mean that when `temp = furnace_off_threshold` we
469+
will write `furnace_on = false` back to the system, and when `temp = furnace_on_threshold` we will write `furnace_on = true` back
470+
to the system.
471+
472+
```@example events
473+
@named sys = ODESystem(
474+
eqs, t, [temp], params; continuous_events = [furnace_disable, furnace_enable])
475+
ss = structural_simplify(sys)
476+
prob = ODEProblem(ss, [temp => 0.0, furnace_on => true], (0.0, 10.0))
477+
sol = solve(prob, Tsit5())
478+
plot(sol)
479+
hline!([sol.ps[furnace_off_threshold], sol.ps[furnace_on_threshold]],
480+
l = (:black, 1), primary = false)
481+
```
482+
483+
Here we see exactly the desired hysteresis. The heater starts on until the temperature hits
484+
`furnace_off_threshold`. The temperature then bleeds away until `furnace_on_threshold` at which
485+
point the furnace turns on again until `furnace_off_threshold` and so on and so forth. The controller
486+
is effectively regulating the temperature of the plant.
487+
488+
### [Quadrature Encoder](@id quadrature)
489+
490+
For a more complex application we'll look at modeling a quadrature encoder attached to a shaft spinning at a constant speed.
491+
Traditionally, a quadrature encoder is built out of a code wheel that interrupts the sensors at constant intervals and two sensors slightly out of phase with one another.
492+
A state machine can take the pattern of pulses produced by the two sensors and determine the number of steps that the shaft has spun. The state machine takes the new value
493+
from each sensor and the old values and decodes them into the direction that the wheel has spun in this step.
494+
495+
```@example events
496+
@variables theta(t) omega(t)
497+
params = @parameters qA=0 qB=0 hA=0 hB=0 cnt::Int=0
498+
eqs = [D(theta) ~ omega
499+
omega ~ 1.0]
500+
```
501+
502+
Our continuous-time system is extremely simple. We have two unknown variables `theta` for the angle of the shaft
503+
and `omega` for the rate at which it's spinning. We then have parameters for the state machine `qA, qB, hA, hB`
504+
(corresponding to the current quadrature of the A/B sensors and the historical ones) and a step count `cnt`.
505+
506+
We'll then implement the decoder as a simple Julia function.
507+
508+
```@example events
509+
function decoder(oldA, oldB, newA, newB)
510+
state = (oldA, oldB, newA, newB)
511+
if state == (0, 0, 1, 0) || state == (1, 0, 1, 1) || state == (1, 1, 0, 1) ||
512+
state == (0, 1, 0, 0)
513+
return 1
514+
elseif state == (0, 0, 0, 1) || state == (0, 1, 1, 1) || state == (1, 1, 1, 0) ||
515+
state == (1, 0, 0, 0)
516+
return -1
517+
elseif state == (0, 0, 0, 0) || state == (0, 1, 0, 1) || state == (1, 0, 1, 0) ||
518+
state == (1, 1, 1, 1)
519+
return 0
520+
else
521+
return 0 # err is interpreted as no movement
522+
end
523+
end
524+
```
525+
526+
Based on the current and old state, this function will return 1 if the wheel spun in the positive direction,
527+
-1 if in the negative, and 0 otherwise.
528+
529+
The encoder state advances when the occlusion begins or ends. We model the
530+
code wheel as simply detecting when `cos(100*theta)` is 0; if we're at a positive
531+
edge of the 0 crossing, then we interpret that as occlusion (so the discrete `qA` goes to 1). Otherwise, if `cos` is
532+
going negative, we interpret that as lack of occlusion (so the discrete goes to 0). The decoder function is
533+
then invoked to update the count with this new information.
534+
535+
We can implement this in one of two ways: using edge sign detection or right root finding. For exposition, we
536+
will implement each sensor differently.
537+
538+
For sensor A, we're using the edge detection method. By providing a different affect to `SymbolicContinuousCallback`'s
539+
`affect_neg` argument, we can specify different behaviour for the negative crossing vs. the positive crossing of the root.
540+
In our encoder, we interpret this as occlusion or nonocclusion of the sensor, update the internal state, and tick the decoder.
541+
542+
```@example events
543+
qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0],
544+
ModelingToolkit.ImperativeAffect((; qA, hA, hB, cnt), (; qB)) do x, o, c, i
545+
@set! x.hA = x.qA
546+
@set! x.hB = o.qB
547+
@set! x.qA = 1
548+
@set! x.cnt += decoder(x.hA, x.hB, x.qA, o.qB)
549+
x
550+
end,
551+
affect_neg = ModelingToolkit.ImperativeAffect(
552+
(; qA, hA, hB, cnt), (; qB)) do x, o, c, i
553+
@set! x.hA = x.qA
554+
@set! x.hB = o.qB
555+
@set! x.qA = 0
556+
@set! x.cnt += decoder(x.hA, x.hB, x.qA, o.qB)
557+
x
558+
end)
559+
```
560+
561+
The other way we can implement a sensor is by changing the root find.
562+
Normally, we use left root finding; the affect will be invoked instantaneously _before_
563+
the root is crossed. This makes it trickier to figure out what the new state is.
564+
Instead, we can use right root finding:
565+
566+
```@example events
567+
qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta - π / 2) ~ 0],
568+
ModelingToolkit.ImperativeAffect((; qB, hA, hB, cnt), (; qA, theta)) do x, o, c, i
569+
@set! x.hA = o.qA
570+
@set! x.hB = x.qB
571+
@set! x.qB = clamp(sign(cos(100 * o.theta - π / 2)), 0.0, 1.0)
572+
@set! x.cnt += decoder(x.hA, x.hB, o.qA, x.qB)
573+
x
574+
end; rootfind = SciMLBase.RightRootFind)
575+
```
576+
577+
Here, sensor B is located `π / 2` behind sensor A in angular space, so we're adjusting our
578+
trigger function accordingly. We here ask for right root finding on the callback, so we know
579+
that the value of said function will have the "new" sign rather than the old one. Thus, we can
580+
determine the new state of the sensor from the sign of the indicator function evaluated at the
581+
affect activation point, with -1 mapped to 0.
582+
583+
We can now simulate the encoder.
584+
585+
```@example events
586+
@named sys = ODESystem(
587+
eqs, t, [theta, omega], params; continuous_events = [qAevt, qBevt])
588+
ss = structural_simplify(sys)
589+
prob = ODEProblem(ss, [theta => 0.0], (0.0, pi))
590+
sol = solve(prob, Tsit5(); dtmax = 0.01)
591+
sol.ps[cnt]
592+
```
593+
594+
`cos(100*theta)` will have 200 crossings in the half rotation we've gone through, so the encoder would notionally count 200 steps.
595+
Our encoder counts 198 steps (it loses one step to initialization and one step due to the final state falling squarely on an edge).

docs/src/examples/higher_order.md

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
ModelingToolkit has a system for transformations of mathematical
44
systems. These transformations allow for symbolically changing
55
the representation of the model to problems that are easier to
6-
numerically solve. One simple to demonstrate transformation is the
6+
numerically solve. One simple to demonstrate transformation, is
77
`structural_simplify`, which does a lot of tricks, one being the
88
transformation that turns an Nth order ODE into N
99
coupled 1st order ODEs.
@@ -15,16 +15,28 @@ We utilize the derivative operator twice here to define the second order:
1515
using ModelingToolkit, OrdinaryDiffEq
1616
using ModelingToolkit: t_nounits as t, D_nounits as D
1717
18-
@parameters σ ρ β
19-
@variables x(t) y(t) z(t)
20-
21-
eqs = [D(D(x)) ~ σ * (y - x),
22-
D(y) ~ x * (ρ - z) - y,
23-
D(z) ~ x * y - β * z]
24-
25-
@named sys = ODESystem(eqs, t)
18+
@mtkmodel SECOND_ORDER begin
19+
@parameters begin
20+
σ = 28.0
21+
ρ = 10.0
22+
β = 8 / 3
23+
end
24+
@variables begin
25+
x(t) = 1.0
26+
y(t) = 0.0
27+
z(t) = 0.0
28+
end
29+
@equations begin
30+
D(D(x)) ~ σ * (y - x)
31+
D(y) ~ x * (ρ - z) - y
32+
D(z) ~ x * y - β * z
33+
end
34+
end
35+
@mtkbuild sys = SECOND_ORDER()
2636
```
2737

38+
The second order ODE has been automatically transformed to two first order ODEs.
39+
2840
Note that we could've used an alternative syntax for 2nd order, i.e.
2941
`D = Differential(t)^2` and then `D(x)` would be the second derivative,
3042
and this syntax extends to `N`-th order. Also, we can use `*` or `` to compose
@@ -33,28 +45,17 @@ and this syntax extends to `N`-th order. Also, we can use `*` or `∘` to compos
3345
Now let's transform this into the `ODESystem` of first order components.
3446
We do this by calling `structural_simplify`:
3547

36-
```@example orderlowering
37-
sys = structural_simplify(sys)
38-
```
39-
4048
Now we can directly numerically solve the lowered system. Note that,
4149
following the original problem, the solution requires knowing the
42-
initial condition for `x'`, and thus we include that in our input
43-
specification:
50+
initial condition for both `x` and `D(x)`.
51+
The former already got assigned a default value in the `@mtkmodel`,
52+
but we still have to provide a value for the latter.
4453

4554
```@example orderlowering
46-
u0 = [D(x) => 2.0,
47-
x => 1.0,
48-
y => 0.0,
49-
z => 0.0]
50-
51-
p = [σ => 28.0,
52-
ρ => 10.0,
53-
β => 8 / 3]
54-
55+
u0 = [D(sys.x) => 2.0]
5556
tspan = (0.0, 100.0)
56-
prob = ODEProblem(sys, u0, tspan, p, jac = true)
57+
prob = ODEProblem(sys, u0, tspan, [], jac = true)
5758
sol = solve(prob, Tsit5())
58-
using Plots;
59-
plot(sol, idxs = (x, y));
59+
using Plots
60+
plot(sol, idxs = (sys.x, sys.y))
6061
```

docs/src/examples/modelingtoolkitize_index_reduction.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ In this tutorial, we will look at the pendulum system:
5151
\end{aligned}
5252
```
5353

54+
These equations can be derived using the [Lagrangian equation of the first kind.](https://en.wikipedia.org/wiki/Lagrangian_mechanics#Lagrangian)
55+
Specifically, for a pendulum with unit mass and length $L$, which thus has
56+
kinetic energy $\frac{1}{2}(v_x^2 + v_y^2)$,
57+
potential energy $gy$,
58+
and holonomic constraint $x^2 + y^2 - L^2 = 0$.
59+
The Lagrange multiplier related to this constraint is equal to half of $T$,
60+
and represents the tension in the rope of the pendulum.
61+
5462
As a good DifferentialEquations.jl user, one would follow
5563
[the mass matrix DAE tutorial](https://docs.sciml.ai/DiffEqDocs/stable/tutorials/dae_example/#Mass-Matrix-Differential-Algebraic-Equations-(DAEs))
5664
to arrive at code for simulating the model:

0 commit comments

Comments
 (0)