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