From 3263646588936fcd506838f1fecab458a3b8b0da Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Tue, 2 Nov 2021 13:24:37 -0700 Subject: [PATCH 01/10] new pid-designer, built on sisotool, for manual tuning of a PID controller --- control/sisotool.py | 144 ++++++++++++++++++++++++++++++++- control/tests/sisotool_test.py | 26 +++++- 2 files changed, 166 insertions(+), 4 deletions(-) diff --git a/control/sisotool.py b/control/sisotool.py index 18c3b5d12..9439a7040 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -1,11 +1,15 @@ -__all__ = ['sisotool'] +__all__ = ['sisotool', 'pid_designer'] from control.exception import ControlMIMONotImplemented from .freqplot import bode_plot from .timeresp import step_response from .lti import issiso, isdtime -from .xferfcn import TransferFunction +from .xferfcn import tf +from .statesp import ss from .bdalg import append, connect +from .iosys import tf2io, ss2io, summing_junction, interconnect +from control.statesp import _convert_to_statespace +from control.lti import common_timebase, isctime import matplotlib import matplotlib.pyplot as plt import warnings @@ -176,3 +180,139 @@ def _SisotoolUpdate(sys, fig, K, bode_plot_params, tvect=None): fig.subplots_adjust(top=0.9,wspace = 0.3,hspace=0.35) fig.canvas.draw() +def pid_designer(plant, gain='P', sign=+1, input_signal='r', + Kp0=0, Ki0=0, Kd0=0, tau=0.01, + C_ff=0, derivative_in_feedback_path=False): + """Manual PID controller design using sisotool + + Uses `Sisotool` to investigate the effect of adding or subtracting an + amount `deltaK` to the proportional, integral, or derivative (PID) gains of + a controller. One of the PID gains, `Kp`, `Ki`, or `Kd`, respectively, can + be modified at a time. `Sisotool` plots the step response, frequency + response, and root locus. + + When first run, `deltaK` is set to 1; click on a branch of the root locus + plot to try a different value. Each click updates plots and prints + the corresponding `deltaK`. To tune all three PID gains, repeatedly call + `pid_designer`, and select a different `gain` each time (`'P'`, `'I'`, + or `'D'`). Make sure to add the resulting `deltaK` to your chosen initial + gain on the next iteration. + + Example: to examine the effect of varying `Kp` starting from an intial + value of 10, use the arguments `gain='P', Kp0=10`. Suppose a `deltaK` + value of 5 gives satisfactory performance. Then on the next iteration, + to tune the derivative gain, use the arguments `gain='D', Kp0=15`. + + By default, all three PID terms are in the forward path C_f in the diagram + shown below, that is, + + C_f = Kp + Ki/s + Kd*s/(tau*s + 1). + + If `plant` is a discrete-time system, then the proportional, integral, and + derivative terms are given instead by Kp, Ki*dt/2*(z+1)/(z-1), and + Kd/dt*(z-1)/z, respectively. + + ------> C_ff ------ d + | | | + r | e V V u y + ------->O---> C_f --->O--->O---> plant ---> + ^- ^- | + | | | + | ----- C_b <-------| + --------------------------------- + + It is also possible to move the derivative term into the feedback path + `C_b` using `derivative_in_feedback_path=True`. This may be desired to + avoid that the plant is subject to an impulse function when the reference + `r` is a step input. `C_b` is otherwise set to zero. + + If `plant` is a 2-input system, the disturbance `d` is fed directly into + its second input rather than being added to `u`. + + Remark: It may be helpful to zoom in using the magnifying glass on the + plot. Just ake sure to deactivate magnification mode when you are done by + clicking the magnifying glass. Otherwise you will not be able to be able to choose + a gain on the root locus plot. + + Parameters + ---------- + plant : :class:`LTI` (:class:`TransferFunction` or :class:`StateSpace` system) + The dynamical system to be controlled + gain : string (optional) + Which gain to vary by deltaK. Must be one of 'P', 'I', or 'D' + (proportional, integral, or derative) + sign : int (optional) + The sign of deltaK gain perturbation + input : string (optional) + The input used for the step response; must be 'r' (reference) or + 'd' (disturbance) (see figure above) + Kp0, Ki0, Kd0 : float (optional) + Initial values for proportional, integral, and derivative gains, + respectively + tau : float (optional) + The time constant associated with the pole in the continuous-time + derivative term. This is required to make the derivative transfer + function proper. + C_ff : float or :class:`LTI` system (optional) + Feedforward controller. If :class:`LTI`, must have timebase that is + compatible with plant. + """ + plant = _convert_to_statespace(plant) + if plant.ninputs == 1: + plant = ss2io(plant, inputs='u', outputs='y') + elif plant.ninputs == 2: + plant = ss2io(plant, inputs=('u', 'd'), outputs='y') + else: + raise ValueError("plant must have one or two inputs") + #plant = ss2io(plant, inputs='u', outputs='y') + C_ff = ss2io(_convert_to_statespace(C_ff), inputs='r', outputs='uff') + dt = common_timebase(plant, C_ff) + + # create systems used for interconnections + e_summer = summing_junction(['r', '-y'], 'e') + if plant.ninputs == 2: + u_summer = summing_junction(['ufb', 'uff'], 'u') + else: + u_summer = summing_junction(['ufb', 'uff', 'd'], 'u') + + prop = tf(1,1) + if isctime(plant): + integ = tf(1,[1, 0]) + deriv = tf([1, 0], [tau, 1]) + else: + integ = tf([dt/2, dt/2],[1, -1], dt) + deriv = tf([1, -1],[dt, 0], dt) + + # add signal names + prop = tf2io(prop, inputs='e', outputs='prop_e') + integ = tf2io(integ, inputs='e', outputs='int_e') + if derivative_in_feedback_path: + deriv = tf2io(-deriv, inputs='y', outputs='deriv_') + else: + deriv = tf2io(deriv, inputs='e', outputs='deriv_') + + # create gain blocks + Kpgain = tf2io(tf(Kp0, 1), inputs='prop_e', outputs='ufb') + Kigain = tf2io(tf(Ki0, 1), inputs='int_e', outputs='ufb') + Kdgain = tf2io(tf(Kd0, 1), inputs='deriv_', outputs='ufb') + + # for the gain that is varied, create a special gain block with an + # 'input' and an 'output' signal to create the loop transfer function + if gain in ('P', 'p'): + Kpgain = ss2io(ss([],[],[],[[0, 1], [-sign, Kp0]]), + inputs=['input', 'prop_e'], outputs=['output', 'ufb']) + elif gain in ('I', 'i'): + Kigain = ss2io(ss([],[],[],[[0, 1], [-sign, Ki0]]), + inputs=['input', 'int_e'], outputs=['output', 'ufb']) + elif gain in ('D', 'd'): + Kdgain = ss2io(ss([],[],[],[[0, 1], [-sign, Kd0]]), + inputs=['input', 'deriv_'], outputs=['output', 'ufb']) + else: + raise ValueError(gain + ' gain not recognized.') + + # the second input and output are used by sisotool to plot step response + loop = interconnect((plant, Kpgain, Kigain, Kdgain, prop, integ, deriv, + C_ff, e_summer, u_summer), + inplist=['input', input_signal], outlist=['output', 'y']) + sisotool(loop) + return loop[1, 1] diff --git a/control/tests/sisotool_test.py b/control/tests/sisotool_test.py index ab5d546dd..f7ecb9207 100644 --- a/control/tests/sisotool_test.py +++ b/control/tests/sisotool_test.py @@ -6,11 +6,11 @@ from numpy.testing import assert_array_almost_equal import pytest -from control.sisotool import sisotool +from control.sisotool import sisotool, pid_designer from control.rlocus import _RLClickDispatcher from control.xferfcn import TransferFunction from control.statesp import StateSpace - +from control import c2d @pytest.mark.usefixtures("mplcleanup") class TestSisotool: @@ -140,3 +140,25 @@ def test_sisotool_mimo(self, sys222, sys221): # but 2 input, 1 output should with pytest.raises(ControlMIMONotImplemented): sisotool(sys221) + +@pytest.mark.usefixtures("mplcleanup") +class TestPidDesigner: + syscont = TransferFunction(1,[1, 3, 0]) + sysdisc1 = c2d(TransferFunction(1,[1, 3, 0]), .1) + syscont221 = StateSpace([[-.3, 0],[1,0]],[[-1,],[.1,]], [0, -.3], 0) + + # cont or discrete, vary P I or D + @pytest.mark.parametrize('plant', (syscont, sysdisc1)) + @pytest.mark.parametrize('gain', ('P', 'I', 'D')) + @pytest.mark.parametrize("kwargs", [{'Kp0':0.01},]) + def test_pid_designer_1(self, plant, gain, kwargs): + pid_designer(plant, gain, **kwargs) + + # input from reference or disturbance + @pytest.mark.parametrize('plant', (syscont, syscont221)) + @pytest.mark.parametrize("kwargs", [ + {'input_signal':'r', 'Kp0':0.01, 'derivative_in_feedback_path':True}, + {'input_signal':'d', 'Kp0':0.01, 'derivative_in_feedback_path':True},]) + def test_pid_designer_2(self, plant, kwargs): + pid_designer(plant, **kwargs) + From c4bd38406f81197228b51c995e8f808752bd230a Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Tue, 2 Nov 2021 13:25:26 -0700 Subject: [PATCH 02/10] attribution --- control/sisotool.py | 1 + 1 file changed, 1 insertion(+) diff --git a/control/sisotool.py b/control/sisotool.py index 9439a7040..641e3fa5e 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -180,6 +180,7 @@ def _SisotoolUpdate(sys, fig, K, bode_plot_params, tvect=None): fig.subplots_adjust(top=0.9,wspace = 0.3,hspace=0.35) fig.canvas.draw() +# contributed by Sawyer Fuller, minster@uw.edu 2021.11.02 def pid_designer(plant, gain='P', sign=+1, input_signal='r', Kp0=0, Ki0=0, Kd0=0, tau=0.01, C_ff=0, derivative_in_feedback_path=False): From ffd7b5fbd4e083f79998328c276fd4f0bf942b7b Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Tue, 2 Nov 2021 13:35:11 -0700 Subject: [PATCH 03/10] changed color of root locus pole markers to black instead of randomly-changing colors in sisotool --- control/rlocus.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/control/rlocus.py b/control/rlocus.py index ee30fe489..a358c73b6 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -180,7 +180,7 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, fig.axes[1].plot( [root.real for root in start_mat], [root.imag for root in start_mat], - marker='s', markersize=8, zorder=20, label='gain_point') + marker='s', markersize=6, zorder=20, color='k', label='gain_point') s = start_mat[0][0] if isdtime(sys, strict=True): zeta = -np.cos(np.angle(np.log(s))) @@ -623,7 +623,7 @@ def _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, sisotool=False): ax_rlocus.plot( [root.real for root in mymat], [root.imag for root in mymat], - marker='s', markersize=8, zorder=20, label='gain_point') + marker='s', markersize=6, zorder=20, label='gain_point', color='k') else: ax_rlocus.plot(s.real, s.imag, 'k.', marker='s', markersize=8, zorder=20, label='gain_point') @@ -769,7 +769,7 @@ def _default_wn(xloc, yloc, max_lines=7): """ sep = xloc[1]-xloc[0] # separation between x-ticks - + # Decide whether to use the x or y axis for determining wn if yloc[-1] / sep > max_lines*10: # y-axis scale >> x-axis scale From cd7faaa12206d4f339c243ab71e346bb818301b4 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Tue, 2 Nov 2021 15:00:24 -0700 Subject: [PATCH 04/10] fixed unit test code --- control/sisotool.py | 1 + control/tests/lti_test.py | 4 ++-- control/tests/sisotool_test.py | 16 +++++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/control/sisotool.py b/control/sisotool.py index 641e3fa5e..ab1d3a143 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -258,6 +258,7 @@ def pid_designer(plant, gain='P', sign=+1, input_signal='r', Feedforward controller. If :class:`LTI`, must have timebase that is compatible with plant. """ + plant = _convert_to_statespace(plant) if plant.ninputs == 1: plant = ss2io(plant, inputs='u', outputs='y') diff --git a/control/tests/lti_test.py b/control/tests/lti_test.py index 7e4f0ddb4..e2f7f2e03 100644 --- a/control/tests/lti_test.py +++ b/control/tests/lti_test.py @@ -77,7 +77,7 @@ def test_damp(self): p2_splane = -wn2 * zeta2 + 1j * wn2 * np.sqrt(1 - zeta2**2) p2_zplane = np.exp(p2_splane * dt) np.testing.assert_almost_equal(p2, p2_zplane) - + def test_dcgain(self): sys = tf(84, [1, 2]) np.testing.assert_allclose(sys.dcgain(), 42) @@ -136,7 +136,7 @@ def test_common_timebase(self, dt1, dt2, expected, sys1, sys2): (0, 1), (1, 2)]) def test_common_timebase_errors(self, i1, i2): - """Test that common_timbase throws errors on invalid combinations""" + """Test that common_timbase raises errors on invalid combinations""" with pytest.raises(ValueError): common_timebase(i1, i2) # Make sure behaviour is symmetric diff --git a/control/tests/sisotool_test.py b/control/tests/sisotool_test.py index f7ecb9207..eba3b9194 100644 --- a/control/tests/sisotool_test.py +++ b/control/tests/sisotool_test.py @@ -143,19 +143,25 @@ def test_sisotool_mimo(self, sys222, sys221): @pytest.mark.usefixtures("mplcleanup") class TestPidDesigner: - syscont = TransferFunction(1,[1, 3, 0]) - sysdisc1 = c2d(TransferFunction(1,[1, 3, 0]), .1) - syscont221 = StateSpace([[-.3, 0],[1,0]],[[-1,],[.1,]], [0, -.3], 0) + @pytest.fixture + def plant(self, request): + plants = { + 'syscont':TransferFunction(1,[1, 3, 0]), + 'sysdisc1':c2d(TransferFunction(1,[1, 3, 0]), .1), + 'syscont221':StateSpace([[-.3, 0],[1,0]],[[-1,],[.1,]], [0, -.3], 0)} + return plants[request.param] # cont or discrete, vary P I or D - @pytest.mark.parametrize('plant', (syscont, sysdisc1)) +# @pytest.mark.parametrize('plant', (syscont, sysdisc1)) + @pytest.mark.parametrize('plant', ('syscont', 'sysdisc1'), indirect=True) @pytest.mark.parametrize('gain', ('P', 'I', 'D')) @pytest.mark.parametrize("kwargs", [{'Kp0':0.01},]) def test_pid_designer_1(self, plant, gain, kwargs): pid_designer(plant, gain, **kwargs) # input from reference or disturbance - @pytest.mark.parametrize('plant', (syscont, syscont221)) + @pytest.mark.parametrize('plant', ('syscont', 'syscont221'), indirect=True) +# @pytest.mark.parametrize('plant', (syscont, syscont221)) @pytest.mark.parametrize("kwargs", [ {'input_signal':'r', 'Kp0':0.01, 'derivative_in_feedback_path':True}, {'input_signal':'d', 'Kp0':0.01, 'derivative_in_feedback_path':True},]) From ec0f0a83f22677b088c5e8c35e7c605c5840d18f Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Thu, 4 Nov 2021 10:17:36 -0700 Subject: [PATCH 05/10] renamed function to highlight that it is based on root locus, set initial gain to 1, new noplot argument for faster testing --- control/rlocus.py | 2 +- control/sisotool.py | 65 ++++++++++++++++++++-------------- control/tests/sisotool_test.py | 9 ++--- 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/control/rlocus.py b/control/rlocus.py index a358c73b6..8c3c1c24f 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -188,7 +188,7 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, zeta = -1 * s.real / abs(s) fig.suptitle( "Clicked at: %10.4g%+10.4gj gain: %10.4g damp: %10.4g" % - (s.real, s.imag, 1, zeta), + (s.real, s.imag, kvect[0], zeta), fontsize=12 if int(mpl.__version__[0]) == 1 else 10) fig.canvas.mpl_connect( 'button_release_event', diff --git a/control/sisotool.py b/control/sisotool.py index ab1d3a143..7a59f1a1e 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -1,4 +1,4 @@ -__all__ = ['sisotool', 'pid_designer'] +__all__ = ['sisotool', 'rootlocus_pid_designer'] from control.exception import ControlMIMONotImplemented from .freqplot import bode_plot @@ -180,11 +180,13 @@ def _SisotoolUpdate(sys, fig, K, bode_plot_params, tvect=None): fig.subplots_adjust(top=0.9,wspace = 0.3,hspace=0.35) fig.canvas.draw() -# contributed by Sawyer Fuller, minster@uw.edu 2021.11.02 -def pid_designer(plant, gain='P', sign=+1, input_signal='r', +# contributed by Sawyer Fuller, minster@uw.edu 2021.11.02, based on +# an implementation in Matlab by Martin Berg. +def rootlocus_pid_designer(plant, gain='P', sign=+1, input_signal='r', Kp0=0, Ki0=0, Kd0=0, tau=0.01, - C_ff=0, derivative_in_feedback_path=False): - """Manual PID controller design using sisotool + C_ff=0, derivative_in_feedback_path=False, + noplot=False): + """Manual PID controller design based on root locus using Sisotool Uses `Sisotool` to investigate the effect of adding or subtracting an amount `deltaK` to the proportional, integral, or derivative (PID) gains of @@ -192,7 +194,7 @@ def pid_designer(plant, gain='P', sign=+1, input_signal='r', be modified at a time. `Sisotool` plots the step response, frequency response, and root locus. - When first run, `deltaK` is set to 1; click on a branch of the root locus + When first run, `deltaK` is set to 0; click on a branch of the root locus plot to try a different value. Each click updates plots and prints the corresponding `deltaK`. To tune all three PID gains, repeatedly call `pid_designer`, and select a different `gain` each time (`'P'`, `'I'`, @@ -240,13 +242,13 @@ def pid_designer(plant, gain='P', sign=+1, input_signal='r', plant : :class:`LTI` (:class:`TransferFunction` or :class:`StateSpace` system) The dynamical system to be controlled gain : string (optional) - Which gain to vary by deltaK. Must be one of 'P', 'I', or 'D' + Which gain to vary by `deltaK`. Must be one of `'P'`, `'I'`, or `'D'` (proportional, integral, or derative) sign : int (optional) The sign of deltaK gain perturbation input : string (optional) - The input used for the step response; must be 'r' (reference) or - 'd' (disturbance) (see figure above) + The input used for the step response; must be `'r'` (reference) or + `'d'` (disturbance) (see figure above) Kp0, Ki0, Kd0 : float (optional) Initial values for proportional, integral, and derivative gains, respectively @@ -257,16 +259,24 @@ def pid_designer(plant, gain='P', sign=+1, input_signal='r', C_ff : float or :class:`LTI` system (optional) Feedforward controller. If :class:`LTI`, must have timebase that is compatible with plant. + derivative_in_feedback_path : bool (optional) + Whether to place the derivative term in feedback transfer function + `C_b` instead of the forward transfer function `C_f`. + noplot : bool (optional) + + Returns + ---------- + closedloop : class:`StateSpace` system + The closed-loop system using initial gains. """ plant = _convert_to_statespace(plant) if plant.ninputs == 1: plant = ss2io(plant, inputs='u', outputs='y') elif plant.ninputs == 2: - plant = ss2io(plant, inputs=('u', 'd'), outputs='y') + plant = ss2io(plant, inputs=['u', 'd'], outputs='y') else: raise ValueError("plant must have one or two inputs") - #plant = ss2io(plant, inputs='u', outputs='y') C_ff = ss2io(_convert_to_statespace(C_ff), inputs='r', outputs='uff') dt = common_timebase(plant, C_ff) @@ -277,29 +287,30 @@ def pid_designer(plant, gain='P', sign=+1, input_signal='r', else: u_summer = summing_junction(['ufb', 'uff', 'd'], 'u') - prop = tf(1,1) if isctime(plant): - integ = tf(1,[1, 0]) + prop = tf(1, 1) + integ = tf(1, [1, 0]) deriv = tf([1, 0], [tau, 1]) - else: - integ = tf([dt/2, dt/2],[1, -1], dt) - deriv = tf([1, -1],[dt, 0], dt) + else: # discrete-time + prop = tf(1, 1, dt) + integ = tf([dt/2, dt/2], [1, -1], dt) + deriv = tf([1, -1], [dt, 0], dt) - # add signal names + # add signal names by turning into iosystems prop = tf2io(prop, inputs='e', outputs='prop_e') integ = tf2io(integ, inputs='e', outputs='int_e') if derivative_in_feedback_path: - deriv = tf2io(-deriv, inputs='y', outputs='deriv_') + deriv = tf2io(-deriv, inputs='y', outputs='deriv') else: - deriv = tf2io(deriv, inputs='e', outputs='deriv_') + deriv = tf2io(deriv, inputs='e', outputs='deriv') # create gain blocks Kpgain = tf2io(tf(Kp0, 1), inputs='prop_e', outputs='ufb') Kigain = tf2io(tf(Ki0, 1), inputs='int_e', outputs='ufb') - Kdgain = tf2io(tf(Kd0, 1), inputs='deriv_', outputs='ufb') + Kdgain = tf2io(tf(Kd0, 1), inputs='deriv', outputs='ufb') - # for the gain that is varied, create a special gain block with an - # 'input' and an 'output' signal to create the loop transfer function + # for the gain that is varied, replace gain block with a special block + # that has an 'input' and an 'output' that creates loop transfer function if gain in ('P', 'p'): Kpgain = ss2io(ss([],[],[],[[0, 1], [-sign, Kp0]]), inputs=['input', 'prop_e'], outputs=['output', 'ufb']) @@ -308,13 +319,15 @@ def pid_designer(plant, gain='P', sign=+1, input_signal='r', inputs=['input', 'int_e'], outputs=['output', 'ufb']) elif gain in ('D', 'd'): Kdgain = ss2io(ss([],[],[],[[0, 1], [-sign, Kd0]]), - inputs=['input', 'deriv_'], outputs=['output', 'ufb']) + inputs=['input', 'deriv'], outputs=['output', 'ufb']) else: raise ValueError(gain + ' gain not recognized.') # the second input and output are used by sisotool to plot step response loop = interconnect((plant, Kpgain, Kigain, Kdgain, prop, integ, deriv, C_ff, e_summer, u_summer), - inplist=['input', input_signal], outlist=['output', 'y']) - sisotool(loop) - return loop[1, 1] + inplist=['input', input_signal], + outlist=['output', 'y']) + if ~noplot: + sisotool(loop, kvect=(0.,)) + return _convert_to_statespace(loop[1, 1]) diff --git a/control/tests/sisotool_test.py b/control/tests/sisotool_test.py index eba3b9194..b007d299d 100644 --- a/control/tests/sisotool_test.py +++ b/control/tests/sisotool_test.py @@ -6,7 +6,7 @@ from numpy.testing import assert_array_almost_equal import pytest -from control.sisotool import sisotool, pid_designer +from control.sisotool import sisotool, rootlocus_pid_designer from control.rlocus import _RLClickDispatcher from control.xferfcn import TransferFunction from control.statesp import StateSpace @@ -151,13 +151,14 @@ def plant(self, request): 'syscont221':StateSpace([[-.3, 0],[1,0]],[[-1,],[.1,]], [0, -.3], 0)} return plants[request.param] + # check # cont or discrete, vary P I or D # @pytest.mark.parametrize('plant', (syscont, sysdisc1)) @pytest.mark.parametrize('plant', ('syscont', 'sysdisc1'), indirect=True) @pytest.mark.parametrize('gain', ('P', 'I', 'D')) - @pytest.mark.parametrize("kwargs", [{'Kp0':0.01},]) + @pytest.mark.parametrize("kwargs", [{'Kp0':0.1, 'noplot':True},]) def test_pid_designer_1(self, plant, gain, kwargs): - pid_designer(plant, gain, **kwargs) + rootlocus_pid_designer(plant, gain, **kwargs) # input from reference or disturbance @pytest.mark.parametrize('plant', ('syscont', 'syscont221'), indirect=True) @@ -166,5 +167,5 @@ def test_pid_designer_1(self, plant, gain, kwargs): {'input_signal':'r', 'Kp0':0.01, 'derivative_in_feedback_path':True}, {'input_signal':'d', 'Kp0':0.01, 'derivative_in_feedback_path':True},]) def test_pid_designer_2(self, plant, kwargs): - pid_designer(plant, **kwargs) + rootlocus_pid_designer(plant, **kwargs) From 3dca645452bb634f752ed3cd6546ce7a71c01072 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Thu, 4 Nov 2021 15:22:26 -0700 Subject: [PATCH 06/10] fix for github #623 --- control/rlocus.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control/rlocus.py b/control/rlocus.py index 8c3c1c24f..4b1af57f7 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -168,8 +168,8 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, else: if ax is None: ax = plt.gca() - fig = ax.figure - ax.set_title('Root Locus') + fig = ax.figure + ax.set_title('Root Locus') if print_gain and not sisotool: fig.canvas.mpl_connect( From 5c3e82376550bad740814b4a905a27a74d023f19 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Thu, 4 Nov 2021 21:58:17 -0700 Subject: [PATCH 07/10] added pointer to new function to docs --- doc/control.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/control.rst b/doc/control.rst index a3e28881b..4b18cfb53 100644 --- a/doc/control.rst +++ b/doc/control.rst @@ -124,6 +124,7 @@ Control system synthesis lqe mixsyn place + rlocus_pid_designer Model simplification tools ========================== From 2af117130d388b91a3b0b742879e3c6250e232a7 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Thu, 4 Nov 2021 22:16:20 -0700 Subject: [PATCH 08/10] more comprehensive system construction tests --- control/sisotool.py | 7 ++++--- control/tests/sisotool_test.py | 26 +++++++++++++++++--------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/control/sisotool.py b/control/sisotool.py index 7a59f1a1e..0ac585124 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -185,7 +185,7 @@ def _SisotoolUpdate(sys, fig, K, bode_plot_params, tvect=None): def rootlocus_pid_designer(plant, gain='P', sign=+1, input_signal='r', Kp0=0, Ki0=0, Kd0=0, tau=0.01, C_ff=0, derivative_in_feedback_path=False, - noplot=False): + plot=True): """Manual PID controller design based on root locus using Sisotool Uses `Sisotool` to investigate the effect of adding or subtracting an @@ -262,7 +262,8 @@ def rootlocus_pid_designer(plant, gain='P', sign=+1, input_signal='r', derivative_in_feedback_path : bool (optional) Whether to place the derivative term in feedback transfer function `C_b` instead of the forward transfer function `C_f`. - noplot : bool (optional) + plot : bool (optional) + Whether to create Sisotool interactive plot. Returns ---------- @@ -328,6 +329,6 @@ def rootlocus_pid_designer(plant, gain='P', sign=+1, input_signal='r', C_ff, e_summer, u_summer), inplist=['input', input_signal], outlist=['output', 'y']) - if ~noplot: + if plot: sisotool(loop, kvect=(0.,)) return _convert_to_statespace(loop[1, 1]) diff --git a/control/tests/sisotool_test.py b/control/tests/sisotool_test.py index b007d299d..fb2ac46e5 100644 --- a/control/tests/sisotool_test.py +++ b/control/tests/sisotool_test.py @@ -151,18 +151,26 @@ def plant(self, request): 'syscont221':StateSpace([[-.3, 0],[1,0]],[[-1,],[.1,]], [0, -.3], 0)} return plants[request.param] - # check - # cont or discrete, vary P I or D -# @pytest.mark.parametrize('plant', (syscont, sysdisc1)) - @pytest.mark.parametrize('plant', ('syscont', 'sysdisc1'), indirect=True) + # test permutations of system construction without plotting + @pytest.mark.parametrize('plant', ('syscont', 'sysdisc1', 'syscont221'), indirect=True) @pytest.mark.parametrize('gain', ('P', 'I', 'D')) - @pytest.mark.parametrize("kwargs", [{'Kp0':0.1, 'noplot':True},]) - def test_pid_designer_1(self, plant, gain, kwargs): - rootlocus_pid_designer(plant, gain, **kwargs) - + @pytest.mark.parametrize('sign', (1,)) + @pytest.mark.parametrize('input_signal', ('r', 'd')) + @pytest.mark.parametrize('Kp0', (0,)) + @pytest.mark.parametrize('Ki0', (1.,)) + @pytest.mark.parametrize('Kd0', (0.1,)) + @pytest.mark.parametrize('tau', (0.01,)) + @pytest.mark.parametrize('C_ff', (0, 1,)) + @pytest.mark.parametrize('derivative_in_feedback_path', (True, False,)) + @pytest.mark.parametrize("kwargs", [{'plot':False},]) + def test_pid_designer_1(self, plant, gain, sign, input_signal, Kp0, Ki0, Kd0, tau, C_ff, + derivative_in_feedback_path, kwargs): + rootlocus_pid_designer(plant, gain, sign, input_signal, Kp0, Ki0, Kd0, tau, C_ff, + derivative_in_feedback_path, **kwargs) + + # test creation of sisotool plot # input from reference or disturbance @pytest.mark.parametrize('plant', ('syscont', 'syscont221'), indirect=True) -# @pytest.mark.parametrize('plant', (syscont, syscont221)) @pytest.mark.parametrize("kwargs", [ {'input_signal':'r', 'Kp0':0.01, 'derivative_in_feedback_path':True}, {'input_signal':'d', 'Kp0':0.01, 'derivative_in_feedback_path':True},]) From 746e089e3d93639a69c9dfc87ac2df29b95ecc4d Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Thu, 4 Nov 2021 22:23:07 -0700 Subject: [PATCH 09/10] return loop transfer function as statespace --- control/sisotool.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/control/sisotool.py b/control/sisotool.py index 0ac585124..2cf3199b7 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -8,7 +8,7 @@ from .statesp import ss from .bdalg import append, connect from .iosys import tf2io, ss2io, summing_junction, interconnect -from control.statesp import _convert_to_statespace +from control.statesp import _convert_to_statespace, StateSpace from control.lti import common_timebase, isctime import matplotlib import matplotlib.pyplot as plt @@ -331,4 +331,5 @@ def rootlocus_pid_designer(plant, gain='P', sign=+1, input_signal='r', outlist=['output', 'y']) if plot: sisotool(loop, kvect=(0.,)) - return _convert_to_statespace(loop[1, 1]) + return StateSpace(loop[1, 1].A, loop[1, 1].B, loop[1, 1].C, loop[1, 1].D, + loop[1, 1].dt) From 4ee9fd172906cf7beac77be396ced251e79eb90c Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Fri, 5 Nov 2021 14:34:17 -0700 Subject: [PATCH 10/10] small docstring fix --- control/sisotool.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/control/sisotool.py b/control/sisotool.py index 2cf3199b7..5accd1453 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -197,9 +197,9 @@ def rootlocus_pid_designer(plant, gain='P', sign=+1, input_signal='r', When first run, `deltaK` is set to 0; click on a branch of the root locus plot to try a different value. Each click updates plots and prints the corresponding `deltaK`. To tune all three PID gains, repeatedly call - `pid_designer`, and select a different `gain` each time (`'P'`, `'I'`, - or `'D'`). Make sure to add the resulting `deltaK` to your chosen initial - gain on the next iteration. + `rootlocus_pid_designer`, and select a different `gain` each time (`'P'`, + `'I'`, or `'D'`). Make sure to add the resulting `deltaK` to your chosen + initial gain on the next iteration. Example: to examine the effect of varying `Kp` starting from an intial value of 10, use the arguments `gain='P', Kp0=10`. Suppose a `deltaK` @@ -331,5 +331,5 @@ def rootlocus_pid_designer(plant, gain='P', sign=+1, input_signal='r', outlist=['output', 'y']) if plot: sisotool(loop, kvect=(0.,)) - return StateSpace(loop[1, 1].A, loop[1, 1].B, loop[1, 1].C, loop[1, 1].D, - loop[1, 1].dt) + cl = loop[1, 1] # closed loop transfer function with initial gains + return StateSpace(cl.A, cl.B, cl.C, cl.D, cl.dt) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy