Skip to content

Commit bcb3821

Browse files
authored
Merge pull request #372 from JuliaControl/code_gen
doc: added C code generation example in "Manual: Linear Design"
2 parents 192cf70 + bf34a14 commit bcb3821

5 files changed

Lines changed: 106 additions & 6 deletions

File tree

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "ModelPredictiveControl"
22
uuid = "61f9bdb8-6ae4-484a-811f-bbf86720c31c"
3-
version = "2.4.1"
3+
version = "2.4.2"
44
authors = ["Francis Gagnon"]
55

66
[deps]

docs/src/manual/linmpc.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,102 @@ savefig("plot4_LinMPC.svg"); nothing # hide
293293
Note that measured disturbances are assumed constant in the future by default but custom
294294
``\mathbf{D̂}`` predictions are possible. The same applies for the setpoint predictions
295295
``\mathbf{R̂_y}``.
296+
297+
## Generating C code
298+
299+
The [`LinearMPC.jl`](@extref LinearMPC) package extension provides code generation
300+
capabilities to export the controller as optimized and standalone C code. First, install the
301+
package with:
302+
303+
```test
304+
using Pkg; Pkg.add("LinearMPC")
305+
```
306+
307+
The feedforward MPC controller can be converted to a [`LinearMPC.MPC`](@ref) object using:
308+
309+
```@example 1
310+
import LinearMPC
311+
c_mpc_d = LinearMPC.MPC(mpc_d);
312+
nothing # hide
313+
```
314+
315+
We test the converted controller in closed-loop to verify that it behaves identically to the
316+
original one, notably because of the two different solvers:
317+
318+
```@example 1
319+
function test_c_mpc_d(c_mpc_d, model)
320+
N = 200
321+
ry, ul = [50, 30], 0
322+
dop = 20
323+
u = model.uop
324+
u_data, y_data, ry_data = zeros(model.nu, N), zeros(model.ny, N), zeros(model.ny, N)
325+
for i = 1:N
326+
i == 51 && (ry = [50, 35])
327+
i == 101 && (ry = [54, 30])
328+
i == 151 && (ul = -20)
329+
d = [ul .+ dop]
330+
y = model()
331+
x̂ = LinearMPC.correct_state!(c_mpc_d, y, d)
332+
u = LinearMPC.compute_control(c_mpc_d, x̂; r=ry, d=d, uprev=u)
333+
u_data[:,i], y_data[:,i], ry_data[:,i] = u, y, ry
334+
LinearMPC.predict_state!(c_mpc_d, u, d)
335+
updatestate!(model, u + [0; ul])
336+
end
337+
return u_data, y_data, ry_data
338+
end
339+
setstate!(model, zeros(model.nx))
340+
LinearMPC.set_state!(c_mpc_d, zeros(c_mpc_d.model.nx))
341+
u_data, y_data, ry_data = test_c_mpc_d(c_mpc_d, model)
342+
plot_data(t_data, u_data, y_data, ry_data)
343+
savefig("plot5_LinMPC.svg"); nothing # hide
344+
```
345+
346+
![plot5_LinMPC](plot5_LinMPC.svg)
347+
348+
The closed-loop simulation matches the results of the previous section, as expected. We
349+
can now generate the C code using:
350+
351+
```julia
352+
LinearMPC.codegen(c_mpc_d; dir="codegen", fname="mpc_funcs")
353+
```
354+
355+
The three C functions to call at each control period are declared in the generated
356+
`codegen/mpc_funcs.h` file, and they receive pointers of `c_float` arrays:
357+
358+
```C
359+
void mpc_correct_state(c_float* state, c_float* measurement, c_float* disturbance);
360+
int mpc_compute_control(c_float* control, c_float* state, c_float* reference, c_float* disturbance);
361+
void mpc_predict_state(c_float* state, c_float* control, c_float* disturbance);
362+
```
363+
364+
For example, on Linux, you can add the following code in a new `main.c` file:
365+
366+
```C
367+
#include "mpc_funcs.h"
368+
#include <stdio.h>
369+
int main(){
370+
// initialize arrays:
371+
c_float u[2] = {20, 20};
372+
c_float x[6] = {0, 0, 0, 0, 0, 0};
373+
c_float r[2] = {50, 35};
374+
c_float y[2] = {50, 30};
375+
c_float d[1] = {20};
376+
// execute one control period:
377+
mpc_correct_state(x, y, d);
378+
mpc_compute_control(u, x, r, d);
379+
mpc_predict_state(x, u, d);
380+
// print the computed control:
381+
printf("The computed u value is: [%f, %f]\n", u[0], u[1]);
382+
return 0;
383+
}
384+
```
385+
386+
compile with using `gcc *.c -o main.bin` and run it with `./main.bin`. The printed `u` value
387+
should be identical to:
388+
389+
```@example 1
390+
LinearMPC.set_state!(c_mpc_d, zeros(c_mpc_d.model.nx))
391+
x̂ = LinearMPC.correct_state!(c_mpc_d, [50, 30], [20])
392+
u = LinearMPC.compute_control(c_mpc_d, x̂; r=[50, 35], d=[20], uprev=[20, 20])
393+
println("The computed u value is: $(round.(u, digits=6))")
394+
```

