@@ -178,6 +178,83 @@ def nyquist(syslist, omega=None):
178
178
# Mark the -1 point
179
179
plt .plot ([- 1 ], [0 ], 'r+' )
180
180
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
+
181
258
# Nichols plot
182
259
# Contributed by Allan McInnes <Allan.McInnes@canterbury.ac.nz>
183
260
#! TODO: need unit test code
@@ -209,7 +286,7 @@ def nichols(syslist, omega=None, grid=True):
209
286
syslist = (syslist ,)
210
287
211
288
# Select a default range if none is provided
212
- if ( omega == None ) :
289
+ if omega is None :
213
290
omega = default_frequency_range (syslist )
214
291
215
292
for sys in syslist :
@@ -236,7 +313,8 @@ def nichols(syslist, omega=None, grid=True):
236
313
nichols_grid ()
237
314
238
315
# Nichols grid
239
- def nichols_grid (** kwargs ):
316
+ #! TODO: Consider making linestyle configurable
317
+ def nichols_grid (cl_mags = None , cl_phases = None ):
240
318
"""Nichols chart grid
241
319
242
320
Usage
@@ -270,10 +348,7 @@ def nichols_grid(**kwargs):
270
348
ol_phase_min , ol_phase_max , ol_mag_min , ol_mag_max = plt .axis ()
271
349
272
350
# 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 :
277
352
# Default chart magnitudes
278
353
# The key set of magnitudes are always generated, since this
279
354
# guarantees a recognizable Nichols chart grid.
@@ -289,11 +364,7 @@ def nichols_grid(**kwargs):
289
364
cl_mags = np .concatenate ((extended_cl_mags , key_cl_mags ))
290
365
291
366
# 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 :
297
368
# Choose a reasonable set of default phases (denser if the open-loop
298
369
# data is restricted to a relatively small range of phases).
299
370
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):
302
373
else :
303
374
other_cl_phases = np .arange (- 10.0 , - 360.0 , - 20.0 )
304
375
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 ))
305
378
306
379
# Find the M-contours
307
380
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):
326
399
for phase_offset in phase_offsets :
327
400
# Draw M and N contours
328
401
plt .plot (m_phase + phase_offset , m_mag , color = 'gray' ,
329
- linestyle = 'dashed ' , zorder = 0 )
402
+ linestyle = 'dotted ' , zorder = 0 )
330
403
plt .plot (n_phase + phase_offset , n_mag , color = 'gray' ,
331
- linestyle = 'dashed ' , zorder = 0 )
404
+ linestyle = 'dotted ' , zorder = 0 )
332
405
333
406
# Add magnitude labels
334
407
for x , y , m in zip (m_phase [:][- 1 ] + phase_offset , m_mag [:][- 1 ], cl_mags ):
335
408
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' )
337
410
338
411
# Fit axes to generated chart
339
412
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):
500
573
"""
501
574
# Convert magnitudes and phase range into a grid suitable for
502
575
# building contours
503
- phases = sp .radians (sp .linspace (phase_min , phase_max , 500 ))
576
+ phases = sp .radians (sp .linspace (phase_min , phase_max , 2000 ))
504
577
Gcl_mags , Gcl_phases = sp .meshgrid (10.0 ** (mags / 20.0 ), phases )
505
578
return closed_loop_contours (Gcl_mags , Gcl_phases )
506
579
0 commit comments