Skip to content

Commit 7bab236

Browse files
authored
add lqi and lqi controller (#127)
* add lqi and lqi controller * args... * up tests
1 parent 2aa4cb6 commit 7bab236

File tree

6 files changed

+192
-9
lines changed

6 files changed

+192
-9
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: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,3 +603,141 @@ 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, args...;
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; ϵ=ϵ, neg=true)
668+
lqr(sys_aug, Q1, Q2, args...)
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, args...)
703+
L = named_ss(ss(lqi(G, Q1, Q2, args...)), 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+
nr = size(L, 2) - nx
710+
te = G.timeevol
711+
nr = ny
712+
aug_state_inds = 1:nx
713+
aug_integrator_inds = nx+1:size(L, 2)
714+
715+
refs = Symbol.(string.(G.y) .* "_r")
716+
feedback_y = Symbol.(string.(G.y) .* "_fb")
717+
718+
add_feedback = named_ss(ss([I(nr) -I(nr)], te), u=[refs; feedback_y], y=:e)
719+
int0 = iscontinuous(G) ? ss(1/s) : ss(c2d(1/s, G.Ts))
720+
integrator = named_ss(I(nr) .* int0, u=:e, y=:ie, x=:x_int)
721+
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
722+
723+
external_inputs = [
724+
refs; G.y;
725+
]
726+
external_outputs = [
727+
L.y;
728+
]
729+
730+
observer_input_inds = 1:nu
731+
observer_output_inds = (1:ny) .+ nu
732+
733+
connections = [
734+
add_feedback.y .=> integrator.u;
735+
integrator.y .=> L.u[aug_integrator_inds];
736+
obs.y .=> L.u[aug_state_inds];
737+
L.y .=> obs.u[observer_input_inds];
738+
unit_gain.y .=> obs.u[observer_output_inds];
739+
unit_gain.y .=> add_feedback.u[nr+1:end]
740+
]
741+
# Negate obs to output -x̂
742+
connect([add_feedback, integrator, L, -obs, unit_gain], connections; external_inputs, external_outputs)
743+
end

src/model_augmentation.jl

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -166,22 +166,32 @@ Gd = add_output_integrator(add_output_differentiator(G), 1)
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)
169+
function add_output_integrator(sys::AbstractStateSpace{<: Discrete}, ind=1; ϵ=0, neg=false)
170170
int = tf(1.0*sys.Ts, [1, -(1-ϵ)], sys.Ts)
171+
neg && (int = int*(-1))
171172
𝟏 = tf(1.0,sys.Ts)
172173
𝟎 = tf(0.0,sys.Ts)
173174
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
175+
M = [M; permutedims([i ind ? int : 𝟎 for i = 1:sys.ny])]
176+
nx = sys.nx
177+
nr = length(ind)
178+
p = [(1:nx).+nr; 1:nr]
179+
T = (1:nx+nr) .== p'
180+
similarity_transform(tf(M)*sys, T)
176181
end
177182

178-
function add_output_integrator(sys::AbstractStateSpace{Continuous}, ind=1; ϵ=0)
183+
function add_output_integrator(sys::AbstractStateSpace{Continuous}, ind=1; ϵ=0, neg=false)
179184
int = tf(1.0, [1, ϵ])
185+
neg && (int = int*(-1))
180186
𝟏 = tf(1.0)
181187
𝟎 = tf(0.0)
182188
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
189+
M = [M; permutedims([i ind ? int : 𝟎 for i = 1:sys.ny])]
190+
nx = sys.nx
191+
nr = length(ind)
192+
p = [(1:nx).+nr; 1:nr]
193+
T = (1:nx+nr) .== p'
194+
similarity_transform(tf(M)*sys, T)
185195
end
186196

187197
"""

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_augmentation.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,15 @@ Gd2 = [tf(1,1); tf([1, -1], [1], 1)]*tf(G)
9696
## Int
9797
Gd = add_output_integrator(G)
9898
Gd2 = [tf(1,1); tf(1, [1, -1], 1)]*G
99-
@test Gd Gd2
99+
@test tf(Gd) tf(Gd2)
100100
@test sminreal(Gd[1,1]) == G # Exact equivalence should hold here
101101
@test Gd.nx == 4 # To guard agains changes in realization of tf as ss
102102

103103

104104
Gc = ssrand(1,1,3, proper=true)
105105
Gdc = add_output_integrator(Gc)
106106
Gd2c = [tf(1); tf(1, [1, 0])]*Gc
107-
@test Gdc Gd2c
107+
@test tf(Gdc) tf(Gd2c)
108108
@test sminreal(Gdc[1,1]) == Gc # Exact equivalence should hold here
109109
@test Gdc.nx == 4 # To guard agains changes in realization of tf as ss
110110

test/test_lqi.jl

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

0 commit comments

Comments
 (0)