@@ -181,7 +181,7 @@ def nyquist(syslist, omega=None):
181181# Nichols plot
182182# Contributed by Allan McInnes <Allan.McInnes@canterbury.ac.nz>
183183#! TODO: need unit test code
184- def nichols (syslist , omega = None ):
184+ def nichols (syslist , omega = None , grid = True ):
185185 """Nichols plot for a system
186186
187187 Usage
@@ -196,6 +196,8 @@ def nichols(syslist, omega=None):
196196 List of linear input/output systems (single system is OK)
197197 omega : freq_range
198198 Range of frequencies (list or bounds) in rad/sec
199+ grid : boolean
200+ True if the plot should include a Nichols-chart grid
199201
200202 Return values
201203 -------------
@@ -228,88 +230,114 @@ def nichols(syslist, omega=None):
228230
229231 # Mark the -180 point
230232 plt .plot ([- 180 ], [0 ], 'r+' )
233+
234+ # Add grid
235+ if grid :
236+ nichols_grid ()
231237
232238# Nichols grid
233- def nichols_grid ():
239+ def nichols_grid (** kwargs ):
234240 """Nichols chart grid
235241
236242 Usage
237243 =====
238244 nichols_grid()
239245
240- Plots a Nichols chart grid on the current axis.
246+ Plots a Nichols chart grid on the current axis, or creates a new chart
247+ if no plot already exists.
241248
242249 Parameters
243250 ----------
244- None
251+ cl_mags : array-like (dB)
252+ Array of closed-loop magnitudes defining the iso-gain lines on a
253+ custom Nichols chart.
254+ cl_phases : array-like (degrees)
255+ Array of closed-loop phases defining the iso-phase lines on a custom
256+ Nichols chart. Must be in the range -360 < cl_phases < 0
245257
246258 Return values
247259 -------------
248260 None
249261 """
250- mag_min_default = - 40.0 # dB
251- mag_step = 20.0 # dB
252-
253- # Chart defaults
254- phase_min , phase_max , mag_min , mag_max = - 360.0 , 0.0 , mag_min_default , 40 .0
255-
256- # Set actual chart bounds based on current plot
262+ # Default chart size
263+ ol_phase_min = - 359.99
264+ ol_phase_max = 0.0
265+ ol_mag_min = - 40.0
266+ ol_mag_max = default_ol_mag_max = 50 .0
267+
268+ # Find bounds of the current dataset, if there is one.
257269 if plt .gcf ().gca ().has_data ():
258- phase_min , phase_max , mag_min , mag_max = plt .axis ()
259-
270+ ol_phase_min , ol_phase_max , ol_mag_min , ol_mag_max = plt .axis ()
271+
260272 # M-circle magnitudes.
261- # The "fixed" set are always generated, since this guarantees a recognizable
262- # Nichols chart grid.
263- mags_fixed = np .array ([- 40.0 , - 20.0 , - 12.0 , - 6.0 , - 3.0 , - 1.0 , - 0.5 , 0.0 ,
264- 0.25 , 0.5 , 1.0 , 3.0 , 6.0 , 12.0 ])
265-
266- if mag_min < mag_min_default :
267- # Outside the "fixed" set of magnitudes, the generated M-circles
268- # are extended in steps of 'mag_step' dB to cover anything made
269- # visible by the range of the existing plot
270- mags_adjust = np .arange (mag_step * np .floor (mag_min / mag_step ),
271- mag_min_default , mag_step )
272- mags = np .concatenate ((mags_adjust , mags_fixed ))
273+ if kwargs .has_key ('cl_mags' ):
274+ # Custom chart
275+ cl_mags = kwargs ['cl_mags' ]
273276 else :
274- mags = mags_fixed
277+ # Default chart magnitudes
278+ # The key set of magnitudes are always generated, since this
279+ # guarantees a recognizable Nichols chart grid.
280+ key_cl_mags = np .array ([- 40.0 , - 20.0 , - 12.0 , - 6.0 , - 3.0 , - 1.0 , - 0.5 , 0.0 ,
281+ 0.25 , 0.5 , 1.0 , 3.0 , 6.0 , 12.0 ])
282+ # Extend the range of magnitudes if necessary. The extended arange
283+ # will end up empty if no extension is required. Assumes that closed-loop
284+ # magnitudes are approximately aligned with open-loop magnitudes beyond
285+ # the value of np.min(key_cl_mags)
286+ cl_mag_step = - 20.0 # dB
287+ extended_cl_mags = np .arange (np .min (key_cl_mags ),
288+ ol_mag_min + cl_mag_step , cl_mag_step )
289+ cl_mags = np .concatenate ((extended_cl_mags , key_cl_mags ))
275290
276291 # N-circle phases (should be in the range -360 to 0)
277- phases = np .array ([- 0.25 , - 10.0 , - 20.0 , - 30.0 , - 45.0 , - 60.0 , - 90.0 ,
278- - 120.0 , - 150.0 , - 180.0 , - 210.0 , - 240.0 , - 270.0 ,
279- - 310.0 , - 325.0 , - 340.0 , - 350.0 , - 359.75 ])
292+ if kwargs .has_key ('cl_phases' ):
293+ # Custom chart
294+ cl_phases = kwargs ['cl_phases' ]
295+ assert ((- 360.0 < np .min (cl_phases )) and (np .max (cl_phases ) < 0.0 ))
296+ else :
297+ # Choose a reasonable set of default phases (denser if the open-loop
298+ # data is restricted to a relatively small range of phases).
299+ key_cl_phases = np .array ([- 0.25 , - 45.0 , - 90.0 , - 180.0 , - 270.0 , - 325.0 , - 359.75 ])
300+ if np .abs (ol_phase_max - ol_phase_min ) < 90.0 :
301+ other_cl_phases = np .arange (- 10.0 , - 360.0 , - 10.0 )
302+ else :
303+ other_cl_phases = np .arange (- 10.0 , - 360.0 , - 20.0 )
304+ cl_phases = np .concatenate ((key_cl_phases , other_cl_phases ))
280305
281306 # Find the M-contours
282- m = m_circles (mags , phase_min = np .min (phases ), phase_max = np .max (phases ))
307+ m = m_circles (cl_mags , phase_min = np .min (cl_phases ), phase_max = np .max (cl_phases ))
283308 m_mag = 20 * sp .log10 (np .abs (m ))
284309 m_phase = sp .mod (sp .degrees (sp .angle (m )), - 360.0 ) # Unwrap
285310
286311 # Find the N-contours
287- n = n_circles (phases , mag_min = np .min (mags ), mag_max = np .max (mags ))
312+ n = n_circles (cl_phases , mag_min = np .min (cl_mags ), mag_max = np .max (cl_mags ))
288313 n_mag = 20 * sp .log10 (np .abs (n ))
289314 n_phase = sp .mod (sp .degrees (sp .angle (n )), - 360.0 ) # Unwrap
290315
291316 # Plot the contours behind other plot elements.
292317 # The "phase offset" is used to produce copies of the chart that cover
293318 # the entire range of the plotted data, starting from a base chart computed
294- # over the range -360 < phase < 0 (see above) . Given the range
319+ # over the range -360 < phase < 0. Given the range
295320 # the base chart is computed over, the phase offset should be 0
296- # for -360 < phase_min < 0.
297- phase_offset_min = 360.0 * np .ceil (phase_min / 360.0 )
298- phase_offset_max = 360.0 * np .ceil (phase_max / 360.0 ) + 360.0
321+ # for -360 < ol_phase_min < 0.
322+ phase_offset_min = 360.0 * np .ceil (ol_phase_min / 360.0 )
323+ phase_offset_max = 360.0 * np .ceil (ol_phase_max / 360.0 ) + 360.0
299324 phase_offsets = np .arange (phase_offset_min , phase_offset_max , 360.0 )
325+
300326 for phase_offset in phase_offsets :
327+ # Draw M and N contours
301328 plt .plot (m_phase + phase_offset , m_mag , color = 'gray' ,
302329 linestyle = 'dashed' , zorder = 0 )
303330 plt .plot (n_phase + phase_offset , n_mag , color = 'gray' ,
304331 linestyle = 'dashed' , zorder = 0 )
305332
306- # Add magnitude labels
307- for x , y , m in zip (m_phase [:][- 1 ], m_mag [:][- 1 ], mags ):
308- align = 'right' if m < 0.0 else 'left'
309- plt .text (x , y , str (m ) + ' dB' , size = 'small' , ha = align )
333+ # Add magnitude labels
334+ for x , y , m in zip (m_phase [:][- 1 ] + phase_offset , m_mag [:][- 1 ], cl_mags ):
335+ align = 'right' if m < 0.0 else 'left'
336+ plt .text (x , y , str (m ) + ' dB' , size = 'small' , ha = align )
310337
311- # Make sure axes conform to any pre-existing plot.
312- plt .axis ([phase_min , phase_max , mag_min , mag_max ])
338+ # Fit axes to generated chart
339+ plt .axis ([phase_offset_min - 360.0 , phase_offset_max - 360.0 ,
340+ np .min (cl_mags ), np .max ([ol_mag_max , default_ol_mag_max ])])
313341
314342# Gang of Four
315343#! TODO: think about how (and whether) to handle lists of systems
@@ -417,38 +445,44 @@ def default_frequency_range(syslist):
417445 return omega
418446
419447# Compute contours of a closed-loop transfer function
420- def closed_loop_contours (Hmag , Hphase ):
421- """Contours of the function H = G/(1+G).
448+ def closed_loop_contours (Gcl_mags , Gcl_phases ):
449+ """Contours of the function Gcl = Gol/(1+Gol), where
450+ Gol is an open-loop transfer function, and Gcl is a corresponding
451+ closed-loop transfer function.
422452
423453 Usage
424454 =====
425- contours = closed_loop_contours(mags, phases )
455+ contours = closed_loop_contours(Gcl_mags, Gcl_phases )
426456
427457 Parameters
428458 ----------
429- mags : array-like
430- Meshgrid array of magnitudes of the contours
431- phases : array-like
432- Meshgrid array of phases in radians of the contours
459+ Gcl_mags : array-like
460+ Array of magnitudes of the contours
461+ Gcl_phases : array-like
462+ Array of phases in radians of the contours
433463
434464 Return values
435465 -------------
436466 contours : complex array
437467 Array of complex numbers corresponding to the contours.
438468 """
439- # Compute the contours in H-space
440- H = Hmag * sp .exp (1.j * Hphase )
469+ # Compute the contours in Gcl-space. Since we're given closed-loop
470+ # magnitudes and phases, this is just a case of converting them into
471+ # a complex number.
472+ Gcl = Gcl_mags * sp .exp (1.j * Gcl_phases )
441473
442- # Invert H = G /(1+G ) to get an expression for the contours in G- space
443- return H / (1.0 - H )
474+ # Invert Gcl = Gol /(1+Gol ) to map the contours into the open-loop space
475+ return Gcl / (1.0 - Gcl )
444476
445477# M-circle
446478def m_circles (mags , phase_min = - 359.75 , phase_max = - 0.25 ):
447- """Constant-magnitude contours of the function H = G/(1+G).
479+ """Constant-magnitude contours of the function Gcl = Gol/(1+Gol), where
480+ Gol is an open-loop transfer function, and Gcl is a corresponding
481+ closed-loop transfer function.
448482
449483 Usage
450484 =====
451- contours = m_circles(mags)
485+ contours = m_circles(mags, phase_min, phase_max )
452486
453487 Parameters
454488 ----------
@@ -467,16 +501,18 @@ def m_circles(mags, phase_min=-359.75, phase_max=-0.25):
467501 # Convert magnitudes and phase range into a grid suitable for
468502 # building contours
469503 phases = sp .radians (sp .linspace (phase_min , phase_max , 500 ))
470- Hmag , Hphase = sp .meshgrid (10.0 ** (mags / 20.0 ), phases )
471- return closed_loop_contours (Hmag , Hphase )
504+ Gcl_mags , Gcl_phases = sp .meshgrid (10.0 ** (mags / 20.0 ), phases )
505+ return closed_loop_contours (Gcl_mags , Gcl_phases )
472506
473507# N-circle
474508def n_circles (phases , mag_min = - 40.0 , mag_max = 12.0 ):
475- """Constant-phase contours of the function H = G/(1+G).
509+ """Constant-phase contours of the function Gcl = Gol/(1+Gol), where
510+ Gol is an open-loop transfer function, and Gcl is a corresponding
511+ closed-loop transfer function.
476512
477513 Usage
478514 =====
479- contour = n_circles(angles )
515+ contours = n_circles(phases, mag_min, mag_max )
480516
481517 Parameters
482518 ----------
@@ -495,5 +531,5 @@ def n_circles(phases, mag_min=-40.0, mag_max=12.0):
495531 # Convert phases and magnitude range into a grid suitable for
496532 # building contours
497533 mags = sp .linspace (10 ** (mag_min / 20.0 ), 10 ** (mag_max / 20.0 ), 2000 )
498- Hphase , Hmag = sp .meshgrid (sp .radians (phases ), mags )
499- return closed_loop_contours (Hmag , Hphase )
534+ Gcl_phases , Gcl_mags = sp .meshgrid (sp .radians (phases ), mags )
535+ return closed_loop_contours (Gcl_mags , Gcl_phases )
0 commit comments