src/controller/transcription.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1561,7 +1561,7 @@ deterministic states extracted from ``\mathbf{X̂_0}`` also in `Z̃`, and they c
15611561
states at the beginning of the interval ``τ_0=0``. The ``\mathbf{k̇}_i`` derivative for the
15621562
``i``th collocation point is computed from the continuous-time function `model.f!` and:
15631563
```math
1564-
\mathbf{k̇}_i(k+j) = \mathbf{f}\Big(\mathbf{k}_i(k+j), \mathbf{û_i}(k+j), \mathbf{d̂}_i(k+j), \mathbf{p}\Big)
1564+
\mathbf{k̇}_i(k+j) = \mathbf{f}\Big(\mathbf{k}_i(k+j), \mathbf{û}_i(k+j), \mathbf{d̂}_i(k+j), \mathbf{p}\Big)
15651565
```
15661566
Based on the normalized time ``τ_i ∈ [0, 1]`` and hold order `transcription.h`, the inputs
15671567
and disturbances are piecewise constant or linear:

src/estimator/internal_model.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ Update `estim.x̂0`/`x̂d`/`x̂s` with current inputs `u0`, measured outputs `y0
281281
The [`InternalModel`](@ref) updates the deterministic `x̂d` and stochastic `x̂s` estimates with:
282282
```math
283283
\begin{aligned}
284-
\mathbf{x̂_d}(k+1) &= \mathbf{f}\Big( \mathbf{x̂_d}(k), \mathbf{u}(k), \mathbf{d}(k) \Big) \\
284+
\mathbf{x̂_d}(k+1) &= \mathbf{f}\Big( \mathbf{x̂_d}(k), \mathbf{u}(k), \mathbf{d}(k), \mathbf{p} \Big) \\
285285
\mathbf{x̂_s}(k+1) &= \mathbf{Â_s x̂_s}(k) + \mathbf{B̂_s ŷ_s}(k)
286286
\end{aligned}
287287
```

src/estimator/mhe/construct.jl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -438,9 +438,10 @@ MovingHorizonEstimator estimator with a sample time Ts = 10.0 s:
438438
439439
The optimization and the update of the arrival covariance depend on `model`:
440440
441-
- If `model` is a [`LinModel`](@ref), the optimization is treated as a quadratic program
442-
with a time-varying Hessian, which is generally cheaper than nonlinear programming. By
443-
default, a [`KalmanFilter`](@ref) estimates the arrival covariance (customizable).
441+
- If `model` is a [`LinModel`](@ref) and `nc=0`, the optimization is treated as a
442+
quadratic program with a time-varying Hessian, which is generally cheaper than
443+
nonlinear programming. By default, a [`KalmanFilter`](@ref) estimates the arrival
444+
covariance (customizable).
444445
- Else, a nonlinear program with dense [`ForwardDiff`](@extref ForwardDiff) automatic
445446
differentiation (AD) compute the objective and constraint derivatives by default
446447
(customizable). Optimizers generally benefit from exact derivatives like AD. However,

0 commit comments

Comments
 (0)