From ed39f46d31348b0397abef0d5beb2c352d3892b0 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Sat, 16 May 2026 07:41:15 +0200 Subject: [PATCH 1/2] Docs: integral action removes tracking error under model mismatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Append a section to docs/src/lqg_disturbance.md that takes the LQI controller designed earlier on the page and applies it to a plant whose time constant is 20% longer than the design model. Using the named-symbol `feedback` interface (`w1=:y_plant_r`, `u1=:y_plant`, `z2=G_pert_n.y`, `pos_feedback=true`) avoids splitting the controller into reference and measurement columns by hand. The step response still settles on 1 — asserted with `@test res.y[end] ≈ 1 atol=1e-3` — making concrete the caveat at the end of the preceding 2-DOF section. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/src/lqg_disturbance.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/src/lqg_disturbance.md b/docs/src/lqg_disturbance.md index bc176a6d..45924232 100644 --- a/docs/src/lqg_disturbance.md +++ b/docs/src/lqg_disturbance.md @@ -182,3 +182,32 @@ plot(res, ylabel="y") The step response settles on `1`, confirming that the pre-compensated 2-DOF controller tracks unit references at DC. For plants with an integrator (e.g., a cart-position channel from a velocity-controlled actuator) `dcgain(cl_xr_to_y)` is already `1` and no compensation is needed. Note, without the integral action in the controller, any error in the DC gain of the model would still lead to a steady-state error in the reference-signal response. + + +## Integral action removes the steady-state error under model mismatch + +To make the point of the previous paragraph concrete, we apply the LQI controller `C` designed earlier (line that builds `C = lqi_controller(G, obs, Q1, Q2)`) to a *perturbed* plant whose time constant is 20 % longer than the design model: + +```@example LQG_DIST +G_pert = c2d(ss(tf(1, [12, 1])), Ts) # 20 % slower than the design model G +G_pert_n = named_ss(G_pert) + +# Close the reference-tracking loop directly on the named systems — no need to +# split C into reference and measurement columns by hand: +# w1 = :y_plant_r → the reference is the external input +# u1 = :y_plant → wire C's feedback input to the plant output +# z2 = G_pert_n.y → keep only the plant output (suppress C's u as an output) +# pos_feedback = true → lqi_controller already bakes in the negative sign of the feedback path +Gcl_pert = feedback(C, G_pert_n; + w1 = :y_plant_r, + z1 = Symbol[], + z2 = G_pert_n.y, + u1 = :y_plant, + pos_feedback = true) + +res = lsim(Gcl_pert, (x, t) -> 1.0, 60) +@test res.y[end] ≈ 1 atol=1e-3 +plot(res, ylabel = "y") +``` + +The plant output still settles on `1` even though the controller was designed for a different plant — the error integrator absorbs the model mismatch. A non-integrating 2-DOF design (like the `extended_controller` above with its `gain_comp` tuned on the nominal plant) would settle off `1` here, in proportion to the gain mismatch between the design model and the real plant. From e35b578b0f864e09aa7c3a7b8f26dea76e3e7433 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Sat, 16 May 2026 09:30:15 +0200 Subject: [PATCH 2/2] up demo --- docs/src/lqg_disturbance.md | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/docs/src/lqg_disturbance.md b/docs/src/lqg_disturbance.md index 45924232..09fb5d11 100644 --- a/docs/src/lqg_disturbance.md +++ b/docs/src/lqg_disturbance.md @@ -186,28 +186,46 @@ Note, without the integral action in the controller, any error in the DC gain of ## Integral action removes the steady-state error under model mismatch -To make the point of the previous paragraph concrete, we apply the LQI controller `C` designed earlier (line that builds `C = lqi_controller(G, obs, Q1, Q2)`) to a *perturbed* plant whose time constant is 20 % longer than the design model: +To make the point of the previous paragraph concrete, we apply both controllers — the integrating `C` from [`lqi_controller`](@ref) and the non-integrating `Ce` from [`extended_controller`](@ref), each designed earlier on the nominal plant `G` — to a *perturbed* plant whose gain is 20 % larger than the design model: ```@example LQG_DIST -G_pert = c2d(ss(tf(1, [12, 1])), Ts) # 20 % slower than the design model G +G_pert = c2d(ss(tf(1.2, [10, 1])), Ts) # 20 % larger gain than the design model G G_pert_n = named_ss(G_pert) -# Close the reference-tracking loop directly on the named systems — no need to -# split C into reference and measurement columns by hand: +# Close the reference-tracking loop directly on the named systems # w1 = :y_plant_r → the reference is the external input # u1 = :y_plant → wire C's feedback input to the plant output # z2 = G_pert_n.y → keep only the plant output (suppress C's u as an output) # pos_feedback = true → lqi_controller already bakes in the negative sign of the feedback path -Gcl_pert = feedback(C, G_pert_n; +Gcl_pert_int = feedback(C, G_pert_n; w1 = :y_plant_r, z1 = Symbol[], z2 = G_pert_n.y, u1 = :y_plant, pos_feedback = true) -res = lsim(Gcl_pert, (x, t) -> 1.0, 60) -@test res.y[end] ≈ 1 atol=1e-3 -plot(res, ylabel = "y") +# Non-integrating closed loop with `Ce` from `extended_controller`. The wiring mirrors what +# `extended_controller(prob, z=[1])` does internally to build `cl_xr_to_y` — plant first, +# default `pos_feedback`, so the same `gain_comp` we computed earlier still applies. +# w1 = Symbol[] → plant has no external input (control comes from the controller) +# w2 = :y_plant_r → state reference is the external input +# u2 = :y_plant → Ce's feedback input is wired to the plant output +# z1 = G_pert_n.y → keep the plant output +# z2 = Symbol[] → suppress Ce's u as an output +Ce_named = named_ss(ss(Ce), y = C.y, u = C.u) +Gcl_pert_noint = feedback(G_pert_n, Ce_named; + w1 = Symbol[], + w2 = :y_plant_r, + u2 = :y_plant, + z1 = G_pert_n.y, + z2 = Symbol[]) + +res_int = lsim(Gcl_pert_int, (x,t)->min(t/10, 1), 60) +res_noint = lsim(gain_comp * Gcl_pert_noint, (x,t)->min(t/10, 1), 60) +@test res_int.y[end] ≈ 1 atol=1e-3 +@test res_noint.y[end] ≈ 1.2 atol=1e-2 # 20 % gain mismatch leaks straight through +plot(res_int, lab = "lqi_controller") +plot!(res_noint, ylabel = "y", lab = "extended_controller") ``` -The plant output still settles on `1` even though the controller was designed for a different plant — the error integrator absorbs the model mismatch. A non-integrating 2-DOF design (like the `extended_controller` above with its `gain_comp` tuned on the nominal plant) would settle off `1` here, in proportion to the gain mismatch between the design model and the real plant. +The integrating controller still settles on `1` despite the controller having been designed for a different plant — the error integrator absorbs the model mismatch. The non-integrating 2-DOF controller settles on `1.2`, in proportion to the gain mismatch between the design model and the real plant.