From cbd4f0ba76e7d31d0965d211f09c3facc6c155a3 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Mon, 8 Jun 2026 16:18:28 -0400 Subject: [PATCH 1/6] doc: minor clarification for MHE --- src/estimator/mhe/construct.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/estimator/mhe/construct.jl b/src/estimator/mhe/construct.jl index a01208d66..2e65a8950 100644 --- a/src/estimator/mhe/construct.jl +++ b/src/estimator/mhe/construct.jl @@ -438,9 +438,10 @@ MovingHorizonEstimator estimator with a sample time Ts = 10.0 s: The optimization and the update of the arrival covariance depend on `model`: - - If `model` is a [`LinModel`](@ref), the optimization is treated as a quadratic program - with a time-varying Hessian, which is generally cheaper than nonlinear programming. By - default, a [`KalmanFilter`](@ref) estimates the arrival covariance (customizable). + - If `model` is a [`LinModel`](@ref) and `nc=0`, the optimization is treated as a + quadratic program with a time-varying Hessian, which is generally cheaper than + nonlinear programming. By default, a [`KalmanFilter`](@ref) estimates the arrival + covariance (customizable). - Else, a nonlinear program with dense [`ForwardDiff`](@extref ForwardDiff) automatic differentiation (AD) compute the objective and constraint derivatives by default (customizable). Optimizers generally benefit from exact derivatives like AD. However, From 77b9fff2be79980a9d498c2ea754b213b7b9475e Mon Sep 17 00:00:00 2001 From: franckgaga Date: Tue, 9 Jun 2026 09:29:59 -0400 Subject: [PATCH 2/6] doc: minor correction --- src/controller/transcription.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/transcription.jl b/src/controller/transcription.jl index eec3566b9..d47baabc4 100644 --- a/src/controller/transcription.jl +++ b/src/controller/transcription.jl @@ -1561,7 +1561,7 @@ deterministic states extracted from ``\mathbf{X̂_0}`` also in `Z̃`, and they c states at the beginning of the interval ``τ_0=0``. The ``\mathbf{k̇}_i`` derivative for the ``i``th collocation point is computed from the continuous-time function `model.f!` and: ```math -\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) +\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) ``` Based on the normalized time ``τ_i ∈ [0, 1]`` and hold order `transcription.h`, the inputs and disturbances are piecewise constant or linear: From 926e12d3a3f77bbde17ecbd48367e5028d3524c9 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Tue, 9 Jun 2026 09:52:46 -0400 Subject: [PATCH 3/6] doc: minor correction --- src/estimator/internal_model.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/estimator/internal_model.jl b/src/estimator/internal_model.jl index aa41957a4..4079e1713 100644 --- a/src/estimator/internal_model.jl +++ b/src/estimator/internal_model.jl @@ -281,7 +281,7 @@ Update `estim.x̂0`/`x̂d`/`x̂s` with current inputs `u0`, measured outputs `y0 The [`InternalModel`](@ref) updates the deterministic `x̂d` and stochastic `x̂s` estimates with: ```math \begin{aligned} - \mathbf{x̂_d}(k+1) &= \mathbf{f}\Big( \mathbf{x̂_d}(k), \mathbf{u}(k), \mathbf{d}(k) \Big) \\ + \mathbf{x̂_d}(k+1) &= \mathbf{f}\Big( \mathbf{x̂_d}(k), \mathbf{u}(k), \mathbf{d}(k), \mathbf{p} \Big) \\ \mathbf{x̂_s}(k+1) &= \mathbf{Â_s x̂_s}(k) + \mathbf{B̂_s ŷ_s}(k) \end{aligned} ``` From 9529eaefe4847237437d45e1dc862375d084bb33 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Tue, 9 Jun 2026 15:59:06 -0400 Subject: [PATCH 4/6] doc: simple code generation example in manual --- docs/src/manual/linmpc.md | 65 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/docs/src/manual/linmpc.md b/docs/src/manual/linmpc.md index a866896a9..4801e5385 100644 --- a/docs/src/manual/linmpc.md +++ b/docs/src/manual/linmpc.md @@ -293,3 +293,68 @@ savefig("plot4_LinMPC.svg"); nothing # hide Note that measured disturbances are assumed constant in the future by default but custom ``\mathbf{D̂}`` predictions are possible. The same applies for the setpoint predictions ``\mathbf{R̂_y}``. + +## Generating C code + +The [`LinearMPC.jl`](@extref LinearMPC) package extension provides code generation +capabilities to export the controller as optimized C code. First, install the package with: + +```test +using Pkg; Pkg.add("LinearMPC") +``` + +The feedforward MPC controller can be converted to a [`LinearMPC.MPC`](@ref) object using: + +```@example 1 +import LinearMPC +c_mpc_d = LinearMPC.MPC(mpc_d); +``` + +We test the converted controller in closed-loop to verify that it behaves identically to the +original one: + +```@example 1 +function test_c_mpc_d(c_mpc_d, model) + N = 200 + ry, ul = [50, 30], 0 + dop = 20 + u = model.uop + u_data, y_data, ry_data = zeros(model.nu, N), zeros(model.ny, N), zeros(model.ny, N) + for i = 1:N + i == 51 && (ry = [50, 35]) + i == 101 && (ry = [54, 30]) + i == 151 && (ul = -20) + d = [ul .+ dop] + y = model() + x̂ = LinearMPC.correct_state!(c_mpc_d, y, d) + u = LinearMPC.compute_control(c_mpc_d, x̂; r=ry, d=d, uprev=u) + u_data[:,i], y_data[:,i], ry_data[:,i] = u, y, ry + LinearMPC.predict_state!(c_mpc_d, u, d) + updatestate!(model, u + [0; ul]) + end + return u_data, y_data, ry_data +end +setstate!(model, zeros(model.nx)) +LinearMPC.set_state!(c_mpc_d, zeros(c_mpc_d.model.nx)) +u_data, y_data, ry_data = test_c_mpc_d(c_mpc_d, model) +plot_data(t_data, u_data, y_data, ry_data) +savefig("plot5_LinMPC.svg"); nothing # hide +``` + +![plot5_LinMPC](plot5_LinMPC.svg) + +The closed-loop simulation matches the results of the previous section, as expected. We +can now generate the C code using: + +```julia +LinearMPC.codegen(c_mpc_d; dir="codegen", fname="mpc_funcs") +``` + +The three C functions to call at each control periods are declared in the generated file +`codegen/mpc_funcs.h`: + +```C +void mpc_correct_state(c_float* state, c_float* measurement, c_float* distrubance); +int mpc_compute_control(c_float* control, c_float* state, c_float* reference, c_float* disturbance); +void mpc_predict_state(c_float* state, c_float* control, c_float* disturbance); +``` From b0c826de1ffcc62aa49b61a2971cb1c45c21c4be Mon Sep 17 00:00:00 2001 From: franckgaga Date: Tue, 9 Jun 2026 17:20:58 -0400 Subject: [PATCH 5/6] doc: more details in manual C code gen example --- Project.toml | 2 +- docs/src/manual/linmpc.md | 42 +++++++++++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/Project.toml b/Project.toml index 5b3fb9dd3..183f65434 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ModelPredictiveControl" uuid = "61f9bdb8-6ae4-484a-811f-bbf86720c31c" -version = "2.4.1" +version = "2.4.2" authors = ["Francis Gagnon"] [deps] diff --git a/docs/src/manual/linmpc.md b/docs/src/manual/linmpc.md index 4801e5385..96f842241 100644 --- a/docs/src/manual/linmpc.md +++ b/docs/src/manual/linmpc.md @@ -297,7 +297,8 @@ Note that measured disturbances are assumed constant in the future by default bu ## Generating C code The [`LinearMPC.jl`](@extref LinearMPC) package extension provides code generation -capabilities to export the controller as optimized C code. First, install the package with: +capabilities to export the controller as optimized and standalone C code. First, install the +package with: ```test using Pkg; Pkg.add("LinearMPC") @@ -308,6 +309,7 @@ The feedforward MPC controller can be converted to a [`LinearMPC.MPC`](@ref) obj ```@example 1 import LinearMPC c_mpc_d = LinearMPC.MPC(mpc_d); +nothing # hide ``` We test the converted controller in closed-loop to verify that it behaves identically to the @@ -350,11 +352,43 @@ can now generate the C code using: LinearMPC.codegen(c_mpc_d; dir="codegen", fname="mpc_funcs") ``` -The three C functions to call at each control periods are declared in the generated file -`codegen/mpc_funcs.h`: +The three C functions to call at each control period are declared in the generated +`codegen/mpc_funcs.h` file, and they receive pointers of `c_float` arrays: ```C -void mpc_correct_state(c_float* state, c_float* measurement, c_float* distrubance); +void mpc_correct_state(c_float* state, c_float* measurement, c_float* disturbance); int mpc_compute_control(c_float* control, c_float* state, c_float* reference, c_float* disturbance); void mpc_predict_state(c_float* state, c_float* control, c_float* disturbance); ``` + +For example, on Linux, you can add the following code in a new `main.c` file: + +```C +#include "mpc_funcs.h" +#include +int main(){ + // initialize arrays: + c_float u[2] = {20, 20}; + c_float x[6] = {0, 0, 0, 0, 0, 0}; + c_float r[2] = {50, 35}; + c_float y[2] = {50, 30}; + c_float d[1] = {20}; + // execute one control period: + mpc_correct_state(x, y, d); + mpc_compute_control(u, x, r, d); + mpc_predict_state(x, u, d); + // print the computed control: + printf("The computed u value is: [%f, %f]\n", u[0], u[1]); + return 0; +} +``` + +compile with using `gcc *.c -o main.bin` and run it with `./main.bin`. The printed `u` value +should be identical to: + +```@example 1 +LinearMPC.set_state!(c_mpc_d, zeros(c_mpc_d.model.nx)) +x̂ = LinearMPC.correct_state!(c_mpc_d, [50, 30], [20]) +u = LinearMPC.compute_control(c_mpc_d, x̂; r=[50, 35], d=[20], uprev=[20, 20]) +println("The computed u value is: $(round.(u, digits=6))") +``` From bf34a14754ac8f09fa6594220deab46e86951c2e Mon Sep 17 00:00:00 2001 From: franckgaga Date: Tue, 9 Jun 2026 17:30:49 -0400 Subject: [PATCH 6/6] doc: minor detail --- docs/src/manual/linmpc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/manual/linmpc.md b/docs/src/manual/linmpc.md index 96f842241..e4deb10dc 100644 --- a/docs/src/manual/linmpc.md +++ b/docs/src/manual/linmpc.md @@ -313,7 +313,7 @@ nothing # hide ``` We test the converted controller in closed-loop to verify that it behaves identically to the -original one: +original one, notably because of the two different solvers: ```@example 1 function test_c_mpc_d(c_mpc_d, model)