Skip to content

Commit 2c1df78

Browse files
committed
add lqi and lqi controller
1 parent 2aa4cb6 commit 2c1df78

File tree

5 files changed

+220
-18
lines changed

5 files changed

+220
-18
lines changed

src/RobustAndOptimalControl.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export h2synthesize
5656
include("h2_design.jl")
5757

5858
export LQGProblem, sensitivity, input_sensitivity, output_sensitivity, comp_sensitivity, input_comp_sensitivity, output_comp_sensitivity, feedback_control, ff_controller, extended_controller, closedloop, static_gain_compensation, G_PS, G_CS, gangoffour
59-
export lqr3, dare3
59+
export lqr3, dare3, lqi, lqi_controller
6060
include("lqg.jl")
6161

6262
export NamedStateSpace, named_ss, expand_symbol, measure, connect, sumblock, splitter

src/lqg.jl

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,3 +603,139 @@ end
603603
Base.@deprecate gangoffour2(P, C) gangoffour(P, C)
604604
Base.@deprecate gangoffourplot2(P, C) gangoffourplot(P, C)
605605

606+
"""
607+
lqi(sys::AbstractStateSpace, Q1::AbstractMatrix, Q2::AbstractMatrix, Q3::AbstractMatrix;
608+
integrator_outputs=1:sys.ny, ϵ=0)
609+
610+
Calculate the feedback gain for the LQI (Linear-Quadratic-Integral) cost function:
611+
```math
612+
x_a^{T} Q_1 x_a + u^{T} Q_2 u
613+
```
614+
where `x_a = [x; e_i]` is the augmented state vector, `e_i` is the integral of the tracking error for the specified outputs.
615+
616+
The system is augmented with integrators on the tracking error for the outputs specified in `integrator_outputs`.
617+
The augmented system has the form:
618+
```math
619+
\\begin{bmatrix} \\dot{x} \\\\ \\dot{x_i} \\end{bmatrix} =
620+
\\begin{bmatrix} A & 0 \\\\ -C & 0 \\end{bmatrix}
621+
\\begin{bmatrix} x \\\\ x_i \\end{bmatrix} +
622+
\\begin{bmatrix} B \\\\ 0 \\end{bmatrix} u
623+
```
624+
625+
where the integrator dynamics are `ẋᵢ = r-Cx` (tracking error), ensuring that the controller has integral action
626+
to eliminate steady-state errors.
627+
628+
# Arguments:
629+
- `sys`: The system to control
630+
- `Q1`: Augmented state cost matrix (must be positive semi-definite)
631+
- `Q2`: Control cost matrix (must be positive definite)
632+
- `integrator_outputs`: Vector of output indices to add integrators for (default: all outputs)
633+
- `ϵ`: Small positive number to move integrator poles slightly into the stable region (default: 0)
634+
635+
# Returns:
636+
- `L`: The optimal feedback gain matrix for the augmented system
637+
638+
# Example:
639+
```julia
640+
using ControlSystemsBase, RobustAndOptimalControl
641+
642+
# Create a simple double integrator system
643+
A = [0 1; 0 0]
644+
B = [0; 1]
645+
C = [1 0]
646+
sys = ss(A, B, C, 0)
647+
648+
# Design LQI controller
649+
Q1 = diagm([1, 1, 10]) # State cost + error-integral cost
650+
Q2 = [1;;] # Control cost
651+
652+
L = lqi(sys, Q1, Q2)
653+
```
654+
655+
See also [`lqi_controller`](@ref).
656+
"""
657+
function lqi(sys::AbstractStateSpace, Q1::AbstractMatrix, Q2::AbstractMatrix;
658+
integrator_outputs=1:sys.ny, ϵ=0)
659+
660+
# Validate inputs
661+
all(1 .≤ integrator_outputs .≤ sys.ny) || throw(ArgumentError("All integrator_outputs must be valid output indices"))
662+
length(integrator_outputs) == 0 && throw(ArgumentError("At least one output must have an integrator"))
663+
664+
size(Q1, 1) == sys.nx+length(integrator_outputs) || throw(ArgumentError("Q1 must have size $(sys.nx+length(integrator_outputs))×$(sys.nx+length(integrator_outputs))"))
665+
size(Q2, 1) == sys.nu || throw(ArgumentError("Q2 must have size $(sys.nu)×$(sys.nu)"))
666+
667+
sys_aug = add_output_integrator(sys, integrator_outputs; ϵ=ϵ)
668+
lqr(sys_aug, Q1, Q2)
669+
end
670+
671+
672+
"""
673+
lqi_controller(G, obs, Q1, Q2)
674+
675+
Return an LQI controller with reference and measurement inputs `[r; y]` designed for the LQI problem where the plant state `x` is augmented with output-error integrators to form the augmented state `x_a = [x; e_i]`.
676+
677+
- `Q1`: Penalty matrix for the augmented state (must be positive semi-definite)
678+
- `Q2`: Penalty matrix for the control input (must be positive definite)
679+
- `obs`: An observer for `G` constructed using `observer_predictor(G, ..., output_state=true)`
680+
681+
Example:
682+
```
683+
G = ss([0 32;-31.25 -0.4],[0; 2.236068],[0.0698771 0],0)
684+
Q = diagm([0,5])
685+
R = [1.0;;]
686+
K = kalman(G,Q,R)
687+
obs = observer_predictor(G, K; output_state=true)
688+
689+
Q1 = diagm([0.488,0,100])
690+
Q2 = [1/100;;]
691+
L = lqi(G,Q1,Q2)
692+
C = lqi_controller(G, obs, Q1, Q2)
693+
694+
Gn = named_ss(G)
695+
H = feedback(C, Gn, w1 = :y_plant_r, z2=Gn.y, u1=:y_plant, pos_feedback=true)
696+
plot(
697+
plot(step(H, 50)),
698+
gangoffourplot(G, -C[:, 2])
699+
)
700+
```
701+
"""
702+
function lqi_controller(G, obs, Q1, Q2)
703+
L = -named_ss(ss(lqi(G, Q1, Q2)), name="L", x=:x_L, y=:y_L, u=:u_L)
704+
G isa NamedStateSpace || (G = named_ss(G, name="plant", x=:x_plant, y=:y_plant, u=:u_plant))
705+
obs isa NamedStateSpace || (obs = named_ss(obs, name="observer", x=:x_observer, y=:y_observer, u=:u_observer))
706+
707+
s = tf('s')
708+
(; nx, nu, ny) = G
709+
te = G.timeevol
710+
nr = ny
711+
aug_state_inds = 1:nx
712+
aug_integrator_inds = nx+1:size(L, 2)
713+
714+
refs = Symbol.(string.(G.y) .* "_r")
715+
feedback_y = Symbol.(string.(G.y) .* "_fb")
716+
717+
add_feedback = named_ss(ss([I(nr) -I(nr)], te), u=[refs; feedback_y], y=:e)
718+
int0 = iscontinuous(G) ? ss(1/s) : ss(c2d(1/s, G.Ts))
719+
integrator = named_ss(I(nr) .* int0, u=:e, y=:ie, x=:x_int)
720+
unit_gain = named_ss(ss(I(nr), te), u=G.y) # The plant output is not available as input in any of the components, so we introduce this unit gain to introduce a component with a plant-input input. This is then fed into multiple places
721+
722+
external_inputs = [
723+
refs; G.y;
724+
]
725+
external_outputs = [
726+
L.y;
727+
]
728+
729+
observer_input_inds = 1:nu
730+
observer_output_inds = (1:ny) .+ nu
731+
732+
connections = [
733+
add_feedback.y .=> integrator.u;
734+
integrator.y .=> L.u[aug_integrator_inds];
735+
obs.y .=> L.u[aug_state_inds];
736+
L.y .=> obs.u[observer_input_inds];
737+
unit_gain.y .=> obs.u[observer_output_inds];
738+
unit_gain.y .=> add_feedback.u[nr+1:end]
739+
]
740+
connect([add_feedback, integrator, L, obs, unit_gain], connections; external_inputs, external_outputs)
741+
end

