From: <all...@us...> - 2011-02-13 13:21:35
|
Revision: 133 http://python-control.svn.sourceforge.net/python-control/?rev=133&view=rev Author: allanmcinnes Date: 2011-02-13 13:21:29 +0000 (Sun, 13 Feb 2011) Log Message: ----------- Split out common code from M-circle and N-circle utility functions. Make variable naming through nichols_grid and utility functions more consistent, and provide more explanation of what's going on. Modified Paths: -------------- trunk/src/freqplot.py Modified: trunk/src/freqplot.py =================================================================== --- trunk/src/freqplot.py 2011-02-13 03:35:38 UTC (rev 132) +++ trunk/src/freqplot.py 2011-02-13 13:21:29 UTC (rev 133) @@ -247,55 +247,69 @@ ------------- None """ - gmin_default = -40.0 # dB - gain_step = 20.0 # dB + mag_min_default = -40.0 # dB + mag_step = 20.0 # dB + # Chart defaults + phase_min, phase_max, mag_min, mag_max = -360.0, 0.0, mag_min_default, 40.0 + + # Set actual chart bounds based on current plot if plt.gcf().gca().has_data(): - pmin, pmax, gmin, gmax = plt.axis() + phase_min, phase_max, mag_min, mag_max = plt.axis() + + # M-circle magnitudes. + # The "fixed" set are always generated, since this guarantees a recognizable + # Nichols chart grid. + mags_fixed = 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]) + + if mag_min < mag_min_default: + # Outside the "fixed" set of magnitudes, the generated M-circles + # are extended in steps of 'mag_step' dB to cover anything made + # visible by the range of the existing plot + mags_adjust = np.arange(mag_step*np.floor(mag_min/mag_step), + mag_min_default, mag_step) + mags = np.concatenate((mags_adjust, mags_fixed)) else: - pmin, pmax = -360.0, 0.0 - gmin, gmax = gmin_default, 40.0 - - # Determine the bounds of the chart - mags_low_end = np.min([gmin_default, gain_step*np.floor(gmin/gain_step)]) - phases_low_end = 360.0*np.ceil(pmin/360.0) - phases_high_end = 360.0*(1.0 + np.ceil(pmax/360.0)) - - # M-circle magnitudes - we adjust the lower end of the range to match - # any existing data - mags_fixed = np.array([-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]) - mags_adjustable = np.arange(mags_low_end, np.min(mags_fixed), gain_step) - mags = np.concatenate((mags_adjustable, mags_fixed)) + mags = mags_fixed # N-circle phases (should be in the range -360 to 0) phases = np.array([-0.25, -10.0, -20.0, -30.0, -45.0, -60.0, -90.0, -120.0, -150.0, -180.0, -210.0, -240.0, -270.0, -310.0, -325.0, -340.0, -350.0, -359.75]) + # Find the M-contours - mcs = m_circles(mags, pmin=np.min(phases), pmax=np.max(phases)) - mag_m = 20*sp.log10(np.abs(mcs)) - phase_m = sp.mod(sp.degrees(sp.angle(mcs)), -360.0) # Unwrap + m = m_circles(mags, phase_min=np.min(phases), phase_max=np.max(phases)) + m_mag = 20*sp.log10(np.abs(m)) + m_phase = sp.mod(sp.degrees(sp.angle(m)), -360.0) # Unwrap # Find the N-contours - ncs = n_circles(phases, gmin=np.min(mags), gmax=np.max(mags)) - mag_n = 20*sp.log10(np.abs(ncs)) - phase_n = sp.mod(sp.degrees(sp.angle(ncs)), -360.0) # Unwrap + n = n_circles(phases, mag_min=np.min(mags), mag_max=np.max(mags)) + n_mag = 20*sp.log10(np.abs(n)) + n_phase = sp.mod(sp.degrees(sp.angle(n)), -360.0) # Unwrap - # Plot the contours - for phase_offset in np.arange(phases_low_end, phases_high_end, 360.0): - plt.plot(phase_m + phase_offset, mag_m, color='gray', + # Plot the contours behind other plot elements. + # The "phase offset" is used to produce copies of the chart that cover + # the entire range of the plotted data, starting from a base chart computed + # over the range -360 < phase < 0 (see above). Given the range + # the base chart is computed over, the phase offset should be 0 + # for -360 < phase_min < 0. + phase_offset_min = 360.0*np.ceil(phase_min/360.0) + phase_offset_max = 360.0*np.ceil(phase_max/360.0) + 360.0 + phase_offsets = np.arange(phase_offset_min, phase_offset_max, 360.0) + for phase_offset in phase_offsets: + plt.plot(m_phase + phase_offset, m_mag, color='gray', linestyle='dashed', zorder=0) - plt.plot(phase_n + phase_offset, mag_n, color='gray', + plt.plot(n_phase + phase_offset, n_mag, color='gray', linestyle='dashed', zorder=0) # Add magnitude labels - for x, y, m in zip(phase_m[:][-1], mag_m[:][-1], mags): + for x, y, m in zip(m_phase[:][-1], m_mag[:][-1], mags): align = 'right' if m < 0.0 else 'left' plt.text(x, y, str(m) + ' dB', size='small', ha=align) # Make sure axes conform to any pre-existing plot. - plt.axis([pmin, pmax, gmin, gmax]) + plt.axis([phase_min, phase_max, mag_min, mag_max]) # Gang of Four #! TODO: think about how (and whether) to handle lists of systems @@ -402,54 +416,84 @@ return omega +# Compute contours of a closed-loop transfer function +def closed_loop_contours(Hmag, Hphase): + """Contours of the function H = G/(1+G). + + Usage + ===== + contours = closed_loop_contours(mags, phases) + + Parameters + ---------- + mags : array-like + Meshgrid array of magnitudes of the contours + phases : array-like + Meshgrid array of phases in radians of the contours + + Return values + ------------- + contours : complex array + Array of complex numbers corresponding to the contours. + """ + # Compute the contours in H-space + H = Hmag*sp.exp(1.j*Hphase) + + # Invert H = G/(1+G) to get an expression for the contours in G-space + return H/(1.0 - H) + # M-circle -def m_circles(mags, pmin=-359.75, pmax=-0.25): +def m_circles(mags, phase_min=-359.75, phase_max=-0.25): """Constant-magnitude contours of the function H = G/(1+G). Usage ===== - contour = m_circle(mags) + contours = m_circles(mags) Parameters ---------- mags : array-like Array of magnitudes in dB of the M-circles + phase_min : degrees + Minimum phase in degrees of the N-circles + phase_max : degrees + Maximum phase in degrees of the N-circles Return values ------------- - contour : complex array - Array of complex numbers corresponding to the contour. + contours : complex array + Array of complex numbers corresponding to the contours. """ - # Compute the contours in H-space - phases = sp.radians(sp.linspace(pmin, pmax, 500)) + # Convert magnitudes and phase range into a grid suitable for + # building contours + phases = sp.radians(sp.linspace(phase_min, phase_max, 500)) Hmag, Hphase = sp.meshgrid(10.0**(mags/20.0), phases) - H = Hmag*sp.exp(1.j*Hphase) - - # Invert H = G/(1+G) to get an expression for the contour in G-space - return H/(1.0 - H) + return closed_loop_contours(Hmag, Hphase) # N-circle -def n_circles(phases, gmin=-40.0, gmax=12.0): +def n_circles(phases, mag_min=-40.0, mag_max=12.0): """Constant-phase contours of the function H = G/(1+G). Usage ===== - contour = n_circle(angles) + contour = n_circles(angles) Parameters ---------- phases : array-like - Array of phases in degrees of the N-circle + Array of phases in degrees of the N-circles + mag_min : dB + Minimum magnitude in dB of the N-circles + mag_max : dB + Maximum magnitude in dB of the N-circles Return values ------------- - contour : complex array + contours : complex array Array of complex numbers corresponding to the contours. """ - # Compute the contours in H-space - mags = sp.linspace(10**(gmin/20.0), 10**(gmax/20.0), 2000) + # Convert phases and magnitude range into a grid suitable for + # building contours + mags = sp.linspace(10**(mag_min/20.0), 10**(mag_max/20.0), 2000) Hphase, Hmag = sp.meshgrid(sp.radians(phases), mags) - H = Hmag*sp.exp(1.j*Hphase) - - # Invert H = G/(1+G) to get an expression for the contours in G-space - return H/(1.0 - H) + return closed_loop_contours(Hmag, Hphase) This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |