diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml deleted file mode 100644 index 144c91b1f..000000000 --- a/.github/workflows/CI.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: CI -on: - push: - branches: - - main - tags: ['*'] - pull_request: -concurrency: - # Skip intermediate builds: always. - # Cancel intermediate builds: only if it is a pull request build. - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} -jobs: - test: - name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} - runs-on: ${{ matrix.os }} - permissions: # needed for julia-actions/cache delete old caches that it has created - actions: write - contents: read - continue-on-error: ${{ matrix.version == 'pre' }} - strategy: - fail-fast: false - matrix: - version: - - 'lts' # long-term support release - - '1' # latest stable 1.x release - - 'pre' # latest stable prerelease - # - 'nightly' # commented since noisy + 'pre' allows testing upcoming versions - os: - - ubuntu-latest - arch: - - x64 - steps: - - name: Set JULIA_DEBUG environment variable if applicable - if: ${{ runner.debug == '1' }} - run: echo "JULIA_DEBUG=ModelPredictiveControl" >> $GITHUB_ENV - - uses: actions/checkout@v7 - - uses: julia-actions/setup-julia@v3 - with: - version: ${{ matrix.version }} - arch: ${{ matrix.arch }} - - uses: julia-actions/cache@v3 - - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-runtest@v1 - - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v7 - with: - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: false \ No newline at end of file diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml deleted file mode 100644 index 3042569cb..000000000 --- a/.github/workflows/TagBot.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: TagBot -on: - issue_comment: - types: - - created - workflow_dispatch: - inputs: - lookback: - default: 3 -permissions: - actions: read - checks: read - contents: write - deployments: read - issues: read - discussions: read - packages: read - pages: read - pull-requests: read - repository-projects: read - security-events: read - statuses: read -jobs: - TagBot: - if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' - runs-on: ubuntu-latest - steps: - - uses: JuliaRegistries/TagBot@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - # Edit the following line to reflect the actual name of the GitHub Secret containing your private key - ssh: ${{ secrets.DOCUMENTER_KEY }} - # ssh: ${{ secrets.NAME_OF_MY_SSH_PRIVATE_KEY_SECRET }} \ No newline at end of file diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 49c57a06e..788c19ffa 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -12,4 +12,4 @@ jobs: steps: - uses: MilesCranmer/AirspeedVelocity.jl@action-v1 with: - julia-version: '1' \ No newline at end of file + julia-version: '1' diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml deleted file mode 100644 index 70a059074..000000000 --- a/.github/workflows/documentation.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Documentation - -on: - push: - branches: - - main # update to match your development branch (master, main, dev, trunk, ...) - tags: '*' - pull_request: - -jobs: - build: - permissions: - contents: write - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - uses: julia-actions/setup-julia@v3 - with: - version: '1' - - name: Install dependencies - run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' - - name: Build and deploy - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # If authenticating with GitHub Actions token - DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # If authenticating with SSH deploy key - run: julia --project=docs/ docs/make.jl \ No newline at end of file diff --git a/.github/workflows/juliaci.yml b/.github/workflows/juliaci.yml new file mode 100644 index 000000000..a1c88592b --- /dev/null +++ b/.github/workflows/juliaci.yml @@ -0,0 +1,25 @@ +name: Julia CI + +on: + push: {branches: [main,master]} + pull_request: {types: [opened,synchronize,reopened,ready_for_review,converted_to_draft]} + issue_comment: {types: [created]} + workflow_dispatch: + inputs: + feature: + type: choice + description: What to run + options: [DocDeploy, LintAndTest, TagBot] + +jobs: + julia-ci: + uses: julia-testitems/testitem-workflow/.github/workflows/juliaci.yml@v2 + with: + memory-threshold: 0.9 + include-rc-versions: true + include-windows-x86: false + include-linux-x86: false + include-macos-x64: false + permissions: write-all + secrets: + codecov_token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/Project.toml b/Project.toml index 32d811880..cf83496c7 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ModelPredictiveControl" uuid = "61f9bdb8-6ae4-484a-811f-bbf86720c31c" -version = "2.12.1" +version = "2.12.2" authors = ["Francis Gagnon"] [deps] diff --git a/docs/src/internals/misc.md b/docs/src/internals/misc.md index a1eba67e9..9dbd5b617 100644 --- a/docs/src/internals/misc.md +++ b/docs/src/internals/misc.md @@ -9,3 +9,11 @@ Pages = ["misc.md"] ```@docs ModelPredictiveControl.init_orthocolloc ``` + +## Update Linear and Quadratic Terms + +```@docs +ModelPredictiveControl.initpred! +ModelPredictiveControl.linconstraint! +ModelPredictiveControl.linconstrainteq! +``` diff --git a/docs/src/internals/predictive_control.md b/docs/src/internals/predictive_control.md index f5ea36706..7f843c2ff 100644 --- a/docs/src/internals/predictive_control.md +++ b/docs/src/internals/predictive_control.md @@ -30,14 +30,6 @@ ModelPredictiveControl.get_nonlinobj_op(::NonLinMPC, ::ModelPredictiveControl.Ge ModelPredictiveControl.get_nonlincon_oracle(::NonLinMPC, ::ModelPredictiveControl.GenericModel) ``` -## Update Quadratic Optimization - -```@docs -ModelPredictiveControl.initpred!(::PredictiveController, ::LinModel, ::Any, ::Any, ::Any, ::Any, ::Any, ::Any) -ModelPredictiveControl.linconstraint!(::PredictiveController, ::LinModel, ::TranscriptionMethod) -ModelPredictiveControl.linconstrainteq! -``` - ## Solve Optimization Problem ```@docs diff --git a/docs/src/internals/state_estim.md b/docs/src/internals/state_estim.md index d6269e70e..359299bfe 100644 --- a/docs/src/internals/state_estim.md +++ b/docs/src/internals/state_estim.md @@ -101,13 +101,6 @@ ModelPredictiveControl.correct_estimate! ModelPredictiveControl.update_estimate! ``` -## Update Quadratic Optimization - -```@docs -ModelPredictiveControl.initpred!(::MovingHorizonEstimator, ::LinModel) -ModelPredictiveControl.linconstraint!(::MovingHorizonEstimator, ::LinModel, ::TranscriptionMethod) -``` - ## Solve Optimization Problem ```@docs diff --git a/src/controller/transcription.jl b/src/controller/transcription.jl index 8e5225881..0bba4380c 100644 --- a/src/controller/transcription.jl +++ b/src/controller/transcription.jl @@ -800,9 +800,9 @@ end deletex̂end_lincon!(i_x̂min, i_x̂max, ::SimModel, ::SingleShooting, _, _, _, _) = i_x̂min, i_x̂max @doc raw""" - linconstraint!(mpc::PredictiveController, model::LinModel) + linconstraint!(mpc::PredictiveController, model::LinModel, ::TranscriptionMethod) -Set `b` vector for the linear model inequality constraints (``\mathbf{A Z̃ ≤ b}``). +Set `b` vector for the linear model inequality constraints (``\mathbf{A Z̃ ≤ b}``) of MPC. Also init ``\mathbf{f_x̂} = \mathbf{g_x̂ d_0}(k) + \mathbf{j_x̂ D̂_0} + \mathbf{k_x̂ x̂_0}(k) + \mathbf{v_x̂ u_0}(k-1) + \mathbf{b_x̂}`` vector for the terminal constraints, see diff --git a/src/estimator/construct.jl b/src/estimator/construct.jl index 328593021..c7f187787 100644 --- a/src/estimator/construct.jl +++ b/src/estimator/construct.jl @@ -359,11 +359,8 @@ and unstable `model` (see Examples). ```jldoctest julia> model = LinModel(append(tf(3, [10, 1]), tf(2, [1, 0]), tf(4,[-5, 1])), 1.0); -julia> nint_ym = default_nint(model) -3-element Vector{Int64}: - 1 - 0 - 1 +julia> default_nint(model) |> println +[1, 0, 1] ``` """ function default_nint(model::LinModel, i_ym=1:model.ny, nint_u=0) diff --git a/src/estimator/luenberger.jl b/src/estimator/luenberger.jl index 6f3c4db36..256f0bd0f 100644 --- a/src/estimator/luenberger.jl +++ b/src/estimator/luenberger.jl @@ -116,7 +116,7 @@ end """ - correct_estimate!(estim::Luenberger, y0m, d0, _ ) + correct_estimate!(estim::Luenberger, y0m, d0) Identical to [`correct_estimate!(::SteadyKalmanFilter)`](@ref) but using [`Luenberger`](@ref). """ diff --git a/src/estimator/mhe/execute.jl b/src/estimator/mhe/execute.jl index ae3a22e02..d8a052f48 100644 --- a/src/estimator/mhe/execute.jl +++ b/src/estimator/mhe/execute.jl @@ -457,7 +457,12 @@ function initpred!(estim::MovingHorizonEstimator{NT}, model::LinModel) where NT< JuMP.set_objective_function(optim, obj_quadprog(Z̃var, H̃, q̃)) return nothing end -"Does nothing if `model` is not a [`LinModel`](@ref)." + +""" + initpred!(estim::MovingHorizonEstimator, LinModel::SimModel) -> nothing + +Does nothing if `model` is not a [`LinModel`](@ref). +""" initpred!(::MovingHorizonEstimator, ::SimModel) = nothing "Get the estimated state at arrival from the decision vector `Z̃`." diff --git a/src/estimator/mhe/transcription.jl b/src/estimator/mhe/transcription.jl index 7ef21432d..d2fbfe46a 100644 --- a/src/estimator/mhe/transcription.jl +++ b/src/estimator/mhe/transcription.jl @@ -79,7 +79,7 @@ from ``j=N_k-1`` to ``0``, also in deviation form, are computed with: ``` The matrices ``\mathbf{E, G, J, B, E_X̂, G_X̂, J_X̂, B_X̂}`` are defined in the Extended Help section. The vectors ``\mathbf{F, F_X̂, f_x̄}`` are recalculated at each discrete time step, -see [`initpred!(::MovingHorizonEstimator, ::LinModel)`](@ref) and [`linconstraint!(::MovingHorizonEstimator, ::LinModel)`](@ref). +see [`initpred!`](@ref) and [`linconstraint!`](@ref). # Extended Help !!! details "Extended Help" @@ -426,7 +426,7 @@ end @doc raw""" init_defectmat_mhe( model::LinModel, transcription::MultipleShooting, direct::Bool, - He, i_ym, Â, B̂u, Ĉm, B̂d, D̂dm, x̂op, f̂op, As, Co, λo + He, Â, B̂u, B̂d, x̂op, f̂op, As, Co, λo ) -> ES, GS, JS, BS Init the matrices for computing the defects over the predicted states. @@ -511,7 +511,7 @@ end @doc raw""" init_defectmat_mhe( model::SimModel, transcription::TranscriptionMethod, direct::Bool, - He, Â, _ , _ , _ , _ , As, _ , _ + He, Â, _ , _ , _ , _ , As, _ , _ ) -> ES, GS, JS, BS Init the matrices for computing the defects of the stochastic states only. @@ -816,9 +816,7 @@ function deleteŴ_lincon!(i_Ŵmin, i_Ŵmax, ::SimModel, ::TranscriptionMethod end @doc raw""" - linconstraint!( - estim::MovingHorizonEstimator, model::LinModel, transcription::TranscriptionMethod - ) + linconstraint!(estim::MovingHorizonEstimator, model::LinModel, ::TranscriptionMethod) Set `b` vector for the linear inequality constraints (``\mathbf{A Z̃ ≤ b}``) of MHE. diff --git a/src/predictive_control.jl b/src/predictive_control.jl index 981f6dd28..3772d4ade 100644 --- a/src/predictive_control.jl +++ b/src/predictive_control.jl @@ -1,4 +1,6 @@ @doc raw""" + abstract type PredictiveController end + Abstract supertype of all predictive controllers. --- diff --git a/src/sim_model.jl b/src/sim_model.jl index 15ed1595e..ef70d4f9a 100644 --- a/src/sim_model.jl +++ b/src/sim_model.jl @@ -1,4 +1,6 @@ @doc raw""" + abstract type SimModel end + Abstract supertype of [`LinModel`](@ref) and [`NonLinModel`](@ref) types. --- @@ -289,7 +291,7 @@ function savetime!(model::SimModel) return model.t[] end -""" +@doc raw""" periodsleep(model::SimModel, busywait=false) -> nothing Sleep for `model.Ts` s minus the time elapsed since the last call to [`savetime!`](@ref). @@ -305,14 +307,14 @@ simple soft real-time simulations, see the example below. restart it at a convenient time e.g.: just before calling `periodsleep`. # Examples -```jldoctest +```julia-repl julia> model = LinModel(tf(2, [0.3, 1]), 0.25); 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)) + println(round(t - t_0, digits=2)) updatestate!(model, [1]) periodsleep(model, true) # last function called end diff --git a/src/state_estim.jl b/src/state_estim.jl index c4beb6f89..1c5943147 100644 --- a/src/state_estim.jl +++ b/src/state_estim.jl @@ -1,4 +1,6 @@ @doc raw""" + abstract type StateEstimator end + Abstract supertype of all state estimators. --- diff --git a/src/transcription.jl b/src/transcription.jl index ac82730ba..f80078830 100644 --- a/src/transcription.jl +++ b/src/transcription.jl @@ -1,6 +1,8 @@ const COLLOCATION_NODE_TYPE = Float64 """ + abstract type TranscriptionMethod end + Abstract supertype of all transcription methods for the optimization problems. The module currently supports [`SingleShooting`](@ref), [`MultipleShooting`](@ref), diff --git a/test/1_test_sim_model.jl b/test/1_test_sim_model.jl index e2eabb750..8a618b864 100644 --- a/test/1_test_sim_model.jl +++ b/test/1_test_sim_model.jl @@ -144,7 +144,7 @@ end updatestate!(linmodel1, [1]) periodsleep(linmodel1) end - @test all(isapprox.(diff(times1[2:end]), 0.25, atol=0.025)) + @test all(isapprox.(diff(times1[2:end]), 0.25, atol=0.05)) linmodel2 = LinModel(tf(2, [0.1, 1]), 0.25) times2 = zeros(5) for i=1:5 @@ -152,7 +152,7 @@ end updatestate!(linmodel2, [1]) periodsleep(linmodel2, true) end - @test all(isapprox.(diff(times2[2:end]), 0.25, atol=0.0001)) + @test all(isapprox.(diff(times2[2:end]), 0.25, atol=0.05)) end @testitem "NonLinModel construction" setup=[SetupMPCtests] begin @@ -406,7 +406,7 @@ end updatestate!(nonlinmodel1, [1]) periodsleep(nonlinmodel1) end - @test all(isapprox.(diff(times1[2:end]), 0.25, atol=0.025)) + @test all(isapprox.(diff(times1[2:end]), 0.25, atol=0.05)) linmodel2 = LinModel(tf(2, [0.1, 1]), 0.25) nonlinmodel2 = NonLinModel( (x,u,_,_)->linmodel2.A*x + linmodel2.Bu*u, @@ -419,5 +419,5 @@ end updatestate!(nonlinmodel2, [1]) periodsleep(nonlinmodel2, true) end - @test all(isapprox.(diff(times2[2:end]), 0.25, atol=0.0001)) + @test all(isapprox.(diff(times2[2:end]), 0.25, atol=0.05)) end \ No newline at end of file diff --git a/test/2_test_state_estim.jl b/test/2_test_state_estim.jl index 3c98ebc2c..b920e0d06 100644 --- a/test/2_test_state_estim.jl +++ b/test/2_test_state_estim.jl @@ -149,7 +149,7 @@ end updatestate!(kalmanfilter1, [1], [1]) periodsleep(kalmanfilter1, true) end - @test all(isapprox.(diff(times1[2:end]), 0.25, atol=0.01)) + @test all(isapprox.(diff(times1[2:end]), 0.25, atol=0.05)) end @testitem "KF construction" setup=[SetupMPCtests] begin @@ -1031,7 +1031,7 @@ end ) end -@testitem "MHE estimation and getinfo (LinModel)" setup=[SetupMPCtests] begin +@testitem "MHE estim. & getinfo (LinModel, SS)" setup=[SetupMPCtests] begin using .SetupMPCtests, ControlSystemsBase, LinearAlgebra, ForwardDiff using JuMP, DAQP linmodel = LinModel(sys,Ts,i_u=[1,2], i_d=[3]) @@ -1123,6 +1123,14 @@ end updatestate!(mhe5, [10, 50], [50, NaN], [5]) ) +end + +@testitem "MHE estim. & getinfo (LinModel, MS)" setup=[SetupMPCtests] begin + using .SetupMPCtests, ControlSystemsBase, LinearAlgebra, ForwardDiff + using JuMP, DAQP + linmodel = LinModel(sys,Ts,i_u=[1,2], i_d=[3]) + linmodel = setop!(linmodel, uop=[10,50], yop=[50,30], dop=[5]) + mhe5 = MovingHorizonEstimator(linmodel, He=2, direct=true, transcription=MultipleShooting()) for i in 1:40 preparestate!(mhe5, [51, 32], [5]) @@ -1141,7 +1149,7 @@ end end -@testitem "MHE estimation and getinfo (NonLinModel)" setup=[SetupMPCtests] begin +@testitem "MHE estim. & getinfo (NonLinModel, SS)" setup=[SetupMPCtests] begin using .SetupMPCtests, ControlSystemsBase, LinearAlgebra, ForwardDiff using JuMP, Ipopt, DifferentiationInterface, SparseMatrixColorings, SparseConnectivityTracer import ForwardDiff @@ -1229,6 +1237,60 @@ end preparestate!(mhe2, [50, 30], [5]) @test mhe2([5]) ≈ [50, 30] atol=5e-3 + Q̂ = diagm([1/4, 1/4, 1/4, 1/4].^2) + R̂ = diagm([1, 1].^2) + optim = Model(Ipopt.Optimizer) + covestim = ExtendedKalmanFilter(nonlinmodel, 1:2, 0, 0, Q̂, Q̂, R̂) + mhe5 = MovingHorizonEstimator(nonlinmodel, 1, 1:2, 0, 0, Q̂, Q̂, R̂; optim, covestim) + preparestate!(mhe5, [50, 30], [5]) + x̂ = updatestate!(mhe5, [10, 50], [50, 30], [5]) + @test x̂ ≈ zeros(4) atol=1e-9 + @test mhe5.x̂0 ≈ zeros(4) atol=1e-9 + preparestate!(mhe5, [50, 30], [5]) + @test evaloutput(mhe5, [5]) ≈ mhe5([5]) ≈ [50, 30] + info = getinfo(mhe5) + @test info[:x̂] ≈ x̂ atol=1e-9 + @test info[:Ŷ][end-1:end] ≈ [50, 30] atol=1e-9 + + # coverage of the branch with error termination status (with an infeasible problem): + mhe_infeas = MovingHorizonEstimator(nonlinmodel, He=1, Cwt=Inf) + mhe_infeas = setconstraint!(mhe_infeas, v̂min=[1, 1], v̂max=[-1, -1]) + @test_logs( + (:error, "MHE terminated without solution: estimation in open-loop "* + "(more info in debug log)"), + preparestate!(mhe_infeas, [0, 0], [0]) + ) + + @test_nowarn ModelPredictiveControl.info2debugstr(info) + @test_throws ErrorException setstate!(mhe1, [1,2,3,4,5,6], diagm(.1:.1:.6)) + + hessian = AutoSparse( + AutoForwardDiff(); + sparsity_detector=TracerSparsityDetector(), + coloring_algorithm=GreedyColoringAlgorithm(), + ) + + mhe8 = MovingHorizonEstimator(nonlinmodel; He=2, hessian) + @test_logs( + (:warn, "NaN values in the MHE measurements ym: ignoring them in the objective"), + preparestate!(mhe8, [50, NaN], [5]) + ) + @test mhe8.x̂0 ≈ zeros(6) atol=1e-9 + +end + +@testitem "MHE estim. & getinfo (NonLinModel, MS)" setup=[SetupMPCtests] begin + using .SetupMPCtests, ControlSystemsBase, LinearAlgebra, ForwardDiff + using JuMP, Ipopt, DifferentiationInterface, SparseMatrixColorings, SparseConnectivityTracer + import ForwardDiff + + linmodel = LinModel(sys,Ts,i_u=[1,2], i_d=[3]) + linmodel = setop!(linmodel, uop=[10,50], yop=[50,30], dop=[5]) + f(x,u,d,model) = model.A*x + model.Bu*u + model.Bd*d + h(x,d,model) = model.C*x + model.Dd*d + nonlinmodel = NonLinModel(f, h, Ts, 2, 4, 2, 1, solver=nothing, p=linmodel) + nonlinmodel = setop!(nonlinmodel, uop=[10,50], yop=[50,30], dop=[5]) + mhe3 = MovingHorizonEstimator( nonlinmodel, He=3, direct=false, transcription=MultipleShooting(f_threads=true) ) @@ -1244,22 +1306,13 @@ end updatestate!(mhe3, [10, 50], [50, 30], [5]) info = getinfo(mhe3) # test getinfo when Nk ẋ .= -0.001x .+ u h! = (y,x,_,_) -> y .= x @@ -1292,6 +1345,16 @@ end info = getinfo(mhe7) # test getinfo when Nk ẋ .= -0.001x .+ u + h! = (y,x,_,_) -> y .= x + nonlinmodel_c = NonLinModel(f!, h!, 500, 1, 1, 1) transcription = OrthogonalCollocation(f_threads=true, h_threads=true) mhe10 = MovingHorizonEstimator( @@ -1313,41 +1376,10 @@ end updatestate!(mhe11, [-6], [13]) end preparestate!(mhe11, [13]) - @test mhe11() ≈ [13] atol=5e-3 - - # coverage of the branch with error termination status (with an infeasible problem): - mhe_infeas = MovingHorizonEstimator(nonlinmodel, He=1, Cwt=Inf) - mhe_infeas = setconstraint!(mhe_infeas, v̂min=[1, 1], v̂max=[-1, -1]) - @test_logs( - (:error, "MHE terminated without solution: estimation in open-loop "* - "(more info in debug log)"), - preparestate!(mhe_infeas, [0, 0], [0]) - ) - - # for coverage of NLP functions, the univariate syntax of JuMP.@operator - mhe7 = MovingHorizonEstimator(nonlinmodel, He=1, Cwt=Inf) - setconstraint!(mhe7, v̂min=[-51,-52], v̂max=[53,54]) - x̂ = preparestate!(mhe7, [50, 30], [5]) - @test x̂ ≈ zeros(6) atol=1e-9 - @test_nowarn ModelPredictiveControl.info2debugstr(info) - @test_throws ErrorException setstate!(mhe1, [1,2,3,4,5,6], diagm(.1:.1:.6)) - - hessian = AutoSparse( - AutoForwardDiff(); - sparsity_detector=TracerSparsityDetector(), - coloring_algorithm=GreedyColoringAlgorithm(), - ) - - mhe8 = MovingHorizonEstimator(nonlinmodel; He=2, hessian) - @test_logs( - (:warn, "NaN values in the MHE measurements ym: ignoring them in the objective"), - preparestate!(mhe8, [50, NaN], [5]) - ) - @test mhe8.x̂0 ≈ zeros(6) atol=1e-9 - + @test mhe11() ≈ [13] atol=5e-3 end -@testitem "MHE estimation with unfilled window" setup=[SetupMPCtests] begin +@testitem "MHE estim. with unfilled window" setup=[SetupMPCtests] begin f(x,u,_,_) = 0.5x + u h(x,_,_) = x model = NonLinModel(f, h, 10.0, 1, 1, 1, solver=nothing) @@ -1373,7 +1405,7 @@ end end -@testitem "MHE fallbacks for arrival covariance estimation" setup=[SetupMPCtests] begin +@testitem "MHE fallbacks for arrival covariance" setup=[SetupMPCtests] begin using .SetupMPCtests, ControlSystemsBase, LinearAlgebra linmodel = setop!(LinModel(sys,Ts,i_u=[1,2], i_d=[3]), uop=[10,50], yop=[50,30], dop=[5]) f(x,u,d,model) = model.A*x + model.Bu*u + model.Bd*d @@ -1592,8 +1624,10 @@ end linmodel2 = LinModel(sys, Ts, i_u=[1,2], i_d=[3]) linmodel2 = setop!(linmodel2, uop=[10,50], yop=[50,30], dop=[5]) function gclv!(LHS, X̂e, _, _, _, _, _, _, _, nx̂, _ ) - for i in 1:div(length(X̂e), nx̂) - LHS[(i-1)+1] = 0.5 - X̂e[(i-1)*nx̂ + 1] # First state >= 0.5 + nc_Nk = length(X̂e) ÷ nx̂ + LHS .= 0 # for when Nk < He, fill unused values with 0s + for i in 1:nc_Nk + LHS[i] = 0.5 - X̂e[(i-1)*nx̂ + 1] # First state >= 0.5 end return nothing end @@ -1678,8 +1712,10 @@ end nonlinmodel2 = NonLinModel(f, h, Ts, 2, 4, 2, 1, solver=nothing, p=linmodel2) nonlinmodel2 = setop!(nonlinmodel2, uop=[10,50], yop=[50,30], dop=[5]) function gcnlv!(LHS, X̂e, _, _, _, _, _, _, _, nx̂, _) - for i in 1:div(length(X̂e), nx̂) - LHS[(i-1)+1] = 0.5 - X̂e[(i-1)*nx̂ + 1] # First state >= 0.5 + nc_Nk = length(X̂e) ÷ nx̂ + LHS .= 0 # for when Nk < He, fill unused values with 0s + for i in 1:nc_Nk + LHS[i] = 0.5 - X̂e[(i-1)*nx̂ + 1] # First state >= 0.5 end return nothing end @@ -1800,7 +1836,7 @@ end updatestate!(mhe, [11, 50], y, [25]) updatestate!(kf, [11, 50], y, [25]) end - @test X̂_mhe ≈ X̂_kf atol=1e-6 rtol=1e-6 + @test X̂_mhe ≈ X̂_kf atol=1e-3 rtol=1e-3 kf = KalmanFilter(linmodel, nint_ym=0, direct=true) # recuperate P̂(-1|-1) exact value using the Kalman filter: preparestate!(kf, [50, 30], [20]) @@ -1818,7 +1854,7 @@ end updatestate!(mhe, [11, 50], y, [25]) updatestate!(kf, [11, 50], y, [25]) end - @test X̂_mhe ≈ X̂_kf atol=1e-6 rtol=1e-6 + @test X̂_mhe ≈ X̂_kf atol=1e-3 rtol=1e-3 f = (x,u,d,model) -> model.A*x + model.Bu*u + model.Bd*d h = (x,d,model) -> model.C*x + model.Dd*d @@ -1842,8 +1878,8 @@ end updatestate!(ukf, [11, 50], y, [25]) updatestate!(ekf, [11, 50], y, [25]) end - @test X̂_mhe ≈ X̂_ukf atol=1e-6 rtol=1e-6 - @test X̂_mhe ≈ X̂_ekf atol=1e-6 rtol=1e-6 + @test X̂_mhe ≈ X̂_ukf atol=1e-3 rtol=1e-3 + @test X̂_mhe ≈ X̂_ekf atol=1e-3 rtol=1e-3 ukf = UnscentedKalmanFilter(nonlinmodel, nint_ym=0, direct=true) ekf = ExtendedKalmanFilter(nonlinmodel, nint_ym=0, direct=true) @@ -1869,8 +1905,8 @@ end updatestate!(ukf, [11, 50], y, [25]) updatestate!(ekf, [11, 50], y, [25]) end - @test X̂_mhe ≈ X̂_ukf atol=1e-6 rtol=1e-6 - @test X̂_mhe ≈ X̂_ekf atol=1e-6 rtol=1e-6 + @test X̂_mhe ≈ X̂_ukf atol=1e-3 rtol=1e-3 + @test X̂_mhe ≈ X̂_ekf atol=1e-3 rtol=1e-3 end @testitem "MHE LinModel v.s. NonLinModel" setup=[SetupMPCtests] begin diff --git a/test/3_test_predictive_control.jl b/test/3_test_predictive_control.jl index 6f4f636e9..83ba626e7 100644 --- a/test/3_test_predictive_control.jl +++ b/test/3_test_predictive_control.jl @@ -89,7 +89,7 @@ @test_throws DimensionMismatch LinMPC(model, Wy=ones(2, model.ny), Wu=ones(3, model.nu)) end -@testitem "LinMPC moves and getinfo" setup=[SetupMPCtests] begin +@testitem "LinMPC moves & getinfo" setup=[SetupMPCtests] begin using .SetupMPCtests, ControlSystemsBase, LinearAlgebra using .SetupMPCtests, ControlSystemsBase, LinearAlgebra linmodel = setop!(LinModel(tf(5, [2, 1]), 3), yop=[10]) @@ -581,7 +581,7 @@ end @testitem "LinMPC real-time simulations" setup=[SetupMPCtests] begin using .SetupMPCtests, ControlSystemsBase, LinearAlgebra - linmodel1 = LinModel(tf(2, [10, 1]), 0.1) + linmodel1 = LinModel(tf(2, [10, 1]), 0.25) mpc1 = LinMPC(linmodel1) times1 = zeros(5) for i=1:5 @@ -590,7 +590,7 @@ end updatestate!(mpc1, [1], [1]) periodsleep(mpc1, true) end - @test all(isapprox.(diff(times1[2:end]), 0.1, atol=0.01)) + @test all(isapprox.(diff(times1[2:end]), 0.25, atol=0.05)) end @testitem "ExplicitMPC construction" setup=[SetupMPCtests] begin @@ -637,7 +637,7 @@ end end -@testitem "ExplicitMPC moves and getinfo" setup=[SetupMPCtests] begin +@testitem "ExplicitMPC moves & getinfo" setup=[SetupMPCtests] begin using .SetupMPCtests, ControlSystemsBase, LinearAlgebra model = LinModel(tf(5, [2, 1]), 3) mpc1 = ExplicitMPC(model, Nwt=[0], Hp=1000, Hc=1) @@ -905,7 +905,7 @@ end @test_throws ArgumentError OrthogonalCollocation(2) end -@testitem "NonLinMPC moves and getinfo (LinModel)" setup=[SetupMPCtests] begin +@testitem "NonLinMPC moves & getinfo (LinModel, SS)" setup=[SetupMPCtests] begin using .SetupMPCtests, ControlSystemsBase, LinearAlgebra using DifferentiationInterface import FiniteDiff @@ -948,14 +948,6 @@ end preparestate!(nmpc6, [0]) @test moveinput!(nmpc6, [0]) ≈ [0.0] atol=5e-2 - nmpc9 = NonLinMPC(linmodel, Nwt=[0], Hp=100, Hc=1, transcription=MultipleShooting()) - preparestate!(nmpc9, [10]) - u = moveinput!(nmpc9, [20]) - @test u ≈ [2] atol=5e-2 - info = getinfo(nmpc9) - @test info[:u] ≈ u - @test info[:Ŷ][end] ≈ 20 atol=5e-2 - # coverage of the branch with error termination status (with an infeasible problem): nmpc_infeas = NonLinMPC(linmodel, Hp=1, Hc=1, Cwt=Inf) nmpc_infeas = setconstraint!(nmpc_infeas, umin=[+1], umax=[-1]) @@ -969,7 +961,22 @@ end @test_nowarn ModelPredictiveControl.info2debugstr(info) end -@testitem "NonLinMPC moves and getinfo (NonLinModel)" setup=[SetupMPCtests] begin +@testitem "NonLinMPC moves & getinfo (LinModel, MS)" setup=[SetupMPCtests] begin + using .SetupMPCtests, ControlSystemsBase, LinearAlgebra + using DifferentiationInterface + import FiniteDiff + linmodel = setop!(LinModel(tf(5, [2000, 1]), 3000.0), yop=[10]) + + nmpc9 = NonLinMPC(linmodel, Nwt=[0], Hp=100, Hc=1, transcription=MultipleShooting()) + preparestate!(nmpc9, [10]) + u = moveinput!(nmpc9, [20]) + @test u ≈ [2] atol=5e-2 + info = getinfo(nmpc9) + @test info[:u] ≈ u + @test info[:Ŷ][end] ≈ 20 atol=5e-2 +end + +@testitem "NonLinMPC moves & getinfo (NonLinModel, SS)" setup=[SetupMPCtests] begin using .SetupMPCtests, ControlSystemsBase, LinearAlgebra using DifferentiationInterface import FiniteDiff @@ -977,10 +984,6 @@ end f = (x,u,d,model) -> model.A*x + model.Bu*u + model.Bd*d h = (x,d,model) -> model.C*x + model.Dd*d nonlinmodel = NonLinModel(f, h, 3000.0, 1, 2, 1, 1, solver=nothing, p=linmodel2) - - f! = (ẋ,x,u,_,_) -> ẋ .= -0.001x .+ u - h! = (y,x,_,_) -> y .= x - nonlinmodel_c = NonLinModel(f!, h!, 500, 1, 1, 1) nmpc1 = NonLinMPC(nonlinmodel, Nwt=[0], Hp=100, Hc=1) preparestate!(nmpc1, [0], [0]) @@ -1004,6 +1007,43 @@ end u = moveinput!(nmpc3, [0], d, R̂u=fill(12, nmpc3.Hp)) @test u ≈ [12] atol=5e-2 + nonlinmodel2 = NonLinModel{Float32}(f, h, 3000.0, 1, 2, 1, 1, solver=nothing, p=linmodel2) + nmpc7 = NonLinMPC(nonlinmodel2, Hp=10) + y = similar(nonlinmodel2.yop) + ModelPredictiveControl.h!(y, nonlinmodel2, Float32[0,0], Float32[0], nonlinmodel2.p) + preparestate!(nmpc7, [0], [0]) + @test moveinput!(nmpc7, [0], [0]) ≈ [0.0] atol=5e-2 + + nmpc10 = setconstraint!(NonLinMPC( + nonlinmodel, Nwt=[0], Hp=100, Hc=1, + gradient=AutoFiniteDiff(), + jacobian=AutoFiniteDiff(), + hessian=AutoFiniteDiff() + ), ymax=[100], ymin=[-100] + ) + preparestate!(nmpc10, [0], [0]) + u = moveinput!(nmpc10, [10], [0]) + @test u ≈ [2] atol=5e-2 + info = getinfo(nmpc10) + @test info[:u] ≈ u + @test info[:Ŷ][end] ≈ 10 atol=5e-2 + + nmpc11 = NonLinMPC(nonlinmodel, Hp=10, Hc=[1, 2, 3, 4], Nwt=[10]) + preparestate!(nmpc11, y, [0]) + moveinput!(nmpc11, [10], [0]) + ΔU_diff = diff(getinfo(nmpc11)[:U]) + @test ΔU_diff[[2, 4, 5, 7, 8, 9]] ≈ zeros(6) atol=1e-9 +end + +@testitem "NonLinMPC moves & getinfo (NonLinModel, MS)" setup=[SetupMPCtests] begin + using .SetupMPCtests, ControlSystemsBase, LinearAlgebra + using DifferentiationInterface + import FiniteDiff + linmodel2 = LinModel([tf(5, [2000, 1]) tf(7, [8000,1])], 3000.0, i_d=[2]) + f = (x,u,d,model) -> model.A*x + model.Bu*u + model.Bd*d + h = (x,d,model) -> model.C*x + model.Dd*d + nonlinmodel = NonLinModel(f, h, 3000.0, 1, 2, 1, 1, solver=nothing, p=linmodel2) + transcription = MultipleShooting() nmpc4 = NonLinMPC(nonlinmodel; Nwt=[0], Hp=100, Hc=1, transcription) preparestate!(nmpc4, [0], [0]) @@ -1022,6 +1062,20 @@ end info = getinfo(nmpc4t) @test info[:u] ≈ u @test info[:Ŷ][end] ≈ 10 atol=5e-2 +end + +@testitem "NonLinMPC moves & getinfo (NonLinModel, TC)" setup=[SetupMPCtests] begin + using .SetupMPCtests, ControlSystemsBase, LinearAlgebra + using DifferentiationInterface + import FiniteDiff + linmodel2 = LinModel([tf(5, [2000, 1]) tf(7, [8000,1])], 3000.0, i_d=[2]) + f = (x,u,d,model) -> model.A*x + model.Bu*u + model.Bd*d + h = (x,d,model) -> model.C*x + model.Dd*d + nonlinmodel = NonLinModel(f, h, 3000.0, 1, 2, 1, 1, solver=nothing, p=linmodel2) + + f! = (ẋ,x,u,_,_) -> ẋ .= -0.001x .+ u + h! = (y,x,_,_) -> y .= x + nonlinmodel_c = NonLinModel(f!, h!, 500, 1, 1, 1) transcription = TrapezoidalCollocation(0, f_threads=true, h_threads=true) nmpc5 = NonLinMPC(nonlinmodel_c; Nwt=[0], Hp=100, Hc=1, transcription) @@ -1034,6 +1088,20 @@ end preparestate!(nmpc5_1, [0.0]) u = moveinput!(nmpc5_1, [1/0.001]) @test u ≈ [1.0] atol=5e-2 +end + +@testitem "NonLinMPC moves & getinfo (NonLinModel, OC)" setup=[SetupMPCtests] begin + using .SetupMPCtests, ControlSystemsBase, LinearAlgebra + using DifferentiationInterface + import FiniteDiff + linmodel2 = LinModel([tf(5, [2000, 1]) tf(7, [8000,1])], 3000.0, i_d=[2]) + f = (x,u,d,model) -> model.A*x + model.Bu*u + model.Bd*d + h = (x,d,model) -> model.C*x + model.Dd*d + nonlinmodel = NonLinModel(f, h, 3000.0, 1, 2, 1, 1, solver=nothing, p=linmodel2) + + f! = (ẋ,x,u,_,_) -> ẋ .= -0.001x .+ u + h! = (y,x,_,_) -> y .= x + nonlinmodel_c = NonLinModel(f!, h!, 500, 1, 1, 1) transcription = OrthogonalCollocation(0, 4) nmpc6 = NonLinMPC(nonlinmodel_c; Nwt=[0], Hp=100, Hc=1, transcription) @@ -1049,33 +1117,6 @@ end setstate!(nmpc6_1, [5.0]) moveinput!(nmpc6_1) @test nmpc6_1.con.FS ≈ nmpc6_1.con.KS*nmpc6_1.estim.x̂0 # OC + IMC: special lin. eq. method - - nonlinmodel2 = NonLinModel{Float32}(f, h, 3000.0, 1, 2, 1, 1, solver=nothing, p=linmodel2) - nmpc7 = NonLinMPC(nonlinmodel2, Hp=10) - y = similar(nonlinmodel2.yop) - ModelPredictiveControl.h!(y, nonlinmodel2, Float32[0,0], Float32[0], nonlinmodel2.p) - preparestate!(nmpc7, [0], [0]) - @test moveinput!(nmpc7, [0], [0]) ≈ [0.0] atol=5e-2 - - nmpc10 = setconstraint!(NonLinMPC( - nonlinmodel, Nwt=[0], Hp=100, Hc=1, - gradient=AutoFiniteDiff(), - jacobian=AutoFiniteDiff(), - hessian=AutoFiniteDiff() - ), ymax=[100], ymin=[-100] - ) - preparestate!(nmpc10, [0], [0]) - u = moveinput!(nmpc10, [10], [0]) - @test u ≈ [2] atol=5e-2 - info = getinfo(nmpc10) - @test info[:u] ≈ u - @test info[:Ŷ][end] ≈ 10 atol=5e-2 - - nmpc11 = NonLinMPC(nonlinmodel, Hp=10, Hc=[1, 2, 3, 4], Nwt=[10]) - preparestate!(nmpc11, y, [0]) - moveinput!(nmpc11, [10], [0]) - ΔU_diff = diff(getinfo(nmpc11)[:U]) - @test ΔU_diff[[2, 4, 5, 7, 8, 9]] ≈ zeros(6) atol=1e-9 end @testitem "NonLinMPC step disturbance rejection" setup=[SetupMPCtests] begin diff --git a/test/5_test_extensions.jl b/test/5_test_extensions.jl index 1384c0bd5..ee4ec83be 100644 --- a/test/5_test_extensions.jl +++ b/test/5_test_extensions.jl @@ -1,3 +1,4 @@ +# TODO: add `skip=(Sys.ARCH==:x86)` because bug in DAQP.jl, and activate Linux x86 CI job in juliaci.yml @testitem "LinearMPCext general" setup=[SetupMPCtests] begin using .SetupMPCtests, ControlSystemsBase, LinearAlgebra, JuMP, DAQP import LinearMPC diff --git a/test/7_test_aqua.jl b/test/7_test_aqua.jl index 2eca61e33..fb907c80b 100644 --- a/test/7_test_aqua.jl +++ b/test/7_test_aqua.jl @@ -1,9 +1,50 @@ -@testitem "Aqua" begin - using Aqua - # All the functions defined inside `ModelPredictiveControl.get_nonlinobj_op` with the - # `Vararg`s have unbound type parameters. This is necessary for the splatting syntax of - # `JuMP.@operator`, and JuMP will never call these functions with 0 argument, so - # defining zero-argument methods would be useless, so I disable this check here. - unbound_args = false - Aqua.test_all(ModelPredictiveControl; unbound_args) +@testitem "Aqua ambiguities" begin + using Aqua + Aqua.test_ambiguities(ModelPredictiveControl) +end + +### All the functions defined inside `ModelPredictiveControl.get_nonlinobj_op` with the +### `Vararg`s have unbound type parameters. This is necessary for the splatting syntax of +### `JuMP.@operator`, and JuMP will never call these functions with 0 argument, so +### defining zero-argument methods would be useless, so I disable this check here. +# @testitem "Aqua unbound args" begin +# using Aqua +# Aqua.test_unbound_args(ModelPredictiveControl) +# end + +@testitem "Aqua undefined exports" begin + using Aqua + Aqua.test_undefined_exports(ModelPredictiveControl) +end + +@testitem "Aqua project extras" begin + using Aqua + Aqua.test_project_extras(ModelPredictiveControl) +end + +@testitem "Aqua stale deps" begin + using Aqua + Aqua.test_stale_deps(ModelPredictiveControl) +end + +@testitem "Aqua deps compat" begin + using Aqua + Aqua.test_deps_compat(ModelPredictiveControl) +end + +@testitem "Aqua piracies" begin + using Aqua + Aqua.test_piracies(ModelPredictiveControl) +end + +### This package does not use a __init__ function, and will very likely never use one, +### so the persistent task test is useless (and slow, because of the long precompilation) +# @testitem "Aqua persistent tasks" begin +# using Aqua +# Aqua.test_persistent_tasks(ModelPredictiveControl) +# end + +@testitem "Aqua undocumented names" begin + using Aqua + Aqua.test_undocumented_names(ModelPredictiveControl) end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 5a707242e..ca1e49c84 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,8 +1,10 @@ using ModelPredictiveControl using Test, TestItems, TestItemRunner -@run_package_tests(verbose=true) +@run_package_tests +# Not needed for TestItems discovery, but including the files means `Pkg.test` parses them +# and catches syntax errors early: include("0_test_module.jl") include("1_test_sim_model.jl") include("2_test_state_estim.jl")