From 6b5cafc5078aa3adc9ad610e66f96165e38c9844 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 2 Aug 2023 22:22:39 -0700 Subject: [PATCH 01/16] refactoring of pzmap into response/plot + unit test changes --- control/pzmap.py | 281 +++++++++++++++++++++++++---------- control/rlocus.py | 47 ------ control/tests/kwargs_test.py | 1 + control/tests/pzmap_test.py | 14 +- control/tests/rlocus_test.py | 7 - 5 files changed, 212 insertions(+), 138 deletions(-) diff --git a/control/pzmap.py b/control/pzmap.py index 5ee3d37c7..47a021a2b 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -1,85 +1,118 @@ # pzmap.py - computations involving poles and zeros # -# Author: Richard M. Murray +# Original author: Richard M. Murray # Date: 7 Sep 2009 # # This file contains functions that compute poles, zeros and related # quantities for a linear system. # -# Copyright (c) 2009 by California Institute of Technology -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# 3. Neither the name of the California Institute of Technology nor -# the names of its contributors may be used to endorse or promote -# products derived from this software without specific prior -# written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH -# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# +import numpy as np from numpy import real, imag, linspace, exp, cos, sin, sqrt +import matplotlib.pyplot as plt from math import pi +import itertools +import warnings + from .lti import LTI from .iosys import isdtime, isctime from .grid import sgrid, zgrid, nogrid +from .statesp import StateSpace +from .xferfcn import TransferFunction +from .freqplot import _freqplot_defaults, _get_line_labels from . import config -__all__ = ['pzmap'] +__all__ = ['pzmap_response', 'pzmap_plot', 'pzmap'] # Define default parameter values for this module _pzmap_defaults = { 'pzmap.grid': False, # Plot omega-damping grid - 'pzmap.plot': True, # Generate plot using Matplotlib + 'pzmap.marker_size': 6, # Size of the markers + 'pzmap.marker_width': 1.5, # Width of the markers } +# Classes for keeping track of pzmap plots +class PoleZeroResponseList(list): + def plot(self, *args, **kwargs): + return pzmap_plot(self, *args, **kwargs) + + +class PoleZeroResponseData: + def __init__( + self, poles, zeros, gains=None, loci=None, dt=None, sysname=None): + self.poles = poles + self.zeros = zeros + self.gains = gains + self.loci = loci + self.dt = dt + self.sysname = sysname + + # Implement functions to allow legacy assignment to tuple + def __iter__(self): + return iter((self.poles, self.zeros)) + + def plot(self, *args, **kwargs): + return pzmap_plot(self, *args, **kwargs) + + +# pzmap response funciton +def pzmap_response(sysdata): + # Convert the first argument to a list + syslist = sysdata if isinstance(sysdata, (list, tuple)) else [sysdata] + + responses = [] + for idx, sys in enumerate(syslist): + responses.append( + PoleZeroResponseData( + sys.poles(), sys.zeros(), dt=sys.dt, sysname=sys.name)) + + if isinstance(sysdata, (list, tuple)): + return PoleZeroResponseList(responses) + else: + return responses[0] + + # TODO: Implement more elegant cross-style axes. See: -# http://matplotlib.sourceforge.net/examples/axes_grid/demo_axisline_style.html -# http://matplotlib.sourceforge.net/examples/axes_grid/demo_curvelinear_grid.html -def pzmap(sys, plot=None, grid=None, title='Pole Zero Map', **kwargs): +# https://matplotlib.org/2.0.2/examples/axes_grid/demo_axisline_style.html +# https://matplotlib.org/2.0.2/examples/axes_grid/demo_curvelinear_grid.html +def pzmap_plot( + data, plot=None, grid=None, title=None, marker_color=None, + marker_size=None, marker_width=None, legend_loc='upper right', + **kwargs): """Plot a pole/zero map for a linear system. Parameters ---------- - sys: LTI (StateSpace or TransferFunction) - Linear system for which poles and zeros are computed. - plot: bool, optional - If ``True`` a graph is generated with Matplotlib, - otherwise the poles and zeros are only computed and returned. + sysdata: List of PoleZeroResponseData objects or LTI systems + List of pole/zero response data objects generated by pzmap_response + or rootlocus_response() that are to be plotted. If a list of systems + is given, the poles and zeros of those systems will be plotted. grid: boolean (default = False) If True plot omega-damping grid. + plot: bool, optional + (legacy) If ``True`` a graph is generated with Matplotlib, + otherwise the poles and zeros are only computed and returned. + If this argument is present, the legacy value of poles and + zero is returned. Returns ------- - poles: array - The systems poles - zeros: array - The system's zeros. + lines : List of Line2D + Array of Line2D objects for each set of markers in the plot. The + shape of the array is given by (nsys, 2) where nsys is the number + of systems or Nyquist responses passed to the function. The second + index specifies the pzmap object type: + + * lines[idx, 0]: poles + * lines[idx, 1]: zeros + + poles, zeros: list of arrays + (legacy) If the `plot` keyword is given, the system poles and zeros + are returned. - Notes + Notes (TODO: update) ----- The pzmap function calls matplotlib.pyplot.axis('equal'), which means that trying to reset the axis limits may not behave as expected. To @@ -87,47 +120,143 @@ def pzmap(sys, plot=None, grid=None, title='Pole Zero Map', **kwargs): then set the axis limits to the desired values. """ - # Check to see if legacy 'Plot' keyword was used - if 'Plot' in kwargs: - import warnings - warnings.warn("'Plot' keyword is deprecated in pzmap; use 'plot'", - FutureWarning) - plot = kwargs.pop('Plot') - - # Make sure there were no extraneous keywords - if kwargs: - raise TypeError("unrecognized keywords: ", str(kwargs)) - # Get parameter values - plot = config._get_param('pzmap', 'plot', plot, True) grid = config._get_param('pzmap', 'grid', grid, False) + marker_size = config._get_param('pzmap', 'marker_size', marker_size, 6) + marker_width = config._get_param('pzmap', 'marker_width', marker_width, 1.5) + freqplot_rcParams = config._get_param( + 'freqplot', 'rcParams', kwargs, _freqplot_defaults, + pop=True, last=True) + + # If argument was a singleton, turn it into a tuple + if not isinstance(data, (list, tuple)): + data = [data] - if not isinstance(sys, LTI): - raise TypeError('Argument ``sys``: must be a linear system.') + # If we are passed a list of systems, compute response first + if all([isinstance( + sys, (StateSpace, TransferFunction)) for sys in data]): + # Get the response, popping off keywords used there + pzmap_responses = pzmap_response(data) + elif all([isinstance(d, PoleZeroResponseData) for d in data]): + pzmap_responses = data + else: + raise TypeError("unknown system data type") - poles = sys.poles() - zeros = sys.zeros() + # Legacy return value processing + if plot is not None: + warnings.warn( + "`pzmap_plot` return values of poles, zeros is deprecated; " + "use pzmap_response()", DeprecationWarning) + + # Extract out the values that we will eventually return + poles = [response.poles for response in pzmap_responses] + zeros = [response.zeros for response in pzmap_responses] + + if plot is False: + if len(data) == 1: + return poles[0], zeros[0] + else: + return poles, zeros - if (plot): - import matplotlib.pyplot as plt + # Initialize the figure + # TODO: turn into standard utility function + fig = plt.gcf() + axs = fig.get_axes() + if len(axs) > 1: + # Need to generate a new figure + fig, axs = plt.figure(), [] + with plt.rc_context(freqplot_rcParams): if grid: - if isdtime(sys, strict=True): + plt.clf() + if all([response.isctime() for response in data]): + ax, fig = sgrid() + elif all([response.isdtime() for response in data]): ax, fig = zgrid() else: - ax, fig = sgrid() - else: + ValueError( + "incompatible time responses; don't know how to grid") + elif len(axs) == 0: ax, fig = nogrid() + else: + # Use the existing axes + ax = axs[0] + + # Handle color cycle manually as all singular values + # of the same systems are expected to be of the same color + color_cycle = plt.rcParams['axes.prop_cycle'].by_key()['color'] + color_offset = 0 + if len(ax.lines) > 0: + last_color = ax.lines[-1].get_color() + if last_color in color_cycle: + color_offset = color_cycle.index(last_color) + 1 + + # Create a list of lines for the output + out = np.empty((len(pzmap_responses), 2), dtype=object) + for i, j in itertools.product(range(out.shape[0]), range(out.shape[1])): + out[i, j] = [] # unique list in each element + + for idx, response in enumerate(pzmap_responses): + poles = response.poles + zeros = response.zeros + + # Get the color to use for this system + if marker_color is None: + color = color_cycle[(color_offset + idx) % len(color_cycle)] + else: + color = maker_color # Plot the locations of the poles and zeros if len(poles) > 0: - ax.scatter(real(poles), imag(poles), s=50, marker='x', - facecolors='k') + out[idx, 0] = ax.plot( + real(poles), imag(poles), marker='x', linestyle='', + markeredgecolor=color, markerfacecolor=color, + markersize=marker_size, markeredgewidth=marker_width, + label=response.sysname) if len(zeros) > 0: - ax.scatter(real(zeros), imag(zeros), s=50, marker='o', - facecolors='none', edgecolors='k') + out[idx, 1] = ax.plot( + real(zeros), imag(zeros), marker='o', linestyle='', + markeredgecolor=color, markerfacecolor='none', + markersize=marker_size, markeredgewidth=marker_width) + + # List of systems that are included in this plot + lines, labels = _get_line_labels(ax) + + # Update the lines to use tuples for poles and zeros + from matplotlib.lines import Line2D + from matplotlib.legend_handler import HandlerTuple + line_tuples = [] + for pole_line in lines: + zero_line = Line2D( + [0], [0], marker='o', linestyle='', + markeredgecolor=pole_line.get_markerfacecolor(), + markerfacecolor='none', markersize=marker_size, + markeredgewidth=marker_width) + handle = (pole_line, zero_line) + line_tuples.append(handle) + print(line_tuples) + + # Add legend if there is more than one system plotted + if len(labels) > 1 and legend_loc is not False: + with plt.rc_context(freqplot_rcParams): + ax.legend( + line_tuples, labels, loc=legend_loc, + handler_map={tuple: HandlerTuple(ndivide=None)}) + + # Add the title + if title is None: + title = "Pole/zero map for " + ", ".join(labels) + with plt.rc_context(freqplot_rcParams): + fig.suptitle(title) + + # Legacy processing: return locations of poles and zeros as a tuple + if plot is True: + if len(data) == 1: + return poles, zeros + else: + TypeError("system lists not supported with legacy return values") + + return out - plt.title(title) - # Return locations of poles and zeros as a tuple - return poles, zeros +pzmap = pzmap_plot diff --git a/control/rlocus.py b/control/rlocus.py index 41cdec058..4f6203189 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -1,38 +1,6 @@ # rlocus.py - code for computing a root locus plot # Code contributed by Ryan Krauss, 2010 # -# Copyright (c) 2010 by Ryan Krauss -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# 3. Neither the name of the California Institute of Technology nor -# the names of its contributors may be used to endorse or promote -# products derived from this software without specific prior -# written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH -# OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -# SUCH DAMAGE. -# # RMM, 17 June 2010: modified to be a standalone piece of code # * Added BSD copyright info to file (per Ryan) # * Added code to convert (num, den) to poly1d's if they aren't already. @@ -46,7 +14,6 @@ # Sawyer B. Fuller (minster@uw.edu) 21 May 2020: # * added compatibility with discrete-time systems. # -# $Id$ # Packages used by this module from functools import partial @@ -127,20 +94,6 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, then set the axis limits to the desired values. """ - # Check to see if legacy 'Plot' keyword was used - if 'Plot' in kwargs: - warnings.warn("'Plot' keyword is deprecated in root_locus; " - "use 'plot'", FutureWarning) - # Map 'Plot' keyword to 'plot' keyword - plot = kwargs.pop('Plot') - - # Check to see if legacy 'PrintGain' keyword was used - if 'PrintGain' in kwargs: - warnings.warn("'PrintGain' keyword is deprecated in root_locus; " - "use 'print_gain'", FutureWarning) - # Map 'PrintGain' keyword to 'print_gain' keyword - print_gain = kwargs.pop('PrintGain') - # Get parameter values plotstr = config._get_param('rlocus', 'plotstr', plotstr, _rlocus_defaults) grid = config._get_param('rlocus', 'grid', grid, _rlocus_defaults) diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py index 0d13e6391..4e85b9852 100644 --- a/control/tests/kwargs_test.py +++ b/control/tests/kwargs_test.py @@ -241,6 +241,7 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo): 'nyquist_response': test_response_plot_kwargs, 'nyquist_plot': test_matplotlib_kwargs, 'pzmap': test_unrecognized_kwargs, + 'pzmap_plot': test_unrecognized_kwargs, 'rlocus': test_unrecognized_kwargs, 'root_locus': test_unrecognized_kwargs, 'rss': test_unrecognized_kwargs, diff --git a/control/tests/pzmap_test.py b/control/tests/pzmap_test.py index 8d41807b8..56eb699de 100644 --- a/control/tests/pzmap_test.py +++ b/control/tests/pzmap_test.py @@ -44,20 +44,23 @@ def test_pzmap(kwargs, setdefaults, dt, editsdefaults, mplcleanup): pzkwargs = kwargs.copy() if setdefaults: - for k in ['plot', 'grid']: + for k in ['grid']: if k in pzkwargs: v = pzkwargs.pop(k) config.set_defaults('pzmap', **{k: v}) + if kwargs.get('plot', None) is None: + pzkwargs['plot'] = True # use to get legacy return values P, Z = pzmap(T, **pzkwargs) np.testing.assert_allclose(P, Pref, rtol=1e-3) np.testing.assert_allclose(Z, Zref, rtol=1e-3) if kwargs.get('plot', True): - ax = plt.gca() + fig, ax = plt.gcf(), plt.gca() - assert ax.get_title() == kwargs.get('title', 'Pole Zero Map') + assert fig._suptitle.get_text().startswith( + kwargs.get('title', 'Pole/zero map')) # FIXME: This won't work when zgrid and sgrid are unified children = ax.get_children() @@ -78,11 +81,6 @@ def test_pzmap(kwargs, setdefaults, dt, editsdefaults, mplcleanup): assert not plt.get_fignums() -def test_pzmap_warns(): - with pytest.warns(FutureWarning): - pzmap(TransferFunction([1], [1, 2]), Plot=True) - - def test_pzmap_raises(): with pytest.raises(TypeError): # not an LTI system diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py index e61f0c8fe..3ce511c15 100644 --- a/control/tests/rlocus_test.py +++ b/control/tests/rlocus_test.py @@ -78,13 +78,6 @@ def test_root_locus_plot_grid(self, sys, grid): assert n_gridlines > 2 # TODO check validity of grid - def test_root_locus_warnings(self): - sys = TransferFunction([1000], [1, 25, 100, 0]) - with pytest.warns(FutureWarning, match="Plot.*deprecated"): - rlist, klist = root_locus(sys, Plot=True) - with pytest.warns(FutureWarning, match="PrintGain.*deprecated"): - rlist, klist = root_locus(sys, PrintGain=True) - def test_root_locus_neg_false_gain_nonproper(self): """ Non proper TranferFunction with negative gain: Not implemented""" with pytest.raises(ValueError, match="with equal order"): From 1451f3a35baebc5f49febaef7d2538483443d9fd Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 8 Aug 2023 21:45:58 -0700 Subject: [PATCH 02/16] pz -> pole_zero + add loci plotting --- control/pzmap.py | 148 +++++++++++++++++++++++++---------- control/tests/kwargs_test.py | 2 +- 2 files changed, 108 insertions(+), 42 deletions(-) diff --git a/control/pzmap.py b/control/pzmap.py index 47a021a2b..5693a1c29 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -22,7 +22,7 @@ from .freqplot import _freqplot_defaults, _get_line_labels from . import config -__all__ = ['pzmap_response', 'pzmap_plot', 'pzmap'] +__all__ = ['pole_zero_map', 'root_locus_map', 'pole_zero_plot', 'pzmap'] # Define default parameter values for this module @@ -34,18 +34,21 @@ # Classes for keeping track of pzmap plots -class PoleZeroResponseList(list): +class RootLocusList(list): def plot(self, *args, **kwargs): - return pzmap_plot(self, *args, **kwargs) + return pole_zero_plot(self, *args, **kwargs) -class PoleZeroResponseData: +class RootLocusData: def __init__( - self, poles, zeros, gains=None, loci=None, dt=None, sysname=None): + self, poles, zeros, gains=None, loci=None, xlim=None, ylim=None, + dt=None, sysname=None): self.poles = poles self.zeros = zeros self.gains = gains self.loci = loci + self.xlim = xlim + self.ylim = ylim self.dt = dt self.sysname = sysname @@ -54,22 +57,62 @@ def __iter__(self): return iter((self.poles, self.zeros)) def plot(self, *args, **kwargs): - return pzmap_plot(self, *args, **kwargs) + return pole_zero_plot(self, *args, **kwargs) -# pzmap response funciton -def pzmap_response(sysdata): +# Pole/zero map +def pole_zero_map(sysdata): # Convert the first argument to a list syslist = sysdata if isinstance(sysdata, (list, tuple)) else [sysdata] responses = [] for idx, sys in enumerate(syslist): responses.append( - PoleZeroResponseData( + RootLocusData( sys.poles(), sys.zeros(), dt=sys.dt, sysname=sys.name)) if isinstance(sysdata, (list, tuple)): - return PoleZeroResponseList(responses) + return RootLocusList(responses) + else: + return responses[0] + + +# Root locus map +def root_locus_map(sysdata, gains=None, xlim=None, ylim=None): + # Convert the first argument to a list + syslist = sysdata if isinstance(sysdata, (list, tuple)) else [sysdata] + + responses = [] + for idx, sys in enumerate(syslist): + from .rlocus import _systopoly1d, _default_gains + from .rlocus import _RLFindRoots, _RLSortRoots + + 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[0, 0]) + + if xlim is None and sys.isdtime(strict=True): + xlim = (-1.2, 1.2) + if ylim is None and sys.isdtime(strict=True): + xlim = (-1.3, 1.3) + + if gains is None: + kvect, root_array, xlim, ylim = _default_gains( + nump, denp, xlim, ylim) + else: + kvect = np.atleast_1d(gains) + root_array = _RLFindRoots(nump, denp, kvect) + root_array = _RLSortRoots(root_array) + + responses.append(RootLocusData( + sys.poles(), sys.zeros(), kvect, root_array, + dt=sys.dt, sysname=sys.name, xlim=xlim, ylim=ylim)) + + if isinstance(sysdata, (list, tuple)): + return RootLocusList(responses) else: return responses[0] @@ -77,15 +120,15 @@ def pzmap_response(sysdata): # TODO: Implement more elegant cross-style axes. See: # https://matplotlib.org/2.0.2/examples/axes_grid/demo_axisline_style.html # https://matplotlib.org/2.0.2/examples/axes_grid/demo_curvelinear_grid.html -def pzmap_plot( +def pole_zero_plot( data, plot=None, grid=None, title=None, marker_color=None, marker_size=None, marker_width=None, legend_loc='upper right', - **kwargs): + xlim=None, ylim=None, **kwargs): """Plot a pole/zero map for a linear system. Parameters ---------- - sysdata: List of PoleZeroResponseData objects or LTI systems + sysdata: List of RootLocusData objects or LTI systems List of pole/zero response data objects generated by pzmap_response or rootlocus_response() that are to be plotted. If a list of systems is given, the poles and zeros of those systems will be plotted. @@ -124,6 +167,7 @@ def pzmap_plot( grid = config._get_param('pzmap', 'grid', grid, False) marker_size = config._get_param('pzmap', 'marker_size', marker_size, 6) marker_width = config._get_param('pzmap', 'marker_width', marker_width, 1.5) + xlim_user, ylim_user = xlim, ylim freqplot_rcParams = config._get_param( 'freqplot', 'rcParams', kwargs, _freqplot_defaults, pop=True, last=True) @@ -136,8 +180,8 @@ def pzmap_plot( if all([isinstance( sys, (StateSpace, TransferFunction)) for sys in data]): # Get the response, popping off keywords used there - pzmap_responses = pzmap_response(data) - elif all([isinstance(d, PoleZeroResponseData) for d in data]): + pzmap_responses = pole_zero_map(data) + elif all([isinstance(d, RootLocusData) for d in data]): pzmap_responses = data else: raise TypeError("unknown system data type") @@ -145,8 +189,8 @@ def pzmap_plot( # Legacy return value processing if plot is not None: warnings.warn( - "`pzmap_plot` return values of poles, zeros is deprecated; " - "use pzmap_response()", DeprecationWarning) + "`pole_zero_plot` return values of poles, zeros is deprecated; " + "use pole_zero_map()", DeprecationWarning) # Extract out the values that we will eventually return poles = [response.poles for response in pzmap_responses] @@ -169,9 +213,9 @@ def pzmap_plot( with plt.rc_context(freqplot_rcParams): if grid: plt.clf() - if all([response.isctime() for response in data]): + if all([response.dt in [0, None] for response in data]): ax, fig = sgrid() - elif all([response.isdtime() for response in data]): + elif all([response.dt > 0 for response in data]): ax, fig = zgrid() else: ValueError( @@ -192,10 +236,11 @@ def pzmap_plot( color_offset = color_cycle.index(last_color) + 1 # Create a list of lines for the output - out = np.empty((len(pzmap_responses), 2), dtype=object) + out = np.empty((len(pzmap_responses), 3), dtype=object) for i, j in itertools.product(range(out.shape[0]), range(out.shape[1])): out[i, j] = [] # unique list in each element + xlim, ylim = ax.get_xlim(), ax.get_ylim() for idx, response in enumerate(pzmap_responses): poles = response.poles zeros = response.zeros @@ -208,44 +253,65 @@ def pzmap_plot( # Plot the locations of the poles and zeros if len(poles) > 0: + label = response.sysname if response.loci is None else None out[idx, 0] = ax.plot( real(poles), imag(poles), marker='x', linestyle='', markeredgecolor=color, markerfacecolor=color, markersize=marker_size, markeredgewidth=marker_width, - label=response.sysname) + label=label) if len(zeros) > 0: out[idx, 1] = ax.plot( real(zeros), imag(zeros), marker='o', linestyle='', markeredgecolor=color, markerfacecolor='none', markersize=marker_size, markeredgewidth=marker_width) + # Plot the loci, if present + if response.loci is not None: + for locus in response.loci.transpose(): + out[idx, 2] += ax.plot( + real(locus), imag(locus), color=color, + label=response.sysname) + + # Compute the axis limits to use + xlim = (min(xlim[0], response.xlim[0]), max(xlim[1], response.xlim[1])) + ylim = (min(ylim[0], response.ylim[0]), max(ylim[1], response.ylim[1])) + + # Set up the limits for the plot + ax.set_xlim(xlim if xlim_user is None else xlim_user) + ax.set_ylim(ylim if ylim_user is None else ylim_user) + # List of systems that are included in this plot lines, labels = _get_line_labels(ax) - # Update the lines to use tuples for poles and zeros - from matplotlib.lines import Line2D - from matplotlib.legend_handler import HandlerTuple - line_tuples = [] - for pole_line in lines: - zero_line = Line2D( - [0], [0], marker='o', linestyle='', - markeredgecolor=pole_line.get_markerfacecolor(), - markerfacecolor='none', markersize=marker_size, - markeredgewidth=marker_width) - handle = (pole_line, zero_line) - line_tuples.append(handle) - print(line_tuples) - # Add legend if there is more than one system plotted if len(labels) > 1 and legend_loc is not False: - with plt.rc_context(freqplot_rcParams): - ax.legend( - line_tuples, labels, loc=legend_loc, - handler_map={tuple: HandlerTuple(ndivide=None)}) + if response.loci is None: + # Use "x o" for the system label, via matplotlib tuple handler + from matplotlib.lines import Line2D + from matplotlib.legend_handler import HandlerTuple + + line_tuples = [] + for pole_line in lines: + zero_line = Line2D( + [0], [0], marker='o', linestyle='', + markeredgecolor=pole_line.get_markerfacecolor(), + markerfacecolor='none', markersize=marker_size, + markeredgewidth=marker_width) + handle = (pole_line, zero_line) + line_tuples.append(handle) + + with plt.rc_context(freqplot_rcParams): + ax.legend( + line_tuples, labels, loc=legend_loc, + handler_map={tuple: HandlerTuple(ndivide=None)}) + else: + # Regular legend, with lines + with plt.rc_context(freqplot_rcParams): + ax.legend(lines, labels, loc=legend_loc) # Add the title if title is None: - title = "Pole/zero map for " + ", ".join(labels) + title = "Pole/zero plot for " + ", ".join(labels) with plt.rc_context(freqplot_rcParams): fig.suptitle(title) @@ -259,4 +325,4 @@ def pzmap_plot( return out -pzmap = pzmap_plot +pzmap = pole_zero_plot diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py index 4e85b9852..4fe965e70 100644 --- a/control/tests/kwargs_test.py +++ b/control/tests/kwargs_test.py @@ -241,7 +241,7 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo): 'nyquist_response': test_response_plot_kwargs, 'nyquist_plot': test_matplotlib_kwargs, 'pzmap': test_unrecognized_kwargs, - 'pzmap_plot': test_unrecognized_kwargs, + 'pole_zero_plot': test_unrecognized_kwargs, 'rlocus': test_unrecognized_kwargs, 'root_locus': test_unrecognized_kwargs, 'rss': test_unrecognized_kwargs, From 1390e3771f76a964b63730a8fb6cae02ad648186 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 11 Aug 2023 22:23:38 -0700 Subject: [PATCH 03/16] remove sisotool dependencies in rlocus --- control/pzmap.py | 8 +++- control/rlocus.py | 72 ++++++++++++++-------------------- control/sisotool.py | 42 ++++++++++++++++---- control/tests/pzmap_test.py | 2 +- control/tests/sisotool_test.py | 10 ++--- 5 files changed, 77 insertions(+), 57 deletions(-) diff --git a/control/pzmap.py b/control/pzmap.py index 5693a1c29..474f86919 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -273,8 +273,12 @@ def pole_zero_plot( label=response.sysname) # Compute the axis limits to use - xlim = (min(xlim[0], response.xlim[0]), max(xlim[1], response.xlim[1])) - ylim = (min(ylim[0], response.ylim[0]), max(ylim[1], response.ylim[1])) + if response.xlim is not None: + xlim = (min(xlim[0], response.xlim[0]), + max(xlim[1], response.xlim[1])) + if response.ylim is not None: + ylim = (min(ylim[0], response.ylim[0]), + max(ylim[1], response.ylim[1])) # Set up the limits for the plot ax.set_xlim(xlim if xlim_user is None else xlim_user) diff --git a/control/rlocus.py b/control/rlocus.py index 4f6203189..e87caf8e8 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -25,7 +25,6 @@ from .iosys import isdtime from .xferfcn import _convert_to_transfer_function from .exception import ControlMIMONotImplemented -from .sisotool import _SisotoolUpdate from .grid import sgrid, zgrid from . import config import warnings @@ -76,7 +75,7 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, 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. + Specify the initial gain to use when marking current gain. [TODO: update] Returns ------- @@ -100,17 +99,12 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, print_gain = config._get_param( 'rlocus', 'print_gain', print_gain, _rlocus_defaults) - # Check for sisotool mode - sisotool = kwargs.get('sisotool', False) - - # make sure siso. sisotool has different requirements - if not sys.issiso() and not sisotool: + if not sys.issiso(): 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_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 @@ -128,31 +122,30 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, root_array = _RLSortRoots(root_array) recompute_on_zoom = False - if sisotool: - start_roots = _RLFindRoots(nump, denp, initial_gain) - # Make sure there were no extraneous keywords - if not sisotool and kwargs: + if kwargs: raise TypeError("unrecognized keywords: ", str(kwargs)) # Create the Plot if plot: - if sisotool: - fig = kwargs['fig'] - ax = fig.axes[1] - else: - if ax is None: - ax = plt.gca() - fig = ax.figure + if ax is None: + ax = plt.gca() ax.set_title('Root Locus') + fig = ax.figure + + # TODO: get rid of extra variable start_roots + if initial_gain is not None: + start_roots = _RLFindRoots(nump, denp, initial_gain) + else: + start_roots = None - if print_gain and not sisotool: + if print_gain and start_roots is None: fig.canvas.mpl_connect( 'button_release_event', partial(_RLClickDispatcher, sys=sys, fig=fig, - ax_rlocus=fig.axes[0], plotstr=plotstr)) - elif sisotool: - fig.axes[1].plot( + ax_rlocus=ax, plotstr=plotstr)) + elif start_roots is not None: + ax.plot( [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') @@ -165,14 +158,6 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, "Clicked at: %10.4g%+10.4gj gain: %10.4g damp: %10.4g" % (s.real, s.imag, initial_gain, zeta), fontsize=12 if int(mpl.__version__[0]) == 1 else 10) - fig.canvas.mpl_connect( - 'button_release_event', - partial(_RLClickDispatcher, sys=sys, fig=fig, - ax_rlocus=fig.axes[1], plotstr=plotstr, - sisotool=sisotool, - bode_plot_params=kwargs['bode_plot_params'], - tvect=kwargs['tvect'])) - if recompute_on_zoom: # update gains and roots when xlim/ylim change. Only then are @@ -526,7 +511,7 @@ def _RLZoomDispatcher(event, sys, ax_rlocus, plotstr): scalex=False, scaley=False) -def _RLClickDispatcher(event, sys, fig, ax_rlocus, plotstr, sisotool=False, +def _RLClickDispatcher(event, sys, fig, ax_rlocus, plotstr, bode_plot_params=None, tvect=None): """Rootlocus plot click dispatcher""" @@ -536,15 +521,14 @@ def _RLClickDispatcher(event, sys, fig, ax_rlocus, plotstr, sisotool=False, {'zoom rect', 'pan/zoom'}: # if a point is clicked on the rootlocus plot visually emphasize it - K = _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, sisotool) - if sisotool and K is not None: - _SisotoolUpdate(sys, fig, K, bode_plot_params, tvect) + K = _RLFeedbackClicksPoint( + event, sys, fig, ax_rlocus, show_clicked=False) # Update the canvas fig.canvas.draw() -def _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, sisotool=False): +def _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, show_clicked=False): """Display root-locus gain feedback point for clicks on root-locus plot""" sys_loop = sys[0,0] (nump, denp) = _systopoly1d(sys_loop) @@ -591,16 +575,20 @@ def _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, sisotool=False): # Remove the previous line _removeLine(label='gain_point', ax=ax_rlocus) - # Visualise clicked point, display all roots for sisotool mode - if sisotool: + if show_clicked: + # Visualise clicked point, display all roots root_array = _RLFindRoots(nump, denp, K.real) ax_rlocus.plot( [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') + 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') + # Just show the clicked point + # TODO: should we keep this? + ax_rlocus.plot( + s.real, s.imag, 'k.', marker='s', markersize=8, zorder=20, + label='gain_point') return K.real diff --git a/control/sisotool.py b/control/sisotool.py index 9af5268b9..ce77c0954 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -1,5 +1,10 @@ __all__ = ['sisotool', 'rootlocus_pid_designer'] +import numpy as np +import matplotlib.pyplot as plt +import warnings +from functools import partial + from control.exception import ControlMIMONotImplemented from .freqplot import bode_plot from .timeresp import step_response @@ -11,9 +16,6 @@ from .nlsys import interconnect from control.statesp import _convert_to_statespace from . import config -import numpy as np -import matplotlib.pyplot as plt -import warnings _sisotool_defaults = { 'sisotool.initial_gain': 1 @@ -123,13 +125,39 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None, initial_gain = config._get_param('sisotool', 'initial_gain', initial_gain, _sisotool_defaults) - # First time call to setup the bode and step response plots + # First time call to setup the Bode and step response plots _SisotoolUpdate(sys, fig, initial_gain, bode_plot_params) - # Setup the root-locus plot window - root_locus(sys, initial_gain=initial_gain, xlim=xlim_rlocus, + root_locus( + sys[0, 0], 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) + ax=fig.axes[1]) + + # Reset the button release callback so that we can update all plots + fig.canvas.mpl_connect( + 'button_release_event', + partial(_click_dispatcher, sys=sys, fig=fig, + ax_rlocus=fig.axes[1], plotstr=plotstr_rlocus, + bode_plot_params=bode_plot_params, tvect=tvect)) + + +def _click_dispatcher(event, sys, fig, ax_rlocus, plotstr, + bode_plot_params=None, tvect=None): + from .rlocus import _RLFeedbackClicksPoint + + # Zoom is handled by specialized callback in rlocus, only handle gain plot + if event.inaxes == ax_rlocus.axes and \ + plt.get_current_fig_manager().toolbar.mode not in \ + {'zoom rect', 'pan/zoom'}: + # if a point is clicked on the rootlocus plot visually emphasize it + K = _RLFeedbackClicksPoint( + event, sys, fig, ax_rlocus, show_clicked=True) + if K is not None: + _SisotoolUpdate(sys, fig, K, bode_plot_params, tvect) + + # Update the canvas + fig.canvas.draw() + def _SisotoolUpdate(sys, fig, K, bode_plot_params, tvect=None): diff --git a/control/tests/pzmap_test.py b/control/tests/pzmap_test.py index 56eb699de..dc2ab5c27 100644 --- a/control/tests/pzmap_test.py +++ b/control/tests/pzmap_test.py @@ -60,7 +60,7 @@ def test_pzmap(kwargs, setdefaults, dt, editsdefaults, mplcleanup): fig, ax = plt.gcf(), plt.gca() assert fig._suptitle.get_text().startswith( - kwargs.get('title', 'Pole/zero map')) + kwargs.get('title', 'Pole/zero plot')) # FIXME: This won't work when zgrid and sgrid are unified children = ax.get_children() diff --git a/control/tests/sisotool_test.py b/control/tests/sisotool_test.py index 5a86c73d0..8b29b5438 100644 --- a/control/tests/sisotool_test.py +++ b/control/tests/sisotool_test.py @@ -7,7 +7,7 @@ import pytest from control.sisotool import sisotool, rootlocus_pid_designer -from control.rlocus import _RLClickDispatcher +from control.sisotool import _click_dispatcher from control.xferfcn import TransferFunction from control.statesp import StateSpace from control import c2d @@ -93,8 +93,8 @@ def test_sisotool(self, tsys): event = type('test', (object,), {'xdata': 2.31206868287, 'ydata': 15.5983051046, 'inaxes': ax_rlocus.axes})() - _RLClickDispatcher(event=event, sys=tsys, fig=fig, - ax_rlocus=ax_rlocus, sisotool=True, plotstr='-', + _click_dispatcher(event=event, sys=tsys, fig=fig, + ax_rlocus=ax_rlocus, plotstr='-', bode_plot_params=bode_plot_params, tvect=None) # Check the moved root locus plot points @@ -143,8 +143,8 @@ def test_sisotool_tvect(self, tsys): event = type('test', (object,), {'xdata': 2.31206868287, 'ydata': 15.5983051046, 'inaxes': ax_rlocus.axes})() - _RLClickDispatcher(event=event, sys=tsys, fig=fig, - ax_rlocus=ax_rlocus, sisotool=True, plotstr='-', + _click_dispatcher(event=event, sys=tsys, fig=fig, + ax_rlocus=ax_rlocus, plotstr='-', bode_plot_params=dict(), tvect=tvect) assert_array_almost_equal(tvect, ax_step.lines[0].get_data()[0]) From 6e335981ec98adde55385dcbf24b0aac4f5e9dea Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 20 Aug 2023 17:47:32 -0700 Subject: [PATCH 04/16] update xlim, ylim handling in pzmap + other supporting changes --- control/grid.py | 17 +++++--- control/iosys.py | 46 +++++++++++++++------ control/pzmap.py | 80 +++++++++++++++++++++++------------- control/rlocus.py | 27 +++++++++--- control/tests/rlocus_test.py | 61 +++++++++++++++++++++++++++ 5 files changed, 180 insertions(+), 51 deletions(-) diff --git a/control/grid.py b/control/grid.py index 785ec2743..232f65527 100644 --- a/control/grid.py +++ b/control/grid.py @@ -8,6 +8,8 @@ from matplotlib.projections import PolarAxes from matplotlib.transforms import Affine2D +from .iosys import isdtime + class FormatterDMS(object): '''Transforms angle ticks to damping ratios''' @@ -142,17 +144,22 @@ def sgrid(): def _final_setup(ax): ax.set_xlabel('Real') ax.set_ylabel('Imaginary') - ax.axhline(y=0, color='black', lw=1) - ax.axvline(x=0, color='black', lw=1) + ax.axhline(y=0, color='black', lw=0.5) + ax.axvline(x=0, color='black', lw=0.5) plt.axis('equal') -def nogrid(): - f = plt.gcf() +def nogrid(dt=None): + fig = plt.gcf() ax = plt.axes() + # Draw the unit circle for discrete time systems + if isdtime(dt=dt, strict=True): + s = np.linspace(0, 2*pi, 100) + ax.plot(np.cos(s), np.sin(s), 'k--', lw=0.5, dashes=(5, 5)) + _final_setup(ax) - return ax, f + return ax, fig def zgrid(zetas=None, wns=None, ax=None): diff --git a/control/iosys.py b/control/iosys.py index 52262250d..7e4978938 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -503,42 +503,64 @@ def common_timebase(dt1, dt2): raise ValueError("Systems have incompatible timebases") # Check to see if a system is a discrete time system -def isdtime(sys, strict=False): +def isdtime(sys=None, dt=None, strict=False): """ Check to see if a system is a discrete time system. Parameters ---------- - sys : I/O or LTI system - System to be checked + sys : I/O system, optional + System to be checked. + dt : None or number, optional + Timebase to be checked. strict: bool (default = False) - If strict is True, make sure that timebase is not None + If strict is True, make sure that timebase is not None. """ - # Check to see if this is a constant + # See if we were passed a timebase instead of a system + if sys is None: + if dt is None: + return True if not strict else False + else: + return dt > 0 + elif dt is not None: + raise TypeError("passing both system and timebase not allowed") + + # Check timebase of the system if isinstance(sys, (int, float, complex, np.number)): - # OK as long as strict checking is off + # Constants OK as long as strict checking is off return True if not strict else False else: return sys.isdtime(strict) # Check to see if a system is a continuous time system -def isctime(sys, strict=False): +def isctime(sys=None, dt=None, strict=False): """ Check to see if a system is a continuous-time system. Parameters ---------- - sys : I/O or LTI system - System to be checked + sys : I/O system, optional + System to be checked. + dt : None or number, optional + Timebase to be checked. strict: bool (default = False) - If strict is True, make sure that timebase is not None + If strict is True, make sure that timebase is not None. """ - # Check to see if this is a constant + # See if we were passed a timebase instead of a system + if sys is None: + if dt is None: + return True if not strict else False + else: + return dt == 0 + elif dt is not None: + raise TypeError("passing both system and timebase not allowed") + + # Check timebase of the system if isinstance(sys, (int, float, complex, np.number)): - # OK as long as strict checking is off + # Constants OK as long as strict checking is off return True if not strict else False else: return sys.isctime(strict) diff --git a/control/pzmap.py b/control/pzmap.py index 474f86919..cc831b0a9 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -4,7 +4,9 @@ # Date: 7 Sep 2009 # # This file contains functions that compute poles, zeros and related -# quantities for a linear system. +# quantities for a linear system, as well as the main functions for +# storing and plotting pole/zero and root locus diagrams. (The actual +# computation of root locus diagrams is in rlocus.py.) # import numpy as np @@ -41,14 +43,11 @@ def plot(self, *args, **kwargs): class RootLocusData: def __init__( - self, poles, zeros, gains=None, loci=None, xlim=None, ylim=None, - dt=None, sysname=None): + self, poles, zeros, gains=None, loci=None, dt=None, sysname=None): self.poles = poles self.zeros = zeros self.gains = gains self.loci = loci - self.xlim = xlim - self.ylim = ylim self.dt = dt self.sysname = sysname @@ -78,7 +77,8 @@ def pole_zero_map(sysdata): # Root locus map -def root_locus_map(sysdata, gains=None, xlim=None, ylim=None): +# TODO: use rlocus.py computation instead +def root_locus_map(sysdata, gains=None): # Convert the first argument to a list syslist = sysdata if isinstance(sysdata, (list, tuple)) else [sysdata] @@ -94,14 +94,8 @@ def root_locus_map(sysdata, gains=None, xlim=None, ylim=None): # Convert numerator and denominator to polynomials if they aren't nump, denp = _systopoly1d(sys[0, 0]) - if xlim is None and sys.isdtime(strict=True): - xlim = (-1.2, 1.2) - if ylim is None and sys.isdtime(strict=True): - xlim = (-1.3, 1.3) - if gains is None: - kvect, root_array, xlim, ylim = _default_gains( - nump, denp, xlim, ylim) + kvect, root_array, _, _ = _default_gains(nump, denp, None, None) else: kvect = np.atleast_1d(gains) root_array = _RLFindRoots(nump, denp, kvect) @@ -109,7 +103,7 @@ def root_locus_map(sysdata, gains=None, xlim=None, ylim=None): responses.append(RootLocusData( sys.poles(), sys.zeros(), kvect, root_array, - dt=sys.dt, sysname=sys.name, xlim=xlim, ylim=ylim)) + dt=sys.dt, sysname=sys.name)) if isinstance(sysdata, (list, tuple)): return RootLocusList(responses) @@ -203,7 +197,7 @@ def pole_zero_plot( return poles, zeros # Initialize the figure - # TODO: turn into standard utility function + # TODO: turn into standard utility function (from plotutil.py?) fig = plt.gcf() axs = fig.get_axes() if len(axs) > 1: @@ -213,21 +207,22 @@ def pole_zero_plot( with plt.rc_context(freqplot_rcParams): if grid: plt.clf() - if all([response.dt in [0, None] for response in data]): + if all([isctime(dt=response.dt) for response in data]): ax, fig = sgrid() - elif all([response.dt > 0 for response in data]): + elif all([isdtime(dt=response.dt) for response in data]): ax, fig = zgrid() else: ValueError( "incompatible time responses; don't know how to grid") elif len(axs) == 0: - ax, fig = nogrid() + ax, fig = nogrid(data[0].dt) # use first response timebase else: - # Use the existing axes + # Use the existing axes and any grid that is there + # TODO: allow axis to be overriden via parameter ax = axs[0] - # Handle color cycle manually as all singular values - # of the same systems are expected to be of the same color + # Handle color cycle manually as all root locus segments + # of the same system are expected to be of the same color color_cycle = plt.rcParams['axes.prop_cycle'].by_key()['color'] color_offset = 0 if len(ax.lines) > 0: @@ -240,6 +235,7 @@ def pole_zero_plot( for i, j in itertools.product(range(out.shape[0]), range(out.shape[1])): out[i, j] = [] # unique list in each element + # Plot the responses (and keep track of axes limits) xlim, ylim = ax.get_xlim(), ax.get_ylim() for idx, response in enumerate(pzmap_responses): poles = response.poles @@ -272,13 +268,14 @@ def pole_zero_plot( real(locus), imag(locus), color=color, label=response.sysname) - # Compute the axis limits to use - if response.xlim is not None: - xlim = (min(xlim[0], response.xlim[0]), - max(xlim[1], response.xlim[1])) - if response.ylim is not None: - ylim = (min(ylim[0], response.ylim[0]), - max(ylim[1], response.ylim[1])) + # Compute the axis limits to use based on the response + resp_xlim, resp_ylim = _compute_root_locus_limits(response.loci) + + # Keep track of the current limits + xlim = [min(xlim[0], resp_xlim[0]), max(xlim[1], resp_xlim[1])] + ylim = [min(ylim[0], resp_ylim[0]), max(ylim[1], resp_ylim[1])] + + # TODO: add arrows to root loci (reuse Nyquist arrow code?) # Set up the limits for the plot ax.set_xlim(xlim if xlim_user is None else xlim_user) @@ -329,4 +326,31 @@ def pole_zero_plot( return out +# Utility function to compute limits for root loci +def _compute_root_locus_limits(loci): + # Go through each locus + xlim, ylim = [0, 0], 0 + for locus in loci.transpose(): + # Include all starting points + xlim = [min(xlim[0], locus[0].real), max(xlim[1], locus[0].real)] + ylim = max(ylim, locus[0].imag) + + # Find the local maxima of root locus curve + xpeaks = np.where( + np.diff(np.abs(locus.real)) < 0, locus.real[0:-1], 0) + xlim = [min(xlim[0], np.min(xpeaks)), max(xlim[1], np.max(xpeaks))] + + ypeaks = np.where( + np.diff(np.abs(locus.imag)) < 0, locus.imag[0:-1], 0) + ylim = max(ylim, np.max(ypeaks)) + + # Adjust the limits to include some space around features + rho = 1.5 + xlim[0] = rho * xlim[0] if xlim[0] < 0 else 0 + xlim[1] = rho * xlim[1] if xlim[1] > 0 else 0 + ylim = rho * ylim if ylim > 0 else np.max(np.abs(xlim)) + + return xlim, [-ylim, ylim] + + pzmap = pole_zero_plot diff --git a/control/rlocus.py b/control/rlocus.py index e87caf8e8..219211b6c 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -32,6 +32,7 @@ __all__ = ['root_locus', 'rlocus'] # Default values for module parameters +# TODO: merge these with pzmap parameters (?) _rlocus_defaults = { 'rlocus.grid': True, 'rlocus.plotstr': 'b' if int(mpl.__version__[0]) == 1 else 'C0', @@ -41,15 +42,16 @@ # Main function: compute a root locus diagram +# TODO: update to use pzmap data structures and plotting def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr=None, plot=True, print_gain=None, grid=None, ax=None, initial_gain=None, **kwargs): """Root locus plot. - Calculate the root locus by finding the roots of 1+k*TF(s) - where TF is self.num(s)/self.den(s) and each k is an element - of kvect. + Calculate the root locus by finding the roots of 1 + k * G(s) where G + is a linear system with transfer function num(s)/den(s) and each k is + an element of kvect. Parameters ---------- @@ -127,6 +129,7 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, raise TypeError("unrecognized keywords: ", str(kwargs)) # Create the Plot + # TODO: replace with pole_zero_plot and move additional functionality there if plot: if ax is None: ax = plt.gca() @@ -139,6 +142,7 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, else: start_roots = None + # TODO: don't rely on `start_roots` (sisotool holdover) if print_gain and start_roots is None: fig.canvas.mpl_connect( 'button_release_event', @@ -148,7 +152,8 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, ax.plot( [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') + marker='s', markersize=6, zorder=20, color='k', + label='gain_point') s = start_roots[0][0] if isdtime(sys, strict=True): zeta = -np.cos(np.angle(np.log(s))) @@ -219,16 +224,23 @@ def _default_gains(num, den, xlim, ylim, zoom_xlim=None, zoom_ylim=None): Saddle River, NJ : New Delhi: Prentice Hall.. """ + # Compute the break points on the real axis for the root locus plot k_break, real_break = _break_points(num, den) + + # Decide on the maximum gain to use and create the gain vector kmax = _k_max(num, den, real_break, k_break) kvect = np.hstack((np.linspace(0, kmax, 50), np.real(k_break))) kvect.sort() + # Find the roots for all of the gains and sort them root_array = _RLFindRoots(num, den, kvect) root_array = _RLSortRoots(root_array) + + # Keep track of the open loop poles and zeros open_loop_poles = den.roots open_loop_zeros = num.roots + # ??? if open_loop_zeros.size != 0 and \ open_loop_zeros.size < open_loop_poles.size: open_loop_zeros_xl = np.append( @@ -283,7 +295,8 @@ 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(root_array, 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: @@ -295,7 +308,8 @@ def _default_gains(num, den, xlim, ylim, zoom_xlim=None, zoom_ylim=None): root_array = np.insert(root_array, index + 1, new_points, axis=0) root_array = _RLSortRoots(root_array) - indexes_too_far = _indexes_filt(root_array, tolerance, zoom_xlim, zoom_ylim) + 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]) @@ -601,6 +615,7 @@ def _removeLine(label, ax): del line +# TODO: remove and replace with sgid()? def _sgrid_func(ax, zeta=None, wn=None): # Get locator function for x-axis, y-axis tick marks xlocator = ax.get_xaxis().get_major_locator() diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py index 3ce511c15..d69e413db 100644 --- a/control/tests/rlocus_test.py +++ b/control/tests/rlocus_test.py @@ -134,3 +134,64 @@ def test_rlocus_default_wn(self): [-1e-2, 1-1e7j, 1+1e7j], [0, -1e7j, 1e7j], 1)) ct.root_locus(sys) + + +# TODO: add additional test cases +@pytest.mark.parametrize( + "sys, grid, xlim, ylim", [ + (ct.tf([1], [1, 2, 1]), None, None, None), + ]) +def test_root_locus_plots(sys, grid, xlim, ylim): + ct.root_locus_map(sys).plot(grid=grid, xlim=xlim, ylim=ylim) + # TODO: add tests to make sure everything "looks" OK + + +if __name__ == "__main__": + # + # Interactive mode: generate plots for manual viewing + # + # Running this script in python (or better ipython) will show a + # collection of figures that should all look OK on the screeen. + # + + # In interactive mode, turn on ipython interactive graphics + plt.ion() + + # Start by clearing existing figures + plt.close('all') + + # Define systems to be tested + sys_secord = ct.tf([1], [1, 1, 1], name="2P") + sys_seczero = ct.tf([1, 0, -1], [1, 1, 1], name="2P, 2Z") + sys_fbs_a = ct.tf([1, 1], [1, 0, 0], name="FBS 12_19a") + sys_fbs_b = ct.tf( + ct.tf([1, 1], [1, 2, 0]) * ct.tf([1], [1, 2 ,4]), name="FBS 12_19b") + sys_fbs_c = ct.tf([1, 1], [1, 0, 1, 0], name="FBS 12_19c") + sys_fbs_d = ct.tf([1, 2, 2], [1, 0, 1, 0], name="FBS 12_19d") + sys_poles = sys_fbs_d.poles() + sys_zeros = sys_fbs_d.zeros() + sys_discrete = ct.zpk( + sys_zeros / 3, sys_poles / 3, 1, dt=True, name="discrete") + + # Run through a large number of test cases + test_cases = [ + # sys grid xlim ylim + (sys_secord, None, None, None), + (sys_seczero, None, None, None), + (sys_fbs_a, None, None, None), + (sys_fbs_b, None, None, None), + (sys_fbs_c, None, None, None), + (sys_fbs_c, None, None, [-2, 2]), + (sys_fbs_c, True, [-3, 3], None), + (sys_fbs_d, None, None, None), + (ct.zpk(sys_zeros * 10, sys_poles * 10, 1, name="12_19d * 10"), + None, None, None), + (ct.zpk(sys_zeros / 10, sys_poles / 10, 1, name="12_19d / 10"), + True, None, None), + (sys_discrete, None, None, None), + (sys_discrete, True, None, None), + ] + + for sys, grid, xlim, ylim in test_cases: + plt.figure() + test_root_locus_plots(sys, grid=grid, xlim=xlim, ylim=ylim) From 1c4322cac20ec823c423d12468ca0bd3f2e6e3b2 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Tue, 19 Sep 2023 20:58:36 -0700 Subject: [PATCH 05/16] reorder ct.isdtime parameters for backward compatibility --- control/iosys.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index 7e4978938..fbd5c1dba 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -503,7 +503,7 @@ def common_timebase(dt1, dt2): raise ValueError("Systems have incompatible timebases") # Check to see if a system is a discrete time system -def isdtime(sys=None, dt=None, strict=False): +def isdtime(sys=None, strict=False, dt=None): """ Check to see if a system is a discrete time system. @@ -513,7 +513,7 @@ def isdtime(sys=None, dt=None, strict=False): System to be checked. dt : None or number, optional Timebase to be checked. - strict: bool (default = False) + strict: bool, default=False If strict is True, make sure that timebase is not None. """ From 57dac87da3c5885ef4912fa4484a0dff811e0b9c Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Fri, 22 Sep 2023 20:36:47 -0700 Subject: [PATCH 06/16] initial refactoring of root_locus_{map,plot} --- control/grid.py | 11 +- control/pzmap.py | 223 ++++++++++++----- control/rlocus.py | 424 ++++----------------------------- control/sisotool.py | 50 ++-- control/tests/kwargs_test.py | 2 + control/tests/rlocus_test.py | 45 ++-- control/tests/sisotool_test.py | 34 ++- 7 files changed, 308 insertions(+), 481 deletions(-) diff --git a/control/grid.py b/control/grid.py index 232f65527..302f3595b 100644 --- a/control/grid.py +++ b/control/grid.py @@ -99,15 +99,19 @@ def sgrid(): ax.axis[:].major_ticklabels.set_visible(visible) ax.axis[:].major_ticks.set_visible(False) ax.axis[:].invert_ticklabel_direction() + ax.axis[:].major_ticklabels.set_color('gray') ax.axis["wnxneg"] = axis = ax.new_floating_axis(0, 180) axis.set_ticklabel_direction("-") axis.label.set_visible(False) + ax.axis["wnxpos"] = axis = ax.new_floating_axis(0, 0) axis.label.set_visible(False) + ax.axis["wnypos"] = axis = ax.new_floating_axis(0, 90) axis.label.set_visible(False) - axis.set_axis_direction("left") + axis.set_axis_direction("right") + ax.axis["wnyneg"] = axis = ax.new_floating_axis(0, 270) axis.label.set_visible(False) axis.set_axis_direction("left") @@ -149,9 +153,10 @@ def _final_setup(ax): plt.axis('equal') -def nogrid(dt=None): +def nogrid(dt=None, ax=None): fig = plt.gcf() - ax = plt.axes() + if ax is None: + ax = fig.gca() # Draw the unit circle for discrete time systems if isdtime(dt=dt, strict=True): diff --git a/control/pzmap.py b/control/pzmap.py index cc831b0a9..773c6df95 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -8,6 +8,16 @@ # storing and plotting pole/zero and root locus diagrams. (The actual # computation of root locus diagrams is in rlocus.py.) # +# TODO (Sep 2023): +# * Test out ability to set line styles +# - Make compatible with other plotting (and refactor?) +# - Allow line fmt to be overwritten (including color=CN for different +# colors for each segment?) +# * Add ability to set style of root locus click point +# - Sort out where default parameter values should live (pzmap vs rlocus) +# * Decide whether click functionality should be in rlocus.py +# * Add back print_gain option to sisotool (and any other options) +# import numpy as np from numpy import real, imag, linspace, exp, cos, sin, sqrt @@ -24,32 +34,44 @@ from .freqplot import _freqplot_defaults, _get_line_labels from . import config -__all__ = ['pole_zero_map', 'root_locus_map', 'pole_zero_plot', 'pzmap'] +__all__ = ['pole_zero_map', 'pole_zero_plot', 'pzmap'] # Define default parameter values for this module _pzmap_defaults = { - 'pzmap.grid': False, # Plot omega-damping grid - 'pzmap.marker_size': 6, # Size of the markers - 'pzmap.marker_width': 1.5, # Width of the markers + 'pzmap.grid': False, # Plot omega-damping grid + 'pzmap.marker_size': 6, # Size of the markers + 'pzmap.marker_width': 1.5, # Width of the markers + 'pzmap.expansion_factor': 2, # Amount to scale plots beyond features } - +# # Classes for keeping track of pzmap plots -class RootLocusList(list): - def plot(self, *args, **kwargs): - return pole_zero_plot(self, *args, **kwargs) - - -class RootLocusData: +# +# The PoleZeroData class keeps track of the information that is on a +# pole-zero plot. +# +# In addition to the locations of poles and zeros, you can also save a set +# of gains and loci for use in generating a root locus plot. The gain +# variable is a 1D array consisting of a list of increasing gains. The +# loci variable is a 2D array indexed by [gain_idx, root_idx] that can be +# plotted using the `pole_zero_plot` function. +# +# The PoleZeroList class is used to return a list of pole-zero plots. It +# is a lightweight wrapper on the built-in list class that includes a +# `plot` method, allowing plotting a set of root locus diagrams. +# +class PoleZeroData: def __init__( - self, poles, zeros, gains=None, loci=None, dt=None, sysname=None): + self, poles, zeros, gains=None, loci=None, dt=None, sysname=None, + sys=None): self.poles = poles self.zeros = zeros self.gains = gains self.loci = loci self.dt = dt self.sysname = sysname + self.sys = sys # Implement functions to allow legacy assignment to tuple def __iter__(self): @@ -59,54 +81,25 @@ def plot(self, *args, **kwargs): return pole_zero_plot(self, *args, **kwargs) +class PoleZeroList(list): + def plot(self, *args, **kwargs): + return pole_zero_plot(self, *args, **kwargs) + + # Pole/zero map def pole_zero_map(sysdata): + # TODO: add docstring (from old pzmap?) # Convert the first argument to a list syslist = sysdata if isinstance(sysdata, (list, tuple)) else [sysdata] responses = [] for idx, sys in enumerate(syslist): responses.append( - RootLocusData( + PoleZeroData( sys.poles(), sys.zeros(), dt=sys.dt, sysname=sys.name)) if isinstance(sysdata, (list, tuple)): - return RootLocusList(responses) - else: - return responses[0] - - -# Root locus map -# TODO: use rlocus.py computation instead -def root_locus_map(sysdata, gains=None): - # Convert the first argument to a list - syslist = sysdata if isinstance(sysdata, (list, tuple)) else [sysdata] - - responses = [] - for idx, sys in enumerate(syslist): - from .rlocus import _systopoly1d, _default_gains - from .rlocus import _RLFindRoots, _RLSortRoots - - 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[0, 0]) - - if gains is None: - kvect, root_array, _, _ = _default_gains(nump, denp, None, None) - else: - kvect = np.atleast_1d(gains) - root_array = _RLFindRoots(nump, denp, kvect) - root_array = _RLSortRoots(root_array) - - responses.append(RootLocusData( - sys.poles(), sys.zeros(), kvect, root_array, - dt=sys.dt, sysname=sys.name)) - - if isinstance(sysdata, (list, tuple)): - return RootLocusList(responses) + return PoleZeroList(responses) else: return responses[0] @@ -117,12 +110,14 @@ def root_locus_map(sysdata, gains=None): def pole_zero_plot( data, plot=None, grid=None, title=None, marker_color=None, marker_size=None, marker_width=None, legend_loc='upper right', - xlim=None, ylim=None, **kwargs): + xlim=None, ylim=None, interactive=False, ax=None, + initial_gain=None, **kwargs): + # TODO: update docstring (see other response/plot functions for style) """Plot a pole/zero map for a linear system. Parameters ---------- - sysdata: List of RootLocusData objects or LTI systems + sysdata: List of PoleZeroData objects or LTI systems List of pole/zero response data objects generated by pzmap_response or rootlocus_response() that are to be plotted. If a list of systems is given, the poles and zeros of those systems will be plotted. @@ -165,6 +160,7 @@ def pole_zero_plot( freqplot_rcParams = config._get_param( 'freqplot', 'rcParams', kwargs, _freqplot_defaults, pop=True, last=True) + user_ax = ax # If argument was a singleton, turn it into a tuple if not isinstance(data, (list, tuple)): @@ -175,7 +171,7 @@ def pole_zero_plot( sys, (StateSpace, TransferFunction)) for sys in data]): # Get the response, popping off keywords used there pzmap_responses = pole_zero_map(data) - elif all([isinstance(d, RootLocusData) for d in data]): + elif all([isinstance(d, PoleZeroData) for d in data]): pzmap_responses = data else: raise TypeError("unknown system data type") @@ -198,8 +194,13 @@ def pole_zero_plot( # Initialize the figure # TODO: turn into standard utility function (from plotutil.py?) - fig = plt.gcf() - axs = fig.get_axes() + if user_ax is None: + fig = plt.gcf() + axs = fig.get_axes() + else: + fig = ax.figure + axs = [ax] + if len(axs) > 1: # Need to generate a new figure fig, axs = plt.figure(), [] @@ -275,6 +276,10 @@ def pole_zero_plot( xlim = [min(xlim[0], resp_xlim[0]), max(xlim[1], resp_xlim[1])] ylim = [min(ylim[0], resp_ylim[0]), max(ylim[1], resp_ylim[1])] + # Plot the initial gain, if given + if initial_gain is not None: + _mark_root_locus_gain(ax, response.sys, initial_gain) + # TODO: add arrows to root loci (reuse Nyquist arrow code?) # Set up the limits for the plot @@ -313,8 +318,36 @@ def pole_zero_plot( # Add the title if title is None: title = "Pole/zero plot for " + ", ".join(labels) - with plt.rc_context(freqplot_rcParams): - fig.suptitle(title) + if user_ax is None: + with plt.rc_context(freqplot_rcParams): + fig.suptitle(title) + + # Add dispather to handle choosing a point on the diagram + if interactive: + if len(pzmap_responses) > 1: + raise NotImplementedError( + "interactive mode only allowed for single system") + elif pzmap_responses[0].sys == None: + raise SystemError("missing system information") + else: + sys = pzmap_responses[0].sys + + # Define function to handle mouse clicks + def _click_dispatcher(event): + # Find the gain corresponding to the clicked point + K, s = _find_root_locus_gain(event, sys, ax) + + if K is not None: + # Mark the gain on the root locus diagram + _mark_root_locus_gain(ax, sys, K) + + # Display the parameters in the axes title + with plt.rc_context(freqplot_rcParams): + ax.set_title(_create_root_locus_label(sys, K, s)) + + ax.figure.canvas.draw() + + fig.canvas.mpl_connect('button_release_event', _click_dispatcher) # Legacy processing: return locations of poles and zeros as a tuple if plot is True: @@ -326,7 +359,82 @@ def pole_zero_plot( return out +# Utility function to find gain corresponding to a click event +# TODO: project onto the root locus plot (here or above?) +def _find_root_locus_gain(event, sys, ax): + # Get the current axis limits to set various thresholds + xlim, ylim = ax.get_xlim(), ax.get_ylim() + + # 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( + complex(event.xdata + 0.05 * abs(xlim[1] - xlim[0]), event.ydata)) + K_ylim = -1. / sys( + complex(event.xdata, event.ydata + 0.05 * abs(ylim[1] - ylim[0]))) + + except TypeError: + K = float('inf') + K_xlim = float('inf') + K_ylim = float('inf') + + # + # Compute tolerances for deciding if we clicked on the root locus + # + # This is a bit of black magic that sets some limits for how close we + # need to be to the root locus in order to consider it a click on the + # actual curve. Otherwise, we will just ignore the click. + + x_tolerance = 0.1 * abs((xlim[1] - xlim[0])) + y_tolerance = 0.1 * abs((ylim[1] - ylim[0])) + gain_tolerance = np.mean([x_tolerance, y_tolerance]) * 0.1 + \ + 0.1 * max([abs(K_ylim.imag/K_ylim.real), abs(K_xlim.imag/K_xlim.real)]) + + # Decide whether to pay attention to this event + if abs(K.real) > 1e-8 and abs(K.imag / K.real) < gain_tolerance and \ + event.inaxes == ax.axes and K.real > 0.: + return K.real, s + + else: + return None, s + + +# Mark points corresponding to a given gain on root locus plot +def _mark_root_locus_gain(ax, sys, K): + from .rlocus import _systopoly1d, _RLFindRoots + + # Remove any previous gain points + for line in reversed(ax.lines): + if line.get_label() == '_gain_point': + line.remove() + del line + + # Visualise clicked point, displaying all roots + # TODO: allow marker parameters to be set + nump, denp = _systopoly1d(sys) + root_array = _RLFindRoots(nump, denp, K.real) + ax.plot( + [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') + + +# Return a string identifying a clicked point +# TODO: project onto the root locus plot (here or above?) +def _create_root_locus_label(sys, K, s): + # Figure out the damping ratio + if isdtime(sys, strict=True): + zeta = -np.cos(np.angle(np.log(s))) + else: + zeta = -1 * s.real / abs(s) + + return "Clicked at: %.4g%+.4gj gain = %.4g damping = %.4g" % \ + (s.real, s.imag, K.real, zeta) + + # Utility function to compute limits for root loci +# TODO: compare to old code and recapture functionality (especially asymptotes) +# TODO: (note that sys is now available => code here may not be needed) def _compute_root_locus_limits(loci): # Go through each locus xlim, ylim = [0, 0], 0 @@ -345,7 +453,8 @@ def _compute_root_locus_limits(loci): ylim = max(ylim, np.max(ypeaks)) # Adjust the limits to include some space around features - rho = 1.5 + # TODO: use _k_max and project out to max k for all value? + rho = config._get_param('pzmap', 'expansion_factor') xlim[0] = rho * xlim[0] if xlim[0] < 0 else 0 xlim[1] = rho * xlim[1] if xlim[1] > 0 else 0 ylim = rho * ylim if ylim > 0 else np.max(np.abs(xlim)) diff --git a/control/rlocus.py b/control/rlocus.py index 219211b6c..07c6b67c6 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -18,7 +18,6 @@ # Packages used by this module from functools import partial import numpy as np -import matplotlib as mpl import matplotlib.pyplot as plt from numpy import array, poly1d, row_stack, zeros_like, real, imag import scipy.signal # signal processing toolbox @@ -29,24 +28,54 @@ from . import config import warnings -__all__ = ['root_locus', 'rlocus'] +__all__ = ['root_locus_map', 'root_locus_plot', 'root_locus', 'rlocus'] # Default values for module parameters # TODO: merge these with pzmap parameters (?) _rlocus_defaults = { 'rlocus.grid': True, - 'rlocus.plotstr': 'b' if int(mpl.__version__[0]) == 1 else 'C0', + 'rlocus.plotstr': 'C0', # default color cycle [TODO: not used?] 'rlocus.print_gain': True, 'rlocus.plot': True } -# Main function: compute a root locus diagram -# TODO: update to use pzmap data structures and plotting -def root_locus(sys, kvect=None, xlim=None, ylim=None, - plotstr=None, plot=True, print_gain=None, grid=None, ax=None, - initial_gain=None, **kwargs): +# Root locus map +# TODO: add docstring +def root_locus_map(sysdata, gains=None): + from .pzmap import PoleZeroData, PoleZeroList + # Convert the first argument to a list + syslist = sysdata if isinstance(sysdata, (list, tuple)) else [sysdata] + + responses = [] + for idx, sys in enumerate(syslist): + 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[0, 0]) + + if gains is None: + kvect, root_array, _, _ = _default_gains(nump, denp, None, None) + else: + kvect = np.atleast_1d(gains) + root_array = _RLFindRoots(nump, denp, kvect) + root_array = _RLSortRoots(root_array) + + responses.append(PoleZeroData( + sys.poles(), sys.zeros(), kvect, root_array, + dt=sys.dt, sysname=sys.name, sys=sys)) + + if isinstance(sysdata, (list, tuple)): + return PoleZeroList(responses) + else: + return responses[0] + + +def root_locus_plot( + sysdata, kvect=None, grid=None, plot=True, **kwargs): """Root locus plot. Calculate the root locus by finding the roots of 1 + k * G(s) where G @@ -95,126 +124,22 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, then set the axis limits to the desired values. """ - # Get parameter values - plotstr = config._get_param('rlocus', 'plotstr', plotstr, _rlocus_defaults) - grid = config._get_param('rlocus', 'grid', grid, _rlocus_defaults) - print_gain = config._get_param( - 'rlocus', 'print_gain', print_gain, _rlocus_defaults) - - 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) - - # if discrete-time system and if xlim and ylim are not given, - # that we a view of the unit circle - if xlim is None and isdtime(sys, strict=True): - xlim = (-1.2, 1.2) - if ylim is None and isdtime(sys, strict=True): - xlim = (-1.3, 1.3) - - if kvect is None: - kvect, root_array, xlim, ylim = _default_gains(nump, denp, xlim, ylim) - recompute_on_zoom = True - else: - kvect = np.atleast_1d(kvect) - root_array = _RLFindRoots(nump, denp, kvect) - root_array = _RLSortRoots(root_array) - recompute_on_zoom = False + from .pzmap import pole_zero_plot - # Make sure there were no extraneous keywords - if kwargs: - raise TypeError("unrecognized keywords: ", str(kwargs)) + # Set default parameters + # TODO: move this to pole_zero_plot() + grid = config._get_param('rlocus', 'grid', grid, _rlocus_defaults) - # Create the Plot - # TODO: replace with pole_zero_plot and move additional functionality there + responses = root_locus_map(sysdata, gains=kvect) + # TODO: update to include legacy keyword processing (use pole_zero_plot?) if plot: - if ax is None: - ax = plt.gca() - ax.set_title('Root Locus') - fig = ax.figure - - # TODO: get rid of extra variable start_roots - if initial_gain is not None: - start_roots = _RLFindRoots(nump, denp, initial_gain) - else: - start_roots = None - - # TODO: don't rely on `start_roots` (sisotool holdover) - if print_gain and start_roots is None: - fig.canvas.mpl_connect( - 'button_release_event', - partial(_RLClickDispatcher, sys=sys, fig=fig, - ax_rlocus=ax, plotstr=plotstr)) - elif start_roots is not None: - ax.plot( - [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_roots[0][0] - if isdtime(sys, strict=True): - zeta = -np.cos(np.angle(np.log(s))) - else: - zeta = -1 * s.real / abs(s) - fig.suptitle( - "Clicked at: %10.4g%+10.4gj gain: %10.4g damp: %10.4g" % - (s.real, s.imag, initial_gain, zeta), - fontsize=12 if int(mpl.__version__[0]) == 1 else 10) - - 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) - ax.plot(real(poles), imag(poles), 'x') - - # plot open loop zeros - zeros = array(nump.r) - if zeros.size > 0: - ax.plot(real(zeros), imag(zeros), 'o') - - # Now plot the loci - for index, col in enumerate(root_array.T): - ax.plot(real(col), imag(col), plotstr, label='rootlocus') - - # Set up plot axes and labels - ax.set_xlabel('Real') - ax.set_ylabel('Imaginary') - - # Set up the limits for the plot - # Note: need to do this before computing grid lines - if xlim: - ax.set_xlim(xlim) - if ylim: - ax.set_ylim(ylim) - - # Draw the grid - if grid: - if isdtime(sys, strict=True): - zgrid(ax=ax) - else: - _sgrid_func(ax) - else: - ax.axhline(0., linestyle=':', color='k', linewidth=.75, zorder=-20) - ax.axvline(0., linestyle=':', color='k', linewidth=.75, zorder=-20) - if isdtime(sys, strict=True): - ax.add_patch(plt.Circle( - (0, 0), radius=1.0, linestyle=':', edgecolor='k', - linewidth=0.75, fill=False, zorder=-20)) + responses.plot(grid=grid, **kwargs) - return root_array, kvect + # TODO: legacy return value; update + return responses.loci, responses.gains +# TODO: get rid of zoom functionality? def _default_gains(num, den, xlim, ylim, zoom_xlim=None, zoom_ylim=None): """Unsupervised gains calculation for root locus plot. @@ -320,7 +245,7 @@ def _default_gains(num, den, xlim, ylim, 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. + """Calculate the distance between points and return the indices. Filter the indexes so only the resolution of points within the xlim and ylim is improved when zoom is used. @@ -510,253 +435,6 @@ def _RLSortRoots(roots): return sorted -def _RLZoomDispatcher(event, sys, ax_rlocus, plotstr): - """Rootlocus plot zoom dispatcher""" - sys_loop = sys[0,0] - nump, denp = _systopoly1d(sys_loop) - xlim, ylim = ax_rlocus.get_xlim(), ax_rlocus.get_ylim() - - 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(root_array.T): - ax_rlocus.plot(real(col), imag(col), plotstr, label='rootlocus', - scalex=False, scaley=False) - - -def _RLClickDispatcher(event, sys, fig, ax_rlocus, plotstr, - bode_plot_params=None, tvect=None): - """Rootlocus plot click dispatcher""" - - # Zoom is handled by specialized callback above, only do gain plot - if event.inaxes == ax_rlocus.axes and \ - plt.get_current_fig_manager().toolbar.mode not in \ - {'zoom rect', 'pan/zoom'}: - - # if a point is clicked on the rootlocus plot visually emphasize it - K = _RLFeedbackClicksPoint( - event, sys, fig, ax_rlocus, show_clicked=False) - - # Update the canvas - fig.canvas.draw() - - -def _RLFeedbackClicksPoint(event, sys, fig, ax_rlocus, show_clicked=False): - """Display root-locus gain feedback point for clicks on root-locus plot""" - sys_loop = sys[0,0] - (nump, denp) = _systopoly1d(sys_loop) - - xlim = ax_rlocus.get_xlim() - ylim = ax_rlocus.get_ylim() - x_tolerance = 0.1 * abs((xlim[1] - xlim[0])) - y_tolerance = 0.1 * abs((ylim[1] - ylim[0])) - gain_tolerance = np.mean([x_tolerance, y_tolerance])*0.1 - - # 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( - complex(event.xdata + 0.05 * abs(xlim[1] - xlim[0]), event.ydata)) - K_ylim = -1. / sys_loop( - complex(event.xdata, event.ydata + 0.05 * abs(ylim[1] - ylim[0]))) - - except TypeError: - K = float('inf') - K_xlim = float('inf') - K_ylim = float('inf') - - gain_tolerance += 0.1 * max([abs(K_ylim.imag/K_ylim.real), - abs(K_xlim.imag/K_xlim.real)]) - - if abs(K.real) > 1e-8 and abs(K.imag / K.real) < gain_tolerance and \ - event.inaxes == ax_rlocus.axes and K.real > 0.: - - if isdtime(sys, strict=True): - zeta = -np.cos(np.angle(np.log(s))) - else: - zeta = -1 * s.real / abs(s) - - # Display the parameters in the output window and figure - print("Clicked at %10.4g%+10.4gj gain %10.4g damp %10.4g" % - (s.real, s.imag, K.real, zeta)) - fig.suptitle( - "Clicked at: %10.4g%+10.4gj gain: %10.4g damp: %10.4g" % - (s.real, s.imag, K.real, zeta), - fontsize=12 if int(mpl.__version__[0]) == 1 else 10) - - # Remove the previous line - _removeLine(label='gain_point', ax=ax_rlocus) - - if show_clicked: - # Visualise clicked point, display all roots - root_array = _RLFindRoots(nump, denp, K.real) - ax_rlocus.plot( - [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: - # Just show the clicked point - # TODO: should we keep this? - ax_rlocus.plot( - s.real, s.imag, 'k.', marker='s', markersize=8, zorder=20, - label='gain_point') - - return K.real - - -def _removeLine(label, ax): - """Remove a line from the ax when a label is specified""" - for line in reversed(ax.lines): - if line.get_label() == label: - line.remove() - del line - - -# TODO: remove and replace with sgid()? -def _sgrid_func(ax, zeta=None, wn=None): - # Get locator function for x-axis, y-axis tick marks - xlocator = ax.get_xaxis().get_major_locator() - ylocator = ax.get_yaxis().get_major_locator() - - # Decide on the location for the labels (?) - ylim = ax.get_ylim() - ytext_pos_lim = ylim[1] - (ylim[1] - ylim[0]) * 0.03 - xlim = ax.get_xlim() - xtext_pos_lim = xlim[0] + (xlim[1] - xlim[0]) * 0.0 - - # Create a list of damping ratios, if needed - if zeta is None: - zeta = _default_zetas(xlim, ylim) - - # Figure out the angles for the different damping ratios - angles = [] - for z in zeta: - if (z >= 1e-4) and (z <= 1): - angles.append(np.pi/2 + np.arcsin(z)) - else: - zeta.remove(z) - y_over_x = np.tan(angles) - - # zeta-constant lines - for index, yp in enumerate(y_over_x): - ax.plot([0, xlocator()[0]], [0, yp * xlocator()[0]], color='gray', - linestyle='dashed', linewidth=0.5) - ax.plot([0, xlocator()[0]], [0, -yp * xlocator()[0]], color='gray', - linestyle='dashed', linewidth=0.5) - an = "%.2f" % zeta[index] - if yp < 0: - xtext_pos = 1/yp * ylim[1] - ytext_pos = yp * xtext_pos_lim - if np.abs(xtext_pos) > np.abs(xtext_pos_lim): - xtext_pos = xtext_pos_lim - else: - ytext_pos = ytext_pos_lim - ax.annotate(an, textcoords='data', xy=[xtext_pos, ytext_pos], - fontsize=8) - ax.plot([0, 0], [ylim[0], ylim[1]], - color='gray', linestyle='dashed', linewidth=0.5) - - # omega-constant lines - angles = np.linspace(-90, 90, 20) * np.pi/180 - if wn is None: - wn = _default_wn(xlocator(), ylocator()) - - for om in wn: - if om < 0: - # Generate the lines for natural frequency curves - yp = np.sin(angles) * np.abs(om) - xp = -np.cos(angles) * np.abs(om) - - # Plot the natural frequency contours - ax.plot(xp, yp, color='gray', linestyle='dashed', linewidth=0.5) - - # Annotate the natural frequencies by listing on x-axis - # Note: need to filter values for proper plotting in Jupyter - if (om > xlim[0]): - an = "%.2f" % -om - ax.annotate(an, textcoords='data', xy=[om, 0], fontsize=8) - - -def _default_zetas(xlim, ylim): - """Return default list of damping coefficients - - This function computes a list of damping coefficients based on the limits - of the graph. A set of 4 damping coefficients are computed for the x-axis - and a set of three damping coefficients are computed for the y-axis - (corresponding to the normal 4:3 plot aspect ratio in `matplotlib`?). - - Parameters - ---------- - xlim : array_like - List of x-axis limits [min, max] - ylim : array_like - List of y-axis limits [min, max] - - Returns - ------- - zeta : list - List of default damping coefficients for the plot - - """ - # Damping coefficient lines that intersect the x-axis - sep1 = -xlim[0] / 4 - ang1 = [np.arctan((sep1*i)/ylim[1]) for i in np.arange(1, 4, 1)] - - # Damping coefficient lines that intersection the y-axis - sep2 = ylim[1] / 3 - ang2 = [np.arctan(-xlim[0]/(ylim[1]-sep2*i)) for i in np.arange(1, 3, 1)] - - # Put the lines together and add one at -pi/2 (negative real axis) - angles = np.concatenate((ang1, ang2)) - angles = np.insert(angles, len(angles), np.pi/2) - - # Return the damping coefficients corresponding to these angles - zeta = np.sin(angles) - return zeta.tolist() - - -def _default_wn(xloc, yloc, max_lines=7): - """Return default wn for root locus plot - - This function computes a list of natural frequencies based on the grid - parameters of the graph. - - Parameters - ---------- - xloc : array_like - List of x-axis tick values - ylim : array_like - List of y-axis limits [min, max] - max_lines : int, optional - Maximum number of frequencies to generate (default = 7) - - Returns - ------- - wn : list - List of default natural frequencies for the plot - - """ - 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 - wn = yloc # one frequency per y-axis tick mark - else: - wn = xloc # one frequency per x-axis tick mark - - # Insert additional frequencies to span the y-axis - while np.abs(wn[0]) < yloc[-1]: - wn = np.insert(wn, 0, wn[0]-sep) - - # If there are too many values, cut them in half - while len(wn) > max_lines: - wn = wn[0:-1:2] - - return wn - - -rlocus = root_locus +# Alternative ways to call these functions +root_locus = root_locus_plot +rlocus = root_locus_plot diff --git a/control/sisotool.py b/control/sisotool.py index ce77c0954..fad1cb68d 100644 --- a/control/sisotool.py +++ b/control/sisotool.py @@ -88,7 +88,7 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None, >>> ct.sisotool(G) # doctest: +SKIP """ - from .rlocus import root_locus + from .rlocus import root_locus_map # sys as loop transfer function if SISO if not sys.issiso(): @@ -128,35 +128,47 @@ def sisotool(sys, initial_gain=None, xlim_rlocus=None, ylim_rlocus=None, # First time call to setup the Bode and step response plots _SisotoolUpdate(sys, fig, initial_gain, bode_plot_params) - root_locus( - sys[0, 0], initial_gain=initial_gain, xlim=xlim_rlocus, - ylim=ylim_rlocus, plotstr=plotstr_rlocus, grid=rlocus_grid, - ax=fig.axes[1]) + # root_locus( + # sys[0, 0], initial_gain=initial_gain, xlim=xlim_rlocus, + # ylim=ylim_rlocus, plotstr=plotstr_rlocus, grid=rlocus_grid, + # ax=fig.axes[1]) + ax_rlocus = fig.axes[1] + root_locus_map(sys[0, 0]).plot( + xlim=xlim_rlocus, ylim=ylim_rlocus, grid=rlocus_grid, + initial_gain=initial_gain, ax=ax_rlocus) + if rlocus_grid is False: + # Need to generate grid manually, since root_locus_plot() won't + from .grid import nogrid + nogrid(sys.dt, ax=ax_rlocus) # Reset the button release callback so that we can update all plots fig.canvas.mpl_connect( - 'button_release_event', - partial(_click_dispatcher, sys=sys, fig=fig, - ax_rlocus=fig.axes[1], plotstr=plotstr_rlocus, - bode_plot_params=bode_plot_params, tvect=tvect)) + 'button_release_event', partial( + _click_dispatcher, sys=sys, ax=fig.axes[1], + bode_plot_params=bode_plot_params, tvect=tvect)) -def _click_dispatcher(event, sys, fig, ax_rlocus, plotstr, - bode_plot_params=None, tvect=None): - from .rlocus import _RLFeedbackClicksPoint - - # Zoom is handled by specialized callback in rlocus, only handle gain plot - if event.inaxes == ax_rlocus.axes and \ +def _click_dispatcher(event, sys, ax, bode_plot_params, tvect): + # Zoom handled by specialized callback in rlocus, only handle gain plot + if event.inaxes == ax.axes and \ plt.get_current_fig_manager().toolbar.mode not in \ {'zoom rect', 'pan/zoom'}: + fig = ax.figure + # if a point is clicked on the rootlocus plot visually emphasize it - K = _RLFeedbackClicksPoint( - event, sys, fig, ax_rlocus, show_clicked=True) + # K = _RLFeedbackClicksPoint( + # event, sys, fig, ax_rlocus, show_clicked=True) + from .pzmap import _find_root_locus_gain, _mark_root_locus_gain, \ + _create_root_locus_label + + K, s = _find_root_locus_gain(event, sys, ax) if K is not None: + _mark_root_locus_gain(ax, sys, K) + fig.suptitle(_create_root_locus_label(sys, K, s), fontsize=10) _SisotoolUpdate(sys, fig, K, bode_plot_params, tvect) - # Update the canvas - fig.canvas.draw() + # Update the canvas + fig.canvas.draw() def _SisotoolUpdate(sys, fig, K, bode_plot_params, tvect=None): diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py index 4fe965e70..bdcae1885 100644 --- a/control/tests/kwargs_test.py +++ b/control/tests/kwargs_test.py @@ -97,6 +97,7 @@ def test_kwarg_search(module, prefix): (control.pzmap, 1, 0, (), {}), (control.rlocus, 0, 1, (), {}), (control.root_locus, 0, 1, (), {}), + (control.root_locus_plot, 0, 1, (), {}), (control.rss, 0, 0, (2, 1, 1), {}), (control.set_defaults, 0, 0, ('control',), {'default_dt': True}), (control.ss, 0, 0, (0, 0, 0, 0), {'dt': 1}), @@ -244,6 +245,7 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo): 'pole_zero_plot': test_unrecognized_kwargs, 'rlocus': test_unrecognized_kwargs, 'root_locus': test_unrecognized_kwargs, + 'root_locus_plot': test_unrecognized_kwargs, 'rss': test_unrecognized_kwargs, 'set_defaults': test_unrecognized_kwargs, 'singular_values_plot': test_matplotlib_kwargs, diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py index d69e413db..12486cb5b 100644 --- a/control/tests/rlocus_test.py +++ b/control/tests/rlocus_test.py @@ -9,7 +9,7 @@ import pytest import control as ct -from control.rlocus import root_locus, _RLClickDispatcher +from control.rlocus import root_locus from control.xferfcn import TransferFunction from control.statesp import StateSpace from control.bdalg import feedback @@ -64,6 +64,7 @@ def test_without_gains(self, sys): roots, kvect = root_locus(sys, plot=False) self.check_cl_poles(sys, roots, kvect) + @pytest.mark.skip("TODO: update test for rlocus gridlines") @pytest.mark.slow @pytest.mark.parametrize('grid', [None, True, False]) def test_root_locus_plot_grid(self, sys, grid): @@ -85,6 +86,7 @@ def test_root_locus_neg_false_gain_nonproper(self): # TODO: cover and validate negative false_gain branch in _default_gains() + @pytest.mark.skip("TODO: update test to check click dispatcher") @pytest.mark.skipif(plt.get_current_fig_manager().toolbar is None, reason="Requires the zoom toolbar") def test_root_locus_zoom(self): @@ -138,11 +140,12 @@ def test_rlocus_default_wn(self): # TODO: add additional test cases @pytest.mark.parametrize( - "sys, grid, xlim, ylim", [ - (ct.tf([1], [1, 2, 1]), None, None, None), + "sys, grid, xlim, ylim, interactive", [ + (ct.tf([1], [1, 2, 1]), None, None, None, False), ]) -def test_root_locus_plots(sys, grid, xlim, ylim): - ct.root_locus_map(sys).plot(grid=grid, xlim=xlim, ylim=ylim) +def test_root_locus_plots(sys, grid, xlim, ylim, interactive): + ct.root_locus_map(sys).plot( + grid=grid, xlim=xlim, ylim=ylim, interactive=interactive) # TODO: add tests to make sure everything "looks" OK @@ -175,23 +178,25 @@ def test_root_locus_plots(sys, grid, xlim, ylim): # Run through a large number of test cases test_cases = [ - # sys grid xlim ylim - (sys_secord, None, None, None), - (sys_seczero, None, None, None), - (sys_fbs_a, None, None, None), - (sys_fbs_b, None, None, None), - (sys_fbs_c, None, None, None), - (sys_fbs_c, None, None, [-2, 2]), - (sys_fbs_c, True, [-3, 3], None), - (sys_fbs_d, None, None, None), + # sys grid xlim ylim inter + (sys_secord, None, None, None, None), + (sys_seczero, None, None, None, None), + (sys_fbs_a, None, None, None, None), + (sys_fbs_b, None, None, None, None), + (sys_fbs_c, None, None, None, None), + (sys_fbs_c, None, None, [-2, 2], None), + (sys_fbs_c, True, [-3, 3], None, None), + (sys_fbs_d, None, None, None, None), (ct.zpk(sys_zeros * 10, sys_poles * 10, 1, name="12_19d * 10"), - None, None, None), + None, None, None, None), (ct.zpk(sys_zeros / 10, sys_poles / 10, 1, name="12_19d / 10"), - True, None, None), - (sys_discrete, None, None, None), - (sys_discrete, True, None, None), + True, None, None, None), + (sys_discrete, None, None, None, None), + (sys_discrete, True, None, None, None), + (sys_fbs_d, True, None, None, True), ] - for sys, grid, xlim, ylim in test_cases: + for sys, grid, xlim, ylim, interactive in test_cases: plt.figure() - test_root_locus_plots(sys, grid=grid, xlim=xlim, ylim=ylim) + test_root_locus_plots( + sys, grid=grid, xlim=xlim, ylim=ylim, interactive=interactive) diff --git a/control/tests/sisotool_test.py b/control/tests/sisotool_test.py index 8b29b5438..b5aaeb900 100644 --- a/control/tests/sisotool_test.py +++ b/control/tests/sisotool_test.py @@ -57,11 +57,11 @@ def test_sisotool(self, tsys): initial_point_0 = (np.array([-22.53155977]), np.array([0.])) initial_point_1 = (np.array([-1.23422011]), np.array([-6.54667031])) initial_point_2 = (np.array([-1.23422011]), np.array([6.54667031])) - assert_array_almost_equal(ax_rlocus.lines[0].get_data(), + assert_array_almost_equal(ax_rlocus.lines[4].get_data(), initial_point_0, 4) - assert_array_almost_equal(ax_rlocus.lines[1].get_data(), + assert_array_almost_equal(ax_rlocus.lines[5].get_data(), initial_point_1, 4) - assert_array_almost_equal(ax_rlocus.lines[2].get_data(), + assert_array_almost_equal(ax_rlocus.lines[6].get_data(), initial_point_2, 4) # Check the step response before moving the point @@ -93,9 +93,8 @@ def test_sisotool(self, tsys): event = type('test', (object,), {'xdata': 2.31206868287, 'ydata': 15.5983051046, 'inaxes': ax_rlocus.axes})() - _click_dispatcher(event=event, sys=tsys, fig=fig, - ax_rlocus=ax_rlocus, plotstr='-', - bode_plot_params=bode_plot_params, tvect=None) + _click_dispatcher(event=event, sys=tsys, ax=ax_rlocus, + bode_plot_params=bode_plot_params, tvect=None) # Check the moved root locus plot points moved_point_0 = (np.array([-29.91742755]), np.array([0.])) @@ -143,9 +142,8 @@ def test_sisotool_tvect(self, tsys): event = type('test', (object,), {'xdata': 2.31206868287, 'ydata': 15.5983051046, 'inaxes': ax_rlocus.axes})() - _click_dispatcher(event=event, sys=tsys, fig=fig, - ax_rlocus=ax_rlocus, plotstr='-', - bode_plot_params=dict(), tvect=tvect) + _click_dispatcher(event=event, sys=tsys, ax=ax_rlocus, + 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, @@ -202,3 +200,21 @@ def test_pid_designer_1(self, plant, gain, sign, input_signal, Kp0, Ki0, Kd0, de def test_pid_designer_2(self, plant, kwargs): rootlocus_pid_designer(plant, **kwargs) + +if __name__ == "__main__": + # + # Interactive mode: generate plots for manual viewing + # + # Running this script in python (or better ipython) will show a + # collection of figures that should all look OK on the screeen. + # + import control as ct + + # In interactive mode, turn on ipython interactive graphics + plt.ion() + + # Start by clearing existing figures + plt.close('all') + + tsys = ct.tf([1000], [1, 25, 100, 0]) + ct.sisotool(tsys) From ee46ac6a2fa1f7375706d28b135ac07cdfd6156f Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Mon, 25 Dec 2023 19:59:40 -0800 Subject: [PATCH 07/16] adjust root locus spacing and add scaling keyword --- control/grid.py | 55 ++++++++++++++++++++++++++-------------------- control/pzmap.py | 57 +++++++++++++++++++++++++++++++----------------- 2 files changed, 68 insertions(+), 44 deletions(-) diff --git a/control/grid.py b/control/grid.py index 302f3595b..b1447f1a3 100644 --- a/control/grid.py +++ b/control/grid.py @@ -1,6 +1,14 @@ +# grid.py - code to add gridlines to root locus and pole-zero diagrams +# +# This code generates grids for pole-zero diagrams (including root locus +# diagrams). Rather than just draw a grid in place, it uses the AxisArtist +# package to generate a custom grid that will scale with the figure. +# + import numpy as np from numpy import cos, sin, sqrt, linspace, pi, exp import matplotlib.pyplot as plt + from mpl_toolkits.axisartist import SubplotHost from mpl_toolkits.axisartist.grid_helper_curvelinear \ import GridHelperCurveLinear @@ -67,14 +75,15 @@ def __call__(self, transform_xy, x1, y1, x2, y2): return lon_min, lon_max, lat_min, lat_max -def sgrid(): +def sgrid(scaling=None): # From matplotlib demos: # https://matplotlib.org/gallery/axisartist/demo_curvelinear_grid.html # https://matplotlib.org/gallery/axisartist/demo_floating_axis.html # PolarAxes.PolarTransform takes radian. However, we want our coordinate - # system in degree + # system in degrees tr = Affine2D().scale(np.pi/180., 1.) + PolarAxes.PolarTransform() + # polar projection, which involves cycle, and also has limits in # its coordinates, needs a special method to find the extremes # (min, max of the coordinate within the view). @@ -91,6 +100,7 @@ def sgrid(): tr, extreme_finder=extreme_finder, grid_locator1=grid_locator1, tick_formatter1=tick_formatter1) + # Set up an axes with a specialized grid helper fig = plt.gcf() ax = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper) @@ -101,6 +111,7 @@ def sgrid(): ax.axis[:].invert_ticklabel_direction() ax.axis[:].major_ticklabels.set_color('gray') + # Set up internal tickmarks and labels along the real/imag axes ax.axis["wnxneg"] = axis = ax.new_floating_axis(0, 180) axis.set_ticklabel_direction("-") axis.label.set_visible(False) @@ -125,35 +136,26 @@ def sgrid(): ax.axis["bottom"].get_helper().nth_coord_ticks = 0 fig.add_subplot(ax) - - # RECTANGULAR X Y AXES WITH SCALE - # par2 = ax.twiny() - # par2.axis["top"].toggle(all=False) - # par2.axis["right"].toggle(all=False) - # new_fixed_axis = par2.get_grid_helper().new_fixed_axis - # par2.axis["left"] = new_fixed_axis(loc="left", - # axes=par2, - # offset=(0, 0)) - # par2.axis["bottom"] = new_fixed_axis(loc="bottom", - # axes=par2, - # offset=(0, 0)) - # FINISH RECTANGULAR - ax.grid(True, zorder=0, linestyle='dotted') - _final_setup(ax) + _final_setup(ax, scaling=scaling) return ax, fig -def _final_setup(ax): +# Utility function used by all grid code +def _final_setup(ax, scaling=None): ax.set_xlabel('Real') ax.set_ylabel('Imaginary') ax.axhline(y=0, color='black', lw=0.5) ax.axvline(x=0, color='black', lw=0.5) - plt.axis('equal') + + # Set up the scaling for the axes + scaling = 'equal' if scaling is None else scaling + plt.axis(scaling) -def nogrid(dt=None, ax=None): +# If not grid is given, at least separate stable/unstable regions +def nogrid(dt=None, ax=None, scaling=None): fig = plt.gcf() if ax is None: ax = fig.gca() @@ -163,11 +165,12 @@ def nogrid(dt=None, ax=None): s = np.linspace(0, 2*pi, 100) ax.plot(np.cos(s), np.sin(s), 'k--', lw=0.5, dashes=(5, 5)) - _final_setup(ax) + _final_setup(ax, scaling=scaling) return ax, fig - -def zgrid(zetas=None, wns=None, ax=None): +# Grid for discrete time system (drawn, not rendered by AxisArtist) +# TODO (at some point): think about using customized grid generator? +def zgrid(zetas=None, wns=None, ax=None, scaling=None): """Draws discrete damping and frequency grid""" fig = plt.gcf() @@ -218,5 +221,9 @@ def zgrid(zetas=None, wns=None, ax=None): ax.annotate(r"$\frac{"+num+r"\pi}{T}$", xy=(an_x, an_y), xytext=(an_x, an_y), size=9) - _final_setup(ax) + # Set default axes to allow some room around the unit circle + ax.set_xlim([-1.1, 1.1]) + ax.set_ylim([-1.1, 1.1]) + + _final_setup(ax, scaling=scaling) return ax, fig diff --git a/control/pzmap.py b/control/pzmap.py index 773c6df95..eaf3897fd 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -42,7 +42,8 @@ 'pzmap.grid': False, # Plot omega-damping grid 'pzmap.marker_size': 6, # Size of the markers 'pzmap.marker_width': 1.5, # Width of the markers - 'pzmap.expansion_factor': 2, # Amount to scale plots beyond features + 'pzmap.expansion_factor': 1.8, # Amount to scale plots beyond features + 'pzmap.buffer_factor': 1.05, # Buffer to leave around plot peaks } # @@ -110,7 +111,7 @@ def pole_zero_map(sysdata): def pole_zero_plot( data, plot=None, grid=None, title=None, marker_color=None, marker_size=None, marker_width=None, legend_loc='upper right', - xlim=None, ylim=None, interactive=False, ax=None, + xlim=None, ylim=None, interactive=False, ax=None, scaling=None, initial_gain=None, **kwargs): # TODO: update docstring (see other response/plot functions for style) """Plot a pole/zero map for a linear system. @@ -144,7 +145,7 @@ def pole_zero_plot( (legacy) If the `plot` keyword is given, the system poles and zeros are returned. - Notes (TODO: update) + Notes (TODO: update, including scaling) ----- The pzmap function calls matplotlib.pyplot.axis('equal'), which means that trying to reset the axis limits may not behave as expected. To @@ -209,14 +210,15 @@ def pole_zero_plot( if grid: plt.clf() if all([isctime(dt=response.dt) for response in data]): - ax, fig = sgrid() + ax, fig = sgrid(scaling=scaling) elif all([isdtime(dt=response.dt) for response in data]): - ax, fig = zgrid() + ax, fig = zgrid(scaling=scaling) else: ValueError( "incompatible time responses; don't know how to grid") elif len(axs) == 0: - ax, fig = nogrid(data[0].dt) # use first response timebase + # use first response timebase + ax, fig = nogrid(data[0].dt, scaling=scaling) else: # Use the existing axes and any grid that is there # TODO: allow axis to be overriden via parameter @@ -270,7 +272,7 @@ def pole_zero_plot( label=response.sysname) # Compute the axis limits to use based on the response - resp_xlim, resp_ylim = _compute_root_locus_limits(response.loci) + resp_xlim, resp_ylim = _compute_root_locus_limits(response) # Keep track of the current limits xlim = [min(xlim[0], resp_xlim[0]), max(xlim[1], resp_xlim[1])] @@ -433,11 +435,22 @@ def _create_root_locus_label(sys, K, s): # Utility function to compute limits for root loci -# TODO: compare to old code and recapture functionality (especially asymptotes) # TODO: (note that sys is now available => code here may not be needed) -def _compute_root_locus_limits(loci): - # Go through each locus - xlim, ylim = [0, 0], 0 +def _compute_root_locus_limits(response): + loci = response.loci + + # Start with information about zeros, if present + if response.sys is not None and response.sys.zeros().size > 0: + xlim = [ + min(0, np.min(response.sys.zeros().real)), + max(0, np.max(response.sys.zeros().real)) + ] + ylim = max(0, np.max(response.sys.zeros().imag)) + else: + xlim, ylim = [0, 0], 0 + + # Go through each locus and look for features + rho = config._get_param('pzmap', 'buffer_factor') for locus in loci.transpose(): # Include all starting points xlim = [min(xlim[0], locus[0].real), max(xlim[1], locus[0].real)] @@ -446,18 +459,22 @@ def _compute_root_locus_limits(loci): # Find the local maxima of root locus curve xpeaks = np.where( np.diff(np.abs(locus.real)) < 0, locus.real[0:-1], 0) - xlim = [min(xlim[0], np.min(xpeaks)), max(xlim[1], np.max(xpeaks))] + xlim = [ + min(xlim[0], np.min(xpeaks) * rho), + max(xlim[1], np.max(xpeaks) * rho) + ] ypeaks = np.where( np.diff(np.abs(locus.imag)) < 0, locus.imag[0:-1], 0) - ylim = max(ylim, np.max(ypeaks)) - - # Adjust the limits to include some space around features - # TODO: use _k_max and project out to max k for all value? - rho = config._get_param('pzmap', 'expansion_factor') - xlim[0] = rho * xlim[0] if xlim[0] < 0 else 0 - xlim[1] = rho * xlim[1] if xlim[1] > 0 else 0 - ylim = rho * ylim if ylim > 0 else np.max(np.abs(xlim)) + ylim = max(ylim, np.max(ypeaks) * rho) + + if isctime(dt=response.dt): + # Adjust the limits to include some space around features + # TODO: use _k_max and project out to max k for all value? + rho = config._get_param('pzmap', 'expansion_factor') + xlim[0] = rho * xlim[0] if xlim[0] < 0 else 0 + xlim[1] = rho * xlim[1] if xlim[1] > 0 else 0 + ylim = rho * ylim if ylim > 0 else np.max(np.abs(xlim)) return xlim, [-ylim, ylim] From 0d23598ce89bb0d8af7187a952351a7c63ce3fdb Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 27 Dec 2023 22:03:25 -0800 Subject: [PATCH 08/16] tweak grid processing, add documentation, interactive by default --- control/grid.py | 4 +-- control/pzmap.py | 72 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/control/grid.py b/control/grid.py index b1447f1a3..d56585aca 100644 --- a/control/grid.py +++ b/control/grid.py @@ -146,8 +146,8 @@ def sgrid(scaling=None): def _final_setup(ax, scaling=None): ax.set_xlabel('Real') ax.set_ylabel('Imaginary') - ax.axhline(y=0, color='black', lw=0.5) - ax.axvline(x=0, color='black', lw=0.5) + ax.axhline(y=0, color='black', lw=0.25) + ax.axvline(x=0, color='black', lw=0.25) # Set up the scaling for the axes scaling = 'equal' if scaling is None else scaling diff --git a/control/pzmap.py b/control/pzmap.py index eaf3897fd..b4cc2fec8 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -39,7 +39,7 @@ # Define default parameter values for this module _pzmap_defaults = { - 'pzmap.grid': False, # Plot omega-damping grid + 'pzmap.grid': None, # Plot omega-damping grid 'pzmap.marker_size': 6, # Size of the markers 'pzmap.marker_width': 1.5, # Width of the markers 'pzmap.expansion_factor': 1.8, # Amount to scale plots beyond features @@ -111,20 +111,28 @@ def pole_zero_map(sysdata): def pole_zero_plot( data, plot=None, grid=None, title=None, marker_color=None, marker_size=None, marker_width=None, legend_loc='upper right', - xlim=None, ylim=None, interactive=False, ax=None, scaling=None, + xlim=None, ylim=None, interactive=None, ax=None, scaling=None, initial_gain=None, **kwargs): # TODO: update docstring (see other response/plot functions for style) """Plot a pole/zero map for a linear system. + If the system data include root loci, a root locus diagram for the + system is plotted. When the root locus for a single system is plotted, + clicking on a location on the root locus will mark the gain on all + branches of the diagram and show the system gain and damping for the + given pole in the axes title. Set to False to turn off this behavior. + Parameters ---------- - sysdata: List of PoleZeroData objects or LTI systems + sysdata : List of PoleZeroData objects or LTI systems List of pole/zero response data objects generated by pzmap_response or rootlocus_response() that are to be plotted. If a list of systems is given, the poles and zeros of those systems will be plotted. - grid: boolean (default = False) - If True plot omega-damping grid. - plot: bool, optional + grid : boolean (default = None) + If True plot omega-damping grid, otherwise show imaginary axis for + continuous time systems, unit circle for discrete time systems. If + `False`, do not draw any additonal lines. + plot : bool, optional (legacy) If ``True`` a graph is generated with Matplotlib, otherwise the poles and zeros are only computed and returned. If this argument is present, the legacy value of poles and @@ -145,16 +153,42 @@ def pole_zero_plot( (legacy) If the `plot` keyword is given, the system poles and zeros are returned. - Notes (TODO: update, including scaling) + Other Parameters + ---------------- + scaling : str or list, optional + Set the type of axis scaling. Can be 'equal' (default), 'auto', or + a list of the form [xmin, xmax, ymin, ymax]. + title : str, optional + Set the title of the plot. Defaults plot type and system name(s). + marker_color : str, optional + Set the color of the markers used for poles and zeros. + marker_color : int, optional + Set the size of the markers used for poles and zeros. + marker_width : int, optional + Set the line width of the markers used for poles and zeros. + legend_loc : str, optional + For plots with multiple lines, a legend will be included in the + given location. Default is 'center right'. Use False to supress. + xlim : list, optional + Set the limits for the x axis. + ylim : list, optional + Set the limits for the y axis. + interactive : bool, optional + Turn off interactive mode for root locus plots. + initial_gain : float, optional + If given, the specified system gain will be marked on the plot. + + Notes ----- - The pzmap function calls matplotlib.pyplot.axis('equal'), which means - that trying to reset the axis limits may not behave as expected. To - change the axis limits, use matplotlib.pyplot.gca().axis('auto') and - then set the axis limits to the desired values. + By default, the pzmap function calls matplotlib.pyplot.axis('equal'), + which means that trying to reset the axis limits may not behave as + expected. To change the axis limits, use the `scaling` keyword of use + matplotlib.pyplot.gca().axis('auto') and then set the axis limits to + the desired values. """ # Get parameter values - grid = config._get_param('pzmap', 'grid', grid, False) + grid = config._get_param('pzmap', 'grid', grid, None) marker_size = config._get_param('pzmap', 'marker_size', marker_size, 6) marker_width = config._get_param('pzmap', 'marker_width', marker_width, 1.5) xlim_user, ylim_user = xlim, ylim @@ -177,6 +211,11 @@ def pole_zero_plot( else: raise TypeError("unknown system data type") + # Turn on interactive mode by default, if allowed + if interactive is None and len(pzmap_responses) == 1 \ + and pzmap_responses[0].sys is not None: + interactive = True + # Legacy return value processing if plot is not None: warnings.warn( @@ -217,11 +256,14 @@ def pole_zero_plot( ValueError( "incompatible time responses; don't know how to grid") elif len(axs) == 0: - # use first response timebase - ax, fig = nogrid(data[0].dt, scaling=scaling) + if grid is False: + # Leave off grid entirely + ax = plt.axes() + else: + # use first response timebase + ax, fig = nogrid(data[0].dt, scaling=scaling) else: # Use the existing axes and any grid that is there - # TODO: allow axis to be overriden via parameter ax = axs[0] # Handle color cycle manually as all root locus segments From 0137056dace0476f6663f20cd099753f70e79e74 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 27 Dec 2023 22:27:19 -0800 Subject: [PATCH 09/16] add legacy processing for root_locus --- control/rlocus.py | 29 +++++++++++++++++++++++------ control/tests/rlocus_test.py | 10 +++++----- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/control/rlocus.py b/control/rlocus.py index 07c6b67c6..970c7b98c 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -75,7 +75,7 @@ def root_locus_map(sysdata, gains=None): def root_locus_plot( - sysdata, kvect=None, grid=None, plot=True, **kwargs): + sysdata, kvect=None, grid=None, plot=None, **kwargs): """Root locus plot. Calculate the root locus by finding the roots of 1 + k * G(s) where G @@ -131,12 +131,29 @@ def root_locus_plot( grid = config._get_param('rlocus', 'grid', grid, _rlocus_defaults) responses = root_locus_map(sysdata, gains=kvect) - # TODO: update to include legacy keyword processing (use pole_zero_plot?) - if plot: - responses.plot(grid=grid, **kwargs) - # TODO: legacy return value; update - return responses.loci, responses.gains + # + # Process `plot` keyword + # + # See bode_plot for a description of how this keyword is handled to + # support legacy implementatoins of root_locus. + # + if plot is not None: + warnings.warn( + "`root_locus` return values of loci, gains is deprecated; " + "use root_locus_map()", DeprecationWarning) + + if plot is False: + return responses.loci, responses.gains + + # Plot the root loci + out = responses.plot(grid=grid, **kwargs) + + # Legacy processing: return locations of poles and zeros as a tuple + if plot is True: + return responses.loci, responses.gains + + return out # TODO: get rid of zoom functionality? diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py index 12486cb5b..7f36c035b 100644 --- a/control/tests/rlocus_test.py +++ b/control/tests/rlocus_test.py @@ -55,7 +55,7 @@ def testRootLocus(self, sys): self.check_cl_poles(sys, roots, klist) # now check with plotting - roots, k_out = root_locus(sys, klist) + roots, k_out = root_locus(sys, klist, plot=True) np.testing.assert_equal(len(roots), len(klist)) np.testing.assert_allclose(klist, k_out) self.check_cl_poles(sys, roots, klist) @@ -68,7 +68,7 @@ def test_without_gains(self, sys): @pytest.mark.slow @pytest.mark.parametrize('grid', [None, True, False]) def test_root_locus_plot_grid(self, sys, grid): - rlist, klist = root_locus(sys, grid=grid) + rlist, klist = root_locus(sys, plot=True, grid=grid) ax = plt.gca() n_gridlines = sum([int(line.get_linestyle() in [':', 'dotted', '--', 'dashed']) @@ -82,7 +82,7 @@ def test_root_locus_plot_grid(self, sys, grid): def test_root_locus_neg_false_gain_nonproper(self): """ Non proper TranferFunction with negative gain: Not implemented""" with pytest.raises(ValueError, match="with equal order"): - root_locus(TransferFunction([-1, 2], [1, 2])) + root_locus(TransferFunction([-1, 2], [1, 2]), plot=True) # TODO: cover and validate negative false_gain branch in _default_gains() @@ -93,7 +93,7 @@ def test_root_locus_zoom(self): """Check the zooming functionality of the Root locus plot""" system = TransferFunction([1000], [1, 25, 100, 0]) plt.figure() - root_locus(system) + root_locus(system, plot=True) fig = plt.gcf() ax_rlocus = fig.axes[0] @@ -135,7 +135,7 @@ def test_rlocus_default_wn(self): sys = ct.tf(*sp.signal.zpk2tf( [-1e-2, 1-1e7j, 1+1e7j], [0, -1e7j, 1e7j], 1)) - ct.root_locus(sys) + ct.root_locus(sys, plot=True) # TODO: add additional test cases From 08e55b133dbe9a15f23108c80caaa4a5a50a0932 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Wed, 27 Dec 2023 23:01:51 -0800 Subject: [PATCH 10/16] add backward compatibility for matlab.{rlocus,pzmap} + suppress warnings --- control/matlab/wrappers.py | 103 ++++++++++++++++++++++++++++++++++- control/rlocus.py | 4 +- control/tests/matlab_test.py | 3 +- control/tests/pzmap_test.py | 1 + control/tests/rlocus_test.py | 4 ++ 5 files changed, 110 insertions(+), 5 deletions(-) diff --git a/control/matlab/wrappers.py b/control/matlab/wrappers.py index b63b19c7e..64743c953 100644 --- a/control/matlab/wrappers.py +++ b/control/matlab/wrappers.py @@ -12,14 +12,14 @@ from ..lti import LTI from ..exception import ControlArgument -__all__ = ['bode', 'nyquist', 'ngrid', 'dcgain', 'connect'] +__all__ = ['bode', 'nyquist', 'ngrid', 'rlocus', 'pzmap', 'dcgain', 'connect'] def bode(*args, **kwargs): """bode(syslist[, omega, dB, Hz, deg, ...]) Bode plot of the frequency response. - Plots a bode gain and phase diagram + Plots a bode gain and phase diagram. Parameters ---------- @@ -195,6 +195,104 @@ def _parse_freqplot_args(*args): return syslist, omega, plotstyle, other +def rlocus(*args, **kwargs): + """rlocus(sys[, klist, xlim, ylim, ...]) + + Root locus diagram. + + Calculate the root locus by finding the roots of 1 + k * G(s) where G + is a linear system with transfer function num(s)/den(s) and each k is + an element of kvect. + + Parameters + ---------- + sys : LTI object + Linear input/output systems (SISO only, for now). + 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`). + ylim : tuple or list, optional + Set limits of y axis, normally with tuple + (see :doc:`matplotlib:api/axes_api`). + + Returns + ------- + 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 + ----- + This function is a wrapper for :func:`~control.root_locus_plot`, + with legacy return arguments. + + """ + from ..rlocus import root_locus_plot + + # Use the plot keyword to get legacy behavior + kwargs = dict(kwargs) # make a copy since we modify this + if 'plot' not in kwargs: + kwargs['plot'] = True + + # Turn off deprecation warning + with warnings.catch_warnings(): + warnings.filterwarnings( + 'ignore', message='.* return values of .* is deprecated', + category=DeprecationWarning) + retval = root_locus_plot(*args, **kwargs) + + return retval + + +def pzmap(*args, **kwargs): + """pzmap(sys[, grid, plot]) + + Plot a pole/zero map for a linear system. + + Parameters + ---------- + sys: LTI (StateSpace or TransferFunction) + Linear system for which poles and zeros are computed. + plot: bool, optional + If ``True`` a graph is generated with Matplotlib, + otherwise the poles and zeros are only computed and returned. + grid: boolean (default = False) + If True plot omega-damping grid. + + Returns + ------- + poles: array + The system's poles. + zeros: array + The system's zeros. + + Notes + ----- + This function is a wrapper for :func:`~control.pole_zero_plot`, + with legacy return arguments. + + """ + from ..pzmap import pole_zero_plot + + # Use the plot keyword to get legacy behavior + kwargs = dict(kwargs) # make a copy since we modify this + if 'plot' not in kwargs: + kwargs['plot'] = True + + # Turn off deprecation warning + with warnings.catch_warnings(): + warnings.filterwarnings( + 'ignore', message='.* return values of .* is deprecated', + category=DeprecationWarning) + retval = pole_zero_plot(*args, **kwargs) + + return retval + + from ..nichols import nichols_grid def ngrid(): return nichols_grid() @@ -254,6 +352,7 @@ def dcgain(*args): from ..bdalg import connect as ct_connect def connect(*args): + """Index-based interconnection of an LTI system. The system `sys` is a system typically constructed with `append`, with diff --git a/control/rlocus.py b/control/rlocus.py index 970c7b98c..339225750 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -112,7 +112,7 @@ def root_locus_plot( ------- roots : ndarray Closed-loop root locations, arranged in which each row corresponds - to a gain in gains + to a gain in gains. gains : ndarray Gains used. Same as kvect keyword argument if provided. @@ -140,7 +140,7 @@ def root_locus_plot( # if plot is not None: warnings.warn( - "`root_locus` return values of loci, gains is deprecated; " + "`root_locus` return values of roots, gains is deprecated; " "use root_locus_map()", DeprecationWarning) if plot is False: diff --git a/control/tests/matlab_test.py b/control/tests/matlab_test.py index e01abcca1..2ba3d5df8 100644 --- a/control/tests/matlab_test.py +++ b/control/tests/matlab_test.py @@ -424,7 +424,8 @@ def testBode(self, siso, mplcleanup): @pytest.mark.parametrize("subsys", ["ss1", "tf1", "tf2"]) def testRlocus(self, siso, subsys, mplcleanup): """Call rlocus()""" - rlocus(getattr(siso, subsys)) + rlist, klist = rlocus(getattr(siso, subsys)) + np.testing.assert_equal(len(rlist), len(klist)) def testRlocus_list(self, siso, mplcleanup): """Test rlocus() with list""" diff --git a/control/tests/pzmap_test.py b/control/tests/pzmap_test.py index dc2ab5c27..ed021f05a 100644 --- a/control/tests/pzmap_test.py +++ b/control/tests/pzmap_test.py @@ -15,6 +15,7 @@ from control import TransferFunction, config, pzmap +@pytest.mark.filterwarnings("ignore:.*return values.*:DeprecationWarning") @pytest.mark.parametrize("kwargs", [pytest.param(dict(), id="default"), pytest.param(dict(plot=False), id="plot=False"), diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py index 7f36c035b..13d36c017 100644 --- a/control/tests/rlocus_test.py +++ b/control/tests/rlocus_test.py @@ -45,6 +45,7 @@ def check_cl_poles(self, sys, pole_list, k_list): poles = np.sort(poles) np.testing.assert_array_almost_equal(poles, poles_expected) + @pytest.mark.filterwarnings("ignore:.*return values.*:DeprecationWarning") def testRootLocus(self, sys): """Basic root locus (no plot)""" klist = [-1, 0, 1] @@ -60,6 +61,7 @@ def testRootLocus(self, sys): np.testing.assert_allclose(klist, k_out) self.check_cl_poles(sys, roots, klist) + @pytest.mark.filterwarnings("ignore:.*return values.*:DeprecationWarning") def test_without_gains(self, sys): roots, kvect = root_locus(sys, plot=False) self.check_cl_poles(sys, roots, kvect) @@ -79,6 +81,7 @@ def test_root_locus_plot_grid(self, sys, grid): assert n_gridlines > 2 # TODO check validity of grid + @pytest.mark.filterwarnings("ignore:.*return values.*:DeprecationWarning") def test_root_locus_neg_false_gain_nonproper(self): """ Non proper TranferFunction with negative gain: Not implemented""" with pytest.raises(ValueError, match="with equal order"): @@ -116,6 +119,7 @@ def test_root_locus_zoom(self): assert_array_almost_equal(zoom_x, zoom_x_valid) assert_array_almost_equal(zoom_y, zoom_y_valid) + @pytest.mark.filterwarnings("ignore:.*return values.*:DeprecationWarning") @pytest.mark.timeout(2) def test_rlocus_default_wn(self): """Check that default wn calculation works properly""" From 8776875cda88692746792abf87bcc247e3c2b8c4 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Thu, 28 Dec 2023 12:42:52 -0800 Subject: [PATCH 11/16] updated grid handling (add 'empty') --- control/freqplot.py | 2 +- control/matlab/wrappers.py | 2 ++ control/pzmap.py | 23 ++++++++----- control/rlocus.py | 66 +++++++----------------------------- control/tests/rlocus_test.py | 42 ++++++++++++++++------- 5 files changed, 60 insertions(+), 75 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 533515415..57f24f8d2 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -162,7 +162,7 @@ def bode_plot( values with no plot. rcParams : dict Override the default parameters used for generating plots. - Default is set up config.default['freqplot.rcParams']. + Default is set by config.default['freqplot.rcParams']. wrap_phase : bool or float If wrap_phase is `False` (default), then the phase will be unwrapped so that it is continuously increasing or decreasing. If wrap_phase is diff --git a/control/matlab/wrappers.py b/control/matlab/wrappers.py index 64743c953..0384215a8 100644 --- a/control/matlab/wrappers.py +++ b/control/matlab/wrappers.py @@ -195,6 +195,7 @@ def _parse_freqplot_args(*args): return syslist, omega, plotstyle, other +# TODO: rewrite to call root_locus_map, without using legacy plot keyword def rlocus(*args, **kwargs): """rlocus(sys[, klist, xlim, ylim, ...]) @@ -248,6 +249,7 @@ def rlocus(*args, **kwargs): return retval +# TODO: rewrite to call pole_zero_map, without using legacy plot keyword def pzmap(*args, **kwargs): """pzmap(sys[, grid, plot]) diff --git a/control/pzmap.py b/control/pzmap.py index b4cc2fec8..f30cbbc76 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -125,13 +125,14 @@ def pole_zero_plot( Parameters ---------- sysdata : List of PoleZeroData objects or LTI systems - List of pole/zero response data objects generated by pzmap_response + List of pole/zero response data objects generated by pzmap_response() or rootlocus_response() that are to be plotted. If a list of systems is given, the poles and zeros of those systems will be plotted. - grid : boolean (default = None) - If True plot omega-damping grid, otherwise show imaginary axis for - continuous time systems, unit circle for discrete time systems. If - `False`, do not draw any additonal lines. + grid : bool or str, optional + If `True` plot omega-damping grid, if `False` show imaginary axis + for continuous time systems, unit circle for discrete time systems. + If `empty`, do not draw any additonal lines. Default value is set + by config.default['pzmap.grid'] or config.default['rlocus.grid']. plot : bool, optional (legacy) If ``True`` a graph is generated with Matplotlib, otherwise the poles and zeros are only computed and returned. @@ -188,7 +189,7 @@ def pole_zero_plot( """ # Get parameter values - grid = config._get_param('pzmap', 'grid', grid, None) + grid = config._get_param('pzmap', 'grid', grid, _pzmap_defaults) marker_size = config._get_param('pzmap', 'marker_size', marker_size, 6) marker_width = config._get_param('pzmap', 'marker_width', marker_width, 1.5) xlim_user, ylim_user = xlim, ylim @@ -246,7 +247,7 @@ def pole_zero_plot( fig, axs = plt.figure(), [] with plt.rc_context(freqplot_rcParams): - if grid: + if grid and grid != 'empty': plt.clf() if all([isctime(dt=response.dt) for response in data]): ax, fig = sgrid(scaling=scaling) @@ -256,16 +257,20 @@ def pole_zero_plot( ValueError( "incompatible time responses; don't know how to grid") elif len(axs) == 0: - if grid is False: + if grid == 'empty': # Leave off grid entirely ax = plt.axes() else: - # use first response timebase + # draw stability boundary; use first response timebase ax, fig = nogrid(data[0].dt, scaling=scaling) else: # Use the existing axes and any grid that is there ax = axs[0] + # Issue a warning if the user tried to set the grid type + if grid: + warnings.warn("axis already exists; grid keyword ignored") + # Handle color cycle manually as all root locus segments # of the same system are expected to be of the same color color_cycle = plt.rcParams['axes.prop_cycle'].by_key()['color'] diff --git a/control/rlocus.py b/control/rlocus.py index 339225750..45dd9f2d2 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -24,7 +24,6 @@ from .iosys import isdtime from .xferfcn import _convert_to_transfer_function from .exception import ControlMIMONotImplemented -from .grid import sgrid, zgrid from . import config import warnings @@ -96,19 +95,25 @@ def root_locus_plot( (see :doc:`matplotlib:api/axes_api`). plotstr : :func:`matplotlib.pyplot.plot` format string, optional plotting style specification + TODO: check plot : boolean, optional If True (default), plot root locus diagram. + TODO: legacy print_gain : bool If True (default), report mouse clicks when close to the root locus branches, calculate gain, damping and print. - grid : bool - If True plot omega-damping grid. Default is False. + TODO: update + grid : bool or str, optional + If `True` plot omega-damping grid, if `False` show imaginary axis + for continuous time systems, unit circle for discrete time systems. + If `empty`, do not draw any additonal lines. Default value is set + by config.default['rlocus.grid']. ax : :class:`matplotlib.axes.Axes` Axes on which to create root locus plot initial_gain : float, optional Specify the initial gain to use when marking current gain. [TODO: update] - Returns + Returns (TODO: update) ------- roots : ndarray Closed-loop root locations, arranged in which each row corresponds @@ -156,8 +161,7 @@ def root_locus_plot( return out -# TODO: get rid of zoom functionality? -def _default_gains(num, den, xlim, ylim, zoom_xlim=None, zoom_ylim=None): +def _default_gains(num, den, xlim, ylim): """Unsupervised gains calculation for root locus plot. References @@ -237,8 +241,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( - root_array, tolerance, zoom_xlim, zoom_ylim) + indexes_too_far = _indexes_filt(root_array, tolerance) # Add more points into the root locus for points that are too far apart while len(indexes_too_far) > 0 and kvect.size < 5000: @@ -250,8 +253,7 @@ def _default_gains(num, den, xlim, ylim, zoom_xlim=None, zoom_ylim=None): root_array = np.insert(root_array, index + 1, new_points, axis=0) root_array = _RLSortRoots(root_array) - indexes_too_far = _indexes_filt( - root_array, tolerance, zoom_xlim, zoom_ylim) + indexes_too_far = _indexes_filt(root_array, tolerance) new_gains = kvect[-1] * np.hstack((np.logspace(0, 3, 4))) new_points = _RLFindRoots(num, den, new_gains[1:4]) @@ -261,7 +263,7 @@ def _default_gains(num, den, xlim, ylim, zoom_xlim=None, zoom_ylim=None): return kvect, root_array, xlim, ylim -def _indexes_filt(root_array, tolerance, zoom_xlim=None, zoom_ylim=None): +def _indexes_filt(root_array, tolerance): """Calculate the distance between points and return the indices. Filter the indexes so only the resolution of points within the xlim and @@ -270,48 +272,6 @@ def _indexes_filt(root_array, tolerance, zoom_xlim=None, zoom_ylim=None): """ 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: - x_tolerance_zoom = 0.05 * (zoom_xlim[1] - zoom_xlim[0]) - y_tolerance_zoom = 0.05 * (zoom_ylim[1] - zoom_ylim[0]) - tolerance_zoom = np.min([x_tolerance_zoom, y_tolerance_zoom]) - indexes_too_far_zoom = list( - np.unique(np.where(distance_points > tolerance_zoom)[0])) - indexes_too_far_filtered = [] - - for index in indexes_too_far_zoom: - 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(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(root_array)-limit) - else: - asign = np.sign(imag(root_array) - limit) - signchange = ((np.roll(asign, 1, axis=0) - - asign) != 0).astype(int) - 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) - - if len(indexes_too_far_filtered) > 0: - 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(root_array) - 2: - indexes_too_far_filtered.append( - indexes_too_far_filtered[-1] + 1) - - indexes_too_far.extend(indexes_too_far_filtered) - - indexes_too_far = list(np.unique(indexes_too_far)) indexes_too_far.sort() return indexes_too_far diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py index 13d36c017..c88be7b3a 100644 --- a/control/tests/rlocus_test.py +++ b/control/tests/rlocus_test.py @@ -66,20 +66,38 @@ def test_without_gains(self, sys): roots, kvect = root_locus(sys, plot=False) self.check_cl_poles(sys, roots, kvect) - @pytest.mark.skip("TODO: update test for rlocus gridlines") - @pytest.mark.slow - @pytest.mark.parametrize('grid', [None, True, False]) + @pytest.mark.parametrize("grid", [None, True, False, 'empty']) def test_root_locus_plot_grid(self, sys, grid): - rlist, klist = root_locus(sys, plot=True, grid=grid) + import mpl_toolkits.axisartist as AA + + # Generate the root locus plot + plt.clf() + ct.root_locus_plot(sys, grid=grid) + + # Count the number of dotted/dashed lines in the plot ax = plt.gca() - n_gridlines = sum([int(line.get_linestyle() in [':', 'dotted', - '--', 'dashed']) - for line in ax.lines]) - if grid is False: - assert n_gridlines == 2 - else: + n_gridlines = sum([int( + line.get_linestyle() in [':', 'dotted', '--', 'dashed'] or + line.get_linewidth() < 1 + ) for line in ax.lines]) + + # Make sure they line up with what we expect + if grid == 'empty': + assert n_gridlines == 0 + assert not isinstance(ax, AA.Axes) + elif grid is False: + assert n_gridlines == 2 if sys.isctime() else 3 + assert not isinstance(ax, AA.Axes) + elif sys.isdtime(strict=True): assert n_gridlines > 2 - # TODO check validity of grid + assert not isinstance(ax, AA.Axes) + else: + # Continuous time, with grid => check that AxisArtist was used + assert isinstance(ax, AA.Axes) + for spine in ['wnxneg', 'wnxpos', 'wnyneg', 'wnypos']: + assert spine in ax.axis + + # TODO: check validity of grid @pytest.mark.filterwarnings("ignore:.*return values.*:DeprecationWarning") def test_root_locus_neg_false_gain_nonproper(self): @@ -89,7 +107,7 @@ def test_root_locus_neg_false_gain_nonproper(self): # TODO: cover and validate negative false_gain branch in _default_gains() - @pytest.mark.skip("TODO: update test to check click dispatcher") + @pytest.mark.skip("Zooming functionality no longer implemented") @pytest.mark.skipif(plt.get_current_fig_manager().toolbar is None, reason="Requires the zoom toolbar") def test_root_locus_zoom(self): From 416dff8b4a46fb3e595c9a92590b068d6d4f9825 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Thu, 28 Dec 2023 21:58:24 -0800 Subject: [PATCH 12/16] updated rlocus/pzmap docs, docstrings, tests --- control/pzmap.py | 114 ++++++++++++++++++++++------ control/rlocus.py | 83 +++++++++++++------- control/tests/kwargs_test.py | 13 +++- control/tests/rlocus_test.py | 40 +++++++++- doc/Makefile | 5 +- doc/plotting.rst | 71 +++++++++++++++-- doc/pzmap-siso_ctime-default.png | Bin 0 -> 15186 bytes doc/rlocus-siso_ctime-clicked.png | Bin 0 -> 86287 bytes doc/rlocus-siso_ctime-default.png | Bin 0 -> 81401 bytes doc/rlocus-siso_dtime-default.png | Bin 0 -> 90229 bytes doc/rlocus-siso_multiple-nogrid.png | Bin 0 -> 24271 bytes 11 files changed, 264 insertions(+), 62 deletions(-) create mode 100644 doc/pzmap-siso_ctime-default.png create mode 100644 doc/rlocus-siso_ctime-clicked.png create mode 100644 doc/rlocus-siso_ctime-default.png create mode 100644 doc/rlocus-siso_dtime-default.png create mode 100644 doc/rlocus-siso_multiple-nogrid.png diff --git a/control/pzmap.py b/control/pzmap.py index f30cbbc76..6b9013508 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -8,16 +8,6 @@ # storing and plotting pole/zero and root locus diagrams. (The actual # computation of root locus diagrams is in rlocus.py.) # -# TODO (Sep 2023): -# * Test out ability to set line styles -# - Make compatible with other plotting (and refactor?) -# - Allow line fmt to be overwritten (including color=CN for different -# colors for each segment?) -# * Add ability to set style of root locus click point -# - Sort out where default parameter values should live (pzmap vs rlocus) -# * Decide whether click functionality should be in rlocus.py -# * Add back print_gain option to sisotool (and any other options) -# import numpy as np from numpy import real, imag, linspace, exp, cos, sin, sqrt @@ -34,7 +24,7 @@ from .freqplot import _freqplot_defaults, _get_line_labels from . import config -__all__ = ['pole_zero_map', 'pole_zero_plot', 'pzmap'] +__all__ = ['pole_zero_map', 'pole_zero_plot', 'pzmap', 'PoleZeroData'] # Define default parameter values for this module @@ -50,7 +40,7 @@ # Classes for keeping track of pzmap plots # # The PoleZeroData class keeps track of the information that is on a -# pole-zero plot. +# pole/zero plot. # # In addition to the locations of poles and zeros, you can also save a set # of gains and loci for use in generating a root locus plot. The gain @@ -58,14 +48,55 @@ # loci variable is a 2D array indexed by [gain_idx, root_idx] that can be # plotted using the `pole_zero_plot` function. # -# The PoleZeroList class is used to return a list of pole-zero plots. It +# The PoleZeroList class is used to return a list of pole/zero plots. It # is a lightweight wrapper on the built-in list class that includes a # `plot` method, allowing plotting a set of root locus diagrams. # class PoleZeroData: + """Pole/zero data object. + + This class is used as the return type for computing pole/zero responses + and root locus diagrams. It contains information on the location of + system poles and zeros, as well as the gains and loci for root locus + diagrams. + + Attributes + ---------- + poles : ndarray + 1D array of system poles. + zeros : ndarray + 1D array of system zeros. + gains : ndarray, optional + 1D array of gains for root locus plots. + loci : ndarray, optiona + 2D array of poles, with each row corresponding to a gain. + sysname : str, optional + System name. + sys : StateSpace or TransferFunction + System corresponding to the data. + + """ def __init__( self, poles, zeros, gains=None, loci=None, dt=None, sysname=None, sys=None): + """Create a pole/zero map object. + + Parameters + ---------- + poles : ndarray + 1D array of system poles. + zeros : ndarray + 1D array of system zeros. + gains : ndarray, optional + 1D array of gains for root locus plots. + loci : ndarray, optiona + 2D array of poles, with each row corresponding to a gain. + sysname : str, optional + System name. + sys : StateSpace or TransferFunction + System corresponding to the data. + + """ self.poles = poles self.zeros = zeros self.gains = gains @@ -79,17 +110,51 @@ def __iter__(self): return iter((self.poles, self.zeros)) def plot(self, *args, **kwargs): + """Plot the pole/zero data. + + See :func:`~control.pole_zero_plot` for description of arguments + and keywords. + + """ + # If this is a root locus plot, use rlocus defaults for grid + if self.loci is not None: + from .rlocus import _rlocus_defaults + kwargs = kwargs.copy() + kwargs['grid'] = config._get_param( + 'rlocus', 'grid', kwargs.get('grid', None), _rlocus_defaults) + return pole_zero_plot(self, *args, **kwargs) class PoleZeroList(list): + """List of PoleZeroData objects.""" def plot(self, *args, **kwargs): + """Plot pole/zero data. + + See :func:`~control.pole_zero_plot` for description of arguments + and keywords. + + """ return pole_zero_plot(self, *args, **kwargs) # Pole/zero map def pole_zero_map(sysdata): - # TODO: add docstring (from old pzmap?) + """Compute the pole/zero map for an LTI system. + + Parameters + ---------- + sys : LTI system (StateSpace or TransferFunction) + Linear system for which poles and zeros are computed. + + Returns + ------- + pzmap_data : PoleZeroMap + Pole/zero map containing the poles and zeros of the system. Use + `pzmap_data.plot()` or `pole_zero_plot(pzmap_data)` to plot the + pole/zero map. + + """ # Convert the first argument to a list syslist = sysdata if isinstance(sysdata, (list, tuple)) else [sysdata] @@ -113,7 +178,6 @@ def pole_zero_plot( marker_size=None, marker_width=None, legend_loc='upper right', xlim=None, ylim=None, interactive=None, ax=None, scaling=None, initial_gain=None, **kwargs): - # TODO: update docstring (see other response/plot functions for style) """Plot a pole/zero map for a linear system. If the system data include root loci, a root locus diagram for the @@ -137,7 +201,7 @@ def pole_zero_plot( (legacy) If ``True`` a graph is generated with Matplotlib, otherwise the poles and zeros are only computed and returned. If this argument is present, the legacy value of poles and - zero is returned. + zeros is returned. Returns ------- @@ -287,6 +351,7 @@ def pole_zero_plot( # Plot the responses (and keep track of axes limits) xlim, ylim = ax.get_xlim(), ax.get_ylim() + loci_count = 0 for idx, response in enumerate(pzmap_responses): poles = response.poles zeros = response.zeros @@ -331,9 +396,17 @@ def pole_zero_plot( # TODO: add arrows to root loci (reuse Nyquist arrow code?) - # Set up the limits for the plot - ax.set_xlim(xlim if xlim_user is None else xlim_user) - ax.set_ylim(ylim if ylim_user is None else ylim_user) + # Set the axis limits to something reasonable + if any([response.loci is not None for response in pzmap_responses]): + # Set up the limits for the plot using information from loci + ax.set_xlim(xlim if xlim_user is None else xlim_user) + ax.set_ylim(ylim if ylim_user is None else ylim_user) + else: + # No root loci => only set axis limits if users specified them + if xlim_user is not None: + ax.set_xlim(xlim_user) + if ylim_user is not None: + ax.set_ylim(ylim_user) # List of systems that are included in this plot lines, labels = _get_line_labels(ax) @@ -409,7 +482,6 @@ def _click_dispatcher(event): # Utility function to find gain corresponding to a click event -# TODO: project onto the root locus plot (here or above?) def _find_root_locus_gain(event, sys, ax): # Get the current axis limits to set various thresholds xlim, ylim = ax.get_xlim(), ax.get_ylim() @@ -469,7 +541,6 @@ def _mark_root_locus_gain(ax, sys, K): # Return a string identifying a clicked point -# TODO: project onto the root locus plot (here or above?) def _create_root_locus_label(sys, K, s): # Figure out the damping ratio if isdtime(sys, strict=True): @@ -482,7 +553,6 @@ def _create_root_locus_label(sys, K, s): # Utility function to compute limits for root loci -# TODO: (note that sys is now available => code here may not be needed) def _compute_root_locus_limits(response): loci = response.loci diff --git a/control/rlocus.py b/control/rlocus.py index 45dd9f2d2..eac21f1e0 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -25,23 +25,46 @@ from .xferfcn import _convert_to_transfer_function from .exception import ControlMIMONotImplemented from . import config +from .lti import LTI import warnings __all__ = ['root_locus_map', 'root_locus_plot', 'root_locus', 'rlocus'] # Default values for module parameters -# TODO: merge these with pzmap parameters (?) _rlocus_defaults = { 'rlocus.grid': True, - 'rlocus.plotstr': 'C0', # default color cycle [TODO: not used?] - 'rlocus.print_gain': True, - 'rlocus.plot': True } # Root locus map -# TODO: add docstring def root_locus_map(sysdata, gains=None): + """Compute the root locus map for an LTI system. + + Calculate the root locus by finding the roots of 1 + k * G(s) where G + is a linear system with transfer function num(s)/den(s) and each k is + an element of kvect. + + Parameters + ---------- + sys : LTI system or list of LTI systems + Linear input/output systems (SISO only, for now). + kvect : array_like, optional + Gains to use in computing plot of closed-loop poles. + + Returns + ------- + rldata : PoleZeroData or list of PoleZeroData + Root locus data object(s) corresponding to the . The loci of + the root locus diagram are available in the array + `rldata.loci`, indexed by the gain index and the locus index, + and the gains are in the array `rldata.gains`. + + Notes + ----- + For backward compatibility, the `rldata` return object can be + assigned to the tuple `roots, gains`. + + """ from .pzmap import PoleZeroData, PoleZeroList # Convert the first argument to a list @@ -75,6 +98,7 @@ def root_locus_map(sysdata, gains=None): def root_locus_plot( sysdata, kvect=None, grid=None, plot=None, **kwargs): + """Root locus plot. Calculate the root locus by finding the roots of 1 + k * G(s) where G @@ -83,7 +107,7 @@ def root_locus_plot( Parameters ---------- - sys : LTI object + sysdata : PoleZeroMap or LTI object or list Linear input/output systems (SISO only, for now). kvect : array_like, optional Gains to use in computing plot of closed-loop poles. @@ -93,16 +117,9 @@ def root_locus_plot( ylim : tuple or list, optional Set limits of y axis, normally with tuple (see :doc:`matplotlib:api/axes_api`). - plotstr : :func:`matplotlib.pyplot.plot` format string, optional - plotting style specification - TODO: check - plot : boolean, optional - If True (default), plot root locus diagram. - TODO: legacy - print_gain : bool - If True (default), report mouse clicks when close to the root locus - branches, calculate gain, damping and print. - TODO: update + plot : bool, optional + (legacy) If given, `root_locus_plot` returns the legacy return values + of roots and gains. If False, just return the values with no plot. grid : bool or str, optional If `True` plot omega-damping grid, if `False` show imaginary axis for continuous time systems, unit circle for discrete time systems. @@ -111,19 +128,29 @@ def root_locus_plot( ax : :class:`matplotlib.axes.Axes` Axes on which to create root locus plot initial_gain : float, optional - Specify the initial gain to use when marking current gain. [TODO: update] + Mark the point on the root locus diagram corresponding to the + given gain. - Returns (TODO: update) + Returns ------- - 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. + lines : List of Line2D + Array of Line2D objects for each set of markers in the plot. The + shape of the array is given by (nsys, 2) where nsys is the number + of systems or Nyquist responses passed to the function. The second + index specifies the pzmap object type: + + * lines[idx, 0]: poles + * lines[idx, 1]: zeros + + roots, gains : ndarray + (legacy) If the `plot` keyword is given, returns the + closed-loop root locations, arranged such that each row + corresponds to a gain in gains, and the array of gains (ame as + kvect keyword argument if provided). Notes ----- - The root_locus function calls matplotlib.pyplot.axis('equal'), which + The root_locus_plot function calls matplotlib.pyplot.axis('equal'), which means that trying to reset the axis limits may not behave as expected. To change the axis limits, use matplotlib.pyplot.gca().axis('auto') and then set the axis limits to the desired values. @@ -132,10 +159,14 @@ def root_locus_plot( from .pzmap import pole_zero_plot # Set default parameters - # TODO: move this to pole_zero_plot() grid = config._get_param('rlocus', 'grid', grid, _rlocus_defaults) - responses = root_locus_map(sysdata, gains=kvect) + if isinstance(sysdata, list) and all( + [isinstance(sys, LTI) for sys in sysdata]) or \ + isinstance(sysdata, LTI): + responses = root_locus_map(sysdata, gains=kvect) + else: + responses = sysdata # # Process `plot` keyword diff --git a/control/tests/kwargs_test.py b/control/tests/kwargs_test.py index bdcae1885..53cc0076b 100644 --- a/control/tests/kwargs_test.py +++ b/control/tests/kwargs_test.py @@ -176,6 +176,8 @@ def test_matplotlib_kwargs(function, nsysargs, moreargs, kwargs, mplcleanup): (control.frequency_response, control.bode, True), (control.frequency_response, control.bode_plot, True), (control.nyquist_response, control.nyquist_plot, False), + (control.pole_zero_map, control.pole_zero_plot, False), + (control.root_locus_map, control.root_locus_plot, False), ]) def test_response_plot_kwargs(data_fcn, plot_fcn, mimo): # Create a system for testing @@ -194,16 +196,18 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo): plot_fcn(response) # Now add an unrecognized keyword and make sure there is an error - with pytest.raises(AttributeError, - match="(has no property|unexpected keyword)"): + with pytest.raises( + (AttributeError, TypeError), + match="(has no property|unexpected keyword|unrecognized keyword)"): plot_fcn(response, unknown=None) # Call the plotting function via the response and make sure it works response.plot() # Now add an unrecognized keyword and make sure there is an error - with pytest.raises(AttributeError, - match="(has no property|unexpected keyword)"): + with pytest.raises( + (AttributeError, TypeError), + match="(has no property|unexpected keyword|unrecognized keyword)"): response.plot(unknown=None) # @@ -277,6 +281,7 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo): 'flatsys.LinearFlatSystem.__init__': test_unrecognized_kwargs, 'NonlinearIOSystem.linearize': test_unrecognized_kwargs, 'NyquistResponseData.plot': test_response_plot_kwargs, + 'PoleZeroData.plot': test_response_plot_kwargs, 'InterconnectedSystem.__init__': interconnect_test.test_interconnect_exceptions, 'StateSpace.__init__': diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py index c88be7b3a..198515fed 100644 --- a/control/tests/rlocus_test.py +++ b/control/tests/rlocus_test.py @@ -160,7 +160,6 @@ def test_rlocus_default_wn(self): ct.root_locus(sys, plot=True) -# TODO: add additional test cases @pytest.mark.parametrize( "sys, grid, xlim, ylim, interactive", [ (ct.tf([1], [1, 2, 1]), None, None, None, False), @@ -171,6 +170,40 @@ def test_root_locus_plots(sys, grid, xlim, ylim, interactive): # TODO: add tests to make sure everything "looks" OK +# Generate plots used in documentation +def test_root_locus_documentation(savefigs=False): + plt.figure() + sys = ct.tf([1, 2], [1, 2, 3], name='SISO transfer function') + response = ct.pole_zero_map(sys) + ct.pole_zero_plot(response) + plt.savefig('pzmap-siso_ctime-default.png') + + plt.figure() + ct.root_locus_map(sys).plot() + plt.savefig('rlocus-siso_ctime-default.png') + + # TODO: generate event in order to generate real title + plt.figure() + out = ct.root_locus_map(sys).plot(initial_gain=2) + ax = ct.get_plot_axes(out)[0, 0] + freqplot_rcParams = ct.config._get_param('freqplot', 'rcParams') + with plt.rc_context(freqplot_rcParams): + ax.set_title( + "Clicked at: -2.729+1.511j gain = 3.506 damping = 0.8748") + plt.savefig('rlocus-siso_ctime-clicked.png') + + plt.figure() + sysd = sys.sample(0.1) + ct.root_locus_plot(sysd) + plt.savefig('rlocus-siso_dtime-default.png') + + plt.figure() + sys1 = ct.tf([1, 2], [1, 2, 3], name='sys1') + sys2 = ct.tf([1, 0.2], [1, 1, 3, 1, 1], name='sys2') + ct.root_locus_plot([sys1, sys2], grid=False) + plt.savefig('rlocus-siso_multiple-nogrid.png') + + if __name__ == "__main__": # # Interactive mode: generate plots for manual viewing @@ -204,7 +237,7 @@ def test_root_locus_plots(sys, grid, xlim, ylim, interactive): (sys_secord, None, None, None, None), (sys_seczero, None, None, None, None), (sys_fbs_a, None, None, None, None), - (sys_fbs_b, None, None, None, None), + (sys_fbs_b, None, None, None, False), (sys_fbs_c, None, None, None, None), (sys_fbs_c, None, None, [-2, 2], None), (sys_fbs_c, True, [-3, 3], None, None), @@ -222,3 +255,6 @@ def test_root_locus_plots(sys, grid, xlim, ylim, interactive): plt.figure() test_root_locus_plots( sys, grid=grid, xlim=xlim, ylim=ylim, interactive=interactive) + + # Run tests that generate plots for the documentation + test_root_locus_documentation(savefigs=True) diff --git a/doc/Makefile b/doc/Makefile index a5f7ec5aa..71e493f23 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -16,7 +16,7 @@ help: # Rules to create figures FIGS = classes.pdf timeplot-mimo_step-default.png \ - freqplot-siso_bode-default.png + freqplot-siso_bode-default.png rlocus-siso_ctime-default.png classes.pdf: classes.fig fig2dev -Lpdf $< $@ @@ -26,6 +26,9 @@ timeplot-mimo_step-default.png: ../control/tests/timeplot_test.py freqplot-siso_bode-default.png: ../control/tests/freqplot_test.py PYTHONPATH=.. python $< +rlocus-siso_ctime-default.png: ../control/tests/rlocus_test.py + PYTHONPATH=.. python $< + # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). html pdf clean doctest: Makefile $(FIGS) diff --git a/doc/plotting.rst b/doc/plotting.rst index be7ae7a55..2f6857c35 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -4,15 +4,15 @@ Plotting data ************* -The Python Control Toolbox contains a number of functions for plotting -input/output responses in the time and frequency domain, root locus -diagrams, and other standard charts used in control system analysis, for -example:: +The Python Control Systems Toolbox contains a number of functions for +plotting input/output responses in the time and frequency domain, root +locus diagrams, and other standard charts used in control system analysis, +for example:: bode_plot(sys) nyquist_plot([sys1, sys2]) - -.. root_locus_plot(sys) # not yet implemented + pole_zero_plot(sys) + root_locus_plot(sys) While plotting functions can be called directly, the standard pattern used in the toolbox is to provide a function that performs the basic computation @@ -35,7 +35,7 @@ analysis object, allowing the following type of calls:: step_response(sys).plot() frequency_response(sys).plot() nyquist_response(sys).plot() - rootlocus_response(sys).plot() # implementation pending + root_locus_map(sys).plot() The remainder of this chapter provides additional documentation on how these response and plotting functions can be customized. @@ -222,6 +222,58 @@ sensitivity functions for a feedback control system in standard form:: .. image:: freqplot-gangof4.png +Pole/zero data +============== + +Pole/zero maps and root locus diagrams provide insights into system +response based on the locations of system poles and zeros in the complex +plane. The :func:`~control.pole_zero_map` function returns the poles and +zeros and can be used to generate a pole/zero plot:: + + sys = ct.tf([1, 2], [1, 2, 3], name='SISO transfer function') + response = ct.pole_zero_map(sys) + ct.pole_zero_plot(response) + +.. image:: pzmap-siso_ctime-default.png + +A root locus plot shows the location of the closed loop poles of a system +as a function of the loop gain:: + + ct.root_locus_map(sys).plot() + +.. image:: rlocus-siso_ctime-default.png + +The grid in the left hand plane shows lines of constant damping ratio as +well as arcs corresponding to the frequency of the complex pole. The grid +can be turned off using the `grid` keyword. Setting `grid` to `False` will +turn off the grid but show the real and imaginary axis. To completely +remove all lines except the root loci, use `grid='empty'`. + +On systems that support interactive plots, clicking on a location on the +root locus diagram will mark the pole locations on all branches of the +diagram and display the gain and damping ratio for the clicked point below +the plot title: + +.. image:: rlocus-siso_ctime-clicked.png + +Root locus diagrams are also supported for discrete time systems, in which +case the grid is show inside the unit circle:: + + sysd = sys.sample(0.1) + ct.root_locus_plot(sysd) + +.. image:: rlocus-siso_dtime-default.png + +Lists of systems can also be given, in which case the root locus diagram +for each system is plotted in different colors:: + + sys1 = ct.tf([1], [1, 2, 1], name='sys1') + sys2 = ct.tf([1, 0.2], [1, 1, 3, 1, 1], name='sys2') + ct.root_locus_plot([sys1, sys2], grid=False) + +.. image:: rlocus-siso_multiple-nogrid.png + + Response and plotting functions =============================== @@ -244,6 +296,8 @@ number of encirclements for a Nyquist plot) as well as plotting (via the ~control.initial_response ~control.input_output_response ~control.nyquist_response + ~control.pole_zero_map + ~control.root_locus_map ~control.singular_values_response ~control.step_response @@ -256,6 +310,8 @@ Plotting functions ~control.bode_plot ~control.describing_function_plot ~control.nichols_plot + ~control.pole_zero_plot + ~control.root_locus_plot ~control.singular_values_plot ~control.time_response_plot @@ -284,4 +340,5 @@ The following classes are used in generating response data. ~control.DescribingFunctionResponse ~control.FrequencyResponseData ~control.NyquistResponseData + ~control.PoleZeroData ~control.TimeResponseData diff --git a/doc/pzmap-siso_ctime-default.png b/doc/pzmap-siso_ctime-default.png new file mode 100644 index 0000000000000000000000000000000000000000..1caa7cadfd014e8c075891676d42ed64aef0d98e GIT binary patch literal 15186 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

;9Ct#$|rPh&BC+n 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

@1~gG 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