7878 'bode.deg' : True , # Plot phase in degrees
7979 'bode.Hz' : False , # Plot frequency in Hertz
8080 'bode.grid' : True , # Turn on grid for gain and phase
81+ 'bode.wrap_phase' : False , # Wrap the phase plot at a given value
8182}
8283
8384
@@ -131,7 +132,19 @@ def bode_plot(syslist, omega=None,
131132 grid : bool
132133 If True, plot grid lines on gain and phase plots. Default is set by
133134 `config.defaults['bode.grid']`.
134-
135+ initial_phase : float
136+ Set the reference phase to use for the lowest frequency. If set, the
137+ initial phase of the Bode plot will be set to the value closest to the
138+ value specified. Units are in either degrees or radians, depending on
139+ the `deg` parameter. Default is -180 if wrap_phase is False, 0 if
140+ wrap_phase is True.
141+ wrap_phase : bool or float
142+ If wrap_phase is `False`, then the phase will be unwrapped so that it
143+ is continuously increasing or decreasing. If wrap_phase is `True` the
144+ phase will be restricted to the range [-180, 180) (or [:math:`-\\ pi`,
145+ :math:`\\ pi`) radians). If `wrap_phase` is specified as a float, the
146+ phase will be offset by 360 degrees if it falls below the specified
147+ value. Default to `False`, set by config.defaults['bode.wrap_phase'].
135148
136149 The default values for Bode plot configuration parameters can be reset
137150 using the `config.defaults` dictionary, with module name 'bode'.
@@ -171,6 +184,10 @@ def bode_plot(syslist, omega=None,
171184 grid = config ._get_param ('bode' , 'grid' , kwargs , _bode_defaults , pop = True )
172185 plot = config ._get_param ('bode' , 'grid' , plot , True )
173186 margins = config ._get_param ('bode' , 'margins' , margins , False )
187+ wrap_phase = config ._get_param (
188+ 'bode' , 'wrap_phase' , kwargs , _bode_defaults , pop = True )
189+ initial_phase = config ._get_param (
190+ 'bode' , 'initial_phase' , kwargs , None , pop = True )
174191
175192 # If argument was a singleton, turn it into a list
176193 if not getattr (syslist , '__iter__' , False ):
@@ -209,11 +226,47 @@ def bode_plot(syslist, omega=None,
209226 # TODO: What distance to the Nyquist frequency is appropriate?
210227 else :
211228 nyquistfrq = None
229+
212230 # Get the magnitude and phase of the system
213231 mag_tmp , phase_tmp , omega_sys = sys .freqresp (omega_sys )
214232 mag = np .atleast_1d (np .squeeze (mag_tmp ))
215233 phase = np .atleast_1d (np .squeeze (phase_tmp ))
216- phase = unwrap (phase )
234+
235+ #
236+ # Post-process the phase to handle initial value and wrapping
237+ #
238+
239+ if initial_phase is None :
240+ # Start phase in the range 0 to -360 w/ initial phase = -180
241+ # If wrap_phase is true, use 0 instead (phase \in (-pi, pi])
242+ initial_phase = - math .pi if wrap_phase is not True else 0
243+ elif isinstance (initial_phase , (int , float )):
244+ # Allow the user to override the default calculation
245+ if deg :
246+ initial_phase = initial_phase / 180. * math .pi
247+ else :
248+ raise ValueError ("initial_phase must be a number." )
249+
250+ # Shift the phase if needed
251+ if abs (phase [0 ] - initial_phase ) > math .pi :
252+ phase -= 2 * math .pi * \
253+ round ((phase [0 ] - initial_phase ) / (2 * math .pi ))
254+
255+ # Phase wrapping
256+ if wrap_phase is False :
257+ phase = unwrap (phase ) # unwrap the phase
258+ elif wrap_phase is True :
259+ pass # default calculation OK
260+ elif isinstance (wrap_phase , (int , float )):
261+ phase = unwrap (phase ) # unwrap the phase first
262+ if deg :
263+ wrap_phase *= math .pi / 180.
264+
265+ # Shift the phase if it is below the wrap_phase
266+ phase += 2 * math .pi * np .maximum (
267+ 0 , np .ceil ((wrap_phase - phase )/ (2 * math .pi )))
268+ else :
269+ raise ValueError ("wrap_phase must be bool or float." )
217270
218271 mags .append (mag )
219272 phases .append (phase )
@@ -270,7 +323,9 @@ def bode_plot(syslist, omega=None,
270323 label = 'control-bode-phase' ,
271324 sharex = ax_mag )
272325
326+ #
273327 # Magnitude plot
328+ #
274329 if dB :
275330 pltline = ax_mag .semilogx (omega_plot , 20 * np .log10 (mag ),
276331 * args , ** kwargs )
@@ -285,19 +340,22 @@ def bode_plot(syslist, omega=None,
285340 ax_mag .grid (grid and not margins , which = 'both' )
286341 ax_mag .set_ylabel ("Magnitude (dB)" if dB else "Magnitude" )
287342
343+ #
288344 # Phase plot
289- if deg :
290- phase_plot = phase * 180. / math .pi
291- else :
292- phase_plot = phase
345+ #
346+ phase_plot = phase * 180. / math .pi if deg else phase
347+
348+ # Plot the data
293349 ax_phase .semilogx (omega_plot , phase_plot , * args , ** kwargs )
294350
295351 # Show the phase and gain margins in the plot
296352 if margins :
353+ # Compute stability margins for the system
297354 margin = stability_margins (sys )
298- gm , pm , Wcg , Wcp = \
299- margin [0 ], margin [1 ], margin [3 ], margin [4 ]
300- # TODO: add some documentation describing why this is here
355+ gm , pm , Wcg , Wcp = (margin [i ] for i in (0 , 1 , 3 , 4 ))
356+
357+ # Figure out sign of the phase at the first gain crossing
358+ # (needed if phase_wrap is True)
301359 phase_at_cp = phases [0 ][(np .abs (omegas [0 ] - Wcp )).argmin ()]
302360 if phase_at_cp >= 0. :
303361 phase_limit = 180.
@@ -307,6 +365,7 @@ def bode_plot(syslist, omega=None,
307365 if Hz :
308366 Wcg , Wcp = Wcg / (2 * math .pi ), Wcp / (2 * math .pi )
309367
368+ # Draw lines at gain and phase limits
310369 ax_mag .axhline (y = 0 if dB else 1 , color = 'k' , linestyle = ':' ,
311370 zorder = - 20 )
312371 ax_phase .axhline (y = phase_limit if deg else
@@ -315,6 +374,7 @@ def bode_plot(syslist, omega=None,
315374 mag_ylim = ax_mag .get_ylim ()
316375 phase_ylim = ax_phase .get_ylim ()
317376
377+ # Annotate the phase margin (if it exists)
318378 if pm != float ('inf' ) and Wcp != float ('nan' ):
319379 if dB :
320380 ax_mag .semilogx (
@@ -327,7 +387,7 @@ def bode_plot(syslist, omega=None,
327387
328388 if deg :
329389 ax_phase .semilogx (
330- [Wcp , Wcp ], [1e5 , phase_limit + pm ],
390+ [Wcp , Wcp ], [1e5 , phase_limit + pm ],
331391 color = 'k' , linestyle = ':' , zorder = - 20 )
332392 ax_phase .semilogx (
333393 [Wcp , Wcp ], [phase_limit + pm , phase_limit ],
@@ -343,6 +403,7 @@ def bode_plot(syslist, omega=None,
343403 math .radians (phase_limit )],
344404 color = 'k' , zorder = - 20 )
345405
406+ # Annotate the gain margin (if it exists)
346407 if gm != float ('inf' ) and Wcg != float ('nan' ):
347408 if dB :
348409 ax_mag .semilogx (
@@ -360,11 +421,11 @@ def bode_plot(syslist, omega=None,
360421
361422 if deg :
362423 ax_phase .semilogx (
363- [Wcg , Wcg ], [1e-8 , phase_limit ],
424+ [Wcg , Wcg ], [0 , phase_limit ],
364425 color = 'k' , linestyle = ':' , zorder = - 20 )
365426 else :
366427 ax_phase .semilogx (
367- [Wcg , Wcg ], [1e-8 , math .radians (phase_limit )],
428+ [Wcg , Wcg ], [0 , math .radians (phase_limit )],
368429 color = 'k' , linestyle = ':' , zorder = - 20 )
369430
370431 ax_mag .set_ylim (mag_ylim )
0 commit comments