@@ -178,6 +178,83 @@ def nyquist(syslist, omega=None):
178178 # Mark the -1 point
179179 plt .plot ([- 1 ], [0 ], 'r+' )
180180
181+ # Nyquist grid
182+ #! TODO: Consider making linestyle configurable
183+ def nyquist_grid (cl_mags = None , cl_phases = None ):
184+ """Nyquist plot grid of M-circles and N-circles (aka "Hall chart")
185+
186+ Usage
187+ =====
188+ nyquist_grid()
189+
190+ Plots a grid of M-circles and N-circles on the current axis, or
191+ creates a default grid if no plot already exists.
192+
193+ Parameters
194+ ----------
195+ cl_mags : array-like (dB)
196+ Array of closed-loop magnitudes defining a custom set of
197+ M-circle iso-gain lines.
198+ cl_phases : array-like (degrees)
199+ Array of closed-loop phases defining a custom set of
200+ N-circle iso-phase lines. Must be in the range -180.0 < cl_phases < 180.0
201+
202+ Return values
203+ -------------
204+ None
205+ """
206+ # Default chart size
207+ re_min = - 4.0
208+ re_max = 3.0
209+ im_min = - 2.0
210+ im_max = 2.0
211+
212+ # Find bounds of the current dataset, if there is one.
213+ if plt .gcf ().gca ().has_data ():
214+ re_min , re_max , im_min , im_max = plt .axis ()
215+
216+ # M-circle magnitudes.
217+ if cl_mags is None :
218+ cl_mags = np .array ([- 20.0 , - 10.0 , - 6.0 , - 4.0 , - 2.0 , 0.0 ,
219+ 2.0 , 4.0 , 6.0 , 10.0 , 20.0 ])
220+
221+ # N-circle phases (should be in the range -180.0 to 180.0)
222+ if cl_phases is None :
223+ cl_phases = np .array ([- 90.0 , - 60.0 , - 45.0 , - 30.0 , - 15.0 ,
224+ 15.0 , 30.0 , 45.0 , 60.0 , 90.0 ])
225+ else :
226+ assert ((- 180.0 < np .min (cl_phases )) and (np .max (cl_phases ) < 180.0 ))
227+
228+ # Find the M-contours and N-contours
229+ m = m_circles (cl_mags , phase_min = 0.0 , phase_max = 359.99 )
230+ n = n_circles (cl_phases , mag_min = - 40.0 , mag_max = 40.0 )
231+
232+ # Draw contours
233+ plt .plot (np .real (m ), np .imag (m ), color = 'gray' , linestyle = 'dotted' , zorder = 0 )
234+ plt .plot (np .real (n ), np .imag (n ), color = 'gray' , linestyle = 'dotted' , zorder = 0 )
235+
236+ # Add magnitude labels
237+ for i in range (0 , len (cl_mags )):
238+ if not cl_mags [i ] == 0.0 :
239+ mag = 10.0 ** (cl_mags [i ]/ 20.0 )
240+ x = - mag ** 2.0 / (mag ** 2.0 - 1.0 ) # Center of the M-circle
241+ y = np .abs (mag / (mag ** 2.0 - 1.0 )) # Maximum point
242+ else :
243+ x , y = - 0.5 , im_max
244+ plt .text (x , y , str (cl_mags [i ]) + ' dB' , size = 'small' , color = 'gray' )
245+
246+ # Add phase labels
247+ for i in range (0 , len (cl_phases )):
248+ y = np .sign (cl_phases [i ])* np .max (np .abs (np .imag (n )[:,i ]))
249+ p = str (cl_phases [i ])
250+ plt .text (- 0.5 , y , p + '$^\circ$' , size = 'small' , color = 'gray' )
251+
252+ # Fit axes to original plot
253+ plt .axis ([re_min , re_max , im_min , im_max ])
254+
255+ # Make an alias
256+ hall_grid = nyquist_grid
257+
181258# Nichols plot
182259# Contributed by Allan McInnes <Allan.McInnes@canterbury.ac.nz>
183260#! TODO: need unit test code
@@ -209,7 +286,7 @@ def nichols(syslist, omega=None, grid=True):
209286 syslist = (syslist ,)
210287
211288 # Select a default range if none is provided
212- if ( omega == None ) :
289+ if omega is None :
213290 omega = default_frequency_range (syslist )
214291
215292 for sys in syslist :
@@ -236,7 +313,8 @@ def nichols(syslist, omega=None, grid=True):
236313 nichols_grid ()
237314
238315# Nichols grid
239- def nichols_grid (** kwargs ):
316+ #! TODO: Consider making linestyle configurable
317+ def nichols_grid (cl_mags = None , cl_phases = None ):
240318 """Nichols chart grid
241319
242320 Usage
@@ -270,10 +348,7 @@ def nichols_grid(**kwargs):
270348 ol_phase_min , ol_phase_max , ol_mag_min , ol_mag_max = plt .axis ()
271349
272350 # M-circle magnitudes.
273- if kwargs .has_key ('cl_mags' ):
274- # Custom chart
275- cl_mags = kwargs ['cl_mags' ]
276- else :
351+ if cl_mags is None :
277352 # Default chart magnitudes
278353 # The key set of magnitudes are always generated, since this
279354 # guarantees a recognizable Nichols chart grid.
@@ -289,11 +364,7 @@ def nichols_grid(**kwargs):
289364 cl_mags = np .concatenate ((extended_cl_mags , key_cl_mags ))
290365
291366 # N-circle phases (should be in the range -360 to 0)
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 :
367+ if cl_phases is None :
297368 # Choose a reasonable set of default phases (denser if the open-loop
298369 # data is restricted to a relatively small range of phases).
299370 key_cl_phases = np .array ([- 0.25 , - 45.0 , - 90.0 , - 180.0 , - 270.0 , - 325.0 , - 359.75 ])
@@ -302,6 +373,8 @@ def nichols_grid(**kwargs):
302373 else :
303374 other_cl_phases = np .arange (- 10.0 , - 360.0 , - 20.0 )
304375 cl_phases = np .concatenate ((key_cl_phases , other_cl_phases ))
376+ else :
377+ assert ((- 360.0 < np .min (cl_phases )) and (np .max (cl_phases ) < 0.0 ))
305378
306379 # Find the M-contours
307380 m = m_circles (cl_mags , phase_min = np .min (cl_phases ), phase_max = np .max (cl_phases ))
@@ -326,14 +399,14 @@ def nichols_grid(**kwargs):
326399 for phase_offset in phase_offsets :
327400 # Draw M and N contours
328401 plt .plot (m_phase + phase_offset , m_mag , color = 'gray' ,
329- linestyle = 'dashed ' , zorder = 0 )
402+ linestyle = 'dotted ' , zorder = 0 )
330403 plt .plot (n_phase + phase_offset , n_mag , color = 'gray' ,
331- linestyle = 'dashed ' , zorder = 0 )
404+ linestyle = 'dotted ' , zorder = 0 )
332405
333406 # Add magnitude labels
334407 for x , y , m in zip (m_phase [:][- 1 ] + phase_offset , m_mag [:][- 1 ], cl_mags ):
335408 align = 'right' if m < 0.0 else 'left'
336- plt .text (x , y , str (m ) + ' dB' , size = 'small' , ha = align )
409+ plt .text (x , y , str (m ) + ' dB' , size = 'small' , ha = align , color = 'gray' )
337410
338411 # Fit axes to generated chart
339412 plt .axis ([phase_offset_min - 360.0 , phase_offset_max - 360.0 ,
@@ -500,7 +573,7 @@ def m_circles(mags, phase_min=-359.75, phase_max=-0.25):
500573 """
501574 # Convert magnitudes and phase range into a grid suitable for
502575 # building contours
503- phases = sp .radians (sp .linspace (phase_min , phase_max , 500 ))
576+ phases = sp .radians (sp .linspace (phase_min , phase_max , 2000 ))
504577 Gcl_mags , Gcl_phases = sp .meshgrid (10.0 ** (mags / 20.0 ), phases )
505578 return closed_loop_contours (Gcl_mags , Gcl_phases )
506579
0 commit comments