diff --git a/control/config.py b/control/config.py index f61469394..02028cfba 100644 --- a/control/config.py +++ b/control/config.py @@ -114,11 +114,11 @@ def use_matlab_defaults(): The following conventions are used: * Bode plots plot gain in dB, phase in degrees, frequency in - Hertz, with grids + rad/sec, with grids * State space class and functions use Numpy matrix objects """ - set_defaults('bode', dB=True, deg=True, Hz=True, grid=True) + set_defaults('bode', dB=True, deg=True, Hz=False, grid=True) set_defaults('statesp', use_numpy_matrix=True) @@ -128,7 +128,7 @@ def use_fbs_defaults(): The following conventions are used: * Bode plots plot gain in powers of ten, phase in degrees, - frequency in Hertz, no grid + frequency in rad/sec, no grid """ set_defaults('bode', dB=False, deg=True, Hz=False, grid=False) diff --git a/control/freqplot.py b/control/freqplot.py index c8b513943..a1772fea7 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -80,7 +80,7 @@ def bode_plot(syslist, omega=None, - Plot=True, omega_limits=None, omega_num=None, + plot=True, omega_limits=None, omega_num=None, margins=None, *args, **kwargs): """Bode plot for a system @@ -100,7 +100,7 @@ def bode_plot(syslist, omega=None, deg : bool If True, plot phase in degrees (else radians). Default value (True) config.defaults['bode.deg'] - Plot : bool + plot : bool If True (default), plot magnitude and phase omega_limits: tuple, list, ... of two values Limits of the to generate frequency vector. @@ -110,9 +110,9 @@ def bode_plot(syslist, omega=None, config.defaults['freqplot.number_of_samples']. margins : bool If True, plot gain and phase margin. - *args - Additional arguments for :func:`matplotlib.plot` (color, linestyle, etc) - **kwargs: + *args : `matplotlib` plot positional properties, optional + Additional arguments for `matplotlib` plots (color, linestyle, etc) + **kwargs : `matplotlib` plot keyword properties, optional Additional keywords (passed to `matplotlib`) Returns @@ -153,12 +153,20 @@ def bode_plot(syslist, omega=None, # Make a copy of the kwargs dictonary since we will modify it kwargs = dict(kwargs) + # Check to see if legacy 'Plot' keyword was used + if 'Plot' in kwargs: + import warnings + warnings.warn("'Plot' keyword is deprecated in bode_plot; use 'plot'", + FutureWarning) + # Map 'Plot' keyword to 'plot' keyword + plot = kwargs.pop('Plot') + # Get values for params (and pop from list to allow keyword use in plot) dB = config._get_param('bode', 'dB', kwargs, _bode_defaults, pop=True) deg = config._get_param('bode', 'deg', kwargs, _bode_defaults, pop=True) Hz = config._get_param('bode', 'Hz', kwargs, _bode_defaults, pop=True) grid = config._get_param('bode', 'grid', kwargs, _bode_defaults, pop=True) - Plot = config._get_param('bode', 'grid', Plot, True) + plot = config._get_param('bode', 'grid', plot, True) margins = config._get_param('bode', 'margins', margins, False) # If argument was a singleton, turn it into a list @@ -211,7 +219,7 @@ def bode_plot(syslist, omega=None, # Get the dimensions of the current axis, which we will divide up # TODO: Not current implemented; just use subplot for now - if Plot: + if plot: nyquistfrq_plot = None if Hz: omega_plot = omega_sys / (2. * math.pi) @@ -429,12 +437,13 @@ def gen_zero_centered_series(val_min, val_max, period): else: return mags, phases, omegas + # # Nyquist plot # -def nyquist_plot(syslist, omega=None, Plot=True, - labelFreq=0, arrowhead_length=0.1, arrowhead_width=0.1, +def nyquist_plot(syslist, omega=None, plot=True, label_freq=0, + arrowhead_length=0.1, arrowhead_width=0.1, color=None, *args, **kwargs): """ Nyquist plot for a system @@ -451,13 +460,13 @@ def nyquist_plot(syslist, omega=None, Plot=True, If True, plot magnitude color : string Used to specify the color of the plot - labelFreq : int + label_freq : int Label every nth frequency on the plot arrowhead_width : arrow head width arrowhead_length : arrow head length - *args - Additional arguments for :func:`matplotlib.plot` (color, linestyle, etc) - **kwargs: + *args : `matplotlib` plot positional properties, optional + Additional arguments for `matplotlib` plots (color, linestyle, etc) + **kwargs : `matplotlib` plot keyword properties, optional Additional keywords (passed to `matplotlib`) Returns @@ -475,6 +484,22 @@ def nyquist_plot(syslist, omega=None, Plot=True, >>> real, imag, freq = nyquist_plot(sys) """ + # Check to see if legacy 'Plot' keyword was used + if 'Plot' in kwargs: + import warnings + warnings.warn("'Plot' keyword is deprecated in nyquist_plot; " + "use 'plot'", FutureWarning) + # Map 'Plot' keyword to 'plot' keyword + plot = kwargs.pop('Plot') + + # Check to see if legacy 'labelFreq' keyword was used + if 'labelFreq' in kwargs: + import warnings + warnings.warn("'labelFreq' keyword is deprecated in nyquist_plot; " + "use 'label_freq'", FutureWarning) + # Map 'labelFreq' keyword to 'label_freq' keyword + label_freq = kwargs.pop('labelFreq') + # If argument was a singleton, turn it into a list if not getattr(syslist, '__iter__', False): syslist = (syslist,) @@ -507,7 +532,7 @@ def nyquist_plot(syslist, omega=None, Plot=True, x = sp.multiply(mag, sp.cos(phase)) y = sp.multiply(mag, sp.sin(phase)) - if Plot: + if plot: # Plot the primary curve and mirror image p = plt.plot(x, y, '-', color=color, *args, **kwargs) c = p[0].get_color() @@ -527,8 +552,8 @@ def nyquist_plot(syslist, omega=None, Plot=True, plt.plot([-1], [0], 'r+') # Label the frequencies of the points - if labelFreq: - ind = slice(None, None, labelFreq) + if label_freq: + ind = slice(None, None, label_freq) for xpt, ypt, omegapt in zip(x[ind], y[ind], omega[ind]): # Convert to Hz f = omegapt / (2 * sp.pi) @@ -550,7 +575,7 @@ def nyquist_plot(syslist, omega=None, Plot=True, str(int(np.round(f / 1000 ** pow1000, 0))) + ' ' + prefix + 'Hz') - if Plot: + if plot: ax = plt.gca() ax.set_xlabel("Real axis") ax.set_ylabel("Imaginary axis") @@ -558,6 +583,7 @@ def nyquist_plot(syslist, omega=None, Plot=True, return x, y, omega + # # Gang of Four plot # @@ -575,6 +601,8 @@ def gangof4_plot(P, C, omega=None, **kwargs): Linear input/output systems (process and control) omega : array Range of frequencies (list or bounds) in rad/sec + **kwargs : `matplotlib` plot keyword properties, optional + Additional keywords (passed to `matplotlib`) Returns ------- @@ -590,16 +618,16 @@ def gangof4_plot(P, C, omega=None, **kwargs): Hz = config._get_param('bode', 'Hz', kwargs, _bode_defaults, pop=True) grid = config._get_param('bode', 'grid', kwargs, _bode_defaults, pop=True) - # Select a default range if none is provided - # TODO: This needs to be made more intelligent - if omega is None: - omega = default_frequency_range((P, C)) - # Compute the senstivity functions L = P * C S = feedback(1, L) T = L * S + # Select a default range if none is provided + # TODO: This needs to be made more intelligent + if omega is None: + omega = default_frequency_range((P, C, S)) + # Set up the axes with labels so that multiple calls to # gangof4_plot will superimpose the data. See details in bode_plot. plot_axes = {'t': None, 's': None, 'ps': None, 'cs': None} @@ -628,36 +656,49 @@ def gangof4_plot(P, C, omega=None, **kwargs): # TODO: Need to add in the mag = 1 lines mag_tmp, phase_tmp, omega = S.freqresp(omega) mag = np.squeeze(mag_tmp) - plot_axes['s'].loglog(omega_plot, 20 * np.log10(mag) if dB else mag) - plot_axes['s'].set_ylabel("$|S|$") + if dB: + plot_axes['s'].semilogx(omega_plot, 20 * np.log10(mag), **kwargs) + else: + plot_axes['s'].loglog(omega_plot, mag, **kwargs) + plot_axes['s'].set_ylabel("$|S|$" + " (dB)" if dB else "") plot_axes['s'].tick_params(labelbottom=False) plot_axes['s'].grid(grid, which='both') mag_tmp, phase_tmp, omega = (P * S).freqresp(omega) mag = np.squeeze(mag_tmp) - plot_axes['ps'].loglog(omega_plot, 20 * np.log10(mag) if dB else mag) + if dB: + plot_axes['ps'].semilogx(omega_plot, 20 * np.log10(mag), **kwargs) + else: + plot_axes['ps'].loglog(omega_plot, mag, **kwargs) plot_axes['ps'].tick_params(labelbottom=False) - plot_axes['ps'].set_ylabel("$|PS|$") + plot_axes['ps'].set_ylabel("$|PS|$" + " (dB)" if dB else "") plot_axes['ps'].grid(grid, which='both') mag_tmp, phase_tmp, omega = (C * S).freqresp(omega) mag = np.squeeze(mag_tmp) - plot_axes['cs'].loglog(omega_plot, 20 * np.log10(mag) if dB else mag) + if dB: + plot_axes['cs'].semilogx(omega_plot, 20 * np.log10(mag), **kwargs) + else: + plot_axes['cs'].loglog(omega_plot, mag, **kwargs) plot_axes['cs'].set_xlabel( "Frequency (Hz)" if Hz else "Frequency (rad/sec)") - plot_axes['cs'].set_ylabel("$|CS|$") + plot_axes['cs'].set_ylabel("$|CS|$" + " (dB)" if dB else "") plot_axes['cs'].grid(grid, which='both') mag_tmp, phase_tmp, omega = T.freqresp(omega) mag = np.squeeze(mag_tmp) - plot_axes['t'].loglog(omega_plot, 20 * np.log10(mag) if dB else mag) + if dB: + plot_axes['t'].semilogx(omega_plot, 20 * np.log10(mag), **kwargs) + else: + plot_axes['t'].loglog(omega_plot, mag, **kwargs) plot_axes['t'].set_xlabel( "Frequency (Hz)" if Hz else "Frequency (rad/sec)") - plot_axes['t'].set_ylabel("$|T|$") + plot_axes['t'].set_ylabel("$|T|$" + " (dB)" if dB else "") plot_axes['t'].grid(grid, which='both') plt.tight_layout() + # # Utility functions # @@ -754,7 +795,7 @@ def default_frequency_range(syslist, Hz=None, number_of_samples=None, # TODO raise NotImplementedError( "type of system in not implemented now") - except: + except NotImplementedError: pass # Make sure there is at least one point in the range @@ -787,15 +828,17 @@ def default_frequency_range(syslist, Hz=None, number_of_samples=None, omega = sp.logspace(lsp_min, lsp_max, endpoint=True) return omega + # -# KLD 5/23/11: Two functions to create nice looking labels +# Utility functions to create nice looking labels (KLD 5/23/11) # def get_pow1000(num): """Determine exponent for which significand of a number is within the range [1, 1000). """ - # Based on algorithm from http://www.mail-archive.com/matplotlib-users@lists.sourceforge.net/msg14433.html, accessed 2010/11/7 + # Based on algorithm from http://www.mail-archive.com/ + # matplotlib-users@lists.sourceforge.net/msg14433.html, accessed 2010/11/7 # by Jason Heeris 2009/11/18 from decimal import Decimal from math import floor diff --git a/control/grid.py b/control/grid.py index ed46ff0f7..8aa583bc0 100644 --- a/control/grid.py +++ b/control/grid.py @@ -2,19 +2,22 @@ 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 +from mpl_toolkits.axisartist.grid_helper_curvelinear \ + import GridHelperCurveLinear import mpl_toolkits.axisartist.angle_helper as angle_helper from matplotlib.projections import PolarAxes from matplotlib.transforms import Affine2D + class FormatterDMS(object): '''Transforms angle ticks to damping ratios''' - def __call__(self,direction,factor,values): + def __call__(self, direction, factor, values): angles_deg = values/factor - damping_ratios = np.cos((180-angles_deg)*np.pi/180) - ret = ["%.2f"%val for val in damping_ratios] + damping_ratios = np.cos((180-angles_deg) * np.pi/180) + ret = ["%.2f" % val for val in damping_ratios] return ret + class ModifiedExtremeFinderCycle(angle_helper.ExtremeFinderCycle): '''Changed to allow only left hand-side polar grid''' def __call__(self, transform_xy, x1, y1, x2, y2): @@ -25,10 +28,14 @@ def __call__(self, transform_xy, x1, y1, x2, y2): with np.errstate(invalid='ignore'): if self.lon_cycle is not None: lon0 = np.nanmin(lon) - lon -= 360. * ((lon - lon0) > 360.) # Changed from 180 to 360 to be able to span only 90-270 (left hand side) + # Changed from 180 to 360 to be able to span only + # 90-270 (left hand side) + lon -= 360. * ((lon - lon0) > 360.) if self.lat_cycle is not None: lat0 = np.nanmin(lat) - lat -= 360. * ((lat - lat0) > 360.) # Changed from 180 to 360 to be able to span only 90-270 (left hand side) + # Changed from 180 to 360 to be able to span only + # 90-270 (left hand side) + lat -= 360. * ((lat - lat0) > 360.) lon_min, lon_max = np.nanmin(lon), np.nanmax(lon) lat_min, lat_max = np.nanmin(lat), np.nanmax(lat) @@ -38,6 +45,7 @@ def __call__(self, transform_xy, x1, y1, x2, y2): return lon_min, lon_max, lat_min, lat_max + def sgrid(): # From matplotlib demos: # https://matplotlib.org/gallery/axisartist/demo_curvelinear_grid.html @@ -52,21 +60,17 @@ def sgrid(): # 20, 20 : number of sampling points along x, y direction sampling_points = 20 - extreme_finder = ModifiedExtremeFinderCycle(sampling_points, sampling_points, - lon_cycle=360, - lat_cycle=None, - lon_minmax=(90,270), - lat_minmax=(0, np.inf),) + extreme_finder = ModifiedExtremeFinderCycle( + sampling_points, sampling_points, lon_cycle=360, lat_cycle=None, + lon_minmax=(90, 270), lat_minmax=(0, np.inf),) grid_locator1 = angle_helper.LocatorDMS(15) tick_formatter1 = FormatterDMS() - grid_helper = GridHelperCurveLinear(tr, - extreme_finder=extreme_finder, - grid_locator1=grid_locator1, - tick_formatter1=tick_formatter1 - ) + grid_helper = GridHelperCurveLinear( + tr, extreme_finder=extreme_finder, grid_locator1=grid_locator1, + tick_formatter1=tick_formatter1) - fig = plt.figure() + fig = plt.gcf() ax = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper) # make ticklabels of right invisible, and top axis visible. @@ -97,24 +101,25 @@ def sgrid(): 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", + # 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", + # par2.axis["bottom"] = new_fixed_axis(loc="bottom", # axes=par2, # offset=(0, 0)) - ### FINISH RECTANGULAR + # FINISH RECTANGULAR - ax.grid(True, zorder=0,linestyle='dotted') + ax.grid(True, zorder=0, linestyle='dotted') _final_setup(ax) return ax, fig + def _final_setup(ax): ax.set_xlabel('Real') ax.set_ylabel('Imaginary') @@ -122,17 +127,19 @@ def _final_setup(ax): ax.axvline(x=0, color='black', lw=1) plt.axis('equal') + def nogrid(): - f = plt.figure() + f = plt.gcf() ax = plt.axes() _final_setup(ax) return ax, f + def zgrid(zetas=None, wns=None): '''Draws discrete damping and frequency grid''' - fig = plt.figure() + fig = plt.gcf() ax = fig.gca() # Constant damping lines @@ -141,42 +148,43 @@ def zgrid(zetas=None, wns=None): for zeta in zetas: # Calculate in polar coordinates factor = zeta/sqrt(1-zeta**2) - x = linspace(0, sqrt(1-zeta**2),200) + x = linspace(0, sqrt(1-zeta**2), 200) ang = pi*x mag = exp(-pi*factor*x) # Draw upper part in retangular coordinates xret = mag*cos(ang) yret = mag*sin(ang) - ax.plot(xret,yret, 'k:', lw=1) + ax.plot(xret, yret, 'k:', lw=1) # Draw lower part in retangular coordinates xret = mag*cos(-ang) yret = mag*sin(-ang) - ax.plot(xret,yret,'k:', lw=1) + ax.plot(xret, yret, 'k:', lw=1) # Annotation an_i = int(len(xret)/2.5) an_x = xret[an_i] an_y = yret[an_i] - ax.annotate(str(round(zeta,2)), xy=(an_x, an_y), xytext=(an_x, an_y), size=7) + ax.annotate(str(round(zeta, 2)), xy=(an_x, an_y), + xytext=(an_x, an_y), size=7) # Constant natural frequency lines if wns is None: wns = linspace(0, 1, 10) for a in wns: # Calculate in polar coordinates - x = linspace(-pi/2,pi/2,200) + x = linspace(-pi/2, pi/2, 200) ang = pi*a*sin(x) mag = exp(-pi*a*cos(x)) # Draw in retangular coordinates xret = mag*cos(ang) yret = mag*sin(ang) - ax.plot(xret,yret,'k:', lw=1) + ax.plot(xret, yret, 'k:', lw=1) # Annotation an_i = -1 an_x = xret[an_i] an_y = yret[an_i] num = '{:1.1f}'.format(a) - ax.annotate(r"$\frac{"+num+r"\pi}{T}$", xy=(an_x, an_y), xytext=(an_x, an_y), size=9) + ax.annotate(r"$\frac{"+num+r"\pi}{T}$", xy=(an_x, an_y), + xytext=(an_x, an_y), size=9) _final_setup(ax) return ax, fig - diff --git a/control/nichols.py b/control/nichols.py index 48abffa0a..c8a98ed5e 100644 --- a/control/nichols.py +++ b/control/nichols.py @@ -60,7 +60,7 @@ # Default parameters values for the nichols module _nichols_defaults = { - 'nichols.grid':True, + 'nichols.grid': True, } @@ -156,12 +156,13 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted'): # Default chart magnitudes # The key set of magnitudes are always generated, since this # guarantees a recognizable Nichols chart grid. - key_cl_mags = np.array([-40.0, -20.0, -12.0, -6.0, -3.0, -1.0, -0.5, 0.0, - 0.25, 0.5, 1.0, 3.0, 6.0, 12.0]) + key_cl_mags = np.array([-40.0, -20.0, -12.0, -6.0, -3.0, -1.0, -0.5, + 0.0, 0.25, 0.5, 1.0, 3.0, 6.0, 12.0]) + # Extend the range of magnitudes if necessary. The extended arange - # will end up empty if no extension is required. Assumes that closed-loop - # magnitudes are approximately aligned with open-loop magnitudes beyond - # the value of np.min(key_cl_mags) + # will end up empty if no extension is required. Assumes that + # closed-loop magnitudes are approximately aligned with open-loop + # magnitudes beyond the value of np.min(key_cl_mags) cl_mag_step = -20.0 # dB extended_cl_mags = np.arange(np.min(key_cl_mags), ol_mag_min + cl_mag_step, cl_mag_step) @@ -171,7 +172,8 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted'): if cl_phases is None: # Choose a reasonable set of default phases (denser if the open-loop # data is restricted to a relatively small range of phases). - key_cl_phases = np.array([-0.25, -45.0, -90.0, -180.0, -270.0, -325.0, -359.75]) + key_cl_phases = np.array([-0.25, -45.0, -90.0, -180.0, -270.0, + -325.0, -359.75]) if np.abs(ol_phase_max - ol_phase_min) < 90.0: other_cl_phases = np.arange(-10.0, -360.0, -10.0) else: @@ -181,7 +183,8 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted'): assert ((-360.0 < np.min(cl_phases)) and (np.max(cl_phases) < 0.0)) # Find the M-contours - m = m_circles(cl_mags, phase_min=np.min(cl_phases), phase_max=np.max(cl_phases)) + m = m_circles(cl_mags, phase_min=np.min(cl_phases), + phase_max=np.max(cl_phases)) m_mag = 20*sp.log10(np.abs(m)) m_phase = sp.mod(sp.degrees(sp.angle(m)), -360.0) # Unwrap @@ -208,9 +211,11 @@ def nichols_grid(cl_mags=None, cl_phases=None, line_style='dotted'): linestyle=line_style, zorder=0) # Add magnitude labels - for x, y, m in zip(m_phase[:][-1] + phase_offset, m_mag[:][-1], cl_mags): + for x, y, m in zip(m_phase[:][-1] + phase_offset, m_mag[:][-1], + cl_mags): align = 'right' if m < 0.0 else 'left' - plt.text(x, y, str(m) + ' dB', size='small', ha=align, color='gray') + plt.text(x, y, str(m) + ' dB', size='small', ha=align, + color='gray') # Fit axes to generated chart plt.axis([phase_offset_min - 360.0, phase_offset_max - 360.0, diff --git a/control/pzmap.py b/control/pzmap.py index a8fb990b5..82960270f 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -1,7 +1,7 @@ # pzmap.py - computations involving poles and zeros # # Author: Richard M. Murray -# Date: 7 Sep 09 +# Date: 7 Sep 2009 # # This file contains functions that compute poles, zeros and related # quantities for a linear system. @@ -38,7 +38,6 @@ # OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # -# $Id:pzmap.py 819 2009-05-29 21:28:07Z murray $ from numpy import real, imag, linspace, exp, cos, sin, sqrt from math import pi @@ -51,15 +50,15 @@ # Define default parameter values for this module _pzmap_defaults = { - 'pzmap.grid':False, # Plot omega-damping grid - 'pzmap.Plot':True, # Generate plot using Matplotlib + 'pzmap.grid': False, # Plot omega-damping grid + 'pzmap.plot': True, # Generate plot using Matplotlib } # 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=True, grid=False, title='Pole Zero Map'): +def pzmap(sys, plot=True, grid=False, title='Pole Zero Map', **kwargs): """ Plot a pole/zero map for a linear system. @@ -67,7 +66,7 @@ def pzmap(sys, Plot=True, grid=False, title='Pole Zero Map'): ---------- sys: LTI (StateSpace or TransferFunction) Linear system for which poles and zeros are computed. - Plot: bool + plot: bool If ``True`` a graph is generated with Matplotlib, otherwise the poles and zeros are only computed and returned. grid: boolean (default = False) @@ -80,17 +79,24 @@ def pzmap(sys, Plot=True, grid=False, title='Pole Zero Map'): zeros: array The system's zeros. """ + # 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['Plot'] + # Get parameter values - Plot = config._get_param('rlocus', 'Plot', Plot, True) + plot = config._get_param('rlocus', 'plot', plot, True) grid = config._get_param('rlocus', 'grid', grid, False) - + if not isinstance(sys, LTI): raise TypeError('Argument ``sys``: must be a linear system.') poles = sys.pole() zeros = sys.zero() - if (Plot): + if (plot): import matplotlib.pyplot as plt if grid: @@ -103,11 +109,11 @@ def pzmap(sys, Plot=True, grid=False, title='Pole Zero Map'): # Plot the locations of the poles and zeros if len(poles) > 0: - ax.scatter(real(poles), imag(poles), s=50, marker='x', facecolors='k') + ax.scatter(real(poles), imag(poles), s=50, marker='x', + facecolors='k') if len(zeros) > 0: ax.scatter(real(zeros), imag(zeros), s=50, marker='o', - facecolors='none', edgecolors='k') - + facecolors='none', edgecolors='k') plt.title(title) diff --git a/control/rlocus.py b/control/rlocus.py index 0c115c26e..955c5c56d 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -62,16 +62,16 @@ # Default values for module parameters _rlocus_defaults = { - 'rlocus.grid':True, - 'rlocus.plotstr':'b' if int(matplotlib.__version__[0]) == 1 else 'C0', - 'rlocus.PrintGain':True, - 'rlocus.Plot':True + 'rlocus.grid': True, + 'rlocus.plotstr': 'b' if int(matplotlib.__version__[0]) == 1 else 'C0', + 'rlocus.print_gain': True, + 'rlocus.plot': True } # Main function: compute a root locus diagram def root_locus(sys, kvect=None, xlim=None, ylim=None, - plotstr=None, Plot=True, PrintGain=None, grid=None, **kwargs): + plotstr=None, plot=True, print_gain=None, grid=None, **kwargs): """Root locus plot @@ -89,9 +89,9 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, Set limits of x axis, normally with tuple (see matplotlib.axes). ylim : tuple or list, optional Set limits of y axis, normally with tuple (see matplotlib.axes). - Plot : boolean, optional + plot : boolean, optional If True (default), plot root locus diagram. - PrintGain : bool + print_gain : bool If True (default), report mouse clicks when close to the root locus branches, calculate gain, damping and print. grid : bool @@ -104,11 +104,27 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, klist : ndarray or list Gains used. Same as klist keyword argument if provided. """ + # Check to see if legacy 'Plot' keyword was used + if 'Plot' in kwargs: + import warnings + warnings.warn("'Plot' keyword is deprecated in root_locus; " + "use 'plot'", FutureWarning) + # Map 'Plot' keyword to 'plot' keyword + plot = kwargs.pop('Plot') + + # Check to see if legacy 'PrintGain' keyword was used + if 'PrintGain' in kwargs: + import warnings + warnings.warn("'PrintGain' keyword is deprecated in root_locus; " + "use 'print_gain'", FutureWarning) + # Map 'PrintGain' keyword to 'print_gain' keyword + 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) - PrintGain = config._get_param( - 'rlocus', 'PrintGain', PrintGain, _rlocus_defaults) + print_gain = config._get_param( + 'rlocus', 'print_gain', print_gain, _rlocus_defaults) # Convert numerator and denominator to polynomials if they aren't (nump, denp) = _systopoly1d(sys) @@ -125,7 +141,7 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, sisotool = False if 'sisotool' not in kwargs else True # Create the Plot - if Plot: + if plot: if sisotool: f = kwargs['fig'] ax = f.axes[1] @@ -143,7 +159,7 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, f = pylab.figure(new_figure_name) ax = pylab.axes() - if PrintGain and not sisotool: + if print_gain and not sisotool: f.canvas.mpl_connect( 'button_release_event', partial(_RLClickDispatcher, sys=sys, fig=f, diff --git a/control/statesp.py b/control/statesp.py index 85d48882a..1779dfbfd 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -70,7 +70,7 @@ # Define module default parameter values _statesp_defaults = { - 'statesp.use_numpy_matrix':True, + 'statesp.use_numpy_matrix': True, } diff --git a/control/tests/config_test.py b/control/tests/config_test.py index c0fc9755b..7b70fdc00 100644 --- a/control/tests/config_test.py +++ b/control/tests/config_test.py @@ -107,8 +107,8 @@ def test_matlab_bode(self): mag_data = mag_line[0].get_data() mag_x, mag_y = mag_data - # Make sure the x-axis is in Hertz and y-axis is in dB - np.testing.assert_almost_equal(mag_x[0], 0.001 / (2*pi), decimal=6) + # Make sure the x-axis is in rad/sec and y-axis is in dB + np.testing.assert_almost_equal(mag_x[0], 0.001, decimal=6) np.testing.assert_almost_equal(mag_y[0], 20*log10(10), decimal=3) # Get the phase line @@ -117,8 +117,8 @@ def test_matlab_bode(self): phase_data = phase_line[0].get_data() phase_x, phase_y = phase_data - # Make sure the x-axis is in Hertz and y-axis is in degrees - np.testing.assert_almost_equal(phase_x[-1], 1000 / (2*pi), decimal=1) + # Make sure the x-axis is in rad/sec and y-axis is in degrees + np.testing.assert_almost_equal(phase_x[-1], 1000, decimal=1) np.testing.assert_almost_equal(phase_y[-1], -180, decimal=0) # Override the defaults and make sure that works as well diff --git a/control/tests/convert_test.py b/control/tests/convert_test.py index 0340fa718..17766b186 100644 --- a/control/tests/convert_test.py +++ b/control/tests/convert_test.py @@ -108,7 +108,7 @@ def testConvert(self): ssorig_mag, ssorig_phase, ssorig_omega = \ bode(_mimo2siso(ssOriginal, \ inputNum, outputNum), \ - deg=False, Plot=False) + deg=False, plot=False) ssorig_real = ssorig_mag * np.cos(ssorig_phase) ssorig_imag = ssorig_mag * np.sin(ssorig_phase) @@ -121,7 +121,7 @@ def testConvert(self): tforig_mag, tforig_phase, tforig_omega = \ bode(tforig, ssorig_omega, \ - deg=False, Plot=False) + deg=False, plot=False) tforig_real = tforig_mag * np.cos(tforig_phase) tforig_imag = tforig_mag * np.sin(tforig_phase) @@ -137,7 +137,7 @@ def testConvert(self): bode(_mimo2siso(ssTransformed, \ inputNum, outputNum), \ ssorig_omega, \ - deg=False, Plot=False) + deg=False, plot=False) ssxfrm_real = ssxfrm_mag * np.cos(ssxfrm_phase) ssxfrm_imag = ssxfrm_mag * np.sin(ssxfrm_phase) np.testing.assert_array_almost_equal( \ @@ -152,7 +152,7 @@ def testConvert(self): tfxfrm = tf(num, den) tfxfrm_mag, tfxfrm_phase, tfxfrm_omega = \ bode(tfxfrm, ssorig_omega, \ - deg=False, Plot=False) + deg=False, plot=False) tfxfrm_real = tfxfrm_mag * np.cos(tfxfrm_phase) tfxfrm_imag = tfxfrm_mag * np.sin(tfxfrm_phase) diff --git a/control/tests/matlab_test.py b/control/tests/matlab_test.py index 0e7060bea..fdbad744e 100644 --- a/control/tests/matlab_test.py +++ b/control/tests/matlab_test.py @@ -132,7 +132,7 @@ def testPZmap(self): # pzmap(self.siso_ss2); not implemented pzmap(self.siso_tf1); pzmap(self.siso_tf2); - pzmap(self.siso_tf2, Plot=False); + pzmap(self.siso_tf2, plot=False); def testStep(self): t = np.linspace(0, 1, 10) @@ -326,7 +326,7 @@ def testBode(self): bode(self.siso_ss1) bode(self.siso_tf1) bode(self.siso_tf2) - (mag, phase, freq) = bode(self.siso_tf2, Plot=False) + (mag, phase, freq) = bode(self.siso_tf2, plot=False) bode(self.siso_tf1, self.siso_tf2) w = logspace(-3, 3); bode(self.siso_ss1, w) @@ -339,7 +339,7 @@ def testRlocus(self): rlocus(self.siso_tf1) rlocus(self.siso_tf2) klist = [1, 10, 100] - rlist, klist_out = rlocus(self.siso_tf2, klist, Plot=False) + rlist, klist_out = rlocus(self.siso_tf2, klist, plot=False) np.testing.assert_equal(len(rlist), len(klist)) np.testing.assert_array_equal(klist, klist_out) @@ -349,7 +349,7 @@ def testNyquist(self): nyquist(self.siso_tf2) w = logspace(-3, 3); nyquist(self.siso_tf2, w) - (real, imag, freq) = nyquist(self.siso_tf2, w, Plot=False) + (real, imag, freq) = nyquist(self.siso_tf2, w, plot=False) def testNichols(self): nichols(self.siso_ss1) diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py index 464f04066..4b2112ea3 100644 --- a/control/tests/rlocus_test.py +++ b/control/tests/rlocus_test.py @@ -35,14 +35,14 @@ def testRootLocus(self): """Basic root locus plot""" klist = [-1, 0, 1] for sys in self.systems: - roots, k_out = root_locus(sys, klist, Plot=False) + roots, k_out = root_locus(sys, klist, plot=False) np.testing.assert_equal(len(roots), len(klist)) np.testing.assert_array_equal(klist, k_out) self.check_cl_poles(sys, roots, klist) def test_without_gains(self): for sys in self.systems: - roots, kvect = root_locus(sys, Plot=False) + roots, kvect = root_locus(sys, plot=False) self.check_cl_poles(sys, roots, kvect) def test_root_locus_zoom(self): diff --git a/control/tests/slycot_convert_test.py b/control/tests/slycot_convert_test.py index eab178954..1c121b1f6 100644 --- a/control/tests/slycot_convert_test.py +++ b/control/tests/slycot_convert_test.py @@ -154,19 +154,19 @@ def testFreqResp(self): for inputNum in range(inputs): for outputNum in range(outputs): [ssOriginalMag, ssOriginalPhase, freq] =\ - matlab.bode(ssOriginal, Plot=False) + matlab.bode(ssOriginal, plot=False) [tfOriginalMag, tfOriginalPhase, freq] =\ matlab.bode(matlab.tf( numOriginal[outputNum][inputNum], - denOriginal[outputNum]), Plot=False) + denOriginal[outputNum]), plot=False) [ssTransformedMag, ssTransformedPhase, freq] =\ matlab.bode(ssTransformed, - freq, Plot=False) + freq, plot=False) [tfTransformedMag, tfTransformedPhase, freq] =\ matlab.bode(matlab.tf( numTransformed[outputNum][inputNum], denTransformed[outputNum]), - freq, Plot=False) + freq, plot=False) # print('numOrig=', # numOriginal[outputNum][inputNum]) # print('denOrig=', diff --git a/examples/pvtol-nested.py b/examples/pvtol-nested.py index 56685599b..7efce9ccd 100644 --- a/examples/pvtol-nested.py +++ b/examples/pvtol-nested.py @@ -26,10 +26,6 @@ Pi = tf([r], [J, 0, 0]) # inner loop (roll) Po = tf([1], [m, c, 0]) # outer loop (position) -# Use state space versions -Pi = tf2ss(Pi) -Po = tf2ss(Po) - # # Inner loop control design # @@ -170,7 +166,7 @@ plt.figure(10) plt.clf() -P, Z = pzmap(T, Plot=True) +P, Z = pzmap(T, plot=True, grid=True) print("Closed loop poles and zeros: ", P, Z) # Gang of Four