Skip to content

Commit ddaeb6d

Browse files
committed
freqplot: use reasonable number of frequency points rather than default of 50 for logspace; unify frequency range specification for bode and nyquist
1 parent 72c5e06 commit ddaeb6d

File tree

1 file changed

+95
-78
lines changed

1 file changed

+95
-78
lines changed

control/freqplot.py

Lines changed: 95 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
# Default values for module parameter variables
6060
_freqplot_defaults = {
6161
'freqplot.feature_periphery_decades': 1,
62-
'freqplot.number_of_samples': None,
62+
'freqplot.number_of_samples': 1000,
6363
}
6464

6565
#
@@ -94,7 +94,7 @@ def bode_plot(syslist, omega=None,
9494
----------
9595
syslist : linsys
9696
List of linear input/output systems (single system is OK)
97-
omega : list
97+
omega : array_like
9898
List of frequencies in rad/sec to be used for frequency response
9999
dB : bool
100100
If True, plot result in dB. Default is false.
@@ -106,10 +106,10 @@ def bode_plot(syslist, omega=None,
106106
config.defaults['bode.deg']
107107
plot : bool
108108
If True (default), plot magnitude and phase
109-
omega_limits: tuple, list, ... of two values
109+
omega_limits : array_like of two values
110110
Limits of the to generate frequency vector.
111111
If Hz=True the limits are in Hz otherwise in rad/s.
112-
omega_num: int
112+
omega_num : int
113113
Number of samples to plot. Defaults to
114114
config.defaults['freqplot.number_of_samples'].
115115
margins : bool
@@ -200,18 +200,18 @@ def bode_plot(syslist, omega=None,
200200
omega = default_frequency_range(syslist, Hz=Hz,
201201
number_of_samples=omega_num)
202202
else:
203-
omega_limits = np.array(omega_limits)
203+
omega_limits = np.asarray(omega_limits)
204+
if len(omega_limits) != 2:
205+
raise ValueError("len(omega_limits) must be 2")
204206
if Hz:
205207
omega_limits *= 2. * math.pi
206208
if omega_num:
207-
omega = np.logspace(np.log10(omega_limits[0]),
208-
np.log10(omega_limits[1]),
209-
num=omega_num,
210-
endpoint=True)
209+
num = omega_num
211210
else:
212-
omega = np.logspace(np.log10(omega_limits[0]),
213-
np.log10(omega_limits[1]),
214-
endpoint=True)
211+
num = config.defaults['freqplot.number_of_samples']
212+
omega = np.logspace(np.log10(omega_limits[0]),
213+
np.log10(omega_limits[1]), num=num,
214+
endpoint=True)
215215

216216
mags, phases, omegas, nyquistfrqs = [], [], [], []
217217
for sys in syslist:
@@ -507,9 +507,9 @@ def gen_zero_centered_series(val_min, val_max, period):
507507
# Nyquist plot
508508
#
509509

510-
def nyquist_plot(syslist, omega=None, plot=True, label_freq=0,
511-
arrowhead_length=0.1, arrowhead_width=0.1,
512-
color=None, *args, **kwargs):
510+
def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None,
511+
omega_num=None, label_freq=0, arrowhead_length=0.1,
512+
arrowhead_width=0.1, color=None, *args, **kwargs):
513513
"""
514514
Nyquist plot for a system
515515
@@ -519,29 +519,36 @@ def nyquist_plot(syslist, omega=None, plot=True, label_freq=0,
519519
----------
520520
syslist : list of LTI
521521
List of linear input/output systems (single system is OK)
522-
omega : freq_range
523-
Range of frequencies (list or bounds) in rad/sec
524-
Plot : boolean
522+
plot : boolean
525523
If True, plot magnitude
524+
omega : array_like
525+
Range of frequencies in rad/sec
526+
omega_limits : array_like of two values
527+
Limits of the to generate frequency vector.
528+
omega_num : int
529+
Number of samples to plot. Defaults to
530+
config.defaults['freqplot.number_of_samples'].
526531
color : string
527-
Used to specify the color of the plot
532+
Used to specify the color of the line and arrowhead
528533
label_freq : int
529534
Label every nth frequency on the plot
530-
arrowhead_width : arrow head width
531-
arrowhead_length : arrow head length
535+
arrowhead_width : float
536+
Arrow head width
537+
arrowhead_length : float
538+
Arrow head length
532539
*args : :func:`matplotlib.pyplot.plot` positional properties, optional
533540
Additional arguments for `matplotlib` plots (color, linestyle, etc)
534541
**kwargs : :func:`matplotlib.pyplot.plot` keyword properties, optional
535542
Additional keywords (passed to `matplotlib`)
536543
537544
Returns
538545
-------
539-
real : array
546+
real : ndarray
540547
real part of the frequency response array
541-
imag : array
548+
imag : ndarray
542549
imaginary part of the frequency response array
543-
freq : array
544-
frequencies
550+
freq : ndarray
551+
frequencies in rad/s
545552
546553
Examples
547554
--------
@@ -571,7 +578,21 @@ def nyquist_plot(syslist, omega=None, plot=True, label_freq=0,
571578

572579
# Select a default range if none is provided
573580
if omega is None:
574-
omega = default_frequency_range(syslist)
581+
if omega_limits is None:
582+
# Select a default range if none is provided
583+
omega = default_frequency_range(syslist, Hz=False,
584+
number_of_samples=omega_num)
585+
else:
586+
omega_limits = np.asarray(omega_limits)
587+
if len(omega_limits) != 2:
588+
raise ValueError("len(omega_limits) must be 2")
589+
if omega_num:
590+
num = omega_num
591+
else:
592+
num = config.defaults['freqplot.number_of_samples']
593+
omega = np.logspace(np.log10(omega_limits[0]),
594+
np.log10(omega_limits[1]), num=num,
595+
endpoint=True)
575596

576597
# Interpolate between wmin and wmax if a tuple or list are provided
577598
elif isinstance(omega, list) or isinstance(omega, tuple):
@@ -580,65 +601,61 @@ def nyquist_plot(syslist, omega=None, plot=True, label_freq=0,
580601
raise ValueError("Supported frequency arguments are (wmin,wmax)"
581602
"tuple or list, or frequency vector. ")
582603
omega = np.logspace(np.log10(omega[0]), np.log10(omega[1]),
583-
num=50, endpoint=True, base=10.0)
604+
num=500, endpoint=True, base=10.0)
584605

585606
for sys in syslist:
586607
if not sys.issiso():
587608
# TODO: Add MIMO nyquist plots.
588609
raise ControlMIMONotImplemented(
589610
"Nyquist is currently only implemented for SISO systems.")
590-
else:
591-
# Get the magnitude and phase of the system
592-
mag_tmp, phase_tmp, omega = sys.frequency_response(omega)
593-
mag = np.squeeze(mag_tmp)
594-
phase = np.squeeze(phase_tmp)
595-
596-
# Compute the primary curve
597-
x = np.multiply(mag, np.cos(phase))
598-
y = np.multiply(mag, np.sin(phase))
599611

600-
if plot:
601-
# Plot the primary curve and mirror image
602-
p = plt.plot(x, y, '-', color=color, *args, **kwargs)
603-
c = p[0].get_color()
604-
ax = plt.gca()
605-
# Plot arrow to indicate Nyquist encirclement orientation
606-
ax.arrow(x[0], y[0], (x[1]-x[0])/2, (y[1]-y[0])/2, fc=c, ec=c,
607-
head_width=arrowhead_width,
608-
head_length=arrowhead_length)
609-
610-
plt.plot(x, -y, '-', color=c, *args, **kwargs)
611-
ax.arrow(
612-
x[-1], -y[-1], (x[-1]-x[-2])/2, (y[-1]-y[-2])/2,
613-
fc=c, ec=c, head_width=arrowhead_width,
614-
head_length=arrowhead_length)
615-
616-
# Mark the -1 point
617-
plt.plot([-1], [0], 'r+')
618-
619-
# Label the frequencies of the points
620-
if label_freq:
621-
ind = slice(None, None, label_freq)
622-
for xpt, ypt, omegapt in zip(x[ind], y[ind], omega[ind]):
623-
# Convert to Hz
624-
f = omegapt / (2 * np.pi)
625-
626-
# Factor out multiples of 1000 and limit the
627-
# result to the range [-8, 8].
628-
pow1000 = max(min(get_pow1000(f), 8), -8)
629-
630-
# Get the SI prefix.
631-
prefix = gen_prefix(pow1000)
632-
633-
# Apply the text. (Use a space before the text to
634-
# prevent overlap with the data.)
635-
#
636-
# np.round() is used because 0.99... appears
637-
# instead of 1.0, and this would otherwise be
638-
# truncated to 0.
639-
plt.text(xpt, ypt, ' ' +
640-
str(int(np.round(f / 1000 ** pow1000, 0))) + ' ' +
641-
prefix + 'Hz')
612+
# Get the magnitude and phase of the system
613+
mag, phase, omega = sys.frequency_response(omega)
614+
615+
# Compute the primary curve
616+
x = mag * np.cos(phase)
617+
y = mag * np.sin(phase)
618+
619+
if plot:
620+
# Plot the primary curve and mirror image
621+
p = plt.plot(np.hstack((x,x)), np.hstack((y,-y)),
622+
'-', color=color, *args, **kwargs)
623+
c = p[0].get_color()
624+
ax = plt.gca()
625+
# Plot arrow to indicate Nyquist encirclement orientation
626+
ax.arrow(x[0], y[0], (x[1]-x[0])/2, (y[1]-y[0])/2,
627+
fc=c, ec=c, head_width=arrowhead_width,
628+
head_length=arrowhead_length, label=None)
629+
ax.arrow(x[-1], -y[-1], (x[-1]-x[-2])/2, (y[-1]-y[-2])/2,
630+
fc=c, ec=c, head_width=arrowhead_width,
631+
head_length=arrowhead_length, label=None)
632+
633+
# Mark the -1 point
634+
plt.plot([-1], [0], 'r+')
635+
636+
# Label the frequencies of the points
637+
if label_freq:
638+
ind = slice(None, None, label_freq)
639+
for xpt, ypt, omegapt in zip(x[ind], y[ind], omega[ind]):
640+
# Convert to Hz
641+
f = omegapt / (2 * np.pi)
642+
643+
# Factor out multiples of 1000 and limit the
644+
# result to the range [-8, 8].
645+
pow1000 = max(min(get_pow1000(f), 8), -8)
646+
647+
# Get the SI prefix.
648+
prefix = gen_prefix(pow1000)
649+
650+
# Apply the text. (Use a space before the text to
651+
# prevent overlap with the data.)
652+
#
653+
# np.round() is used because 0.99... appears
654+
# instead of 1.0, and this would otherwise be
655+
# truncated to 0.
656+
plt.text(xpt, ypt, ' ' +
657+
str(int(np.round(f / 1000 ** pow1000, 0))) + ' ' +
658+
prefix + 'Hz')
642659

643660
if plot:
644661
ax = plt.gca()

0 commit comments

Comments
 (0)