38
38
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
39
39
# SUCH DAMAGE.
40
40
#
41
- # $Id: freqplot.py 33 2010-11-26 21:59:57Z murrayrm $
41
+ # $Id$
42
42
43
43
import matplotlib .pyplot as plt
44
44
import scipy as sp
@@ -212,16 +212,15 @@ def nyquist(syslist, omega=None, Plot=True):
212
212
plt .plot ([- 1 ], [0 ], 'r+' )
213
213
214
214
return x , y , omega
215
-
216
215
# Nichols plot
217
216
# Contributed by Allan McInnes <Allan.McInnes@canterbury.ac.nz>
218
217
#! TODO: need unit test code
219
- def nichols (syslist , omega = None , Plot = True ):
218
+ def nichols (syslist , omega = None ):
220
219
"""Nichols plot for a system
221
220
222
221
Usage
223
222
=====
224
- mag, phase, freq = nichols(sys, omega=None, Plot=True )
223
+ magh = nichols(sys, omega=None)
225
224
226
225
Plots a Nichols plot for the system over a (optional) frequency range.
227
226
@@ -231,15 +230,10 @@ def nichols(syslist, omega=None, Plot=True):
231
230
List of linear input/output systems (single system is OK)
232
231
omega : freq_range
233
232
Range of frequencies (list or bounds) in rad/sec
234
- Plot : boolean
235
- if True, plot the Nichols frequency response
236
233
237
234
Return values
238
235
-------------
239
- mag : array of magnitudes
240
- phase : array of phases
241
- freq : array of frequencies
242
-
236
+ None
243
237
"""
244
238
245
239
# If argument was a singleton, turn it into a list
@@ -250,34 +244,106 @@ def nichols(syslist, omega=None, Plot=True):
250
244
if (omega == None ):
251
245
omega = default_frequency_range (syslist )
252
246
253
-
254
247
for sys in syslist :
255
- if (sys .inputs > 1 or sys .outputs > 1 ):
256
- #TODO: Add MIMO nichols plots.
257
- raise NotImplementedError ("Nichols is currently only implemented for SISO systems." )
258
- else :
259
- # Get the magnitude and phase of the system
260
- mag_tmp , phase_tmp , omega = sys .freqresp (omega )
261
- mag = np .squeeze (mag_tmp )
262
- phase = np .squeeze (phase_tmp )
263
-
264
- # Convert to Nichols-plot format (phase in degrees,
265
- # and magnitude in dB)
266
- x = unwrap (sp .degrees (phase ), 360 )
267
- y = 20 * sp .log10 (mag )
248
+ # Get the magnitude and phase of the system
249
+ mag , phase , omega = sys .freqresp (omega )
268
250
269
- if (Plot ):
270
- # Generate the plot
271
- plt .plot (x , y )
251
+ # Convert to Nichols-plot format (phase in degrees,
252
+ # and magnitude in dB)
253
+ x = unwrap (sp .degrees (phase ), 360 )
254
+ y = 20 * sp .log10 (mag )
255
+
256
+ # Generate the plot
257
+ plt .plot (x , y )
272
258
273
- plt .xlabel ('Phase (deg)' )
274
- plt .ylabel ('Magnitude (dB)' )
275
- plt .title ('Nichols Plot' )
259
+ plt .xlabel ('Phase (deg)' )
260
+ plt .ylabel ('Magnitude (dB)' )
261
+ plt .title ('Nichols Plot' )
276
262
277
- # Mark the -180 point
278
- plt .plot ([- 180 ], [0 ], 'r+' )
263
+ # Mark the -180 point
264
+ plt .plot ([- 180 ], [0 ], 'r+' )
279
265
280
- return mag , phase , omega
266
+ # Nichols grid
267
+ def nichols_grid ():
268
+ """Nichols chart grid
269
+
270
+ Usage
271
+ =====
272
+ nichols_grid()
273
+
274
+ Plots a Nichols chart grid on the current axis.
275
+
276
+ Parameters
277
+ ----------
278
+ None
279
+
280
+ Return values
281
+ -------------
282
+ None
283
+ """
284
+ mag_min_default = - 40.0 # dB
285
+ mag_step = 20.0 # dB
286
+
287
+ # Chart defaults
288
+ phase_min , phase_max , mag_min , mag_max = - 360.0 , 0.0 , mag_min_default , 40.0
289
+
290
+ # Set actual chart bounds based on current plot
291
+ if plt .gcf ().gca ().has_data ():
292
+ phase_min , phase_max , mag_min , mag_max = plt .axis ()
293
+
294
+ # M-circle magnitudes.
295
+ # The "fixed" set are always generated, since this guarantees a recognizable
296
+ # Nichols chart grid.
297
+ mags_fixed = np .array ([- 40.0 , - 20.0 , - 12.0 , - 6.0 , - 3.0 , - 1.0 , - 0.5 , 0.0 ,
298
+ 0.25 , 0.5 , 1.0 , 3.0 , 6.0 , 12.0 ])
299
+
300
+ if mag_min < mag_min_default :
301
+ # Outside the "fixed" set of magnitudes, the generated M-circles
302
+ # are extended in steps of 'mag_step' dB to cover anything made
303
+ # visible by the range of the existing plot
304
+ mags_adjust = np .arange (mag_step * np .floor (mag_min / mag_step ),
305
+ mag_min_default , mag_step )
306
+ mags = np .concatenate ((mags_adjust , mags_fixed ))
307
+ else :
308
+ mags = mags_fixed
309
+
310
+ # N-circle phases (should be in the range -360 to 0)
311
+ phases = np .array ([- 0.25 , - 10.0 , - 20.0 , - 30.0 , - 45.0 , - 60.0 , - 90.0 ,
312
+ - 120.0 , - 150.0 , - 180.0 , - 210.0 , - 240.0 , - 270.0 ,
313
+ - 310.0 , - 325.0 , - 340.0 , - 350.0 , - 359.75 ])
314
+
315
+ # Find the M-contours
316
+ m = m_circles (mags , phase_min = np .min (phases ), phase_max = np .max (phases ))
317
+ m_mag = 20 * sp .log10 (np .abs (m ))
318
+ m_phase = sp .mod (sp .degrees (sp .angle (m )), - 360.0 ) # Unwrap
319
+
320
+ # Find the N-contours
321
+ n = n_circles (phases , mag_min = np .min (mags ), mag_max = np .max (mags ))
322
+ n_mag = 20 * sp .log10 (np .abs (n ))
323
+ n_phase = sp .mod (sp .degrees (sp .angle (n )), - 360.0 ) # Unwrap
324
+
325
+ # Plot the contours behind other plot elements.
326
+ # The "phase offset" is used to produce copies of the chart that cover
327
+ # the entire range of the plotted data, starting from a base chart computed
328
+ # over the range -360 < phase < 0 (see above). Given the range
329
+ # the base chart is computed over, the phase offset should be 0
330
+ # for -360 < phase_min < 0.
331
+ phase_offset_min = 360.0 * np .ceil (phase_min / 360.0 )
332
+ phase_offset_max = 360.0 * np .ceil (phase_max / 360.0 ) + 360.0
333
+ phase_offsets = np .arange (phase_offset_min , phase_offset_max , 360.0 )
334
+ for phase_offset in phase_offsets :
335
+ plt .plot (m_phase + phase_offset , m_mag , color = 'gray' ,
336
+ linestyle = 'dashed' , zorder = 0 )
337
+ plt .plot (n_phase + phase_offset , n_mag , color = 'gray' ,
338
+ linestyle = 'dashed' , zorder = 0 )
339
+
340
+ # Add magnitude labels
341
+ for x , y , m in zip (m_phase [:][- 1 ], m_mag [:][- 1 ], mags ):
342
+ align = 'right' if m < 0.0 else 'left'
343
+ plt .text (x , y , str (m ) + ' dB' , size = 'small' , ha = align )
344
+
345
+ # Make sure axes conform to any pre-existing plot.
346
+ plt .axis ([phase_min , phase_max , mag_min , mag_max ])
281
347
282
348
# Gang of Four
283
349
#! TODO: think about how (and whether) to handle lists of systems
@@ -395,3 +461,85 @@ def default_frequency_range(syslist):
395
461
np .ceil (np .max (features ))+ 1 )
396
462
397
463
return omega
464
+
465
+ # Compute contours of a closed-loop transfer function
466
+ def closed_loop_contours (Hmag , Hphase ):
467
+ """Contours of the function H = G/(1+G).
468
+
469
+ Usage
470
+ =====
471
+ contours = closed_loop_contours(mags, phases)
472
+
473
+ Parameters
474
+ ----------
475
+ mags : array-like
476
+ Meshgrid array of magnitudes of the contours
477
+ phases : array-like
478
+ Meshgrid array of phases in radians of the contours
479
+
480
+ Return values
481
+ -------------
482
+ contours : complex array
483
+ Array of complex numbers corresponding to the contours.
484
+ """
485
+ # Compute the contours in H-space
486
+ H = Hmag * sp .exp (1.j * Hphase )
487
+
488
+ # Invert H = G/(1+G) to get an expression for the contours in G-space
489
+ return H / (1.0 - H )
490
+
491
+ # M-circle
492
+ def m_circles (mags , phase_min = - 359.75 , phase_max = - 0.25 ):
493
+ """Constant-magnitude contours of the function H = G/(1+G).
494
+
495
+ Usage
496
+ =====
497
+ contours = m_circles(mags)
498
+
499
+ Parameters
500
+ ----------
501
+ mags : array-like
502
+ Array of magnitudes in dB of the M-circles
503
+ phase_min : degrees
504
+ Minimum phase in degrees of the N-circles
505
+ phase_max : degrees
506
+ Maximum phase in degrees of the N-circles
507
+
508
+ Return values
509
+ -------------
510
+ contours : complex array
511
+ Array of complex numbers corresponding to the contours.
512
+ """
513
+ # Convert magnitudes and phase range into a grid suitable for
514
+ # building contours
515
+ phases = sp .radians (sp .linspace (phase_min , phase_max , 500 ))
516
+ Hmag , Hphase = sp .meshgrid (10.0 ** (mags / 20.0 ), phases )
517
+ return closed_loop_contours (Hmag , Hphase )
518
+
519
+ # N-circle
520
+ def n_circles (phases , mag_min = - 40.0 , mag_max = 12.0 ):
521
+ """Constant-phase contours of the function H = G/(1+G).
522
+
523
+ Usage
524
+ =====
525
+ contour = n_circles(angles)
526
+
527
+ Parameters
528
+ ----------
529
+ phases : array-like
530
+ Array of phases in degrees of the N-circles
531
+ mag_min : dB
532
+ Minimum magnitude in dB of the N-circles
533
+ mag_max : dB
534
+ Maximum magnitude in dB of the N-circles
535
+
536
+ Return values
537
+ -------------
538
+ contours : complex array
539
+ Array of complex numbers corresponding to the contours.
540
+ """
541
+ # Convert phases and magnitude range into a grid suitable for
542
+ # building contours
543
+ mags = sp .linspace (10 ** (mag_min / 20.0 ), 10 ** (mag_max / 20.0 ), 2000 )
544
+ Hphase , Hmag = sp .meshgrid (sp .radians (phases ), mags )
545
+ return closed_loop_contours (Hmag , Hphase )
0 commit comments