Skip to content

Commit 3263646

Browse files
committed
new pid-designer, built on sisotool, for manual tuning of a PID controller
1 parent affe4d3 commit 3263646

2 files changed

Lines changed: 166 additions & 4 deletions

File tree

control/sisotool.py

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
__all__ = ['sisotool']
1+
__all__ = ['sisotool', 'pid_designer']
22

33
from control.exception import ControlMIMONotImplemented
44
from .freqplot import bode_plot
55
from .timeresp import step_response
66
from .lti import issiso, isdtime
7-
from .xferfcn import TransferFunction
7+
from .xferfcn import tf
8+
from .statesp import ss
89
from .bdalg import append, connect
10+
from .iosys import tf2io, ss2io, summing_junction, interconnect
11+
from control.statesp import _convert_to_statespace
12+
from control.lti import common_timebase, isctime
913
import matplotlib
1014
import matplotlib.pyplot as plt
1115
import warnings
@@ -176,3 +180,139 @@ def _SisotoolUpdate(sys, fig, K, bode_plot_params, tvect=None):
176180
fig.subplots_adjust(top=0.9,wspace = 0.3,hspace=0.35)
177181
fig.canvas.draw()
178182

183+
def pid_designer(plant, gain='P', sign=+1, input_signal='r',
184+
Kp0=0, Ki0=0, Kd0=0, tau=0.01,
185+
C_ff=0, derivative_in_feedback_path=False):
186+
"""Manual PID controller design using sisotool
187+
188+
Uses `Sisotool` to investigate the effect of adding or subtracting an
189+
amount `deltaK` to the proportional, integral, or derivative (PID) gains of
190+
a controller. One of the PID gains, `Kp`, `Ki`, or `Kd`, respectively, can
191+
be modified at a time. `Sisotool` plots the step response, frequency
192+
response, and root locus.
193+
194+
When first run, `deltaK` is set to 1; click on a branch of the root locus
195+
plot to try a different value. Each click updates plots and prints
196+
the corresponding `deltaK`. To tune all three PID gains, repeatedly call
197+
`pid_designer`, and select a different `gain` each time (`'P'`, `'I'`,
198+
or `'D'`). Make sure to add the resulting `deltaK` to your chosen initial
199+
gain on the next iteration.
200+
201+
Example: to examine the effect of varying `Kp` starting from an intial
202+
value of 10, use the arguments `gain='P', Kp0=10`. Suppose a `deltaK`
203+
value of 5 gives satisfactory performance. Then on the next iteration,
204+
to tune the derivative gain, use the arguments `gain='D', Kp0=15`.
205+
206+
By default, all three PID terms are in the forward path C_f in the diagram
207+
shown below, that is,
208+
209+
C_f = Kp + Ki/s + Kd*s/(tau*s + 1).
210+
211+
If `plant` is a discrete-time system, then the proportional, integral, and
212+
derivative terms are given instead by Kp, Ki*dt/2*(z+1)/(z-1), and
213+
Kd/dt*(z-1)/z, respectively.
214+
215+
------> C_ff ------ d
216+
| | |
217+
r | e V V u y
218+
------->O---> C_f --->O--->O---> plant --->
219+
^- ^- |
220+
| | |
221+
| ----- C_b <-------|
222+
---------------------------------
223+
224+
It is also possible to move the derivative term into the feedback path
225+
`C_b` using `derivative_in_feedback_path=True`. This may be desired to
226+
avoid that the plant is subject to an impulse function when the reference
227+
`r` is a step input. `C_b` is otherwise set to zero.
228+
229+
If `plant` is a 2-input system, the disturbance `d` is fed directly into
230+
its second input rather than being added to `u`.
231+
232+
Remark: It may be helpful to zoom in using the magnifying glass on the
233+
plot. Just ake sure to deactivate magnification mode when you are done by
234+
clicking the magnifying glass. Otherwise you will not be able to be able to choose
235+
a gain on the root locus plot.
236+
237+
Parameters
238+
----------
239+
plant : :class:`LTI` (:class:`TransferFunction` or :class:`StateSpace` system)
240+
The dynamical system to be controlled
241+
gain : string (optional)
242+
Which gain to vary by deltaK. Must be one of 'P', 'I', or 'D'
243+
(proportional, integral, or derative)
244+
sign : int (optional)
245+
The sign of deltaK gain perturbation
246+
input : string (optional)
247+
The input used for the step response; must be 'r' (reference) or
248+
'd' (disturbance) (see figure above)
249+
Kp0, Ki0, Kd0 : float (optional)
250+
Initial values for proportional, integral, and derivative gains,
251+
respectively
252+
tau : float (optional)
253+
The time constant associated with the pole in the continuous-time
254+
derivative term. This is required to make the derivative transfer
255+
function proper.
256+
C_ff : float or :class:`LTI` system (optional)
257+
Feedforward controller. If :class:`LTI`, must have timebase that is
258+
compatible with plant.
259+
"""
260+
plant = _convert_to_statespace(plant)
261+
if plant.ninputs == 1:
262+
plant = ss2io(plant, inputs='u', outputs='y')
263+
elif plant.ninputs == 2:
264+
plant = ss2io(plant, inputs=('u', 'd'), outputs='y')
265+
else:
266+
raise ValueError("plant must have one or two inputs")
267+
#plant = ss2io(plant, inputs='u', outputs='y')
268+
C_ff = ss2io(_convert_to_statespace(C_ff), inputs='r', outputs='uff')
269+
dt = common_timebase(plant, C_ff)
270+
271+
# create systems used for interconnections
272+
e_summer = summing_junction(['r', '-y'], 'e')
273+
if plant.ninputs == 2:
274+
u_summer = summing_junction(['ufb', 'uff'], 'u')
275+
else:
276+
u_summer = summing_junction(['ufb', 'uff', 'd'], 'u')
277+
278+
prop = tf(1,1)
279+
if isctime(plant):
280+
integ = tf(1,[1, 0])
281+
deriv = tf([1, 0], [tau, 1])
282+
else:
283+
integ = tf([dt/2, dt/2],[1, -1], dt)
284+
deriv = tf([1, -1],[dt, 0], dt)
285+
286+
# add signal names
287+
prop = tf2io(prop, inputs='e', outputs='prop_e')
288+
integ = tf2io(integ, inputs='e', outputs='int_e')
289+
if derivative_in_feedback_path:
290+
deriv = tf2io(-deriv, inputs='y', outputs='deriv_')
291+
else:
292+
deriv = tf2io(deriv, inputs='e', outputs='deriv_')
293+
294+
# create gain blocks
295+
Kpgain = tf2io(tf(Kp0, 1), inputs='prop_e', outputs='ufb')
296+
Kigain = tf2io(tf(Ki0, 1), inputs='int_e', outputs='ufb')
297+
Kdgain = tf2io(tf(Kd0, 1), inputs='deriv_', outputs='ufb')
298+
299+
# for the gain that is varied, create a special gain block with an
300+
# 'input' and an 'output' signal to create the loop transfer function
301+
if gain in ('P', 'p'):
302+
Kpgain = ss2io(ss([],[],[],[[0, 1], [-sign, Kp0]]),
303+
inputs=['input', 'prop_e'], outputs=['output', 'ufb'])
304+
elif gain in ('I', 'i'):
305+
Kigain = ss2io(ss([],[],[],[[0, 1], [-sign, Ki0]]),
306+
inputs=['input', 'int_e'], outputs=['output', 'ufb'])
307+
elif gain in ('D', 'd'):
308+
Kdgain = ss2io(ss([],[],[],[[0, 1], [-sign, Kd0]]),
309+
inputs=['input', 'deriv_'], outputs=['output', 'ufb'])
310+
else:
311+
raise ValueError(gain + ' gain not recognized.')
312+
313+
# the second input and output are used by sisotool to plot step response
314+
loop = interconnect((plant, Kpgain, Kigain, Kdgain, prop, integ, deriv,
315+
C_ff, e_summer, u_summer),
316+
inplist=['input', input_signal], outlist=['output', 'y'])
317+
sisotool(loop)
318+
return loop[1, 1]

