diff --git a/Project.toml b/Project.toml index 8b582f39d..70e091680 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ModelPredictiveControl" uuid = "61f9bdb8-6ae4-484a-811f-bbf86720c31c" -version = "1.14.4" +version = "1.15.0" authors = ["Francis Gagnon"] [deps] @@ -22,6 +22,12 @@ SparseConnectivityTracer = "9f842d2f-2579-4b1d-911e-f412cf18a3f5" SparseMatrixColorings = "0a514795-09f3-496d-8182-132a7b665d35" StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +[weakdeps] +LinearMPC = "82e1c212-e1a2-49d2-b26a-a31d6968e3bd" + +[extensions] +LinearMPCext = "LinearMPC" + [compat] ControlSystemsBase = "1.18.2" DAQP = "0.6, 0.7.1" @@ -32,6 +38,7 @@ ForwardDiff = "0.10, 1" Ipopt = "1" JuMP = "1.21" LinearAlgebra = "1.10" +LinearMPC = "0.7.0" Logging = "1.10" MathOptInterface = "1.46" OSQP = "0.8" @@ -52,10 +59,11 @@ julia = "1.10" DAQP = "c47d62df-3981-49c8-9651-128b1cd08617" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" FiniteDiff = "6a86dc24-6348-571c-b903-95158fe2bd41" +LinearMPC = "82e1c212-e1a2-49d2-b26a-a31d6968e3bd" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe" [targets] -test = ["Test", "TestItems", "TestItemRunner", "Documenter", "Plots", "DAQP", "FiniteDiff"] +test = ["Test", "TestItems", "TestItemRunner", "Documenter", "Plots", "DAQP", "FiniteDiff", "LinearMPC"] diff --git a/README.md b/README.md index b4f7167a0..bbe274473 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ for more detailed examples. - 📝 **Transcription**: Direct single/multiple shooting and trapezoidal collocation. - 🩺 **Troubleshooting**: Detailed diagnostic information about optimum. - ⏱️ **Real-Time**: Optimized for low memory allocations with soft real-time utilities. +- 📟️ **Embedded**: Lightweight C code generation via `LinearMPC.jl` ### 🔭 State Estimation Features diff --git a/docs/Project.toml b/docs/Project.toml index 6a821fa04..f3223373d 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -5,6 +5,7 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +LinearMPC = "82e1c212-e1a2-49d2-b26a-a31d6968e3bd" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" @@ -15,6 +16,7 @@ DAQP = "0.6, 0.7.1" Documenter = "1" JuMP = "1" LinearAlgebra = "1.10" +LinearMPC = "0.7.0" Logging = "1.10" ModelingToolkit = "10" Plots = "1" diff --git a/docs/make.jl b/docs/make.jl index 1107afc69..4a1c3092b 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -5,6 +5,7 @@ push!(LOAD_PATH,"../src/") using Documenter, DocumenterInterLinks using ModelPredictiveControl +import LinearMPC links = InterLinks( "Julia" => "https://docs.julialang.org/en/v1/objects.inv", @@ -14,6 +15,7 @@ links = InterLinks( "DifferentiationInterface" => "https://juliadiff.org/DifferentiationInterface.jl/DifferentiationInterface/stable/objects.inv", "ForwardDiff" => "https://juliadiff.org/ForwardDiff.jl/stable/objects.inv", "LowLevelParticleFilters" => "https://baggepinnen.github.io/LowLevelParticleFilters.jl/stable/objects.inv", + "LinearMPC" => "https://darnstrom.github.io/LinearMPC.jl/stable/objects.inv", ) DocMeta.setdocmeta!( diff --git a/docs/src/manual/linmpc.md b/docs/src/manual/linmpc.md index 8d11aa9fa..a866896a9 100644 --- a/docs/src/manual/linmpc.md +++ b/docs/src/manual/linmpc.md @@ -201,7 +201,7 @@ For the CSTR, we will bound the innovation term ``\mathbf{y}(k) - \mathbf{ŷ}(k estim = MovingHorizonEstimator(model, He=10, nint_u=[1, 1], σQint_u = [1, 2]) estim = setconstraint!(estim, v̂min=[-1, -0.5], v̂max=[+1, +0.5]) mpc_mhe = LinMPC(estim, Hp=10, Hc=2, Mwt=[1, 1], Nwt=[0.1, 0.1]) -mpc_mhe = setconstraint!(mpc_mhe, ymin=[45, -Inf]) +mpc_mhe = setconstraint!(mpc_mhe, ymin=[48, -Inf]) ``` The rejection is indeed improved: diff --git a/docs/src/public/predictive_control.md b/docs/src/public/predictive_control.md index 1e73553de..4250a4b14 100644 --- a/docs/src/public/predictive_control.md +++ b/docs/src/public/predictive_control.md @@ -72,6 +72,12 @@ PredictiveController LinMPC ``` +### Conversion to LinearMPC.jl (code generation) + +```@docs +LinearMPC.MPC +``` + ## ExplicitMPC ```@docs @@ -114,4 +120,4 @@ MultipleShooting ```@docs TrapezoidalCollocation -``` +``` \ No newline at end of file diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl new file mode 100644 index 000000000..ba89654bb --- /dev/null +++ b/ext/LinearMPCext.jl @@ -0,0 +1,254 @@ +module LinearMPCext + +using ModelPredictiveControl +using LinearAlgebra, SparseArrays +using JuMP + +import LinearMPC +import ModelPredictiveControl: isblockdiag + +function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) + model, estim, weights = mpc.estim.model, mpc.estim, mpc.weights + nu, ny, nx̂ = model.nu, model.ny, estim.nx̂ + Hp, Hc = mpc.Hp, mpc.Hc + nΔU = Hc * nu + validate_compatibility(mpc) + # --- Model parameters --- + F, G, Gd = estim.Â, estim.B̂u, estim.B̂d + C, Dd = estim.Ĉ, estim.D̂d + Np = Hp + Nc = Hc + newmpc = LinearMPC.MPC(F, G; Gd, C, Dd, Np, Nc) + # --- Operating points --- + uoff = model.uop + doff = model.dop + yoff = model.yop + xoff = estim.x̂op + foff = estim.f̂op + LinearMPC.set_offset!(newmpc; uo=uoff, ho=yoff, doff=doff, xo=xoff, fo=foff) + # --- State observer parameters --- + Q, R = estim.cov.Q̂, estim.cov.R̂ + LinearMPC.set_state_observer!(newmpc; C=estim.Ĉm, Q, R) + # --- Objective function weights --- + Q = weights.M_Hp[1:ny, 1:ny] + Qf = weights.M_Hp[end-ny+1:end, end-ny+1:end] + Rr = weights.Ñ_Hc[1:nu, 1:nu] + R = weights.L_Hp[1:nu, 1:nu] + LinearMPC.set_objective!(newmpc; Q, Rr, R, Qf) + # --- Custom move blocking --- + LinearMPC.move_block!(newmpc, mpc.nb) # un-comment when debugged + # ---- Constraint softening --- + only_hard = weights.isinf_C + if !only_hard + issoft(C) = any(x -> x > 0, C) + C_u = -mpc.con.A_Umin[:, end] + C_Δu = -mpc.con.A_ΔŨmin[1:nΔU, end] + C_y = -mpc.con.A_Ymin[:, end] + c_x̂ = -mpc.con.A_x̂min[:, end] + if sum(mpc.con.i_b) > 1 # ignore the slack variable ϵ bound + if issoft(C_u) || issoft(C_Δu) || issoft(C_y) || issoft(C_x̂) + @warn "The LinearMPC conversion is approximate for the soft constraints.\n"* + "You may need to adjust the soft_weight field of the "* + "LinearMPC.MPC object to replicate behaviors." + end + end + # LinearMPC relies on a different softening mechanism (new implicit slacks for each + # softened bounds), so we apply an approximate conversion factor on the Cwt weight: + Cwt = weights.Ñ_Hc[end, end] + nsoft = sum((mpc.con.A[:,end] .< 0) .& (mpc.con.i_b)) - 1 + newmpc.settings.soft_weight = 10*sqrt(nsoft*Cwt) + else + C_u = zeros(nu*Hp) + C_Δu = zeros(nu*Hc) + C_y = zeros(ny*Hp) + c_x̂ = zeros(nx̂) + end + # --- Manipulated inputs constraints --- + Umin, Umax = mpc.con.U0min + mpc.Uop, mpc.con.U0max + mpc.Uop + I_u = Matrix{Float64}(I, nu, nu) + # add_constraint! does not support u bounds pass the control horizon Hc + # so we compute the extremum bounds from k=Hc-1 to Hp, and apply them at k=Hc-1 + Umin_finals = reshape(Umin[nu*(Hc-1)+1:end], nu, :) + Umax_finals = reshape(Umax[nu*(Hc-1)+1:end], nu, :) + umin_end = mapslices(maximum, Umin_finals; dims=2) + umax_end = mapslices(minimum, Umax_finals; dims=2) + for k in 0:Hc-1 + if k < Hc - 1 + umin_k, umax_k = Umin[k*nu+1:(k+1)*nu], Umax[k*nu+1:(k+1)*nu] + else + umin_k, umax_k = umin_end, umax_end + end + c_u_k = C_u[k*nu+1:(k+1)*nu] + ks = [k + 1] # a `1` in ks argument corresponds to the present time step k+0 + for i in 1:nu + lb = isfinite(umin_k[i]) ? [umin_k[i]] : zeros(0) + ub = isfinite(umax_k[i]) ? [umax_k[i]] : zeros(0) + soft = !only_hard && c_u_k[i] > 0 + Au = I_u[i:i, :] + LinearMPC.add_constraint!(newmpc; Au, lb, ub, ks, soft) + end + end + # --- Input increment constraints --- + ΔUmin, ΔUmax = mpc.con.ΔŨmin[1:nΔU], mpc.con.ΔŨmax[1:nΔU] + I_Δu = Matrix{Float64}(I, nu, nu) + for k in 0:Hc-1 + Δumin_k, Δumax_k = ΔUmin[k*nu+1:(k+1)*nu], ΔUmax[k*nu+1:(k+1)*nu] + c_Δu_k = C_Δu[k*nu+1:(k+1)*nu] + ks = [k + 1] # a `1` in ks argument corresponds to the present time step k+0 + for i in 1:nu + lb = isfinite(Δumin_k[i]) ? [Δumin_k[i]] : zeros(0) + ub = isfinite(Δumax_k[i]) ? [Δumax_k[i]] : zeros(0) + soft = !only_hard && c_Δu_k[i] > 0 + Au, Aup = I_Δu[i:i, :], -I_Δu[i:i, :] + LinearMPC.add_constraint!(newmpc; Au, Aup, lb, ub, ks, soft) + end + end + # --- Output constraints --- + Y0min, Y0max = mpc.con.Y0min, mpc.con.Y0max + for k in 1:Hp + ymin_k, ymax_k = Y0min[(k-1)*ny+1:k*ny], Y0max[(k-1)*ny+1:k*ny] + c_y_k = C_y[(k-1)*ny+1:k*ny] + ks = [k + 1] # a `1` in ks argument corresponds to the present time step k+0 + for i in 1:ny + lb = isfinite(ymin_k[i]) ? [ymin_k[i]] : zeros(0) + ub = isfinite(ymax_k[i]) ? [ymax_k[i]] : zeros(0) + soft = !only_hard && c_y_k[i] > 0 + Ax, Ad = C[i:i, :], Dd[i:i, :] + LinearMPC.add_constraint!(newmpc; Ax, Ad, lb, ub, ks, soft) + end + end + # --- Terminal constraints --- + x̂0min, x̂0max = mpc.con.x̂0min, mpc.con.x̂0max + I_x̂ = Matrix{Float64}(I, nx̂, nx̂) + ks = [Hp + 1] # a `1` in ks argument corresponds to the present time step k+0 + for i in 1:nx̂ + lb = isfinite(x̂0min[i]) ? [x̂0min[i]] : zeros(0) + ub = isfinite(x̂0max[i]) ? [x̂0max[i]] : zeros(0) + soft = !only_hard && c_x̂[i] > 0 + Ax = I_x̂[i:i, :] + LinearMPC.add_constraint!(newmpc; Ax, lb, ub, ks, soft) + end + return newmpc +end + +function validate_compatibility(mpc::ModelPredictiveControl.LinMPC) + if mpc.transcription isa MultipleShooting + error("LinearMPC only supports SingleShooting transcription.") + end + if !(mpc.estim isa SteadyKalmanFilter) || !mpc.estim.direct + error("LinearMPC only supports SteadyKalmanFilter with direct=true option.") + end + if JuMP.solver_name(mpc.optim) != "DAQP" + @warn "LinearMPC relies on DAQP, and the solver in the mpc object " * + "is currently $(JuMP.solver_name(mpc.optim)).\n" * + "The results in closed-loop may be different." + end + validate_weights(mpc) + validate_constraints(mpc) + return nothing +end + +function validate_weights(mpc::ModelPredictiveControl.LinMPC) + ny, nu = mpc.estim.model.ny, mpc.estim.model.nu + Hp, Hc = mpc.Hp, mpc.Hc + nΔU = Hc * nu + M_Hp, N_Hc, L_Hp = mpc.weights.M_Hp, mpc.weights.Ñ_Hc[1:nΔU, 1:nΔU], mpc.weights.L_Hp + M_1, N_1, L_1 = M_Hp[1:ny, 1:ny], N_Hc[1:nu, 1:nu], L_Hp[1:nu, 1:nu] + for i in 2:mpc.Hp-1 # last block is terminal weight, can be different + M_i = M_Hp[(i-1)*ny+1:i*ny, (i-1)*ny+1:i*ny] + if !isapprox(M_i, M_1) + error("LinearMPC only supports identical weights for each stages in M_Hp.") + end + end + isblockdiag(M_Hp, ny, Hp) || error("M_Hp must be block diagonal.") + for i in 2:mpc.Hc + N_i = N_Hc[(i-1)*nu+1:i*nu, (i-1)*nu+1:i*nu] + if !isapprox(N_i, N_1) + error("LinearMPC only supports identical weights for each stages in Ñ_Hc.") + end + end + isblockdiag(N_Hc, nu, Hc) || error("Ñ_Hc must be block diagonal.") + for i in 2:mpc.Hp + L_i = L_Hp[(i-1)*nu+1:i*nu, (i-1)*nu+1:i*nu] + if !isapprox(L_i, L_1) + error("LinearMPC only supports identical weights for each stages in L_Hp.") + end + end + isblockdiag(L_Hp, nu, Hp) || error("L_Hp must be block diagonal.") + return nothing +end + +function validate_constraints(mpc::ModelPredictiveControl.LinMPC) + nΔU = mpc.Hc * mpc.estim.model.nu + mpc.weights.isinf_C && return nothing # only hard constraints are entirely supported + C_umin, C_umax = -mpc.con.A_Umin[:, end], -mpc.con.A_Umax[:, end] + C_Δumin, C_Δumax = -mpc.con.A_ΔŨmin[1:nΔU, end], -mpc.con.A_ΔŨmax[1:nΔU, end] + C_ymin, C_ymax = -mpc.con.A_Ymin[:, end], -mpc.con.A_Ymax[:, end] + C_x̂min, C_x̂max = -mpc.con.A_x̂min[:, end], -mpc.con.A_x̂max[:, end] + is0or1(C) = all(x -> x ≈ 0 || x ≈ 1, C) + if ( + !is0or1(C_umin) || !is0or1(C_umax) || + !is0or1(C_Δumin) || !is0or1(C_Δumax) || + !is0or1(C_ymin) || !is0or1(C_ymax) || + !is0or1(C_x̂min) || !is0or1(C_x̂max) + + ) + error("LinearMPC only supports softness parameters c = 0 or 1.") + end + if ( + !isapprox(C_umin, C_umax) || + !isapprox(C_Δumin, C_Δumax) || + !isapprox(C_ymin, C_ymax) || + !isapprox(C_x̂min, C_x̂max) + ) + error("LinearMPC only supports identical softness parameters for lower and upper bounds.") + end + return nothing +end + +@doc raw""" + LinearMPC.MPC(mpc::LinMPC) + +Convert a `ModelPredictiveControl.LinMPC` object to a `LinearMPC.MPC` object. + +The `LinearMPC` package needs to be installed and available in the activated Julia +environment. The converted object can be used to generate lightweight C-code for embedded +applications using the `LinearMPC.codegen` function. Note that not all features of [`LinMPC`](@ref) +are supported, including these restrictions: + +- the solver is limited to [`DAQP`](https://darnstrom.github.io/daqp/). +- the transcription method must be [`SingleShooting`](@ref). +- the state estimator must be a [`SteadyKalmanFilter`](@ref) with `direct=true`. +- only block-diagonal weights are allowed. +- the constraint relaxation mechanism is different, so a 1-on-1 conversion of the soft + constraints is impossible (use `Cwt=Inf` to disable relaxation). + +But the package has also several exclusive functionalities, such as pre-stabilization, +constrained explicit MPC, and binary manipulated inputs. See the [`LinearMPC.jl`](@extref LinearMPC) +documentation for more details on the supported features and how to generate code. + +# Examples +```jldoctest +julia> import LinearMPC, JuMP, DAQP; + +julia> mpc1 = LinMPC(LinModel(tf(2, [10, 1]), 1.0); optim=JuMP.Model(DAQP.Optimizer)); + +julia> preparestate!(mpc1, [1.0]); + +julia> u = moveinput!(mpc1, [10.0]); round.(u, digits=6) +1-element Vector{Float64}: + 17.577311 + +julia> mpc2 = LinearMPC.MPC(mpc1); + +julia> x̂ = LinearMPC.correct_state!(mpc2, [1.0]); + +julia> u = LinearMPC.compute_control(mpc2, x̂, r=[10.0]); round.(u, digits=6) +1-element Vector{Float64}: + 17.577311 +``` +""" +LinearMPC.MPC(mpc::ModelPredictiveControl.LinMPC) = convert(LinearMPC.MPC, mpc) + + +end # LinearMPCext \ No newline at end of file diff --git a/src/controller/transcription.jl b/src/controller/transcription.jl index b8dc0c17d..a7b503bea 100644 --- a/src/controller/transcription.jl +++ b/src/controller/transcription.jl @@ -256,7 +256,7 @@ end @doc raw""" init_predmat( model::LinModel, estim, transcription::SingleShooting, Hp, Hc, nb - ) -> E, G, J, K, V, ex̂, gx̂, jx̂, kx̂, vx̂ + ) -> E, G, J, K, V, B, ex̂, gx̂, jx̂, kx̂, vx̂, bx̂ Construct the prediction matrices for [`LinModel`](@ref) and [`SingleShooting`](@ref). @@ -484,7 +484,9 @@ function init_predmat( end """ - init_predmat(model::NonLinModel, estim, transcription::SingleShooting, Hp, Hc, nb) + init_predmat( + model::NonLinModel, estim, transcription::SingleShooting, Hp, Hc, nb + ) -> E, G, J, K, V, B, ex̂, gx̂, jx̂, kx̂, vx̂, bx̂ Return empty matrices for [`SingleShooting`](@ref) of [`NonLinModel`](@ref) """ @@ -504,7 +506,9 @@ function init_predmat( end @doc raw""" - init_predmat(model::NonLinModel, estim, transcription::TranscriptionMethod, Hp, Hc, nb) + init_predmat( + model::NonLinModel, estim, transcription::TranscriptionMethod, Hp, Hc, nb + ) -> E, G, J, K, V, B, ex̂, gx̂, jx̂, kx̂, vx̂, bx̂ Return the terminal state matrices for [`NonLinModel`](@ref) and other [`TranscriptionMethod`](@ref). @@ -537,7 +541,9 @@ function init_predmat( end @doc raw""" - init_defectmat(model::LinModel, estim, transcription::MultipleShooting, Hp, Hc, nb) + init_defectmat( + model::LinModel, estim, transcription::MultipleShooting, Hp, Hc, nb + ) -> Eŝ, Gŝ, Jŝ, Kŝ, Vŝ, Bŝ Init the matrices for computing the defects over the predicted states. @@ -645,7 +651,9 @@ function init_defectmat( end """ - init_defectmat(model::SimModel, estim, transcription::TranscriptionMethod, Hp, Hc, nb) + init_defectmat( + model::SimModel, estim, transcription::TranscriptionMethod, Hp, Hc, nb + ) -> Eŝ, Gŝ, Jŝ, Kŝ, Vŝ, Bŝ Return empty matrices for all other cases (N/A). """ diff --git a/src/general.jl b/src/general.jl index c43e138dc..1e994bfe2 100644 --- a/src/general.jl +++ b/src/general.jl @@ -173,6 +173,14 @@ function repeatdiag(A::Hermitian{NT, Diagonal{NT, Vector{NT}}}, n::Int) where {N return Hermitian(repeatdiag(A.data, n), :L) # to return hermitian of a `Diagonal` end +"Check if matrix `A` is block diagonal with `m` blocks, where each block is `n × n`." +function isblockdiag(A::AbstractMatrix, n::Int, m::Int) + @assert size(A) == (n*m, n*m) "A size does not match the specified block dimensions." + blocks = [A[(i-1)*n+1:i*n, (i-1)*n+1:i*n] for i in 1:m] + A_blockdiag = blockdiag(sparse.(blocks)...) + return isapprox(A, A_blockdiag) +end + "In-place version of `repeat` but for vectors only." function repeat!(Y::AbstractVector, a::AbstractVector, n::Int) na = length(a) diff --git a/src/plot_sim.jl b/src/plot_sim.jl index 5e010a73d..737153dd2 100644 --- a/src/plot_sim.jl +++ b/src/plot_sim.jl @@ -45,7 +45,7 @@ julia> model = LinModel(tf(1, [1, 1]), 1.0); julia> N = 5; U_data = fill(1.0, 1, N); Y_data = zeros(1, N); -julia> for i=1:N; updatestate!(model, U_data[:, i]); Y_data[:, i] = model(); end; Y_data +julia> foreach(i->(updatestate!(model, U_data[:, i]); Y_data[:, i] = model()), 1:N); Y_data 1×5 Matrix{Float64}: 0.632121 0.864665 0.950213 0.981684 0.993262 diff --git a/test/5_test_extensions.jl b/test/5_test_extensions.jl new file mode 100644 index 000000000..aaf3ee95b --- /dev/null +++ b/test/5_test_extensions.jl @@ -0,0 +1,47 @@ +@testitem "LinearMPCext extension" setup=[SetupMPCtests] begin + using .SetupMPCtests, ControlSystemsBase, LinearAlgebra, JuMP, DAQP + import LinearMPC + model = LinModel(sys, Ts, i_u=1:2) + model = setop!(model, uop=[20, 20], yop=[50, 30]) + optim = JuMP.Model(DAQP.Optimizer) + mpc1 = LinMPC(model, Hp=15, Hc=[2, 3, 10], optim=optim) + mpc1 = setconstraint!(mpc1, ymin=[48, -Inf], umax=[Inf, 30]) + mpc2 = LinearMPC.MPC(mpc1) + function sim_both(model, mpc1, mpc2, N) + r = [55.0; 30.0] + u1 = [20.0, 20.0] + u2 = [20.0, 20.0] + model.x0 .= 0 + y_data = zeros(model.ny, N) + u_data1, u_data2 = zeros(model.nu, N), zeros(model.nu, N) + for k in 0:N-1 + k == 10 && (r .= [45; 30.0]) + k == 25 && (r .= [50; 45.0]) + y = model() + y_data[:, k+1] = y + preparestate!(mpc1, y) + x̂ = LinearMPC.correct_state!(mpc2, y) + u1 = moveinput!(mpc1, r) + u2 = LinearMPC.compute_control(mpc2, x̂, r=r, uprev=u2) + u_data1[:, k+1], u_data2[:, k+1] = u1, u2 + updatestate!(model, u1) + updatestate!(mpc1, u1, y) + LinearMPC.predict_state!(mpc2, u2) + end + return y_data, u_data1, u_data2 + end + N = 50 + y_data, u_data1, u_data2 = sim_both(model, mpc1, mpc2, N) + @test u_data1 ≈ u_data2 atol=1e-3 rtol=1e-3 + + mpc_ms = LinMPC(model; transcription=MultipleShooting(), optim) + @test_throws ErrorException LinearMPC.MPC(mpc_ms) + mpc_kf = LinMPC(KalmanFilter(model, direct=false); optim) + @test_throws ErrorException LinearMPC.MPC(mpc_kf) + mpc_osqp = LinMPC(model) + @test_logs( + (:warn, "LinearMPC relies on DAQP, and the solver in the mpc object is currently "* + "OSQP.\nThe results in closed-loop may be different."), + LinearMPC.MPC(mpc_osqp) + ) +end diff --git a/test/5_test_doctest.jl b/test/6_test_doctest.jl similarity index 100% rename from test/5_test_doctest.jl rename to test/6_test_doctest.jl diff --git a/test/runtests.jl b/test/runtests.jl index 9be719de0..4d7de9002 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,10 +4,11 @@ using Test, TestItems, TestItemRunner @run_package_tests(verbose=true) include("0_test_module.jl") -include("1_test_sim_model.jl") -include("2_test_state_estim.jl") -include("3_test_predictive_control.jl") -include("4_test_plot_sim.jl") -include("5_test_doctest.jl") +#include("1_test_sim_model.jl") +#include("2_test_state_estim.jl") +#include("3_test_predictive_control.jl") +#include("4_test_plot_sim.jl") +include("5_test_extensions.jl") +include("6_test_doctest.jl") nothing \ No newline at end of file