Skip to content

Added: soft real time utilities #94

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 19, 2024
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ for more detailed examples.
- [x] quickly compare multiple optimizers
- [x] nonlinear solvers relying on automatic differentiation (exact derivative)
- [x] additional information about the optimum to ease troubleshooting
- [x] implementation that carefully limits allocations for real-time applications
- [x] real-time control loop features:
- [x] implementations that carefully limits the allocations
- [x] simple soft real-time utilities

### State Estimation Features

Expand Down
28 changes: 24 additions & 4 deletions docs/src/public/generic_func.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,27 @@ setconstraint!
evaloutput
```

## Prepare State x
## Change State x

### Prepare State x

```@docs
preparestate!
```

## Update State x
### Update State x

```@docs
updatestate!
```

## Init State x
### Init State x

```@docs
initstate!
```

## Set State x
### Set State x

```@docs
setstate!
Expand All @@ -54,3 +56,21 @@ setmodel!
```@docs
getinfo
```

## Real-Time Simulate and Control

!!! danger "Disclaimer"
These utilities are for soft real-time applications. They are not suitable for hard
real-time environnement like safety-critical processes.

### Save current time t

```@docs
savetime!
```

### Period Sleep

```@docs
periodsleep
```
1 change: 1 addition & 0 deletions src/ModelPredictiveControl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export SimModel, LinModel, NonLinModel
export DiffSolver, RungeKutta
export setop!, setname!
export setstate!, setmodel!, preparestate!, updatestate!, evaloutput, linearize, linearize!
export savetime!, periodsleep
export StateEstimator, InternalModel, Luenberger
export SteadyKalmanFilter, KalmanFilter, UnscentedKalmanFilter, ExtendedKalmanFilter
export MovingHorizonEstimator
Expand Down
14 changes: 14 additions & 0 deletions src/controller/execute.jl
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,20 @@ function updatestate!(mpc::PredictiveController, u, ym, d=mpc.estim.buffer.empty
end
updatestate!(::PredictiveController, _ ) = throw(ArgumentError("missing measured outputs ym"))

"""
savetime!(mpc::PredictiveController) -> t

Call `savetime!(mpc.estim.model)` and return the time `t`.
"""
savetime!(mpc::PredictiveController) = savetime!(mpc.estim.model)

"""
periodsleep(mpc::PredictiveController) -> nothing

Call `periodsleep(mpc.estim.model)`.
"""
periodsleep(mpc::PredictiveController) = periodsleep(mpc.estim.model)

"""
setstate!(mpc::PredictiveController, x̂) -> mpc

Expand Down
16 changes: 16 additions & 0 deletions src/estimator/execute.jl
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,22 @@ function updatestate!(estim::StateEstimator, u, ym, d=estim.buffer.empty)
end
updatestate!(::StateEstimator, _ ) = throw(ArgumentError("missing measured outputs ym"))


"""
savetime!(estim::StateEstimator) -> t

Call `savetime!(estim.model)` and return the time `t`.
"""
savetime!(estim::StateEstimator) = savetime!(estim.model)

"""
periodsleep(estim::StateEstimator) -> nothing

Call `periodsleep(estim.model)`.
"""
periodsleep(estim::StateEstimator) = periodsleep(estim.model)


"""
validate_args(estim::StateEstimator, ym, d, u=nothing)

Expand Down
6 changes: 4 additions & 2 deletions src/model/linmodel.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct LinModel{NT<:Real} <: SimModel{NT}
Dd ::Matrix{NT}
x0::Vector{NT}
Ts::NT
t::Vector{NT}
nu::Int
nx::Int
ny::Int
Expand Down Expand Up @@ -44,12 +45,13 @@ struct LinModel{NT<:Real} <: SimModel{NT}
yname = ["\$y_{$i}\$" for i in 1:ny]
dname = ["\$d_{$i}\$" for i in 1:nd]
xname = ["\$x_{$i}\$" for i in 1:nx]
x0 = zeros(NT, nx)
x0 = zeros(NT, nx)
t = zeros(NT, 1)
buffer = SimModelBuffer{NT}(nu, nx, ny, nd)
return new{NT}(
A, Bu, C, Bd, Dd,
x0,
Ts,
Ts, t,
nu, nx, ny, nd,
uop, yop, dop, xop, fop,
uname, yname, dname, xname,
Expand Down
7 changes: 5 additions & 2 deletions src/model/nonlinmodel.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ struct NonLinModel{NT<:Real, F<:Function, H<:Function, DS<:DiffSolver} <: SimMod
h!::H
solver::DS
Ts::NT
t::Vector{NT}
nu::Int
nx::Int
ny::Int
Expand Down Expand Up @@ -31,12 +32,14 @@ struct NonLinModel{NT<:Real, F<:Function, H<:Function, DS<:DiffSolver} <: SimMod
yname = ["\$y_{$i}\$" for i in 1:ny]
dname = ["\$d_{$i}\$" for i in 1:nd]
xname = ["\$x_{$i}\$" for i in 1:nx]
x0 = zeros(NT, nx)
x0 = zeros(NT, nx)
t = zeros(NT, 1)
buffer = SimModelBuffer{NT}(nu, nx, ny, nd)
return new{NT, F, H, DS}(
x0,
f!, h!,
solver, Ts,
solver,
Ts, t,
nu, nx, ny, nd,
uop, yop, dop, xop, fop,
uname, yname, dname, xname,
Expand Down
61 changes: 61 additions & 0 deletions src/sim_model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,67 @@ function evaloutput(model::SimModel{NT}, d=model.buffer.empty) where NT <: Real
return y
end

"""
savetime!(model::SimModel) -> t

Set `model.t` to `time()` and return the value.

Used in conjunction with [`periodsleep`](@ref) for simple soft real-time simulations. Call
this function before any other in the simulation loop.
"""
function savetime!(model::SimModel)
model.t[] = time()
return model.t[]
end

"""
periodsleep(model::SimModel, busywait=false) -> nothing

Sleep for `model.Ts` s minus the time elapsed since the last call to [`savetime!`](@ref).

It calls [`sleep`](https://docs.julialang.org/en/v1/base/parallel/#Base.sleep) if `busywait`
is `false`. Else, a simple `while` loop implements busy-waiting. As a rule-of-thumb,
busy-waiting should be used if `model.Ts < 0.1` s, since the accuracy of `sleep` is around 1
ms. Can be used to implement simple soft real-time simulations, see the example below.

!!! warning
The allocations in Julia are garbage-collected (GC) automatically. This can affect the
timings. In such cases, you can temporarily stop the GC with `GC.enable(false)`, and
restart it at a convenient time e.g.: just before calling `periodsleep`.

# Examples
```jldoctest
julia> model = LinModel(tf(2, [0.3, 1]), 0.1);

julia> function sim_realtime!(model)
t_0 = time()
for i=1:3
t = savetime!(model) # first function called
println(round(t - t_0, digits=3))
updatestate!(model, [1])
periodsleep(model, true) # last function called
end
end;

julia> sim_realtime!(model)
0.0
0.1
0.2
```
"""
function periodsleep(model::SimModel, busywait=false)
if !busywait
model.Ts < 0.1 && @warn "busy-waiting is recommended for Ts < 0.1 s"
computing_time = time() - model.t[]
sleep_time = model.Ts - computing_time
sleep_time > 0 && sleep(sleep_time)
else
while time() - model.t[] < model.Ts
end
end
return nothing
end

"""
validate_args(model::SimModel, d, u=nothing)

Expand Down
13 changes: 13 additions & 0 deletions test/test_predictive_control.jl
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,19 @@ end
@test mpc.L_Hp ≈ diagm(1.1:1000.1)
end

@testset "LinMPC real-time simulations" begin
linmodel1 = LinModel(tf(2, [10, 1]), 0.1)
mpc1 = LinMPC(linmodel1)
times1 = zeros(5)
for i=1:5
times1[i] = savetime!(mpc1)
preparestate!(mpc1, [1])
updatestate!(mpc1, [1], [1])
periodsleep(mpc1)
end
@test all(isapprox.(diff(times1[2:end]), 0.1, atol=0.01))
end

@testset "ExplicitMPC construction" begin
model = LinModel(sys, Ts, i_d=[3])
mpc1 = ExplicitMPC(model, Hp=15)
Expand Down
48 changes: 48 additions & 0 deletions test/test_sim_model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,25 @@ end
@test_throws DimensionMismatch evaloutput(linmodel1, zeros(1))
end

@testset "LinModel real time simulations" begin
linmodel1 = LinModel(tf(2, [10, 1]), 0.1)
times1 = zeros(5)
for i=1:5
times1[i] = savetime!(linmodel1)
updatestate!(linmodel1, [1])
periodsleep(linmodel1)
end
@test all(isapprox.(diff(times1[2:end]), 0.1, atol=0.01))
linmodel2 = LinModel(tf(2, [0.1, 1]), 0.001)
times2 = zeros(5)
for i=1:5
times2[i] = savetime!(linmodel2)
updatestate!(linmodel2, [1])
periodsleep(linmodel2, true)
end
@test all(isapprox.(diff(times2[2:end]), 0.001, atol=0.0001))
end

@testset "NonLinModel construction" begin
linmodel1 = LinModel(sys,Ts,i_u=[1,2])
f1(x,u,_) = linmodel1.A*x + linmodel1.Bu*u
Expand Down Expand Up @@ -274,4 +293,33 @@ end
updatestate!(linmodel3, u, d)
end
@test all(isapprox.(Ynl, Yl, atol=1e-6))
end

@testset "NonLinModel real time simulations" begin
linmodel1 = LinModel(tf(2, [10, 1]), 0.1)
nonlinmodel1 = NonLinModel(
(x,u,_)->linmodel1.A*x + linmodel1.Bu*u,
(x,_)->linmodel1.C*x,
linmodel1.Ts, 1, 1, 1, 0, solver=nothing
)
times1 = zeros(5)
for i=1:5
times1[i] = savetime!(nonlinmodel1)
updatestate!(nonlinmodel1, [1])
periodsleep(nonlinmodel1)
end
@test all(isapprox.(diff(times1[2:end]), 0.1, atol=0.01))
linmodel2 = LinModel(tf(2, [0.1, 1]), 0.001)
nonlinmodel2 = NonLinModel(
(x,u,_)->linmodel2.A*x + linmodel2.Bu*u,
(x,_)->linmodel2.C*x,
linmodel2.Ts, 1, 1, 1, 0, solver=nothing
)
times2 = zeros(5)
for i=1:5
times2[i] = savetime!(nonlinmodel2)
updatestate!(nonlinmodel2, [1])
periodsleep(nonlinmodel2, true)
end
@test all(isapprox.(diff(times2[2:end]), 0.001, atol=0.0001))
end
13 changes: 13 additions & 0 deletions test/test_state_estim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@ end
skalmanfilter = SteadyKalmanFilter(linmodel, nint_ym=0)
@test_throws ErrorException setmodel!(skalmanfilter, linmodel)
end

@testset "SteadyKalmanFilter real-time simulations" begin
linmodel1 = LinModel(tf(2, [10, 1]), 0.1)
skalmanfilter1 = SteadyKalmanFilter(linmodel1)
times1 = zeros(5)
for i=1:5
times1[i] = savetime!(skalmanfilter1)
preparestate!(skalmanfilter1, [1])
updatestate!(skalmanfilter1, [1], [1])
periodsleep(skalmanfilter1)
end
@test all(isapprox.(diff(times1[2:end]), 0.1, atol=0.01))
end

@testset "KalmanFilter construction" begin
linmodel1 = setop!(LinModel(sys,Ts,i_u=[1,2]), uop=[10,50], yop=[50,30])
Expand Down
Loading