78
78
'bode.deg' : True , # Plot phase in degrees
79
79
'bode.Hz' : False , # Plot frequency in Hertz
80
80
'bode.grid' : True , # Turn on grid for gain and phase
81
+ 'bode.wrap_phase' : False , # Wrap the phase plot at a given value
81
82
}
82
83
83
84
@@ -131,7 +132,19 @@ def bode_plot(syslist, omega=None,
131
132
grid : bool
132
133
If True, plot grid lines on gain and phase plots. Default is set by
133
134
`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'].
135
148
136
149
The default values for Bode plot configuration parameters can be reset
137
150
using the `config.defaults` dictionary, with module name 'bode'.
@@ -171,6 +184,10 @@ def bode_plot(syslist, omega=None,
171
184
grid = config ._get_param ('bode' , 'grid' , kwargs , _bode_defaults , pop = True )
172
185
plot = config ._get_param ('bode' , 'grid' , plot , True )
173
186
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 )
174
191
175
192
# If argument was a singleton, turn it into a list
176
193
if not getattr (syslist , '__iter__' , False ):
@@ -209,11 +226,47 @@ def bode_plot(syslist, omega=None,
209
226
# TODO: What distance to the Nyquist frequency is appropriate?
210
227
else :
211
228
nyquistfrq = None
229
+
212
230
# Get the magnitude and phase of the system
213
231
mag_tmp , phase_tmp , omega_sys = sys .freqresp (omega_sys )
214
232
mag = np .atleast_1d (np .squeeze (mag_tmp ))
215
233
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." )
217
270
218
271
mags .append (mag )
219
272
phases .append (phase )
@@ -270,7 +323,9 @@ def bode_plot(syslist, omega=None,
270
323
label = 'control-bode-phase' ,
271
324
sharex = ax_mag )
272
325
326
+ #
273
327
# Magnitude plot
328
+ #
274
329
if dB :
275
330
pltline = ax_mag .semilogx (omega_plot , 20 * np .log10 (mag ),
276
331
* args , ** kwargs )
@@ -285,19 +340,22 @@ def bode_plot(syslist, omega=None,
285
340
ax_mag .grid (grid and not margins , which = 'both' )
286
341
ax_mag .set_ylabel ("Magnitude (dB)" if dB else "Magnitude" )
287
342
343
+ #
288
344
# 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
293
349
ax_phase .semilogx (omega_plot , phase_plot , * args , ** kwargs )
294
350
295
351
# Show the phase and gain margins in the plot
296
352
if margins :
353
+ # Compute stability margins for the system
297
354
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)
301
359
phase_at_cp = phases [0 ][(np .abs (omegas [0 ] - Wcp )).argmin ()]
302
360
if phase_at_cp >= 0. :
303
361
phase_limit = 180.
@@ -307,6 +365,7 @@ def bode_plot(syslist, omega=None,
307
365
if Hz :
308
366
Wcg , Wcp = Wcg / (2 * math .pi ), Wcp / (2 * math .pi )
309
367
368
+ # Draw lines at gain and phase limits
310
369
ax_mag .axhline (y = 0 if dB else 1 , color = 'k' , linestyle = ':' ,
311
370
zorder = - 20 )
312
371
ax_phase .axhline (y = phase_limit if deg else
@@ -315,6 +374,7 @@ def bode_plot(syslist, omega=None,
315
374
mag_ylim = ax_mag .get_ylim ()
316
375
phase_ylim = ax_phase .get_ylim ()
317
376
377
+ # Annotate the phase margin (if it exists)
318
378
if pm != float ('inf' ) and Wcp != float ('nan' ):
319
379
if dB :
320
380
ax_mag .semilogx (
@@ -327,7 +387,7 @@ def bode_plot(syslist, omega=None,
327
387
328
388
if deg :
329
389
ax_phase .semilogx (
330
- [Wcp , Wcp ], [1e5 , phase_limit + pm ],
390
+ [Wcp , Wcp ], [1e5 , phase_limit + pm ],
331
391
color = 'k' , linestyle = ':' , zorder = - 20 )
332
392
ax_phase .semilogx (
333
393
[Wcp , Wcp ], [phase_limit + pm , phase_limit ],
@@ -343,6 +403,7 @@ def bode_plot(syslist, omega=None,
343
403
math .radians (phase_limit )],
344
404
color = 'k' , zorder = - 20 )
345
405
406
+ # Annotate the gain margin (if it exists)
346
407
if gm != float ('inf' ) and Wcg != float ('nan' ):
347
408
if dB :
348
409
ax_mag .semilogx (
@@ -360,11 +421,11 @@ def bode_plot(syslist, omega=None,
360
421
361
422
if deg :
362
423
ax_phase .semilogx (
363
- [Wcg , Wcg ], [1e-8 , phase_limit ],
424
+ [Wcg , Wcg ], [0 , phase_limit ],
364
425
color = 'k' , linestyle = ':' , zorder = - 20 )
365
426
else :
366
427
ax_phase .semilogx (
367
- [Wcg , Wcg ], [1e-8 , math .radians (phase_limit )],
428
+ [Wcg , Wcg ], [0 , math .radians (phase_limit )],
368
429
color = 'k' , linestyle = ':' , zorder = - 20 )
369
430
370
431
ax_mag .set_ylim (mag_ylim )
0 commit comments