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) 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