control/tests/sisotool_test.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
from numpy.testing import assert_array_almost_equal
77
import pytest
88

9-
from control.sisotool import sisotool
9+
from control.sisotool import sisotool, pid_designer
1010
from control.rlocus import _RLClickDispatcher
1111
from control.xferfcn import TransferFunction
1212
from control.statesp import StateSpace
13-
13+
from control import c2d
1414

1515
@pytest.mark.usefixtures("mplcleanup")
1616
class TestSisotool:
@@ -140,3 +140,25 @@ def test_sisotool_mimo(self, sys222, sys221):
140140
# but 2 input, 1 output should
141141
with pytest.raises(ControlMIMONotImplemented):
142142
sisotool(sys221)
143+
144+
@pytest.mark.usefixtures("mplcleanup")
145+
class TestPidDesigner:
146+
syscont = TransferFunction(1,[1, 3, 0])
147+
sysdisc1 = c2d(TransferFunction(1,[1, 3, 0]), .1)
148+
syscont221 = StateSpace([[-.3, 0],[1,0]],[[-1,],[.1,]], [0, -.3], 0)
149+
150+
# cont or discrete, vary P I or D
151+
@pytest.mark.parametrize('plant', (syscont, sysdisc1))
152+
@pytest.mark.parametrize('gain', ('P', 'I', 'D'))
153+
@pytest.mark.parametrize("kwargs", [{'Kp0':0.01},])
154+
def test_pid_designer_1(self, plant, gain, kwargs):
155+
pid_designer(plant, gain, **kwargs)
156+
157+
# input from reference or disturbance
158+
@pytest.mark.parametrize('plant', (syscont, syscont221))
159+
@pytest.mark.parametrize("kwargs", [
160+
{'input_signal':'r', 'Kp0':0.01, 'derivative_in_feedback_path':True},
161+
{'input_signal':'d', 'Kp0':0.01, 'derivative_in_feedback_path':True},])
162+
def test_pid_designer_2(self, plant, kwargs):
163+
pid_designer(plant, **kwargs)
164+

0 commit comments

Comments
 (0)