Skip to content

Commit 41f8220

Browse files
Allan McInnesAllan McInnes
Allan McInnes
authored and
Allan McInnes
committed
Add a Nyquist grid (aka Hall chart) showing M-circles and N-circles on the complex plane. Tweak Nichols grid to eliminate unnecessary use of kwargs.
1 parent 6338d2b commit 41f8220

File tree

1 file changed

+88
-15
lines changed

1 file changed

+88
-15
lines changed

src/freqplot.py

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)