From dc8410c04bbd92ad9ed81eca5f17c2f63122d831 Mon Sep 17 00:00:00 2001 From: Victor Date: Mon, 19 Feb 2018 11:06:56 +0100 Subject: [PATCH 1/5] Added discrete grid to pzmap function. --- control/statesp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control/statesp.py b/control/statesp.py index bff14d241..62c5f617f 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -460,10 +460,10 @@ def freqresp(self, omega): if isdtime(self, strict=True): dt = timebase(self) cmplx_freqs = exp(1.j * omega * dt) - if ((omega * dt).any() > pi): + if ((omega * dt).any() > math.pi): warn_message = ("evalfr: frequency evaluation" " above Nyquist frequency") - warnings.warn(warn_message) + warn(warn_message) else: cmplx_freqs = omega * 1.j From 4117489ec3128e25b65c3f4d7ab9c2bdb1d44038 Mon Sep 17 00:00:00 2001 From: Victor Date: Mon, 19 Feb 2018 12:44:57 +0100 Subject: [PATCH 2/5] Revert "Added discrete grid to pzmap function." This reverts commit dc8410c --- control/statesp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control/statesp.py b/control/statesp.py index 62c5f617f..bff14d241 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -460,10 +460,10 @@ def freqresp(self, omega): if isdtime(self, strict=True): dt = timebase(self) cmplx_freqs = exp(1.j * omega * dt) - if ((omega * dt).any() > math.pi): + if ((omega * dt).any() > pi): warn_message = ("evalfr: frequency evaluation" " above Nyquist frequency") - warn(warn_message) + warnings.warn(warn_message) else: cmplx_freqs = omega * 1.j From f8c497e0dabb757b7a698d2f1bdf4c637786ff33 Mon Sep 17 00:00:00 2001 From: Victor Date: Mon, 19 Feb 2018 10:25:39 +0100 Subject: [PATCH 3/5] Added discrete grid to pzmap function. (cherry picked from commit bfd9279) --- control/pzmap.py | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/control/pzmap.py b/control/pzmap.py index ee91ec9b9..5fc900748 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -40,11 +40,42 @@ # # $Id:pzmap.py 819 2009-05-29 21:28:07Z murray $ -from numpy import real, imag -from .lti import LTI +from numpy import real, imag, linspace, pi, exp, cos, sin, sqrt +from .lti import LTI, isdtime, isctime __all__ = ['pzmap'] +def zgrid(plt): + for zeta in linspace(0, 0.9,10): + factor = zeta/sqrt(1-zeta**2) + x = linspace(0, sqrt(1-zeta**2),200) + ang = pi*x + mag = exp(-pi*factor*x) + xret = mag*cos(ang) + yret = mag*sin(ang) + plt.plot(xret,yret, 'k:', lw=1) + xret = mag*cos(-ang) + yret = mag*sin(-ang) + plt.plot(xret,yret,'k:', lw=1) + an_i = int(len(xret)/2.5) + an_x = xret[an_i] + an_y = yret[an_i] + plt.annotate(str(zeta), xy=(an_x, an_y), xytext=(an_x, an_y), size=7) + + for a in linspace(0,1,10): + x = linspace(-pi/2,pi/2,200) + ang = pi*a*sin(x) + mag = exp(-pi*a*cos(x)) + xret = mag*cos(ang) + yret = mag*sin(ang) + plt.plot(xret,yret,'k:', lw=1) + an_i = -1 #int(len(xret)/2.5) + an_x = xret[an_i] + an_y = yret[an_i] + num = '{:1.1f}'.format(a) + plt.annotate("$\\frac{"+num+"\pi}{T}$", xy=(an_x, an_y), xytext=(an_x, an_y), size=8) + + # 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 @@ -75,6 +106,10 @@ def pzmap(sys, Plot=True, title='Pole Zero Map'): if (Plot): import matplotlib.pyplot as plt + + if isdtime(sys): + zgrid(plt) + # Plot the locations of the poles and zeros if len(poles) > 0: plt.scatter(real(poles), imag(poles), s=50, marker='x') From 3d0bf039c4715f2477b7fd7eccab00b3bf830639 Mon Sep 17 00:00:00 2001 From: Victor Date: Mon, 19 Feb 2018 12:30:57 +0100 Subject: [PATCH 4/5] Tweaked axis lines and zero/pole markers. (cherry picked from commit 382b4e9) --- control/pzmap.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/control/pzmap.py b/control/pzmap.py index 5fc900748..c76f4aa80 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -112,14 +112,14 @@ def pzmap(sys, Plot=True, title='Pole Zero Map'): # Plot the locations of the poles and zeros if len(poles) > 0: - plt.scatter(real(poles), imag(poles), s=50, marker='x') + plt.scatter(real(poles), imag(poles), s=50, marker='x', facecolors='k') if len(zeros) > 0: plt.scatter(real(zeros), imag(zeros), s=50, marker='o', - facecolors='none') + facecolors='none', edgecolors='k') # Add axes #Somewhat silly workaround - plt.axhline(y=0, color='black') - plt.axvline(x=0, color='black') + plt.axhline(y=0, color='black', lw=1) + plt.axvline(x=0, color='black', lw=1) plt.xlabel('Re') plt.ylabel('Im') From 89dc9374b85300f917c4a599f210f57e283263cc Mon Sep 17 00:00:00 2001 From: victor Date: Sun, 11 Mar 2018 02:39:16 +0100 Subject: [PATCH 5/5] Refactored sgrid based on matplotlib projections and moved sgrid and zgrid to new python module. --- control/grid.py | 182 ++++++++++++++++++++++++++++++++++++++++++++++ control/pzmap.py | 59 ++++----------- control/rlocus.py | 107 ++++----------------------- 3 files changed, 212 insertions(+), 136 deletions(-) create mode 100644 control/grid.py diff --git a/control/grid.py b/control/grid.py new file mode 100644 index 000000000..33fc9e975 --- /dev/null +++ b/control/grid.py @@ -0,0 +1,182 @@ +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 +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): + angles_deg = values/factor + 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): + x_, y_ = np.linspace(x1, x2, self.nx), np.linspace(y1, y2, self.ny) + x, y = np.meshgrid(x_, y_) + lon, lat = transform_xy(np.ravel(x), np.ravel(y)) + + 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) + 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) + + lon_min, lon_max = np.nanmin(lon), np.nanmax(lon) + lat_min, lat_max = np.nanmin(lat), np.nanmax(lat) + + lon_min, lon_max, lat_min, lat_max = \ + self._adjust_extremes(lon_min, lon_max, lat_min, lat_max) + + return lon_min, lon_max, lat_min, lat_max + +def sgrid(): + # 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 + 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). + + # 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),) + + 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 + ) + + fig = plt.figure() + ax = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper) + + # make ticklabels of right invisible, and top axis visible. + visible = True + ax.axis[:].major_ticklabels.set_visible(visible) + ax.axis[:].major_ticks.set_visible(False) + ax.axis[:].invert_ticklabel_direction() + + 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") + ax.axis["wnyneg"] = axis = ax.new_floating_axis(0, 270) + axis.label.set_visible(False) + axis.set_axis_direction("left") + axis.invert_ticklabel_direction() + axis.set_ticklabel_direction("-") + + # let left axis shows ticklabels for 1st coordinate (angle) + ax.axis["left"].get_helper().nth_coord_ticks = 0 + ax.axis["right"].get_helper().nth_coord_ticks = 0 + ax.axis["left"].get_helper().nth_coord_ticks = 0 + 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) + return ax, fig + +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) + plt.axis('equal') + +def nogrid(): + f = plt.figure() + ax = plt.axes() + + _final_setup(ax) + return ax, f + +def zgrid(zetas=None, wns=None): + '''Draws discrete damping and frequency grid''' + + fig = plt.figure() + ax = fig.gca() + + # Constant damping lines + if zetas is None: + zetas = linspace(0, 0.9, 10) + for zeta in zetas: + # Calculate in polar coordinates + factor = zeta/sqrt(1-zeta**2) + 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) + # Draw lower part in retangular coordinates + xret = mag*cos(-ang) + yret = mag*sin(-ang) + 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) + + # 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) + 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) + # Annotation + an_i = -1 + an_x = xret[an_i] + an_y = yret[an_i] + num = '{:1.1f}'.format(a) + ax.annotate("$\\frac{"+num+"\pi}{T}$", xy=(an_x, an_y), xytext=(an_x, an_y), size=9) + + _final_setup(ax) + return ax, fig + diff --git a/control/pzmap.py b/control/pzmap.py index c76f4aa80..252d10011 100644 --- a/control/pzmap.py +++ b/control/pzmap.py @@ -40,46 +40,17 @@ # # $Id:pzmap.py 819 2009-05-29 21:28:07Z murray $ -from numpy import real, imag, linspace, pi, exp, cos, sin, sqrt +from numpy import real, imag, linspace, exp, cos, sin, sqrt +from math import pi from .lti import LTI, isdtime, isctime +from .grid import sgrid, zgrid, nogrid __all__ = ['pzmap'] -def zgrid(plt): - for zeta in linspace(0, 0.9,10): - factor = zeta/sqrt(1-zeta**2) - x = linspace(0, sqrt(1-zeta**2),200) - ang = pi*x - mag = exp(-pi*factor*x) - xret = mag*cos(ang) - yret = mag*sin(ang) - plt.plot(xret,yret, 'k:', lw=1) - xret = mag*cos(-ang) - yret = mag*sin(-ang) - plt.plot(xret,yret,'k:', lw=1) - an_i = int(len(xret)/2.5) - an_x = xret[an_i] - an_y = yret[an_i] - plt.annotate(str(zeta), xy=(an_x, an_y), xytext=(an_x, an_y), size=7) - - for a in linspace(0,1,10): - x = linspace(-pi/2,pi/2,200) - ang = pi*a*sin(x) - mag = exp(-pi*a*cos(x)) - xret = mag*cos(ang) - yret = mag*sin(ang) - plt.plot(xret,yret,'k:', lw=1) - an_i = -1 #int(len(xret)/2.5) - an_x = xret[an_i] - an_y = yret[an_i] - num = '{:1.1f}'.format(a) - plt.annotate("$\\frac{"+num+"\pi}{T}$", xy=(an_x, an_y), xytext=(an_x, an_y), size=8) - - # 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, title='Pole Zero Map'): +def pzmap(sys, Plot=True, grid=False, title='Pole Zero Map'): """ Plot a pole/zero map for a linear system. @@ -90,6 +61,8 @@ def pzmap(sys, Plot=True, title='Pole Zero Map'): Plot: bool 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 ------- @@ -107,21 +80,21 @@ def pzmap(sys, Plot=True, title='Pole Zero Map'): if (Plot): import matplotlib.pyplot as plt - if isdtime(sys): - zgrid(plt) + if grid: + if isdtime(sys, strict=True): + ax, fig = zgrid() + else: + ax, fig = sgrid() + else: + ax, fig = nogrid() # Plot the locations of the poles and zeros if len(poles) > 0: - plt.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: - plt.scatter(real(zeros), imag(zeros), s=50, marker='o', + ax.scatter(real(zeros), imag(zeros), s=50, marker='o', facecolors='none', edgecolors='k') - # Add axes - #Somewhat silly workaround - plt.axhline(y=0, color='black', lw=1) - plt.axvline(x=0, color='black', lw=1) - plt.xlabel('Re') - plt.ylabel('Im') + plt.title(title) diff --git a/control/rlocus.py b/control/rlocus.py index e22a31c25..487ca3863 100644 --- a/control/rlocus.py +++ b/control/rlocus.py @@ -47,12 +47,15 @@ # Packages used by this module import numpy as np -from scipy import array, poly1d, row_stack, zeros_like, real, imag +from scipy import array, poly1d, row_stack, zeros_like, real, imag, exp, sin, cos, linspace, sqrt +from math import pi import scipy.signal # signal processing toolbox import pylab # plotting routines from .xferfcn import _convertToTransferFunction from .exception import ControlMIMONotImplemented from functools import partial +from .lti import isdtime +from .grid import sgrid, zgrid, nogrid __all__ = ['root_locus', 'rlocus'] @@ -82,7 +85,7 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr='-', Plot=True, If True, report mouse clicks when close to the root-locus branches, calculate gain, damping and print grid: boolean (default = False) - If True plot s-plane grid. + If True plot omega-damping grid. Returns ------- @@ -110,12 +113,18 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr='-', Plot=True, while new_figure_name in figure_title: new_figure_name = "Root Locus " + str(rloc_num) rloc_num += 1 - f = pylab.figure(new_figure_name) + if grid: + if isdtime(sys, strict=True): + ax, f = zgrid() + else: + ax, f = sgrid() + else: + ax, f = nogrid() + pylab.title(new_figure_name) if PrintGain: f.canvas.mpl_connect( 'button_release_event', partial(_RLFeedbackClicks, sys=sys)) - ax = pylab.axes() # plot open loop poles poles = array(denp.r) @@ -128,17 +137,13 @@ def root_locus(sys, kvect=None, xlim=None, ylim=None, plotstr='-', Plot=True, # Now plot the loci for col in mymat.T: - ax.plot(real(col), imag(col), plotstr) + ax.plot(real(col), imag(col), plotstr, lw=3) # Set up plot axes and labels if xlim: ax.set_xlim(xlim) if ylim: ax.set_ylim(ylim) - ax.set_xlabel('Real') - ax.set_ylabel('Imaginary') - if grid: - _sgrid_func() return mymat, kvect @@ -350,88 +355,4 @@ def _RLFeedbackClicks(event, sys): print("Clicked at %10.4g%+10.4gj gain %10.4g damp %10.4g" % (s.real, s.imag, K.real, -1 * s.real / abs(s))) - -def _sgrid_func(fig=None, zeta=None, wn=None): - if fig is None: - fig = pylab.gcf() - ax = fig.gca() - xlocator = ax.get_xaxis().get_major_locator() - - 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 - - if zeta is None: - zeta = _default_zetas(xlim, ylim) - - angules = [] - for z in zeta: - if (z >= 1e-4) and (z <= 1): - angules.append(np.pi/2 + np.arcsin(z)) - else: - zeta.remove(z) - y_over_x = np.tan(angules) - - # zeta-constant lines - - index = 0 - - for yp in 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) - index += 1 - ax.plot([0, 0], [ylim[0], ylim[1]], color='gray', linestyle='dashed', linewidth=0.5) - - angules = np.linspace(-90, 90, 20)*np.pi/180 - if wn is None: - wn = _default_wn(xlocator(), ylim) - - for om in wn: - if om < 0: - yp = np.sin(angules)*np.abs(om) - xp = -np.cos(angules)*np.abs(om) - ax.plot(xp, yp, color='gray', - linestyle='dashed', linewidth=0.5) - an = "%.2f" % -om - ax.annotate(an, textcoords='data', xy=[om, 0], fontsize=8) - - -def _default_zetas(xlim, ylim): - """Return default list of dumps coefficients""" - sep1 = -xlim[0]/4 - ang1 = [np.arctan((sep1*i)/ylim[1]) for i in np.arange(1, 4, 1)] - sep2 = ylim[1] / 3 - ang2 = [np.arctan(-xlim[0]/(ylim[1]-sep2*i)) for i in np.arange(1, 3, 1)] - - angules = np.concatenate((ang1, ang2)) - angules = np.insert(angules, len(angules), np.pi/2) - zeta = np.sin(angules) - return zeta.tolist() - - -def _default_wn(xloc, ylim): - """Return default wn for root locus plot""" - - wn = xloc - sep = xloc[1]-xloc[0] - while np.abs(wn[0]) < ylim[1]: - wn = np.insert(wn, 0, wn[0]-sep) - - while len(wn) > 7: - wn = wn[0:-1:2] - - return wn - rlocus = root_locus pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy