4444import matplotlib .pyplot as plt
4545import scipy as sp
4646import numpy as np
47- #from warnings import warn
4847from .ctrlutil import unwrap
4948from .bdalg import feedback
50- #from .lti import isdtime, timebaseEqual
5149
5250__all__ = ['bode_plot' , 'nyquist_plot' , 'gangof4_plot' ,
5351 'bode' , 'nyquist' , 'gangof4' ]
@@ -132,14 +130,15 @@ def bode_plot(syslist, omega=None, omega_num=None, dB=None, Hz=None, deg=None,
132130 mags , phases , omegas , nyquistfrqs = [], [], [], []
133131 for sys in syslist :
134132 if (sys .inputs > 1 or sys .outputs > 1 ):
135- #TODO: Add MIMO bode plots.
133+ # TODO: Add MIMO bode plots.
136134 raise NotImplementedError ("Bode is currently only implemented for SISO systems." )
137135 else :
138136 omega_sys = np .array (omega )
139137 if sys .isdtime (True ):
140- nyquistfrq = 2. * np .pi * 1. / sys .dt / 2.
141- nyquistfrq_limit = nyquistfrq - (omega_sys [- 1 ] - omega_sys [- 2 ]) / 2. # floating point!
142- omega_sys = omega_sys [omega_sys < nyquistfrq_limit ]
138+ nyquistfrq = 2. * np .pi * 1. / sys .dt / 2.
139+ omega_sys = omega_sys [omega_sys < nyquistfrq ]
140+ nyquistfrq_limit = nyquistfrq - (omega_sys [- 1 ] - omega_sys [- 2 ]) / 2. # floating point!
141+ omega_sys = omega_sys [omega_sys < nyquistfrq_limit ] # in two steps to get the right increment
143142 else :
144143 nyquistfrq = None
145144 # Get the magnitude and phase of the system
@@ -162,41 +161,57 @@ def bode_plot(syslist, omega=None, omega_num=None, dB=None, Hz=None, deg=None,
162161 omegas .append (omega_sys )
163162 nyquistfrqs .append (nyquistfrq )
164163 # Get the dimensions of the current axis, which we will divide up
165- #! TODO: Not current implemented; just use subplot for now
164+ # ! TODO: Not current implemented; just use subplot for now
166165
167166 if (Plot ):
168167 # Magnitude plot
169168 ax_mag = plt .subplot (211 );
170169 if dB :
171- pltline = plt .semilogx (omega_plot , 20 * np .log10 (mag ), * args , ** kwargs )
170+ pltline = ax_mag .semilogx (omega_plot , 20 * np .log10 (mag ), * args , ** kwargs )
172171 else :
173- pltline = plt .loglog (omega_plot , mag , * args , ** kwargs )
172+ pltline = ax_mag .loglog (omega_plot , mag , * args , ** kwargs )
174173 plt .hold (True );
175174 if nyquistfrq_plot :
176- plt .axvline (nyquistfrq_plot , color = pltline [0 ].get_color ())
175+ ax_mag .axvline (nyquistfrq_plot , color = pltline [0 ].get_color ())
176+
177177 # Add a grid to the plot + labeling
178- plt .grid (True )
179- plt .grid (True , which = 'minor' )
180- plt .ylabel ("Magnitude (dB)" if dB else "Magnitude" )
178+ ax_mag .grid (True , which = 'both' )
179+ ax_mag .set_ylabel ("Magnitude (dB)" if dB else "Magnitude" )
181180
182181 # Phase plot
183- plt .subplot (212 , sharex = ax_mag );
182+ ax_phase = plt .subplot (212 , sharex = ax_mag );
184183 if deg :
185184 phase_plot = phase * 180. / np .pi
186185 else :
187186 phase_plot = phase
188- plt .semilogx (omega_plot , phase_plot , * args , ** kwargs )
189- plt .hold (True );
187+ ax_phase .semilogx (omega_plot , phase_plot , * args , ** kwargs )
188+ ax_phase .hold (True );
190189 if nyquistfrq_plot :
191- plt .axvline (nyquistfrq_plot , color = pltline [0 ].get_color ())
190+ ax_phase .axvline (nyquistfrq_plot , color = pltline [0 ].get_color ())
191+
192192 # Add a grid to the plot + labeling
193- plt .grid (True )
194- plt .grid (True , which = 'minor' )
195- plt .ylabel ("Phase (deg)" if deg else "Phase (rad)" )
196-
193+ ax_phase .set_ylabel ("Phase (deg)" if deg else "Phase (rad)" )
194+ def genZeroCenteredSeries (val_min , val_max , period ):
195+ v1 = np .ceil (val_min / period - 0.2 )
196+ v2 = np .floor (val_max / period + 0.2 )
197+ return np .arange (v1 , v2 + 1 ) * period
198+ if deg :
199+ ylim = ax_phase .get_ylim ()
200+ ax_phase .set_yticks (genZeroCenteredSeries (ylim [0 ], ylim [1 ], 45. ))
201+ ax_phase .set_yticks (genZeroCenteredSeries (ylim [0 ], ylim [1 ], 15. ), minor = True )
202+ else :
203+ ylim = ax_phase .get_ylim ()
204+ ax_phase .set_yticks (genZeroCenteredSeries (ylim [0 ], ylim [1 ], np .pi / 4. ))
205+ ax_phase .set_yticks (genZeroCenteredSeries (ylim [0 ], ylim [1 ], np .pi / 12. ), minor = True )
206+ ax_phase .grid (True , which = 'both' )
207+ # ax_mag.grid(which='minor', alpha=0.3)
208+ # ax_mag.grid(which='major', alpha=0.9)
209+ # ax_phase.grid(which='minor', alpha=0.3)
210+ # ax_phase.grid(which='major', alpha=0.9)
211+
197212 # Label the frequency axis
198- plt . xlabel ("Frequency (Hz)" if Hz else "Frequency (rad/sec)" )
199-
213+ ax_phase . set_xlabel ("Frequency (Hz)" if Hz else "Frequency (rad/sec)" )
214+
200215 if len (syslist ) == 1 :
201216 return mags [0 ], phases [0 ], omegas [0 ]
202217 else :
@@ -242,19 +257,19 @@ def nyquist_plot(syslist, omega=None, Plot=True, color='b',
242257
243258 # Select a default range if none is provided
244259 if omega is None :
245- #! TODO: think about doing something smarter for discrete
260+ # ! TODO: think about doing something smarter for discrete
246261 omega = default_frequency_range (syslist )
247262
248263 # Interpolate between wmin and wmax if a tuple or list are provided
249- elif (isinstance (omega ,list ) | isinstance (omega ,tuple )):
264+ elif (isinstance (omega , list ) | isinstance (omega , tuple )):
250265 # Only accept tuple or list of length 2
251266 if (len (omega ) != 2 ):
252267 raise ValueError ("Supported frequency arguments are (wmin,wmax) tuple or list, or frequency vector. " )
253268 omega = np .logspace (np .log10 (omega [0 ]), np .log10 (omega [1 ]),
254269 num = 50 , endpoint = True , base = 10.0 )
255270 for sys in syslist :
256271 if (sys .inputs > 1 or sys .outputs > 1 ):
257- #TODO: Add MIMO nyquist plots.
272+ # TODO: Add MIMO nyquist plots.
258273 raise NotImplementedError ("Nyquist is currently only implemented for SISO systems." )
259274 else :
260275 # Get the magnitude and phase of the system
@@ -278,11 +293,11 @@ def nyquist_plot(syslist, omega=None, Plot=True, color='b',
278293 ind = slice (None , None , labelFreq )
279294 for xpt , ypt , omegapt in zip (x [ind ], y [ind ], omega [ind ]):
280295 # Convert to Hz
281- f = omegapt / ( 2 * sp .pi )
296+ f = omegapt / ( 2 * sp .pi )
282297
283298 # Factor out multiples of 1000 and limit the
284299 # result to the range [-8, 8].
285- pow1000 = max (min (get_pow1000 (f ),8 ),- 8 )
300+ pow1000 = max (min (get_pow1000 (f ), 8 ), - 8 )
286301
287302 # Get the SI prefix.
288303 prefix = gen_prefix (pow1000 )
@@ -294,12 +309,12 @@ def nyquist_plot(syslist, omega=None, Plot=True, color='b',
294309 # instead of 1.0, and this would otherwise be
295310 # truncated to 0.
296311 plt .text (xpt , ypt ,
297- ' ' + str (int (np .round (f / 1000 ** pow1000 , 0 ))) +
312+ ' ' + str (int (np .round (f / 1000 ** pow1000 , 0 ))) +
298313 ' ' + prefix + 'Hz' )
299314 return x , y , omega
300315
301316# Gang of Four
302- #! TODO: think about how (and whether) to handle lists of systems
317+ # ! TODO: think about how (and whether) to handle lists of systems
303318def gangof4_plot (P , C , omega = None ):
304319 """Plot the "Gang of 4" transfer functions for a system
305320
@@ -317,34 +332,34 @@ def gangof4_plot(P, C, omega=None):
317332 -------
318333 None
319334 """
320- if (P .inputs > 1 or P .outputs > 1 or C .inputs > 1 or C .outputs > 1 ):
321- #TODO: Add MIMO go4 plots.
335+ if (P .inputs > 1 or P .outputs > 1 or C .inputs > 1 or C .outputs > 1 ):
336+ # TODO: Add MIMO go4 plots.
322337 raise NotImplementedError ("Gang of four is currently only implemented for SISO systems." )
323338 else :
324339
325340 # Select a default range if none is provided
326- #! TODO: This needs to be made more intelligent
341+ # ! TODO: This needs to be made more intelligent
327342 if omega is None :
328- omega = default_frequency_range ((P ,C ))
343+ omega = default_frequency_range ((P , C ))
329344
330345 # Compute the senstivity functions
331- L = P * C ;
346+ L = P * C ;
332347 S = feedback (1 , L );
333348 T = L * S ;
334349
335350 # Plot the four sensitivity functions
336- #! TODO: Need to add in the mag = 1 lines
351+ # ! TODO: Need to add in the mag = 1 lines
337352 mag_tmp , phase_tmp , omega = T .freqresp (omega );
338353 mag = np .squeeze (mag_tmp )
339354 phase = np .squeeze (phase_tmp )
340355 plt .subplot (221 ); plt .loglog (omega , mag );
341356
342- mag_tmp , phase_tmp , omega = (P * S ).freqresp (omega );
357+ mag_tmp , phase_tmp , omega = (P * S ).freqresp (omega );
343358 mag = np .squeeze (mag_tmp )
344359 phase = np .squeeze (phase_tmp )
345360 plt .subplot (222 ); plt .loglog (omega , mag );
346361
347- mag_tmp , phase_tmp , omega = (C * S ).freqresp (omega );
362+ mag_tmp , phase_tmp , omega = (C * S ).freqresp (omega );
348363 mag = np .squeeze (mag_tmp )
349364 phase = np .squeeze (phase_tmp )
350365 plt .subplot (223 ); plt .loglog (omega , mag );
@@ -418,14 +433,13 @@ def default_frequency_range(syslist, Hz=None, number_of_samples=None, feature_pe
418433 features_ = features_ [features_ != 0.0 ];
419434 features = np .concatenate ((features , features_ ))
420435 elif sys .isdtime (strict = True ):
421- freq_interesting .append (np .pi * 1. / sys .dt )
436+ freq_interesting .append (np .pi * 1. / sys .dt )
422437 p = sys .pole ()
423438 p = p [p != - 1. ]
424- features = np .concatenate ((features , np .abs (np .log (p )/ sys .dt )))
439+ features = np .concatenate ((features , np .abs (np .log (p ) / sys .dt )))
425440 z = sys .zero ()
426- z = z [(z .imag != 0.0 )]
427- #z = z[(z.imag != 0.0) | (z.real > 0.0)]
428- features = np .concatenate ((features , np .abs (np .log (z )/ sys .dt )))
441+ z = z [(z .imag != 0.0 )] # Get rid of poles and zeros at the origin and real <= 0 & imag==0
442+ features = np .concatenate ((features , np .abs (np .log (z ) / sys .dt )))
429443 else :
430444 pass
431445 # TODO:
@@ -440,20 +454,19 @@ def default_frequency_range(syslist, Hz=None, number_of_samples=None, feature_pe
440454 if Hz :
441455 features /= 2. * np .pi
442456 features = np .log10 (features )
443- lsp_min = np .floor (np .min (features )) - feature_periphery_decade
444- lsp_max = np .ceil (np .max (features )) + feature_periphery_decade
457+ lsp_min = np .floor (np .min (features ) - feature_periphery_decade )
458+ lsp_max = np .ceil (np .max (features ) + feature_periphery_decade )
445459 lsp_min += np .log10 (2. * np .pi )
446460 lsp_max += np .log10 (2. * np .pi )
447461 else :
448- # Take the log of the features
449462 features = np .log10 (features )
450- lsp_min = np .floor (np .min (features )) - feature_periphery_decade
451- lsp_max = np .ceil (np .max (features )) + feature_periphery_decade
463+ lsp_min = np .floor (np .min (features ) - feature_periphery_decade )
464+ lsp_max = np .ceil (np .max (features ) + feature_periphery_decade )
452465 if freq_interesting :
453466 lsp_min = min (lsp_min , np .log10 (min (freq_interesting )))
454467 lsp_max = max (lsp_max , np .log10 (max (freq_interesting )))
455468
456- #! TODO: Add a check in discrete case to make sure we don't get aliasing (Attention: there is a list of system but only one omega vector)
469+ # ! TODO: Add a check in discrete case to make sure we don't get aliasing (Attention: there is a list of system but only one omega vector)
457470
458471 # Set the range to be an order of magnitude beyond any features
459472 if number_of_samples :
@@ -478,7 +491,7 @@ def get_pow1000(num):
478491 return 0
479492 elif dnum < 0 :
480493 dnum = - dnum
481- return int (floor (dnum .log10 ()/ 3 ))
494+ return int (floor (dnum .log10 () / 3 ))
482495
483496def gen_prefix (pow1000 ):
484497 '''Return the SI prefix for a power of 1000.
@@ -487,23 +500,23 @@ def gen_prefix(pow1000):
487500 # deca, deci, and centi).
488501 if pow1000 < - 8 or pow1000 > 8 :
489502 raise ValueError ("Value is out of the range covered by the SI prefixes." )
490- return ['Y' , # yotta (10^24)
491- 'Z' , # zetta (10^21)
492- 'E' , # exa (10^18)
493- 'P' , # peta (10^15)
494- 'T' , # tera (10^12)
495- 'G' , # giga (10^9)
496- 'M' , # mega (10^6)
497- 'k' , # kilo (10^3)
498- '' , # (10^0)
499- 'm' , # milli (10^-3)
500- r'$\mu$' , # micro (10^-6)
501- 'n' , # nano (10^-9)
502- 'p' , # pico (10^-12)
503- 'f' , # femto (10^-15)
504- 'a' , # atto (10^-18)
505- 'z' , # zepto (10^-21)
506- 'y' ][8 - pow1000 ] # yocto (10^-24)
503+ return ['Y' , # yotta (10^24)
504+ 'Z' , # zetta (10^21)
505+ 'E' , # exa (10^18)
506+ 'P' , # peta (10^15)
507+ 'T' , # tera (10^12)
508+ 'G' , # giga (10^9)
509+ 'M' , # mega (10^6)
510+ 'k' , # kilo (10^3)
511+ '' , # (10^0)
512+ 'm' , # milli (10^-3)
513+ r'$\mu$' , # micro (10^-6)
514+ 'n' , # nano (10^-9)
515+ 'p' , # pico (10^-12)
516+ 'f' , # femto (10^-15)
517+ 'a' , # atto (10^-18)
518+ 'z' , # zepto (10^-21)
519+ 'y' ][8 - pow1000 ] # yocto (10^-24)
507520
508521# Function aliases
509522bode = bode_plot
0 commit comments