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,18 @@ 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. Default is 180 if wrap_phase is False, 0 if
139+ wrap_phase is True.
140+ wrap_phase : bool or float
141+ If wrap_phase is `False`, then the phase will be unwrapped so that it
142+ is continuously increasing or decreasing. If wrap_phase is `True` the
143+ phase will be restricted to the range [-180, 180) (or [:math:`-\\ pi`,
144+ :math:`\\ pi`) radians). If `wrap_phase` is specified as a float, the
145+ phase will be offset by 360 degrees if it falls below the specified
146+ value. Default to `False`, set by config.defaults['bode.wrap_phase'].
135147
136148 The default values for Bode plot configuration parameters can be reset
137149 using the `config.defaults` dictionary, with module name 'bode'.
@@ -171,6 +183,10 @@ def bode_plot(syslist, omega=None,
171183 grid = config ._get_param ('bode' , 'grid' , kwargs , _bode_defaults , pop = True )
172184 plot = config ._get_param ('bode' , 'grid' , plot , True )
173185 margins = config ._get_param ('bode' , 'margins' , margins , False )
186+ wrap_phase = config ._get_param (
187+ 'bode' , 'wrap_phase' , kwargs , _bode_defaults , pop = True )
188+ initial_phase = config ._get_param (
189+ 'bode' , 'initial_phase' , kwargs , None , pop = True )
174190
175191 # If argument was a singleton, turn it into a list
176192 if not getattr (syslist , '__iter__' , False ):
@@ -209,11 +225,47 @@ def bode_plot(syslist, omega=None,
209225 # TODO: What distance to the Nyquist frequency is appropriate?
210226 else :
211227 nyquistfrq = None
228+
212229 # Get the magnitude and phase of the system
213230 mag_tmp , phase_tmp , omega_sys = sys .freqresp (omega_sys )
214231 mag = np .atleast_1d (np .squeeze (mag_tmp ))
215232 phase = np .atleast_1d (np .squeeze (phase_tmp ))
216- phase = unwrap (phase )
233+
234+ #
235+ # Post-process the phase to handle initial value and wrapping
236+ #
237+
238+ if initial_phase is None :
239+ # Start phase in the range 0 to -360 w/ initial phase = -180
240+ # If wrap_phase is true, use 0 instead (phase \in (-pi, pi])
241+ initial_phase = - math .pi if wrap_phase is not True else 0
242+ elif isinstance (initial_phase , (int , float )):
243+ # Allow the user to override the default calculation
244+ if deg :
245+ initial_phase = initial_phase / 180. * math .pi
246+ else :
247+ raise ValueError ("initial_phase must be a number." )
248+
249+ # Shift the phase if needed
250+ if abs (phase [0 ] - initial_phase ) > math .pi :
251+ phase -= 2 * math .pi * \
252+ round ((phase [0 ] - initial_phase ) / (2 * math .pi ))
253+
254+ # Phase wrapping
255+ if wrap_phase is False :
256+ phase = unwrap (phase ) # unwrap the phase
257+ elif wrap_phase is True :
258+ pass # default calculation OK
259+ elif isinstance (wrap_phase , (int , float )):
260+ phase = unwrap (phase ) # unwrap the phase first
261+ if deg :
262+ wrap_phase *= math .pi / 180.
263+
264+ # Shift the phase if it is below the wrap_phase
265+ phase += 2 * math .pi * np .maximum (
266+ 0 , np .ceil ((wrap_phase - phase )/ (2 * math .pi )))
267+ else :
268+ raise ValueError ("wrap_phase must be bool or float." )
217269
218270 mags .append (mag )
219271 phases .append (phase )
@@ -270,7 +322,9 @@ def bode_plot(syslist, omega=None,
270322 label = 'control-bode-phase' ,
271323 sharex = ax_mag )
272324
325+ #
273326 # Magnitude plot
327+ #
274328 if dB :
275329 pltline = ax_mag .semilogx (omega_plot , 20 * np .log10 (mag ),
276330 * args , ** kwargs )
@@ -285,19 +339,22 @@ def bode_plot(syslist, omega=None,
285339 ax_mag .grid (grid and not margins , which = 'both' )
286340 ax_mag .set_ylabel ("Magnitude (dB)" if dB else "Magnitude" )
287341
342+ #
288343 # Phase plot
289- if deg :
290- phase_plot = phase * 180. / math .pi
291- else :
292- phase_plot = phase
344+ #
345+ phase_plot = phase * 180. / math .pi if deg else phase
346+
347+ # Plot the data
293348 ax_phase .semilogx (omega_plot , phase_plot , * args , ** kwargs )
294349
295350 # Show the phase and gain margins in the plot
296351 if margins :
352+ # Compute stability margins for the system
297353 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
354+ gm , pm , Wcg , Wcp = (margin [i ] for i in (0 , 1 , 3 , 4 ))
355+
356+ # Figure out sign of the phase at the first gain crossing
357+ # (needed if phase_wrap is True)
301358 phase_at_cp = phases [0 ][(np .abs (omegas [0 ] - Wcp )).argmin ()]
302359 if phase_at_cp >= 0. :
303360 phase_limit = 180.
@@ -307,6 +364,7 @@ def bode_plot(syslist, omega=None,
307364 if Hz :
308365 Wcg , Wcp = Wcg / (2 * math .pi ), Wcp / (2 * math .pi )
309366
367+ # Draw lines at gain and phase limits
310368 ax_mag .axhline (y = 0 if dB else 1 , color = 'k' , linestyle = ':' ,
311369 zorder = - 20 )
312370 ax_phase .axhline (y = phase_limit if deg else
@@ -315,6 +373,7 @@ def bode_plot(syslist, omega=None,
315373 mag_ylim = ax_mag .get_ylim ()
316374 phase_ylim = ax_phase .get_ylim ()
317375
376+ # Annotate the phase margin (if it exists)
318377 if pm != float ('inf' ) and Wcp != float ('nan' ):
319378 if dB :
320379 ax_mag .semilogx (
@@ -327,7 +386,7 @@ def bode_plot(syslist, omega=None,
327386
328387 if deg :
329388 ax_phase .semilogx (
330- [Wcp , Wcp ], [1e5 , phase_limit + pm ],
389+ [Wcp , Wcp ], [1e5 , phase_limit + pm ],
331390 color = 'k' , linestyle = ':' , zorder = - 20 )
332391 ax_phase .semilogx (
333392 [Wcp , Wcp ], [phase_limit + pm , phase_limit ],
@@ -343,6 +402,7 @@ def bode_plot(syslist, omega=None,
343402 math .radians (phase_limit )],
344403 color = 'k' , zorder = - 20 )
345404
405+ # Annotate the gain margin (if it exists)
346406 if gm != float ('inf' ) and Wcg != float ('nan' ):
347407 if dB :
348408 ax_mag .semilogx (
@@ -360,11 +420,11 @@ def bode_plot(syslist, omega=None,
360420
361421 if deg :
362422 ax_phase .semilogx (
363- [Wcg , Wcg ], [1e-8 , phase_limit ],
423+ [Wcg , Wcg ], [0 , phase_limit ],
364424 color = 'k' , linestyle = ':' , zorder = - 20 )
365425 else :
366426 ax_phase .semilogx (
367- [Wcg , Wcg ], [1e-8 , math .radians (phase_limit )],
427+ [Wcg , Wcg ], [0 , math .radians (phase_limit )],
368428 color = 'k' , linestyle = ':' , zorder = - 20 )
369429
370430 ax_mag .set_ylim (mag_ylim )
0 commit comments