|
| 1 | +#= |
| 2 | +This file demonstrates |
| 3 | +- Adding integral action to an LQG controller by means of state augmentation |
| 4 | +- Conversion of the LQG controller into an MPC contorller with added constraints |
| 5 | +=# |
| 6 | +using RobustAndOptimalControl, ControlSystemsBase, Plots, LinearAlgebra |
| 7 | +Ts = 1 # Sample time |
| 8 | +G = c2d(ss(tf(1, [10, 1])), Ts) # Process model |
| 9 | + |
| 10 | +nx = G.nx |
| 11 | +nu = G.nu |
| 12 | +ny = G.ny |
| 13 | +x0 = zeros(G.nx) # Initial condition |
| 14 | + |
| 15 | +Q1 = 100diagm(ones(G.nx)) # state cost matrix |
| 16 | +Q2 = 0.01diagm(ones(nu)) # control cost matrix |
| 17 | + |
| 18 | +R1 = 0.001I(nx) # State noise covariance |
| 19 | +R2 = I(ny) # measurement noise covariance |
| 20 | +prob = LQGProblem(G, Q1, Q2, R1, R2) |
| 21 | + |
| 22 | +disturbance = (x, t) -> t * Ts ≥ 10 # This is our load disturbance, a step at ``t = 10`` |
| 23 | +Gcl = G_PS(prob) # This forms the transfer function from load disturbance to output |
| 24 | +res = lsim(Gcl, disturbance, 100) |
| 25 | +plot(res) |
| 26 | + |
| 27 | +Gd1 = add_low_frequency_disturbance(G, ϵ = 1e-6, measurement=false) |
| 28 | +Gd2 = add_low_frequency_disturbance(G, ϵ = 1e-6, measurement=true) # The ϵ moves the integrator pole slightly into the stable region |
| 29 | +plots = map([Gd1, Gd2]) do Gd |
| 30 | + nx = Gd.nx |
| 31 | + |
| 32 | + C = Gd.C |
| 33 | + Q1 = 100C'diagm(ones(G.nx)) * C # state cost matrix |
| 34 | + x0 = zeros(nx) |
| 35 | + |
| 36 | + R1 = diagm([0.001, 1]) |
| 37 | + R2 = I(ny) |
| 38 | + prob = LQGProblem(Gd, Q1, Q2, R1, R2) |
| 39 | + Gcl = [G_PS(prob); -comp_sensitivity(prob)] # -comp_sensitivity(prob) is the same as the transfer function from load disturbance to control signal |
| 40 | + res = lsim(Gcl, disturbance, 100) |
| 41 | + f1 = plot(res, ylabel=["y" "u"]); ylims!((-0.05, 0.3), sp = 1) |
| 42 | + |
| 43 | + w = exp10.(LinRange(-3, log10(pi / Ts), 200)) |
| 44 | + f2 = gangoffourplot(prob, w, lab = "", legend = :bottomright) |
| 45 | + |
| 46 | + R1 = diagm([0.001, 0.2]) # Reduce the noise on the integrator state from 1 to 0.2 |
| 47 | + R2 = I(ny) |
| 48 | + prob = LQGProblem(Gd, Q1, Q2, R1, R2) |
| 49 | + |
| 50 | + Gcl = [G_PS(prob); -comp_sensitivity(prob)] |
| 51 | + res = lsim(Gcl, disturbance, 100) |
| 52 | + f3 = plot(res, ylabel=["y" "u"]); ylims!((-0.05, 0.3), sp = 1) |
| 53 | + f4 = gangoffourplot(prob, w, lab = "", legend = :bottomright) |
| 54 | + |
| 55 | + plot(f1, f2, f3, f4, titlefontsize=10) |
| 56 | +end |
| 57 | + |
| 58 | +plot(plots..., size=(1200,1000)) |
| 59 | + |
| 60 | + |
| 61 | + |
| 62 | +# ============================================================================== |
| 63 | +## LinearMPC |
| 64 | +# The example below illustrate how we can convert the LQGProblem into an MPC contorller by loading LinearMPC.jl |
| 65 | +# We then perform a rather low-level simulation with a manual loop, where we form the observer `obs` and step the plant |
| 66 | +# ============================================================================== |
| 67 | + |
| 68 | +using LinearMPC |
| 69 | + |
| 70 | +Gd = add_low_frequency_disturbance(G, ϵ = 1e-6, measurement=false) |
| 71 | + |
| 72 | +C = Gd.C |
| 73 | +Q1 = 100diagm([1.0]) # output cost matrix |
| 74 | +R1 = diagm([0.001, 1]) # Dynamics noise covariance |
| 75 | +R2 = I(ny) # Measurement noise covariance |
| 76 | +Gde = ExtendedStateSpace(Gd, B1=I) # Since B1=I, R1 has size determined by state dimension, but C1=C, so Q1 has size determined by the output dimension |
| 77 | +prob = LQGProblem(Gde, Q1, Q2, R1, R2) |
| 78 | +obs = observer_predictor(prob, direct=false) # If a predictor is used, the observer update should be carried out in the end of the loop, if we use the filter below, we should instead perform the observer update in the beginning of the loop directly after obtaining the new measurement but before computing a new control signal. |
| 79 | +obs = observer_filter(prob) |
| 80 | + |
| 81 | +mpc = LinearMPC.MPC(prob; N=20, umin=[-3], umax=[3]) |
| 82 | +x = zeros(G.nx) # True plant state |
| 83 | +xh = zeros(Gd.nx) # Observer state |
| 84 | +X = [x[]] # For storage |
| 85 | +U = Float64[] |
| 86 | +u_mpc = [0.0] |
| 87 | +for i = 1:50 |
| 88 | + global x, xh, u_mpc |
| 89 | + y = G.C*x # Compute the true measurement output |
| 90 | + xh = obs.A * xh + obs.B*[u_mpc; y] # Predict one step with the observer, u here is the control signal from the previous iteration, if using the predictor, use u from the current iteration and perform the observer update in the end of the loop instead |
| 91 | + u_disturbance = i * Ts ≥ 10 ? 1.0 : 0.0 |
| 92 | + r = [1.0] # output reference |
| 93 | + u_mpc = LinearMPC.compute_control(mpc, xh; r) # Call MPC optimizer with estimated state |
| 94 | + u_tot = u_mpc .+ u_disturbance # Total input is control signal + disturbance |
| 95 | + x = G.A*x + G.B*u_tot # Advance the true plant state |
| 96 | + push!(X, x[]) # Store data for plotting |
| 97 | + push!(U, u_mpc[]) |
| 98 | +end |
| 99 | + |
| 100 | +using Test |
| 101 | +@test (G.C*X[end])[] ≈ 1 rtol=1e-4 |
| 102 | + |
| 103 | +plot(X*G.C', layout=2, sp=1, label="\$y\$") |
| 104 | +plot!(U, sp=2, label="\$u\$") |
| 105 | +hline!([1.0 3.0], linestyle=:dash, color=:black, label=["Reference" "\$u_{max}\$"], sp=[1 2]) |
| 106 | + |
0 commit comments