From: <mur...@us...> - 2012-08-25 16:11:26
|
Revision: 183 http://python-control.svn.sourceforge.net/python-control/?rev=183&view=rev Author: murrayrm Date: 2012-08-25 16:11:20 +0000 (Sat, 25 Aug 2012) Log Message: ----------- improvements to nyquist and bode plots Modified Paths: -------------- trunk/src/__init__.py trunk/src/freqplot.py Modified: trunk/src/__init__.py =================================================================== --- trunk/src/__init__.py 2012-02-15 06:25:15 UTC (rev 182) +++ trunk/src/__init__.py 2012-08-25 16:11:20 UTC (rev 183) @@ -72,3 +72,11 @@ from timeresp import forced_response, initial_response, step_response, \ impulse_response from xferfcn import TransferFunction + +# Import some of the more common (and benign) MATLAB shortcuts +from matlab import ss, tf, ss2tf, tf2ss, drss +from matlab import pole, zero, evalfr, freqresp, dcgain +from matlab import nichols, rlocus, margin + # bode and nyquist come directly from freqplot.py +from matlab import step, impulse, initial, lsim + Modified: trunk/src/freqplot.py =================================================================== --- trunk/src/freqplot.py 2012-02-15 06:25:15 UTC (rev 182) +++ trunk/src/freqplot.py 2012-08-25 16:11:20 UTC (rev 183) @@ -56,7 +56,7 @@ # Bode plot def bode_plot(syslist, omega=None, dB=False, Hz=False, deg=True, - color=None, Plot=True): + Plot=True, *args, **kwargs): """Bode plot for a system Plots a Bode plot for the system over a (optional) frequency range. @@ -71,12 +71,12 @@ If True, plot result in dB Hz : boolean If True, plot frequency in Hz (omega must be provided in rad/sec) - color : matplotlib color - Color of line in bode plot deg : boolean If True, return phase in degrees (else radians) Plot : boolean If True, plot magnitude and phase + *args, **kwargs: + Additional options to matplotlib (color, linestyle, etc) Returns ------- @@ -95,7 +95,6 @@ Examples -------- - >>> from matlab import ss >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") >>> mag, phase, omega = bode(sys) """ @@ -132,45 +131,28 @@ # Magnitude plot plt.subplot(211); if dB: - if color==None: - plt.semilogx(omega, mag) - else: - plt.semilogx(omega, mag, color=color) - plt.ylabel("Magnitude (dB)") + plt.semilogx(omega, mag, *args, **kwargs) else: - if color==None: - plt.loglog(omega, mag) - else: - plt.loglog(omega, mag, color=color) - plt.ylabel("Magnitude") + plt.loglog(omega, mag, *args, **kwargs) + plt.hold(True); - # Add a grid to the plot + # Add a grid to the plot + labeling plt.grid(True) plt.grid(True, which='minor') - plt.hold(True); + plt.ylabel("Magnitude (dB)" if dB else "Magnitude") # Phase plot plt.subplot(212); - if deg: - phase_deg = phase - else: - phase_deg = phase * 180 / sp.pi - if color==None: - plt.semilogx(omega, phase_deg) - else: - plt.semilogx(omega, phase_deg, color=color) - plt.hold(True) + plt.semilogx(omega, phase, *args, **kwargs) + plt.hold(True); - # Add a grid to the plot + # Add a grid to the plot + labeling plt.grid(True) plt.grid(True, which='minor') - plt.ylabel("Phase (deg)") + plt.ylabel("Phase (deg)" if deg else "Phase (rad)") # Label the frequency axis - if Hz: - plt.xlabel("Frequency (Hz)") - else: - plt.xlabel("Frequency (rad/sec)") + plt.xlabel("Frequency (Hz)" if Hz else "Frequency (rad/sec)") if len(syslist) == 1: return mags[0], phases[0], omegas[0] @@ -178,7 +160,8 @@ return mags, phases, omegas # Nyquist plot -def nyquist_plot(syslist, omega=None, Plot=True): +def nyquist_plot(syslist, omega=None, Plot=True, color='b', + labelFreq=0, *args, **kwargs): """Nyquist plot for a system Plots a Nyquist plot for the system over a (optional) frequency range. @@ -190,7 +173,11 @@ omega : freq_range Range of frequencies (list or bounds) in rad/sec Plot : boolean - if True, plot magnitude + If True, plot magnitude + labelFreq : int + Label every nth frequency on the plot + *args, **kwargs: + Additional options to matplotlib (color, linestyle, etc) Returns ------- @@ -203,7 +190,6 @@ Examples -------- - >>> from matlab import ss >>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.") >>> real, imag, freq = nyquist(sys) """ @@ -214,12 +200,14 @@ # Select a default range if none is provided if (omega == None): omega = default_frequency_range(syslist) + # Interpolate between wmin and wmax if a tuple or list are provided elif (isinstance(omega,list) | isinstance(omega,tuple)): # Only accept tuple or list of length 2 if (len(omega) != 2): raise ValueError("Supported frequency arguments are (wmin,wmax) tuple or list, or frequency vector. ") - omega = np.logspace(np.log10(omega[0]),np.log10(omega[1]),num=50,endpoint=True,base=10.0) + omega = np.logspace(np.log10(omega[0]), np.log10(omega[1]), + num=50, endpoint=True, base=10.0) for sys in syslist: if (sys.inputs > 1 or sys.outputs > 1): #TODO: Add MIMO nyquist plots. @@ -236,11 +224,33 @@ if (Plot): # Plot the primary curve and mirror image - plt.plot(x, y, '-'); - plt.plot(x, -y, '--'); + plt.plot(x, y, '-', color=color, *args, **kwargs); + plt.plot(x, -y, '--', color=color, *args, **kwargs); # Mark the -1 point plt.plot([-1], [0], 'r+') + # Label the frequencies of the points + if (labelFreq): + for xpt, ypt, omegapt in zip(x, y, omega)[::labelFreq]: + # Convert to Hz + f = omegapt/(2*sp.pi) + + # Factor out multiples of 1000 and limit the + # result to the range [-8, 8]. + pow1000 = max(min(get_pow1000(f),8),-8) + + # Get the SI prefix. + prefix = gen_prefix(pow1000) + + # Apply the text. (Use a space before the text to + # prevent overlap with the data.) + # + # np.round() is used because 0.99... appears + # instead of 1.0, and this would otherwise be + # truncated to 0. + plt.text(xpt, ypt, + ' ' + str(int(np.round(f/1000**pow1000, 0))) + + ' ' + prefix + 'Hz') return x, y, omega # Gang of Four @@ -362,6 +372,49 @@ return omega +# +# KLD 5/23/11: Two functions to create nice looking labels +# +def get_pow1000(num): + '''Determine the exponent for which the significand of a number is within the + range [1, 1000). + ''' + # Based on algorithm from http://www.mail-archive.com/mat...@li.../msg14433.html, accessed 2010/11/7 + # by Jason Heeris 2009/11/18 + from decimal import Decimal + from math import floor + dnum = Decimal(str(num)) + if dnum == 0: + return 0 + elif dnum < 0: + dnum = -dnum + return int(floor(dnum.log10()/3)) + +def gen_prefix(pow1000): + '''Return the SI prefix for a power of 1000. + ''' + # Prefixes according to Table 5 of [BIPM 2006] (excluding hecto, + # deca, deci, and centi). + if pow1000 < -8 or pow1000 > 8: + raise ValueError("Value is out of the range covered by the SI prefixes.") + return ['Y', # yotta (10^24) + 'Z', # zetta (10^21) + 'E', # exa (10^18) + 'P', # peta (10^15) + 'T', # tera (10^12) + 'G', # giga (10^9) + 'M', # mega (10^6) + 'k', # kilo (10^3) + '', # (10^0) + 'm', # milli (10^-3) + r'$\mu$', # micro (10^-6) + 'n', # nano (10^-9) + 'p', # pico (10^-12) + 'f', # femto (10^-15) + 'a', # atto (10^-18) + 'z', # zepto (10^-21) + 'y'][8 - pow1000] # yocto (10^-24) + # Function aliases bode = bode_plot nyquist = nyquist_plot This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |