From: <bli...@us...> - 2011-04-07 20:09:50
|
Revision: 153 http://python-control.svn.sourceforge.net/python-control/?rev=153&view=rev Author: blinkminster Date: 2011-04-07 20:09:44 +0000 (Thu, 07 Apr 2011) Log Message: ----------- added margin command and associated matlab-like wrapper (does not plot yet). bugfixes to bode and default_frequency_range. Modified Paths: -------------- trunk/src/freqplot.py trunk/src/matlab.py Modified: trunk/src/freqplot.py =================================================================== --- trunk/src/freqplot.py 2011-04-03 04:55:51 UTC (rev 152) +++ trunk/src/freqplot.py 2011-04-07 20:09:44 UTC (rev 153) @@ -55,12 +55,13 @@ # # Bode plot -def bode(syslist, omega=None, dB=False, Hz=False, color=None, Plot=True): +def bode(syslist, omega=None, dB=False, Hz=False, deg=True, + color=None, Plot=True): """Bode plot for a system Usage ===== - (mag, phase, omega) = bode(syslist, omega=None, dB=False, Hz=False, color=None, Plot=True) + (mag, phase, omega) = bode(syslist, omega=None, dB=False, Hz=False, color=None, deg=True, Plot=True) Plots a Bode plot for the system over a (optional) frequency range. @@ -74,23 +75,30 @@ 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 Return values ------------- - mag : magnitude array - phase : phase array - omega : frequency array + mag : magnitude array (list if len(syslist) > 1) + phase : phase array (list if len(syslist) > 1) + omega : frequency array (list if len(syslist) > 1) Notes ----- - 1. Alternatively, may use (mag, phase, freq) = sys.freqresp(freq) to generate the frequency response for a system. + 1. Alternatively, you may use the lower-level method + (mag, phase, freq) = sys.freqresp(freq) to generate the frequency + response for a system, but it returns a MIMO response. """ # If argument was a singleton, turn it into a list if (not getattr(syslist, '__iter__', False)): syslist = (syslist,) + mags, phases, omegas = [], [], [] for sys in syslist: if (sys.inputs > 1 or sys.outputs > 1): #TODO: Add MIMO bode plots. @@ -104,10 +112,14 @@ mag_tmp, phase_tmp, omega = sys.freqresp(omega) mag = np.squeeze(mag_tmp) phase = np.squeeze(phase_tmp) + phase = unwrap(phase) if Hz: omega = omega/(2*sp.pi) if dB: mag = 20*sp.log10(mag) - phase = unwrap(phase*180/sp.pi, 360) - + if deg: phase = phase * 180 / sp.pi + + mags.append(mag) + phases.append(phase) + omegas.append(omega) # Get the dimensions of the current axis, which we will divide up #! TODO: Not current implemented; just use subplot for now @@ -134,10 +146,14 @@ # Phase plot plt.subplot(212); + if deg: + phase_deg = phase + else: + phase_deg = phase * 180 / sp.pi if color==None: - plt.semilogx(omega, phase) + plt.semilogx(omega, phase_deg) else: - plt.semilogx(omega, phase, color=color) + plt.semilogx(omega, phase_deg, color=color) plt.hold(True) # Add a grid to the plot @@ -151,7 +167,10 @@ else: plt.xlabel("Frequency (rad/sec)") - return mag, phase, omega + if len(syslist) == 1: + return mags[0], phases[0], omegas[0] + else: + return mags, phases, omegas # Nyquist plot def nyquist(syslist, omega=None, Plot=True): @@ -274,6 +293,101 @@ phase = np.squeeze(phase_tmp) plt.subplot(224); plt.loglog(omega, mag); + +# gain and phase margins +# contributed by Sawyer B. Fuller <mi...@ca...> +def margin(sysdata, deg=True): + """Calculate gain and phase margins and associated crossover frequencies + + Usage: + + gm, pm, sm, wg, wp, ws = margin(sysdata, deg=True) + + Parameters + ---------- + sysdata: linsys or (mag, phase, omega) sequence + sys : linsys + Linear SISO system + mag, phase, omega : sequence of array_like + Input magnitude, phase, and frequencies (rad/sec) sequence from + bode frequency response data + deg=True: boolean + If true, all input and output phases in degrees, else in radians + + Returns + ------- + gm, pm, sm, wg, wp, ws: float + Gain margin gm, phase margin pm, stability margin sm, and + associated crossover + frequencies wg, wp, and ws of SISO open-loop. If more than + one crossover frequency is detected, returns the lowest corresponding + margin. + """ + #TODO do this precisely without the effects of discretization of frequencies? + #TODO assumes SISO + #TODO unit tests, margin plot + + if (not getattr(sysdata, '__iter__', False)): + sys = sysdata + mag, phase, omega = bode(sys, deg=deg, Plot=False) + elif len(sysdata) == 3: + mag, phase, omega = sysdata + else: + raise ValueError("Margin sysdata must be either a linear system or a 3-sequence of mag, phase, omega.") + + if deg: + cycle = 360. + crossover = 180. + else: + cycle = 2 * np.pi + crossover = np.pi + + wrapped_phase = -np.mod(phase, cycle) + + # phase margin from minimum phase among all gain crossovers + neg_mag_crossings_i = np.nonzero(np.diff(mag < 1) > 0)[0] + mag_crossings_p = wrapped_phase[neg_mag_crossings_i] + if len(neg_mag_crossings_i) == 0: + if mag[0] < 1: # gain always less than one + wp = np.nan + pm = np.inf + else: # gain always greater than one + print "margin: no magnitude crossings found" + wp = np.nan + pm = np.nan + else: + min_mag_crossing_i = neg_mag_crossings_i[np.argmin(mag_crossings_p)] + wp = omega[min_mag_crossing_i] + pm = crossover + phase[min_mag_crossing_i] + if pm < 0: + print "warning: system unstable: negative phase margin" + + # gain margin from minimum gain margin among all phase crossovers + neg_phase_crossings_i = np.nonzero(np.diff(wrapped_phase < -crossover) > 0)[0] + neg_phase_crossings_g = mag[neg_phase_crossings_i] + if len(neg_phase_crossings_i) == 0: + wg = np.nan + gm = np.inf + else: + min_phase_crossing_i = neg_phase_crossings_i[ + np.argmax(neg_phase_crossings_g)] + wg = omega[min_phase_crossing_i] + gm = abs(1/mag[min_phase_crossing_i]) + if gm < 1: + print "warning: system unstable: gain margin < 1" + + # stability margin from minimum abs distance from -1 point + if deg: + phase_rad = phase * np.pi / 180. + else: + phase_rad = phase + L = mag * np.exp(1j * phase_rad) # complex loop response to -1 pt + min_Lplus1_i = np.argmin(np.abs(L + 1)) + sm = np.abs(L[min_Lplus1_i] + 1) + ws = phase[min_Lplus1_i] + + return gm, pm, sm, wg, wp, ws + # # Utility functions # @@ -311,6 +425,10 @@ # Find the list of all poles and zeros in the systems features = np.array(()) + + # detect if single sys passed by checking if it is sequence-like + if (not getattr(syslist, '__iter__', False)): + syslist = (syslist,) for sys in syslist: # Add new features to the list features = np.concatenate((features, np.abs(sys.pole()))) Modified: trunk/src/matlab.py =================================================================== --- trunk/src/matlab.py 2011-04-03 04:55:51 UTC (rev 152) +++ trunk/src/matlab.py 2011-04-07 20:09:44 UTC (rev 153) @@ -781,6 +781,39 @@ return rlist, klist +def margin(*args): + """Calculate gain and phase margins and associated crossover frequencies + + Usage: + gm, pm, wg, wp = margin(sys) + gm, pm, wg, wp = margin(mag,phase,w) + + Parameters + ---------- + sys : linsys + Linear SISO system + mag, phase, w : array_like + Input magnitude, phase (in deg.), and frequencies (rad/sec) from bode + frequency response data + + Returns + ------- + gm, pm, wg, wp : float + Gain margin gm, phase margin pm (in deg), and associated crossover + frequencies wg and wp (in rad/sec) of SISO open-loop. If more than + one crossover frequency is detected, returns the lowest corresponding + margin. + """ + if len(args) == 1: + sys = args[0] + margins = freqplot.margin(sys) + elif len(args) == 3: + margins = freqplot.margin(args) + else: + raise ValueError("Margin needs 1 or 3 arguments; received %i." + % len(args)) + + return margins[0], margins[1], margins[3], margins[4] # # Modifications to scipy.signal functions # This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |