Skip to content

Commit 5982cc3

Browse files
authored
Merge pull request #951 from JuliaControl/pid2dof
add 2DOF pid constructor
2 parents ceb9ff9 + 048908f commit 5982cc3

File tree

3 files changed

+112
-12
lines changed

3 files changed

+112
-12
lines changed

docs/src/examples/example.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ See also the following tutorial video on LQR and LQG design
6565
```
6666

6767
## PID design functions
68-
A basic PID controller can be constructed using the constructor [`pid`](@ref).
68+
A basic PID controller can be constructed using the constructors [`pid`](@ref), [`pid_2dof`](@ref).
6969
In ControlSystems.jl, we often refer to three different formulations of the PID controller, which are defined as
7070

7171
* Standard form: ``K_p(1 + \frac{1}{T_i s} + T_ds)``

lib/ControlSystemsBase/src/pid_design.jl

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
1-
export pid, pid_tf, pid_ss, pidplots, leadlink, laglink, leadlinkat, leadlinkcurve, stabregionPID, loopshapingPI, placePI, loopshapingPID
1+
export pid, pid_tf, pid_ss, pid_2dof, pid_ss_2dof, pidplots, leadlink, laglink, leadlinkat, leadlinkcurve, stabregionPID, loopshapingPI, placePI, loopshapingPID
22

33
"""
44
C = pid(param_p, param_i, [param_d]; form=:standard, state_space=false, [Tf], [Ts])
55
66
Calculates and returns a PID controller.
77
8-
The `form` can be chosen as one of the following
8+
The `form` can be chosen as one of the following (determines how the arguments `param_p, param_i, param_d` are interpreted)
99
* `:standard` - `Kp*(1 + 1/(Ti*s) + Td*s)`
1010
* `:series` - `Kc*(1 + 1/(τi*s))*(τd*s + 1)`
1111
* `:parallel` - `Kp + Ki/s + Kd*s`
1212
1313
If `state_space` is set to `true`, either `Kd` has to be zero
1414
or a positive `Tf` has to be provided for creating a filter on
15-
the input to allow for a state space realization.
15+
the input to allow for a state-space realization.
1616
The filter used is `1 / (1 + s*Tf + (s*Tf)^2/2)`, where `Tf` can typically
1717
be chosen as `Ti/N` for a PI controller and `Td/N` for a PID controller,
1818
and `N` is commonly in the range 2 to 20.
19-
The state space will be returned on controllable canonical form.
19+
A balanced state-space realization is returned, unless `balance = false`
20+
in which case a controllable canonical form is used.
2021
2122
For a discrete controller a positive `Ts` can be supplied.
2223
In this case, the continuous-time controller is discretized using the Tustin method.
@@ -25,15 +26,15 @@ In this case, the continuous-time controller is discretized using the Tustin met
2526
```
2627
C1 = pid(3.3, 1, 2) # Kd≠0 works without filter in tf form
2728
C2 = pid(3.3, 1, 2; Tf=0.3, state_space=true) # In statespace a filter is needed
28-
C3 = pid(2., 3, 0; Ts=0.4, state_space=true) # Discrete
29+
C3 = pid(2., 3, 0; Ts=0.4, state_space=true) # Discrete
2930
```
3031
3132
The functions `pid_tf` and `pid_ss` are also exported. They take the same parameters
3233
and is what is actually called in `pid` based on the `state_space` parameter.
3334
"""
34-
function pid(param_p, param_i, param_d=zero(typeof(param_p)); form=:standard, Ts=nothing, Tf=nothing, state_space=false)
35+
function pid(param_p, param_i, param_d=zero(typeof(param_p)); form=:standard, Ts=nothing, Tf=nothing, state_space=false, balance=true)
3536
C = if state_space # Type instability? Can it be fixed easily, does it matter?
36-
pid_ss(param_p, param_i, param_d; form, Tf)
37+
pid_ss(param_p, param_i, param_d; form, Tf, balance)
3738
else
3839
pid_tf(param_p, param_i, param_d; form, Tf)
3940
end
@@ -64,9 +65,8 @@ function pid_tf(param_p, param_i, param_d=zero(typeof(param_p)); form=:standard,
6465
end
6566
end
6667

67-
function pid_ss(param_p, param_i, param_d=zero(typeof(param_p)); form=:standard, Tf=nothing)
68+
function pid_ss(param_p, param_i, param_d=zero(typeof(param_p)); form=:standard, Tf=nothing, balance=true)
6869
Kp, Ki, Kd = convert_pidparams_to_parallel(param_p, param_i, param_d, form)
69-
TE = Continuous()
7070
if !isnothing(Tf)
7171
if Ki != 0
7272
A = [0 1 0; 0 0 1; 0 -2/Tf^2 -2/Tf]
@@ -88,11 +88,84 @@ function pid_ss(param_p, param_i, param_d=zero(typeof(param_p)); form=:standard,
8888
return ss([Kp])
8989
end
9090
else
91-
throw(DomainError("cannot create controller as a state space if Td != 0 without a filter. Either create the controller as a transfer function, pid(TransferFunction; params...), or supply Tf to create a filter."))
91+
throw(DomainError("cannot create controller as a state space if Td != 0 without a filter. Either create the controller as a transfer function, pid(params..., state_space=false), or supply keyword argument Tf to add a filter."))
92+
end
93+
K = ss(A, B, C, D)
94+
balance ? first(balance_statespace(K)) : K
95+
end
96+
97+
"""
98+
C = pid_2dof(param_p, param_i, [param_d]; form=:standard, state_space=true, N = 10, [Ts], b=1, c=0, disc=:tustin)
99+
100+
Calculates and returns a PID controller on 2DOF form with inputs `[r; y]` and outputs `u` where `r` is the reference signal, `y` is the measured output and `u` is the control signal.
101+
102+
Belowm we show two different depections of the contorller, one as a 2-input system (left) and one where the tw internal SISO systems of the controller are shown (right).
103+
```
104+
┌──────┐
105+
r │ │
106+
───►│ Cr ├────┐
107+
r ┌─────┐ ┌─────┐ │ │ │ ┌─────┐
108+
──►│ │ u │ │ y └──────┘ │ │ │ y
109+
│ C ├────►│ P ├─┬─► +───►│ P ├─┬───►
110+
┌►│ │ │ │ │ ┌──────┐ │ │ │ │
111+
│ └─────┘ └─────┘ │ y │ │ │ └─────┘ │
112+
│ │ ┌─►│ Cy ├────┘ │
113+
└─────────────────────┘ │ │ │ │
114+
│ └──────┘ │
115+
│ │
116+
└───────────────────────────┘
117+
```
118+
119+
The `form` can be chosen as one of the following (determines how the arguments `param_p, param_i, param_d` are interpreted)
120+
* `:standard` - `Kp*(b*r-y + (r-y)/(Ti*s) + Td*s*(c*r-y)/(Tf*s + 1))`
121+
* `:parallel` - `Kp*(b*r-y) + Ki*(r-y)/s + Kd*s*(c*r-y)/(Tf*s + 1)`
122+
123+
- `b` is a set-point weighting for the proportional term
124+
- `c` is a set-point weighting for the derivative term, this defaults to 0.
125+
- If both `b` and `c` are set to zero, the feedforward path of the controller will be strictly proper.
126+
- `Tf` is a time constant for a filter on the derivative term, this defaults to `Td/N` where `N` is set to 10. Instead of passing `Tf` one can also pass `N` directly. The proportional term is not affected by this filter. **Please note**: this derivative filter is not the same as the one used in the `pid` function, where the filter is of second order and applied in series with the contorller, i.e., it affects all three PID terms.
127+
- A PD controller is constructed by setting `param_i` to zero.
128+
- A balanced state-space realization is returned, unless `balance = false`
129+
- If `Ts` is supplied, the controller is discretized using the method `disc` (defaults to `:tustin`).
130+
131+
This controller has negative feedback built in, and the closed-loop system from `r` to `y` is thus formed as
132+
```
133+
Cr, Cy = C[1, 1], C[1, 2]
134+
feedback(P, Cy, pos_feedback=true)*Cr # Alternative 1
135+
feedback(P, -Cy)*Cr # Alternative 2
136+
```
137+
"""
138+
function pid_2dof(args...; state_space = true, Ts = nothing, disc = :tustin, kwargs...)
139+
C = pid_ss_2dof(args...; kwargs...)
140+
Ccd = Ts === nothing ? C : c2d(C, Ts, disc)
141+
state_space ? Ccd : tf(Ccd)
142+
end
143+
144+
function pid_ss_2dof(param_p, param_i, param_d=zero(typeof(param_p)); form=:standard, b = 1, c = 0, Tf=nothing, N=nothing, balance=true)
145+
# On standard form we use N
146+
Tf !== nothing && N !== nothing && throw(ArgumentError("Cannot supply both Tf and N"))
147+
if Tf === nothing && N === nothing
148+
N = 10 # Default value
149+
end
150+
kp, ki, kd = convert_pidparams_to_parallel(param_p, param_i, param_d, form)
151+
Tf = @something(Tf, kd / N)
152+
Tf <= 0 && throw(ArgumentError("Tf must be strictly positive"))
153+
if ki == 0
154+
A = [-(1 / Tf);;]
155+
B = [-kd*c/(Tf^2) kd/(Tf^2)]
156+
C = [1.0]
157+
D = [kd*c/Tf+kp*b -(kd/Tf + kp)]
158+
else
159+
A = [0 0; 0 -(1 / Tf)]
160+
B = [ki -ki; -kd*c/Tf^2 kd/Tf^2]
161+
C = [1.0 1]
162+
D = [kd*c/Tf+kp*b -(kd/Tf + kp)]
92163
end
93-
return first(balance_statespace(ss(A, B, C, D)))
164+
K = ss(A, B, C, D)
165+
balance ? first(balance_statespace(K)) : K
94166
end
95167

168+
96169
"""
97170
pidplots(P, args...; params_p, params_i, params_d=0, form=:standard, ω=0, grid=false, kwargs...)
98171

lib/ControlSystemsBase/test/test_pid_design.jl

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,33 @@ Tf = 0.01
4848

4949
@test tf(pid(2.0, 0, 1; state_space=true, Tf)) minreal(pid(2.0, 0, 1; state_space=false, Tf))
5050

51+
# pid 2 DOF
52+
53+
# PID controller on 2DOF form constructed with transfer functions for comparison
54+
s = tf('s')
55+
kp, ki, kd, b, c, Tf = rand(6)
56+
ki = 0
57+
Ktf = [(kp*b + kd*s*c/(Tf*s + 1)) -(kp + kd*s/(Tf*s + 1))]
58+
Kss = ControlSystemsBase.pid_ss_2dof(kp, ki, kd; Tf, b, c, form=:parallel)
59+
@test norm(freqresp(Kss-Ktf, exp10.(LinRange(-3, 3, 10)))) < 1e-10
60+
61+
kp, ki, kd, b, c, Tf = rand(6)
62+
Ktf = [(kp*b + ki/s + kd*s*c/(Tf*s + 1)) -(kp + ki/s + kd*s/(Tf*s + 1))]
63+
Kss = ControlSystemsBase.pid_ss_2dof(kp, ki, kd; Tf, b, c, form=:parallel)
64+
@test norm(freqresp(Kss-Ktf, exp10.(LinRange(-3, 3, 10)))) < 1e-10
65+
66+
kp, ki, kd, b, c, N = rand(6)
67+
Tf = kd/N
68+
Ktf = [(kp*b + ki/s + kd*s*c/(Tf*s + 1)) -(kp + ki/s + kd*s/(Tf*s + 1))]
69+
Kss = ControlSystemsBase.pid_ss_2dof(kp, ki, kd; N, b, c, form=:parallel)
70+
@test norm(freqresp(Kss-Ktf, exp10.(LinRange(-3, 3, 10)))) < 1e-10
71+
72+
73+
kp, ki, kd, b, c, Tf = rand(6)
74+
Ktf = c2d(ss([(kp*b + ki/s + kd*s*c/(Tf*s + 1)) -(kp + ki/s + kd*s/(Tf*s + 1))]), 0.01, :tustin)
75+
Kss = pid_2dof(kp, ki, kd; Tf, b, c, form=:parallel, Ts=0.01, state_space = false)
76+
@test norm(freqresp(Kss-Ktf, exp10.(LinRange(-3, 3, 10)))) < 1e-5
77+
5178
# Test pidplots
5279
C = pid(1.0, 1, 1)
5380
pidplots(C, :nyquist, :gof, :pz, :controller; params_p=[1.0, 2], params_i=[2, 3], grid=true) # Simply test that the functions runs and not errors

0 commit comments

Comments
 (0)