src/model_augmentation.jl

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -155,35 +155,69 @@ function ControlSystemsBase.tf(M::AbstractArray{TransferFunction{TE,ControlSyste
155155
end
156156

157157
"""
158-
add_output_integrator(sys::StateSpace{<:Discrete}, ind = 1; ϵ = 0)
158+
add_output_integrator(sys::StateSpace{<:Discrete}, inds::AbstractVector{<:Integer}=[1]; ϵ=0)
159159
160-
Augment the output of `sys` with the integral of output at index `ind`, i.e.,
161-
`y_aug = [y; ∫y[ind]]`
160+
Augment the output of `sys` with the integral of output at indices `inds`, i.e.,
161+
`y_aug = [y; ∫y[inds]]`
162162
To add both an integrator and a differentiator to a SISO system, use
163163
```julia
164164
Gd = add_output_integrator(add_output_differentiator(G), 1)
165165
```
166166
167167
Note: numerical integration is subject to numerical drift. If the output of the system corresponds to, e.g., a velocity reference and the integral to position reference, consider methods for mitigating this drift.
168168
"""
169-
function add_output_integrator(sys::AbstractStateSpace{<: Discrete}, ind=1; ϵ=0)
170-
int = tf(1.0*sys.Ts, [1, -(1-ϵ)], sys.Ts)
171-
𝟏 = tf(1.0,sys.Ts)
172-
𝟎 = tf(0.0,sys.Ts)
173-
M = [i==j ? 𝟏 : 𝟎 for i = 1:sys.ny, j = 1:sys.ny]
174-
M = [M; permutedims([i==ind ? int : 𝟎 for i = 1:sys.ny])]
175-
tf(M)*sys
169+
function add_output_integrator(sys::AbstractStateSpace{<: Discrete}, inds::AbstractVector{<:Integer}=[1]; ϵ=0)
170+
all(1 .≤ inds .≤ sys.ny) || throw(ArgumentError("All indices must be valid output indices"))
171+
length(inds) == 0 && return sys
172+
173+
A,B,C,D = ControlSystemsBase.ssdata(sys)
174+
T = eltype(A)
175+
nx,nu,ny = sys.nx,sys.nu,sys.ny
176+
n_int = length(inds)
177+
178+
# Create the augmented system matrices
179+
# The integrator dynamics are: ẋᵢ = -C[ind,:]x (tracking error)
180+
Ae = [A zeros(T, nx, n_int);
181+
-C[inds, :] zeros(T, n_int, n_int)]
182+
183+
Be = [B; zeros(T, n_int, nu)]
184+
185+
# Output matrix: [y; ∫y[inds]]
186+
Ce = [C zeros(T, ny, n_int);
187+
zeros(T, n_int, nx) I(n_int)]
188+
189+
De = [D; zeros(T, n_int, nu)]
190+
191+
ss(Ae, Be, Ce, De, sys.timeevol)
176192
end
177193

178-
function add_output_integrator(sys::AbstractStateSpace{Continuous}, ind=1; ϵ=0)
179-
int = tf(1.0, [1, ϵ])
180-
𝟏 = tf(1.0)
181-
𝟎 = tf(0.0)
182-
M = [i==j ? 𝟏 : 𝟎 for i = 1:sys.ny, j = 1:sys.ny]
183-
M = [M; permutedims([i==ind ? int : 𝟎 for i = 1:sys.ny])]
184-
tf(M)*sys
194+
function add_output_integrator(sys::AbstractStateSpace{Continuous}, inds::AbstractVector{<:Integer}=[1]; ϵ=0)
195+
all(1 .≤ inds .≤ sys.ny) || throw(ArgumentError("All indices must be valid output indices"))
196+
length(inds) == 0 && return sys
197+
198+
A,B,C,D = ControlSystemsBase.ssdata(sys)
199+
T = eltype(A)
200+
nx,nu,ny = sys.nx,sys.nu,sys.ny
201+
n_int = length(inds)
202+
203+
# Create the augmented system matrices
204+
# The integrator dynamics are: ẋᵢ = -C[ind,:]x (tracking error)
205+
Ae = [A zeros(T, nx, n_int);
206+
-C[inds, :] zeros(T, n_int, n_int)]
207+
208+
Be = [B; zeros(T, n_int, nu)]
209+
210+
# Output matrix: [y; ∫y[inds]]
211+
Ce = [C zeros(T, ny, n_int);
212+
zeros(T, n_int, nx) I(n_int)]
213+
214+
De = [D; zeros(T, n_int, nu)]
215+
216+
ss(Ae, Be, Ce, De, sys.timeevol)
185217
end
186218

219+
add_output_integrator(sys::AbstractStateSpace, ind::Integer; ϵ=0) = add_output_integrator(sys, [ind]; ϵ=ϵ)
220+
187221
"""
188222
add_input_integrator(sys::StateSpace, ui = 1, ϵ = 0)
189223

test/runtests.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ using Test
6565
include("test_lqg.jl")
6666
end
6767

68+
@testset "LQI" begin
69+
@info "Testing LQI"
70+
include("test_lqi.jl")
71+
end
72+
6873
@testset "Named systems" begin
6974
@info "Testing Named systems"
7075
include("test_named_systems2.jl")

test/test_lqi.jl

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using Test
2+
using ControlSystemsBase
3+
using RobustAndOptimalControl
4+
using LinearAlgebra
5+
6+
7+
G = ss([0 32;-31.25 -0.4],[0; 2.236068],[0.0698771 0],0)
8+
Q = diagm([0,5])
9+
R = [1.0;;]
10+
K = kalman(G,Q,R)
11+
obs = observer_predictor(G,K; output_state=true)
12+
13+
Q1 = diagm([0.488,0,100])
14+
Q2 = [1/100;;]
15+
L = lqi(G,Q1,Q2)
16+
17+
C0 = lqi_controller(G, obs, Q1, Q2)
18+
19+
@test C0.nu == 2
20+
@test 0 poles(C0)
21+
22+
Gn = named_ss(G)
23+
H = feedback(C0, Gn, w1 = :y_plant_r, z2=Gn.y, u1=:y_plant, pos_feedback=true)
24+
25+
@test dcgain(H)[2] 1
26+
27+
plot(step(H, 50))

0 commit comments

Comments
 (0)