From 385405086bc0010e634c77b877240814c4bd11b7 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Wed, 7 Dec 2022 11:20:53 -0800 Subject: [PATCH 1/6] allow sisotool to receive kvect as a singleton rather than always an array --- control/sisotool.py | 4 ++++ control/tests/sisotool_test.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/control/sisotool.py b/control/sisotool.py index 781fabf40..7f5e2fc69 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -9,6 +9,7 @@ from .bdalg import append, connect from .iosys import tf2io, ss2io, summing_junction, interconnect from control.statesp import _convert_to_statespace, StateSpace +import numpy as np import matplotlib.pyplot as plt import warnings @@ -101,6 +102,9 @@ def sisotool(sys, kvect=None, xlim_rlocus=None, ylim_rlocus=None, 'margins': margins_bode } + # make sure kvect is an array + if kvect is not None and ~hasattr(kvect, '__len__'): + kvect = np.atleast_1d(kvect) # First time call to setup the bode and step response plots _SisotoolUpdate(sys, fig, 1 if kvect is None else kvect[0], bode_plot_params) diff --git a/control/tests/sisotool_test.py b/control/tests/sisotool_test.py index a1f468eea..beb7ee098 100644 --- a/control/tests/sisotool_test.py +++ b/control/tests/sisotool_test.py @@ -136,6 +136,16 @@ def test_sisotool_tvect(self, tsys): bode_plot_params=dict(), tvect=tvect) assert_array_almost_equal(tvect, ax_step.lines[0].get_data()[0]) + @pytest.mark.skipif(plt.get_current_fig_manager().toolbar is None, + reason="Requires the zoom toolbar") + def test_sisotool_kvect(self, tsys): + # test supply kvect + kvect = np.linspace(0, 1, 10) + # check if it can receive an array + sisotool(tsys, kvect=kvect) + # check if it can receive a singleton + sisotool(tsys, kvect=1) + def test_sisotool_mimo(self, sys222, sys221): # a 2x2 should not raise an error: From feb901033f095038054f4869fdb7bf14ae225d3b Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Fri, 9 Dec 2022 10:03:18 -0800 Subject: [PATCH 2/6] improved docstring for kvect in sisotool --- control/matlab/wrappers.py | 2 +- control/sisotool.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/control/matlab/wrappers.py b/control/matlab/wrappers.py index 8eafdaad2..4f9d97e31 100644 --- a/control/matlab/wrappers.py +++ b/control/matlab/wrappers.py @@ -117,7 +117,7 @@ def nyquist(*args, **kwargs): def _parse_freqplot_args(*args): """Parse arguments to frequency plot routines (bode, nyquist)""" syslist, plotstyle, omega, other = [], [], None, {} - i = 0; + i = 0 while i < len(args): # Check to see if this is a system of some sort if issys(args[i]): diff --git a/control/sisotool.py b/control/sisotool.py index 7f5e2fc69..d4d4b9d68 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -37,8 +37,13 @@ def sisotool(sys, kvect=None, xlim_rlocus=None, ylim_rlocus=None, the step response. This allows you to see the step responses of more complex systems, for example, systems with a feedforward path into the plant or in which the gain appears in the feedback path. - kvect : list or ndarray, optional - List of gains to use for plotting root locus + kvect : float or array_like, optional + List of gains to use for plotting root locus. If only one value is + provided, the set of gains in the root locus plot is calculated + automatically, and kvect is interpreted as if it was the value of + the gain associated with the first mouse click on the root locus + plot. This is useful if it is not possible to use interactive + plotting. xlim_rlocus : tuple or list, optional control of x-axis range, normally with tuple (see :doc:`matplotlib:api/axes_api`). From e808adb08d6d85d24584d807e9d9a3e574068090 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Fri, 9 Dec 2022 11:25:33 -0800 Subject: [PATCH 3/6] docstring improvements, pep8 cleanup, more descriptive names for internal variables --- control/rlocus.py | 69 ++++++++++++++++++++++----------------------- control/sisotool.py | 23 +++++++++------ 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/control/rlocus.py b/control/rlocus.py index 9d531de94..facb9251a 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -88,7 +88,7 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, ---------- sys : LTI object Linear input/output systems (SISO only, for now). - kvect : list or ndarray, optional + kvect : float or array_like, optional List of gains to use in computing diagram. xlim : tuple or list, optional Set limits of x axis, normally with tuple @@ -110,10 +110,11 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, Returns ------- - rlist : ndarray - Computed root locations, given as a 2D array - klist : ndarray or list - Gains used. Same as klist keyword argument if provided. + roots : ndarray + Closed-loop root locations, arranged in which each row corresponds + to a gain in gains + gains : ndarray + Gains used. Same as kvect keyword argument if provided. Notes ----- @@ -145,10 +146,12 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, print_gain = config._get_param( 'rlocus', 'print_gain', print_gain, _rlocus_defaults) - sys_loop = sys if sys.issiso() else sys[0, 0] + if not sys.issiso(): + raise ControlMIMONotImplemented( + 'sys must be single-input single-output (SISO)') # Convert numerator and denominator to polynomials if they aren't - (nump, denp) = _systopoly1d(sys_loop) + (nump, denp) = _systopoly1d(sys) # if discrete-time system and if xlim and ylim are not given, # that we a view of the unit circle @@ -158,12 +161,13 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, xlim = (-1.3, 1.3) if kvect is None: - start_mat = _RLFindRoots(nump, denp, [1]) - kvect, mymat, xlim, ylim = _default_gains(nump, denp, xlim, ylim) + start_roots = _RLFindRoots(nump, denp, 1) + kvect, root_array, xlim, ylim = _default_gains(nump, denp, xlim, ylim) else: - start_mat = _RLFindRoots(nump, denp, [kvect[0]]) - mymat = _RLFindRoots(nump, denp, kvect) - mymat = _RLSortRoots(mymat) + kvect = np.atleast_1d(kvect) + start_roots = _RLFindRoots(nump, denp, kvect[0]) + root_array = _RLFindRoots(nump, denp, kvect) + root_array = _RLSortRoots(root_array) # Check for sisotool mode sisotool = False if 'sisotool' not in kwargs else True @@ -190,10 +194,10 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, ax_rlocus=fig.axes[0], plotstr=plotstr)) elif sisotool: fig.axes[1].plot( - [root.real for root in start_mat], - [root.imag for root in start_mat], + [root.real for root in start_roots], + [root.imag for root in start_roots], marker='s', markersize=6, zorder=20, color='k', label='gain_point') - s = start_mat[0][0] + s = start_roots[0][0] if isdtime(sys, strict=True): zeta = -np.cos(np.angle(np.log(s))) else: @@ -229,7 +233,7 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, ax.plot(real(zeros), imag(zeros), 'o') # Now plot the loci - for index, col in enumerate(mymat.T): + for index, col in enumerate(root_array.T): ax.plot(real(col), imag(col), plotstr, label='rootlocus') # Set up plot axes and labels @@ -257,7 +261,7 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, (0, 0), radius=1.0, linestyle=':', edgecolor='k', linewidth=0.75, fill=False, zorder=-20)) - return mymat, kvect + return root_array, kvect def _default_gains(num, den, xlim, ylim, zoom_xlim=None, zoom_ylim=None): @@ -509,28 +513,27 @@ def _RLFindRoots(nump, denp, kvect): """Find the roots for the root locus.""" # Convert numerator and denominator to polynomials if they aren't roots = [] - for k in np.array(kvect, ndmin=1): + for k in np.atleast_1d(kvect): curpoly = denp + k * nump curroots = curpoly.r if len(curroots) < denp.order: # if I have fewer poles than open loop, it is because i have # one at infinity - curroots = np.insert(curroots, len(curroots), np.inf) + curroots = np.append(curroots, np.inf) curroots.sort() roots.append(curroots) - mymat = row_stack(roots) - return mymat + return row_stack(roots) -def _RLSortRoots(mymat): - """Sort the roots from sys._RLFindRoots, so that the root +def _RLSortRoots(roots): + """Sort the roots from _RLFindRoots, so that the root locus doesn't show weird pseudo-branches as roots jump from one branch to another.""" - sorted = zeros_like(mymat) - for n, row in enumerate(mymat): + sorted = zeros_like(roots) + for n, row in enumerate(roots): if n == 0: sorted[n, :] = row else: @@ -539,7 +542,7 @@ def _RLSortRoots(mymat): # previous row available = list(range(len(prevrow))) for elem in row: - evect = elem-prevrow[available] + evect = elem - prevrow[available] ind1 = abs(evect).argmin() ind = available.pop(ind1) sorted[n, ind] = elem @@ -549,9 +552,7 @@ def _RLSortRoots(mymat): def _RLZoomDispatcher(event, sys, ax_rlocus, plotstr): """Rootlocus plot zoom dispatcher""" - sys_loop = sys if sys.issiso() else sys[0,0] - - nump, denp = _systopoly1d(sys_loop) + nump, denp = _systopoly1d(sys) xlim, ylim = ax_rlocus.get_xlim(), ax_rlocus.get_ylim() kvect, mymat, xlim, ylim = _default_gains( @@ -583,9 +584,7 @@ def _RLClickDispatcher(event, sys, fig, ax_rlocus, plotstr, sisotool=False, def _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, sisotool=False): """Display root-locus gain feedback point for clicks on root-locus plot""" - sys_loop = sys if sys.issiso() else sys[0,0] - - (nump, denp) = _systopoly1d(sys_loop) + (nump, denp) = _systopoly1d(sys) xlim = ax_rlocus.get_xlim() ylim = ax_rlocus.get_ylim() @@ -596,10 +595,10 @@ def _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, sisotool=False): # Catch type error when event click is in the figure but not in an axis try: s = complex(event.xdata, event.ydata) - K = -1. / sys_loop(s) - K_xlim = -1. / sys_loop( + K = -1. / sys(s) + K_xlim = -1. / sys( complex(event.xdata + 0.05 * abs(xlim[1] - xlim[0]), event.ydata)) - K_ylim = -1. / sys_loop( + K_ylim = -1. / sys( complex(event.xdata, event.ydata + 0.05 * abs(ylim[1] - ylim[0]))) except TypeError: diff --git a/control/sisotool.py b/control/sisotool.py index d4d4b9d68..018514d31 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -29,14 +29,19 @@ def sisotool(sys, kvect=None, xlim_rlocus=None, ylim_rlocus=None, sys : LTI object Linear input/output systems. If sys is SISO, use the same system for the root locus and step response. If it is desired to - see a different step response than feedback(K*loop,1), sys can be - provided as a two-input, two-output system (e.g. by using - :func:`bdgalg.connect' or :func:`iosys.interconnect`). Sisotool - inserts the negative of the selected gain K between the first output - and first input and uses the second input and output for computing - the step response. This allows you to see the step responses of more - complex systems, for example, systems with a feedforward path into the - plant or in which the gain appears in the feedback path. + see a different step response than feedback(K*sys,1), such as a + disturbance response, sys can be provided as a two-input, two-output + system (e.g. by using :func:`bdgalg.connect' or + :func:`iosys.interconnect`). For two-input, two-output + system, sisotool inserts the negative of the selected gain K between + the first output and first input and uses the second input and output + for computing the step response. To see the disturbance response, + configure your plant to have as its second input the disturbance input. + To view the step response with a feedforward controller, give your + plant two identical inputs, and sum your feedback controller and your + feedforward controller and multiply them into your plant's second + input. It is also possible to accomodate a system with a gain in the + feedback. kvect : float or array_like, optional List of gains to use for plotting root locus. If only one value is provided, the set of gains in the root locus plot is calculated @@ -115,7 +120,7 @@ def sisotool(sys, kvect=None, xlim_rlocus=None, ylim_rlocus=None, 1 if kvect is None else kvect[0], bode_plot_params) # Setup the root-locus plot window - root_locus(sys, kvect=kvect, xlim=xlim_rlocus, + root_locus(sys[0,0], kvect=kvect, xlim=xlim_rlocus, ylim=ylim_rlocus, plotstr=plotstr_rlocus, grid=rlocus_grid, fig=fig, bode_plot_params=bode_plot_params, tvect=tvect, sisotool=True) From 4107950d57e9722fd66eae32ecd0880c48aa3090 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Fri, 9 Dec 2022 14:50:53 -0800 Subject: [PATCH 4/6] introduce new keyword for sisotool: initial_gain, which denotes gain that the plots start with. Deprecate kvect kwarg with warning. rlocus now doesn't recomupte kvect if it is passed --- control/config.py | 3 ++ control/rlocus.py | 60 ++++++++++++++++++++-------------- control/sisotool.py | 35 +++++++++++--------- control/tests/sisotool_test.py | 13 +++----- 4 files changed, 63 insertions(+), 48 deletions(-) diff --git a/control/config.py b/control/config.py index ccee252fc..37763a6b8 100644 --- a/control/config.py +++ b/control/config.py @@ -97,6 +97,9 @@ def reset_defaults(): from .rlocus import _rlocus_defaults defaults.update(_rlocus_defaults) + from .sisotool import _sisotool_defaults + defaults.update(_sisotool_defaults) + from .namedio import _namedio_defaults defaults.update(_namedio_defaults) diff --git a/control/rlocus.py b/control/rlocus.py index facb9251a..c6ed717e2 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -61,6 +61,7 @@ from .sisotool import _SisotoolUpdate from .grid import sgrid, zgrid from . import config +import warnings __all__ = ['root_locus', 'rlocus'] @@ -76,7 +77,7 @@ # Main function: compute a root locus diagram def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr=None, plot=True, print_gain=None, grid=None, ax=None, - **kwargs): + initial_gain=None, **kwargs): """Root locus plot @@ -88,8 +89,8 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, ---------- sys : LTI object Linear input/output systems (SISO only, for now). - kvect : float or array_like, optional - List of gains to use in computing diagram. + kvect : array_like, optional + Gains to use in computing plot of closed-loop poles. xlim : tuple or list, optional Set limits of x axis, normally with tuple (see :doc:`matplotlib:api/axes_api`). @@ -107,6 +108,8 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, If True plot omega-damping grid. Default is False. ax : :class:`matplotlib.axes.Axes` Axes on which to create root locus plot + initial_gain : float, optional + Used by :func:`sisotool` to indicate initial gain. Returns ------- @@ -126,7 +129,6 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, """ # Check to see if legacy 'Plot' keyword was used if 'Plot' in kwargs: - import warnings warnings.warn("'Plot' keyword is deprecated in root_locus; " "use 'plot'", FutureWarning) # Map 'Plot' keyword to 'plot' keyword @@ -134,7 +136,6 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, # Check to see if legacy 'PrintGain' keyword was used if 'PrintGain' in kwargs: - import warnings warnings.warn("'PrintGain' keyword is deprecated in root_locus; " "use 'print_gain'", FutureWarning) # Map 'PrintGain' keyword to 'print_gain' keyword @@ -146,12 +147,17 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, print_gain = config._get_param( 'rlocus', 'print_gain', print_gain, _rlocus_defaults) - if not sys.issiso(): + # Check for sisotool mode + sisotool = kwargs.get('sisotool', False) + + # make sure siso. sisotool has different requirements + if not sys.issiso() and not sisotool: raise ControlMIMONotImplemented( 'sys must be single-input single-output (SISO)') + sys_loop = sys[0,0] # Convert numerator and denominator to polynomials if they aren't - (nump, denp) = _systopoly1d(sys) + (nump, denp) = _systopoly1d(sys_loop) # if discrete-time system and if xlim and ylim are not given, # that we a view of the unit circle @@ -161,16 +167,16 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, xlim = (-1.3, 1.3) if kvect is None: - start_roots = _RLFindRoots(nump, denp, 1) kvect, root_array, xlim, ylim = _default_gains(nump, denp, xlim, ylim) + recompute_on_zoom = True else: kvect = np.atleast_1d(kvect) - start_roots = _RLFindRoots(nump, denp, kvect[0]) root_array = _RLFindRoots(nump, denp, kvect) root_array = _RLSortRoots(root_array) + recompute_on_zoom = False - # Check for sisotool mode - sisotool = False if 'sisotool' not in kwargs else True + if sisotool: + start_roots = _RLFindRoots(nump, denp, initial_gain) # Make sure there were no extraneous keywords if not sisotool and kwargs: @@ -204,7 +210,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, kvect[0], zeta), + (s.real, s.imag, initial_gain, zeta), fontsize=12 if int(mpl.__version__[0]) == 1 else 10) fig.canvas.mpl_connect( 'button_release_event', @@ -214,14 +220,16 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, bode_plot_params=kwargs['bode_plot_params'], tvect=kwargs['tvect'])) - # zoom update on xlim/ylim changed, only then data on new limits - # is available, i.e., cannot combine with _RLClickDispatcher - dpfun = partial( - _RLZoomDispatcher, sys=sys, ax_rlocus=ax, plotstr=plotstr) - # TODO: the next too lines seem to take a long time to execute - # TODO: is there a way to speed them up? (RMM, 6 Jun 2019) - ax.callbacks.connect('xlim_changed', dpfun) - ax.callbacks.connect('ylim_changed', dpfun) + + if recompute_on_zoom: + # update gains and roots when xlim/ylim change. Only then are + # data on available. I.e., cannot combine with _RLClickDispatcher + dpfun = partial( + _RLZoomDispatcher, sys=sys, ax_rlocus=ax, plotstr=plotstr) + # TODO: the next too lines seem to take a long time to execute + # TODO: is there a way to speed them up? (RMM, 6 Jun 2019) + ax.callbacks.connect('xlim_changed', dpfun) + ax.callbacks.connect('ylim_changed', dpfun) # plot open loop poles poles = array(denp.r) @@ -552,7 +560,8 @@ def _RLSortRoots(roots): def _RLZoomDispatcher(event, sys, ax_rlocus, plotstr): """Rootlocus plot zoom dispatcher""" - nump, denp = _systopoly1d(sys) + sys_loop = sys[0,0] + nump, denp = _systopoly1d(sys_loop) xlim, ylim = ax_rlocus.get_xlim(), ax_rlocus.get_ylim() kvect, mymat, xlim, ylim = _default_gains( @@ -584,7 +593,8 @@ def _RLClickDispatcher(event, sys, fig, ax_rlocus, plotstr, sisotool=False, def _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, sisotool=False): """Display root-locus gain feedback point for clicks on root-locus plot""" - (nump, denp) = _systopoly1d(sys) + sys_loop = sys[0,0] + (nump, denp) = _systopoly1d(sys_loop) xlim = ax_rlocus.get_xlim() ylim = ax_rlocus.get_ylim() @@ -595,10 +605,10 @@ def _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, sisotool=False): # Catch type error when event click is in the figure but not in an axis try: s = complex(event.xdata, event.ydata) - K = -1. / sys(s) - K_xlim = -1. / sys( + K = -1. / sys_loop(s) + K_xlim = -1. / sys_loop( complex(event.xdata + 0.05 * abs(xlim[1] - xlim[0]), event.ydata)) - K_ylim = -1. / sys( + K_ylim = -1. / sys_loop( complex(event.xdata, event.ydata + 0.05 * abs(ylim[1] - ylim[0]))) except TypeError: diff --git a/control/sisotool.py b/control/sisotool.py index 018514d31..ae2497b66 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -9,14 +9,19 @@ from .bdalg import append, connect from .iosys import tf2io, ss2io, summing_junction, interconnect from control.statesp import _convert_to_statespace, StateSpace +from . import config import numpy as np import matplotlib.pyplot as plt import warnings -def sisotool(sys, kvect=None, xlim_rlocus=None, ylim_rlocus=None, +_sisotool_defaults = { + 'sisotool.initial_gain': 1 +} + +def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None, plotstr_rlocus='C0', rlocus_grid=False, omega=None, dB=None, Hz=None, deg=None, omega_limits=None, omega_num=None, - margins_bode=True, tvect=None): + margins_bode=True, tvect=None, kvect=None): """ Sisotool style collection of plots inspired by MATLAB's sisotool. The left two plots contain the bode magnitude and phase diagrams. @@ -42,13 +47,9 @@ def sisotool(sys, kvect=None, xlim_rlocus=None, ylim_rlocus=None, feedforward controller and multiply them into your plant's second input. It is also possible to accomodate a system with a gain in the feedback. - kvect : float or array_like, optional - List of gains to use for plotting root locus. If only one value is - provided, the set of gains in the root locus plot is calculated - automatically, and kvect is interpreted as if it was the value of - the gain associated with the first mouse click on the root locus - plot. This is useful if it is not possible to use interactive - plotting. + initial_gain : float, optional + Initial gain to use for plotting root locus. Defaults to 1 + (config.defaults['sisotool.initial_gain']). xlim_rlocus : tuple or list, optional control of x-axis range, normally with tuple (see :doc:`matplotlib:api/axes_api`). @@ -112,15 +113,19 @@ def sisotool(sys, kvect=None, xlim_rlocus=None, ylim_rlocus=None, 'margins': margins_bode } - # make sure kvect is an array - if kvect is not None and ~hasattr(kvect, '__len__'): - kvect = np.atleast_1d(kvect) + # Check to see if legacy 'PrintGain' keyword was used + if kvect is not None: + warnings.warn("'kvect' keyword is deprecated in sisotool; " + "use 'initial_gain' instead", FutureWarning) + initial_gain = np.atleast1d(kvect)[0] + initial_gain = config._get_param('sisotool', 'initial_gain', + initial_gain, _sisotool_defaults) + # First time call to setup the bode and step response plots - _SisotoolUpdate(sys, fig, - 1 if kvect is None else kvect[0], bode_plot_params) + _SisotoolUpdate(sys, fig, initial_gain, bode_plot_params) # Setup the root-locus plot window - root_locus(sys[0,0], kvect=kvect, xlim=xlim_rlocus, + root_locus(sys, initial_gain=initial_gain, xlim=xlim_rlocus, ylim=ylim_rlocus, plotstr=plotstr_rlocus, grid=rlocus_grid, fig=fig, bode_plot_params=bode_plot_params, tvect=tvect, sisotool=True) diff --git a/control/tests/sisotool_test.py b/control/tests/sisotool_test.py index beb7ee098..d4a291052 100644 --- a/control/tests/sisotool_test.py +++ b/control/tests/sisotool_test.py @@ -138,14 +138,11 @@ def test_sisotool_tvect(self, tsys): @pytest.mark.skipif(plt.get_current_fig_manager().toolbar is None, reason="Requires the zoom toolbar") - def test_sisotool_kvect(self, tsys): - # test supply kvect - kvect = np.linspace(0, 1, 10) - # check if it can receive an array - sisotool(tsys, kvect=kvect) - # check if it can receive a singleton - sisotool(tsys, kvect=1) - + def test_sisotool_initial_gain(self, tsys): + sisotool(tsys, initial_gain=1.2) + # kvect keyword should give deprecation warning + with pytest.warns(FutureWarning): + sisotool(tsys, kvect=1.2) def test_sisotool_mimo(self, sys222, sys221): # a 2x2 should not raise an error: From eadd496bf5810273b89cd7315663c36ebd9e8ace Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Fri, 9 Dec 2022 14:55:56 -0800 Subject: [PATCH 5/6] test to make sure kvect gains are respected even if plotting --- control/tests/rlocus_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py index 4fbe70c4f..a25928e27 100644 --- a/control/tests/rlocus_test.py +++ b/control/tests/rlocus_test.py @@ -54,6 +54,12 @@ def testRootLocus(self, sys): np.testing.assert_allclose(klist, k_out) self.check_cl_poles(sys, roots, klist) + # now check with plotting + roots, k_out = root_locus(sys, klist) + np.testing.assert_equal(len(roots), len(klist)) + np.testing.assert_allclose(klist, k_out) + self.check_cl_poles(sys, roots, klist) + def test_without_gains(self, sys): roots, kvect = root_locus(sys, plot=False) self.check_cl_poles(sys, roots, kvect) From 15543ffe9d00cdc734dc7398f5f765444b1226fa Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Fri, 9 Dec 2022 15:02:11 -0800 Subject: [PATCH 6/6] rename mymat to root_array for clarity --- control/rlocus.py | 72 ++++++++++++++++++++++----------------------- control/sisotool.py | 2 +- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/control/rlocus.py b/control/rlocus.py index c6ed717e2..53c5c9031 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -286,8 +286,8 @@ def _default_gains(num, den, xlim, ylim, zoom_xlim=None, zoom_ylim=None): kvect = np.hstack((np.linspace(0, kmax, 50), np.real(k_break))) kvect.sort() - mymat = _RLFindRoots(num, den, kvect) - mymat = _RLSortRoots(mymat) + root_array = _RLFindRoots(num, den, kvect) + root_array = _RLSortRoots(root_array) open_loop_poles = den.roots open_loop_zeros = num.roots @@ -297,13 +297,13 @@ def _default_gains(num, den, xlim, ylim, zoom_xlim=None, zoom_ylim=None): open_loop_zeros, np.ones(open_loop_poles.size - open_loop_zeros.size) * open_loop_zeros[-1]) - mymat_xl = np.append(mymat, open_loop_zeros_xl) + root_array_xl = np.append(root_array, open_loop_zeros_xl) else: - mymat_xl = mymat + root_array_xl = root_array singular_points = np.concatenate((num.roots, den.roots), axis=0) important_points = np.concatenate((singular_points, real_break), axis=0) important_points = np.concatenate((important_points, np.zeros(2)), axis=0) - mymat_xl = np.append(mymat_xl, important_points) + root_array_xl = np.append(root_array_xl, important_points) false_gain = float(den.coeffs[0]) / float(num.coeffs[0]) if false_gain < 0 and not den.order > num.order: @@ -312,27 +312,27 @@ def _default_gains(num, den, xlim, ylim, zoom_xlim=None, zoom_ylim=None): "with equal order of numerator and denominator.") if xlim is None and false_gain > 0: - x_tolerance = 0.05 * (np.max(np.real(mymat_xl)) - - np.min(np.real(mymat_xl))) - xlim = _ax_lim(mymat_xl) + x_tolerance = 0.05 * (np.max(np.real(root_array_xl)) + - np.min(np.real(root_array_xl))) + xlim = _ax_lim(root_array_xl) elif xlim is None and false_gain < 0: axmin = np.min(np.real(important_points)) \ - (np.max(np.real(important_points)) - np.min(np.real(important_points))) - axmin = np.min(np.array([axmin, np.min(np.real(mymat_xl))])) + axmin = np.min(np.array([axmin, np.min(np.real(root_array_xl))])) axmax = np.max(np.real(important_points)) \ + np.max(np.real(important_points)) \ - np.min(np.real(important_points)) - axmax = np.max(np.array([axmax, np.max(np.real(mymat_xl))])) + axmax = np.max(np.array([axmax, np.max(np.real(root_array_xl))])) xlim = [axmin, axmax] x_tolerance = 0.05 * (axmax - axmin) else: x_tolerance = 0.05 * (xlim[1] - xlim[0]) if ylim is None: - y_tolerance = 0.05 * (np.max(np.imag(mymat_xl)) - - np.min(np.imag(mymat_xl))) - ylim = _ax_lim(mymat_xl * 1j) + y_tolerance = 0.05 * (np.max(np.imag(root_array_xl)) + - np.min(np.imag(root_array_xl))) + ylim = _ax_lim(root_array_xl * 1j) else: y_tolerance = 0.05 * (ylim[1] - ylim[0]) @@ -345,7 +345,7 @@ def _default_gains(num, den, xlim, ylim, zoom_xlim=None, zoom_ylim=None): tolerance = x_tolerance else: tolerance = np.min([x_tolerance, y_tolerance]) - indexes_too_far = _indexes_filt(mymat, tolerance, zoom_xlim, zoom_ylim) + indexes_too_far = _indexes_filt(root_array, tolerance, zoom_xlim, zoom_ylim) # Add more points into the root locus for points that are too far apart while len(indexes_too_far) > 0 and kvect.size < 5000: @@ -354,27 +354,27 @@ def _default_gains(num, den, xlim, ylim, zoom_xlim=None, zoom_ylim=None): new_gains = np.linspace(kvect[index], kvect[index + 1], 5) new_points = _RLFindRoots(num, den, new_gains[1:4]) kvect = np.insert(kvect, index + 1, new_gains[1:4]) - mymat = np.insert(mymat, index + 1, new_points, axis=0) + root_array = np.insert(root_array, index + 1, new_points, axis=0) - mymat = _RLSortRoots(mymat) - indexes_too_far = _indexes_filt(mymat, tolerance, zoom_xlim, zoom_ylim) + root_array = _RLSortRoots(root_array) + indexes_too_far = _indexes_filt(root_array, tolerance, zoom_xlim, zoom_ylim) new_gains = kvect[-1] * np.hstack((np.logspace(0, 3, 4))) new_points = _RLFindRoots(num, den, new_gains[1:4]) kvect = np.append(kvect, new_gains[1:4]) - mymat = np.concatenate((mymat, new_points), axis=0) - mymat = _RLSortRoots(mymat) - return kvect, mymat, xlim, ylim + root_array = np.concatenate((root_array, new_points), axis=0) + root_array = _RLSortRoots(root_array) + return kvect, root_array, xlim, ylim -def _indexes_filt(mymat, tolerance, zoom_xlim=None, zoom_ylim=None): +def _indexes_filt(root_array, tolerance, zoom_xlim=None, zoom_ylim=None): """Calculate the distance between points and return the indexes. Filter the indexes so only the resolution of points within the xlim and ylim is improved when zoom is used. """ - distance_points = np.abs(np.diff(mymat, axis=0)) + distance_points = np.abs(np.diff(root_array, axis=0)) indexes_too_far = list(np.unique(np.where(distance_points > tolerance)[0])) if zoom_xlim is not None and zoom_ylim is not None: @@ -386,23 +386,23 @@ def _indexes_filt(mymat, tolerance, zoom_xlim=None, zoom_ylim=None): indexes_too_far_filtered = [] for index in indexes_too_far_zoom: - for point in mymat[index]: + for point in root_array[index]: if (zoom_xlim[0] <= point.real <= zoom_xlim[1]) and \ (zoom_ylim[0] <= point.imag <= zoom_ylim[1]): indexes_too_far_filtered.append(index) break # Check if zoom box is not overshot & insert points where neccessary - if len(indexes_too_far_filtered) == 0 and len(mymat) < 500: + if len(indexes_too_far_filtered) == 0 and len(root_array) < 500: limits = [zoom_xlim[0], zoom_xlim[1], zoom_ylim[0], zoom_ylim[1]] for index, limit in enumerate(limits): if index <= 1: - asign = np.sign(real(mymat)-limit) + asign = np.sign(real(root_array)-limit) else: - asign = np.sign(imag(mymat) - limit) + asign = np.sign(imag(root_array) - limit) signchange = ((np.roll(asign, 1, axis=0) - asign) != 0).astype(int) - signchange[0] = np.zeros((len(mymat[0]))) + signchange[0] = np.zeros((len(root_array[0]))) if len(np.where(signchange == 1)[0]) > 0: indexes_too_far_filtered.append( np.where(signchange == 1)[0][0]-1) @@ -411,7 +411,7 @@ def _indexes_filt(mymat, tolerance, zoom_xlim=None, zoom_ylim=None): if indexes_too_far_filtered[0] != 0: indexes_too_far_filtered.insert( 0, indexes_too_far_filtered[0]-1) - if not indexes_too_far_filtered[-1] + 1 >= len(mymat) - 2: + if not indexes_too_far_filtered[-1] + 1 >= len(root_array) - 2: indexes_too_far_filtered.append( indexes_too_far_filtered[-1] + 1) @@ -441,10 +441,10 @@ def _break_points(num, den): return k_break, real_break_pts -def _ax_lim(mymat): +def _ax_lim(root_array): """Utility to get the axis limits""" - axmin = np.min(np.real(mymat)) - axmax = np.max(np.real(mymat)) + axmin = np.min(np.real(root_array)) + axmax = np.max(np.real(root_array)) if axmax != axmin: deltax = (axmax - axmin) * 0.02 else: @@ -564,11 +564,11 @@ def _RLZoomDispatcher(event, sys, ax_rlocus, plotstr): nump, denp = _systopoly1d(sys_loop) xlim, ylim = ax_rlocus.get_xlim(), ax_rlocus.get_ylim() - kvect, mymat, xlim, ylim = _default_gains( + kvect, root_array, xlim, ylim = _default_gains( nump, denp, xlim=None, ylim=None, zoom_xlim=xlim, zoom_ylim=ylim) _removeLine('rootlocus', ax_rlocus) - for i, col in enumerate(mymat.T): + for i, col in enumerate(root_array.T): ax_rlocus.plot(real(col), imag(col), plotstr, label='rootlocus', scalex=False, scaley=False) @@ -640,10 +640,10 @@ def _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, sisotool=False): # Visualise clicked point, display all roots for sisotool mode if sisotool: - mymat = _RLFindRoots(nump, denp, K.real) + root_array = _RLFindRoots(nump, denp, K.real) ax_rlocus.plot( - [root.real for root in mymat], - [root.imag for root in mymat], + [root.real for root in root_array], + [root.imag for root in root_array], marker='s', markersize=6, zorder=20, label='gain_point', color='k') else: ax_rlocus.plot(s.real, s.imag, 'k.', marker='s', markersize=8, diff --git a/control/sisotool.py b/control/sisotool.py index ae2497b66..d3f597d77 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -117,7 +117,7 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None, if kvect is not None: warnings.warn("'kvect' keyword is deprecated in sisotool; " "use 'initial_gain' instead", FutureWarning) - initial_gain = np.atleast1d(kvect)[0] + initial_gain = np.atleast_1d(kvect)[0] initial_gain = config._get_param('sisotool', 'initial_gain', initial_gain, _sisotool_defaults)