Skip to content

Commit 1e4d3f8

Browse files
authored
Merge pull request #94 from JuliaControl/soft_real_time_simple
Added: soft real time utilities
2 parents 5bdfd14 + 084f8ae commit 1e4d3f8

File tree

11 files changed

+202
-9
lines changed

11 files changed

+202
-9
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@ for more detailed examples.
110110
- [x] quickly compare multiple optimizers
111111
- [x] nonlinear solvers relying on automatic differentiation (exact derivative)
112112
- [x] additional information about the optimum to ease troubleshooting
113-
- [x] implementation that carefully limits allocations for real-time applications
113+
- [x] real-time control loop features:
114+
- [x] implementations that carefully limits the allocations
115+
- [x] simple soft real-time utilities
114116

115117
### State Estimation Features
116118

docs/src/public/generic_func.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,27 @@ setconstraint!
1919
evaloutput
2020
```
2121

22-
## Prepare State x
22+
## Change State x
23+
24+
### Prepare State x
2325

2426
```@docs
2527
preparestate!
2628
```
2729

28-
## Update State x
30+
### Update State x
2931

3032
```@docs
3133
updatestate!
3234
```
3335

34-
## Init State x
36+
### Init State x
3537

3638
```@docs
3739
initstate!
3840
```
3941

40-
## Set State x
42+
### Set State x
4143

4244
```@docs
4345
setstate!
@@ -54,3 +56,21 @@ setmodel!
5456
```@docs
5557
getinfo
5658
```
59+
60+
## Real-Time Simulate and Control
61+
62+
!!! danger "Disclaimer"
63+
These utilities are for soft real-time applications. They are not suitable for hard
64+
real-time environnement like safety-critical processes.
65+
66+
### Save current time t
67+
68+
```@docs
69+
savetime!
70+
```
71+
72+
### Period Sleep
73+
74+
```@docs
75+
periodsleep
76+
```

src/ModelPredictiveControl.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export SimModel, LinModel, NonLinModel
2626
export DiffSolver, RungeKutta
2727
export setop!, setname!
2828
export setstate!, setmodel!, preparestate!, updatestate!, evaloutput, linearize, linearize!
29+
export savetime!, periodsleep
2930
export StateEstimator, InternalModel, Luenberger
3031
export SteadyKalmanFilter, KalmanFilter, UnscentedKalmanFilter, ExtendedKalmanFilter
3132
export MovingHorizonEstimator

src/controller/execute.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,20 @@ function updatestate!(mpc::PredictiveController, u, ym, d=mpc.estim.buffer.empty
510510
end
511511
updatestate!(::PredictiveController, _ ) = throw(ArgumentError("missing measured outputs ym"))
512512

513+
"""
514+
savetime!(mpc::PredictiveController) -> t
515+
516+
Call `savetime!(mpc.estim.model)` and return the time `t`.
517+
"""
518+
savetime!(mpc::PredictiveController) = savetime!(mpc.estim.model)
519+
520+
"""
521+
periodsleep(mpc::PredictiveController) -> nothing
522+
523+
Call `periodsleep(mpc.estim.model)`.
524+
"""
525+
periodsleep(mpc::PredictiveController) = periodsleep(mpc.estim.model)
526+
513527
"""
514528
setstate!(mpc::PredictiveController, x̂) -> mpc
515529

src/estimator/execute.jl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,22 @@ function updatestate!(estim::StateEstimator, u, ym, d=estim.buffer.empty)
274274
end
275275
updatestate!(::StateEstimator, _ ) = throw(ArgumentError("missing measured outputs ym"))
276276

277+
278+
"""
279+
savetime!(estim::StateEstimator) -> t
280+
281+
Call `savetime!(estim.model)` and return the time `t`.
282+
"""
283+
savetime!(estim::StateEstimator) = savetime!(estim.model)
284+
285+
"""
286+
periodsleep(estim::StateEstimator) -> nothing
287+
288+
Call `periodsleep(estim.model)`.
289+
"""
290+
periodsleep(estim::StateEstimator) = periodsleep(estim.model)
291+
292+
277293
"""
278294
validate_args(estim::StateEstimator, ym, d, u=nothing)
279295

src/model/linmodel.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ struct LinModel{NT<:Real} <: SimModel{NT}
66
Dd ::Matrix{NT}
77
x0::Vector{NT}
88
Ts::NT
9+
t::Vector{NT}
910
nu::Int
1011
nx::Int
1112
ny::Int
@@ -44,12 +45,13 @@ struct LinModel{NT<:Real} <: SimModel{NT}
4445
yname = ["\$y_{$i}\$" for i in 1:ny]
4546
dname = ["\$d_{$i}\$" for i in 1:nd]
4647
xname = ["\$x_{$i}\$" for i in 1:nx]
47-
x0 = zeros(NT, nx)
48+
x0 = zeros(NT, nx)
49+
t = zeros(NT, 1)
4850
buffer = SimModelBuffer{NT}(nu, nx, ny, nd)
4951
return new{NT}(
5052
A, Bu, C, Bd, Dd,
5153
x0,
52-
Ts,
54+
Ts, t,
5355
nu, nx, ny, nd,
5456
uop, yop, dop, xop, fop,
5557
uname, yname, dname, xname,

src/model/nonlinmodel.jl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ struct NonLinModel{NT<:Real, F<:Function, H<:Function, DS<:DiffSolver} <: SimMod
44
h!::H
55
solver::DS
66
Ts::NT
7+
t::Vector{NT}
78
nu::Int
89
nx::Int
910
ny::Int
@@ -31,12 +32,14 @@ struct NonLinModel{NT<:Real, F<:Function, H<:Function, DS<:DiffSolver} <: SimMod
3132
yname = ["\$y_{$i}\$" for i in 1:ny]
3233
dname = ["\$d_{$i}\$" for i in 1:nd]
3334
xname = ["\$x_{$i}\$" for i in 1:nx]
34-
x0 = zeros(NT, nx)
35+
x0 = zeros(NT, nx)
36+
t = zeros(NT, 1)
3537
buffer = SimModelBuffer{NT}(nu, nx, ny, nd)
3638
return new{NT, F, H, DS}(
3739
x0,
3840
f!, h!,
39-
solver, Ts,
41+
solver,
42+
Ts, t,
4043
nu, nx, ny, nd,
4144
uop, yop, dop, xop, fop,
4245
uname, yname, dname, xname,

src/sim_model.jl

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,67 @@ function evaloutput(model::SimModel{NT}, d=model.buffer.empty) where NT <: Real
285285
return y
286286
end
287287

288+
"""
289+
savetime!(model::SimModel) -> t
290+
291+
Set `model.t` to `time()` and return the value.
292+
293+
Used in conjunction with [`periodsleep`](@ref) for simple soft real-time simulations. Call
294+
this function before any other in the simulation loop.
295+
"""
296+
function savetime!(model::SimModel)
297+
model.t[] = time()
298+
return model.t[]
299+
end
300+
301+
"""
302+
periodsleep(model::SimModel, busywait=false) -> nothing
303+
304+
Sleep for `model.Ts` s minus the time elapsed since the last call to [`savetime!`](@ref).
305+
306+
It calls [`sleep`](https://docs.julialang.org/en/v1/base/parallel/#Base.sleep) if `busywait`
307+
is `false`. Else, a simple `while` loop implements busy-waiting. As a rule-of-thumb,
308+
busy-waiting should be used if `model.Ts < 0.1` s, since the accuracy of `sleep` is around 1
309+
ms. Can be used to implement simple soft real-time simulations, see the example below.
310+
311+
!!! warning
312+
The allocations in Julia are garbage-collected (GC) automatically. This can affect the
313+
timings. In such cases, you can temporarily stop the GC with `GC.enable(false)`, and
314+
restart it at a convenient time e.g.: just before calling `periodsleep`.
315+
316+
# Examples
317+
```jldoctest
318+
julia> model = LinModel(tf(2, [0.3, 1]), 0.1);
319+
320+
julia> function sim_realtime!(model)
321+
t_0 = time()
322+
for i=1:3
323+
t = savetime!(model) # first function called
324+
println(round(t - t_0, digits=3))
325+
updatestate!(model, [1])
326+
periodsleep(model, true) # last function called
327+
end
328+
end;
329+
330+
julia> sim_realtime!(model)
331+
0.0
332+
0.1
333+
0.2
334+
```
335+
"""
336+
function periodsleep(model::SimModel, busywait=false)
337+
if !busywait
338+
model.Ts < 0.1 && @warn "busy-waiting is recommended for Ts < 0.1 s"
339+
computing_time = time() - model.t[]
340+
sleep_time = model.Ts - computing_time
341+
sleep_time > 0 && sleep(sleep_time)
342+
else
343+
while time() - model.t[] < model.Ts
344+
end
345+
end
346+
return nothing
347+
end
348+
288349
"""
289350
validate_args(model::SimModel, d, u=nothing)
290351

test/test_predictive_control.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,19 @@ end
302302
@test mpc.L_Hp diagm(1.1:1000.1)
303303
end
304304

305+
@testset "LinMPC real-time simulations" begin
306+
linmodel1 = LinModel(tf(2, [10, 1]), 0.1)
307+
mpc1 = LinMPC(linmodel1)
308+
times1 = zeros(5)
309+
for i=1:5
310+
times1[i] = savetime!(mpc1)
311+
preparestate!(mpc1, [1])
312+
updatestate!(mpc1, [1], [1])
313+
periodsleep(mpc1)
314+
end
315+
@test all(isapprox.(diff(times1[2:end]), 0.1, atol=0.01))
316+
end
317+
305318
@testset "ExplicitMPC construction" begin
306319
model = LinModel(sys, Ts, i_d=[3])
307320
mpc1 = ExplicitMPC(model, Hp=15)

test/test_sim_model.jl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,25 @@ end
111111
@test_throws DimensionMismatch evaloutput(linmodel1, zeros(1))
112112
end
113113

114+
@testset "LinModel real time simulations" begin
115+
linmodel1 = LinModel(tf(2, [10, 1]), 0.1)
116+
times1 = zeros(5)
117+
for i=1:5
118+
times1[i] = savetime!(linmodel1)
119+
updatestate!(linmodel1, [1])
120+
periodsleep(linmodel1)
121+
end
122+
@test all(isapprox.(diff(times1[2:end]), 0.1, atol=0.01))
123+
linmodel2 = LinModel(tf(2, [0.1, 1]), 0.001)
124+
times2 = zeros(5)
125+
for i=1:5
126+
times2[i] = savetime!(linmodel2)
127+
updatestate!(linmodel2, [1])
128+
periodsleep(linmodel2, true)
129+
end
130+
@test all(isapprox.(diff(times2[2:end]), 0.001, atol=0.0001))
131+
end
132+
114133
@testset "NonLinModel construction" begin
115134
linmodel1 = LinModel(sys,Ts,i_u=[1,2])
116135
f1(x,u,_) = linmodel1.A*x + linmodel1.Bu*u
@@ -274,4 +293,33 @@ end
274293
updatestate!(linmodel3, u, d)
275294
end
276295
@test all(isapprox.(Ynl, Yl, atol=1e-6))
296+
end
297+
298+
@testset "NonLinModel real time simulations" begin
299+
linmodel1 = LinModel(tf(2, [10, 1]), 0.1)
300+
nonlinmodel1 = NonLinModel(
301+
(x,u,_)->linmodel1.A*x + linmodel1.Bu*u,
302+
(x,_)->linmodel1.C*x,
303+
linmodel1.Ts, 1, 1, 1, 0, solver=nothing
304+
)
305+
times1 = zeros(5)
306+
for i=1:5
307+
times1[i] = savetime!(nonlinmodel1)
308+
updatestate!(nonlinmodel1, [1])
309+
periodsleep(nonlinmodel1)
310+
end
311+
@test all(isapprox.(diff(times1[2:end]), 0.1, atol=0.01))
312+
linmodel2 = LinModel(tf(2, [0.1, 1]), 0.001)
313+
nonlinmodel2 = NonLinModel(
314+
(x,u,_)->linmodel2.A*x + linmodel2.Bu*u,
315+
(x,_)->linmodel2.C*x,
316+
linmodel2.Ts, 1, 1, 1, 0, solver=nothing
317+
)
318+
times2 = zeros(5)
319+
for i=1:5
320+
times2[i] = savetime!(nonlinmodel2)
321+
updatestate!(nonlinmodel2, [1])
322+
periodsleep(nonlinmodel2, true)
323+
end
324+
@test all(isapprox.(diff(times2[2:end]), 0.001, atol=0.0001))
277325
end

0 commit comments

Comments
 (0)