Skip to content

Commit 89291e6

Browse files
authored
Merge pull request #662 from sawyerbfuller/pid-designer
new Pid design function built on sisotool
2 parents a05351b + 4ee9fd1 commit 89291e6

File tree

5 files changed

+207
-12
lines changed

5 files changed

+207
-12
lines changed

control/rlocus.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,8 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None,
168168
else:
169169
if ax is None:
170170
ax = plt.gca()
171-
fig = ax.figure
172-
ax.set_title('Root Locus')
171+
fig = ax.figure
172+
ax.set_title('Root Locus')
173173

174174
if print_gain and not sisotool:
175175
fig.canvas.mpl_connect(
@@ -180,15 +180,15 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None,
180180
fig.axes[1].plot(
181181
[root.real for root in start_mat],
182182
[root.imag for root in start_mat],
183-
marker='s', markersize=8, zorder=20, label='gain_point')
183+
marker='s', markersize=6, zorder=20, color='k', label='gain_point')
184184
s = start_mat[0][0]
185185
if isdtime(sys, strict=True):
186186
zeta = -np.cos(np.angle(np.log(s)))
187187
else:
188188
zeta = -1 * s.real / abs(s)
189189
fig.suptitle(
190190
"Clicked at: %10.4g%+10.4gj gain: %10.4g damp: %10.4g" %
191-
(s.real, s.imag, 1, zeta),
191+
(s.real, s.imag, kvect[0], zeta),
192192
fontsize=12 if int(mpl.__version__[0]) == 1 else 10)
193193
fig.canvas.mpl_connect(
194194
'button_release_event',
@@ -623,7 +623,7 @@ def _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, sisotool=False):
623623
ax_rlocus.plot(
624624
[root.real for root in mymat],
625625
[root.imag for root in mymat],
626-
marker='s', markersize=8, zorder=20, label='gain_point')
626+
marker='s', markersize=6, zorder=20, label='gain_point', color='k')
627627
else:
628628
ax_rlocus.plot(s.real, s.imag, 'k.', marker='s', markersize=8,
629629
zorder=20, label='gain_point')
@@ -769,7 +769,7 @@ def _default_wn(xloc, yloc, max_lines=7):
769769
770770
"""
771771
sep = xloc[1]-xloc[0] # separation between x-ticks
772-
772+
773773
# Decide whether to use the x or y axis for determining wn
774774
if yloc[-1] / sep > max_lines*10:
775775
# y-axis scale >> x-axis scale

control/sisotool.py

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

control/tests/lti_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def test_damp(self):
7777
p2_splane = -wn2 * zeta2 + 1j * wn2 * np.sqrt(1 - zeta2**2)
7878
p2_zplane = np.exp(p2_splane * dt)
7979
np.testing.assert_almost_equal(p2, p2_zplane)
80-
80+
8181
def test_dcgain(self):
8282
sys = tf(84, [1, 2])
8383
np.testing.assert_allclose(sys.dcgain(), 42)
@@ -136,7 +136,7 @@ def test_common_timebase(self, dt1, dt2, expected, sys1, sys2):
136136
(0, 1),
137137
(1, 2)])
138138
def test_common_timebase_errors(self, i1, i2):
139-
"""Test that common_timbase throws errors on invalid combinations"""
139+
"""Test that common_timbase raises errors on invalid combinations"""
140140
with pytest.raises(ValueError):
141141
common_timebase(i1, i2)
142142
# Make sure behaviour is symmetric

control/tests/sisotool_test.py

Lines changed: 39 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, rootlocus_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,40 @@ 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+
@pytest.fixture
147+
def plant(self, request):
148+
plants = {
149+
'syscont':TransferFunction(1,[1, 3, 0]),
150+
'sysdisc1':c2d(TransferFunction(1,[1, 3, 0]), .1),
151+
'syscont221':StateSpace([[-.3, 0],[1,0]],[[-1,],[.1,]], [0, -.3], 0)}
152+
return plants[request.param]
153+
154+
# test permutations of system construction without plotting
155+
@pytest.mark.parametrize('plant', ('syscont', 'sysdisc1', 'syscont221'), indirect=True)
156+
@pytest.mark.parametrize('gain', ('P', 'I', 'D'))
157+
@pytest.mark.parametrize('sign', (1,))
158+
@pytest.mark.parametrize('input_signal', ('r', 'd'))
159+
@pytest.mark.parametrize('Kp0', (0,))
160+
@pytest.mark.parametrize('Ki0', (1.,))
161+
@pytest.mark.parametrize('Kd0', (0.1,))
162+
@pytest.mark.parametrize('tau', (0.01,))
163+
@pytest.mark.parametrize('C_ff', (0, 1,))
164+
@pytest.mark.parametrize('derivative_in_feedback_path', (True, False,))
165+
@pytest.mark.parametrize("kwargs", [{'plot':False},])
166+
def test_pid_designer_1(self, plant, gain, sign, input_signal, Kp0, Ki0, Kd0, tau, C_ff,
167+
derivative_in_feedback_path, kwargs):
168+
rootlocus_pid_designer(plant, gain, sign, input_signal, Kp0, Ki0, Kd0, tau, C_ff,
169+
derivative_in_feedback_path, **kwargs)
170+
171+
# test creation of sisotool plot
172+
# input from reference or disturbance
173+
@pytest.mark.parametrize('plant', ('syscont', 'syscont221'), indirect=True)
174+
@pytest.mark.parametrize("kwargs", [
175+
{'input_signal':'r', 'Kp0':0.01, 'derivative_in_feedback_path':True},
176+
{'input_signal':'d', 'Kp0':0.01, 'derivative_in_feedback_path':True},])
177+
def test_pid_designer_2(self, plant, kwargs):
178+
rootlocus_pid_designer(plant, **kwargs)
179+

doc/control.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ Control system synthesis
114114
lqe
115115
mixsyn
116116
place
117+
rlocus_pid_designer
117118

118119
Model simplification tools
119120
==========================

0 commit comments

Comments
 (0)