Skip to content

Round to nearest integer decade for default omega vector #688

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions control/freqplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ def bode_plot(syslist, omega=None,
syslist = (syslist,)

omega, omega_range_given = _determine_omega_vector(
syslist, omega, omega_limits, omega_num)
syslist, omega, omega_limits, omega_num, Hz=Hz)

if plot:
# Set up the axes with labels so that multiple calls to
Expand Down Expand Up @@ -965,7 +965,7 @@ def gangof4_plot(P, C, omega=None, **kwargs):
# Select a default range if none is provided
# TODO: This needs to be made more intelligent
if omega is None:
omega = _default_frequency_range((P, C, S))
omega = _default_frequency_range((P, C, S), Hz=Hz)

# Set up the axes with labels so that multiple calls to
# gangof4_plot will superimpose the data. See details in bode_plot.
Expand Down Expand Up @@ -1115,7 +1115,7 @@ def singular_values_plot(syslist, omega=None,
syslist = (syslist,)

omega, omega_range_given = _determine_omega_vector(
syslist, omega, omega_limits, omega_num)
syslist, omega, omega_limits, omega_num, Hz=Hz)

omega = np.atleast_1d(omega)

Expand Down Expand Up @@ -1210,7 +1210,8 @@ def singular_values_plot(syslist, omega=None,


# Determine the frequency range to be used
def _determine_omega_vector(syslist, omega_in, omega_limits, omega_num):
def _determine_omega_vector(syslist, omega_in, omega_limits, omega_num,
Hz=None):
"""Determine the frequency range for a frequency-domain plot
according to a standard logic.

Expand All @@ -1236,6 +1237,10 @@ def _determine_omega_vector(syslist, omega_in, omega_limits, omega_num):
omega_num : int
Number of points to be used for the frequency
range (if the frequency range is not user-specified)
Hz : bool, optional
If True, the limits (first and last value) of the frequencies
are set to full decades in Hz so it fits plotting with logarithmic
scale in Hz otherwise in rad/s. Omega is always returned in rad/sec.

Returns
-------
Expand All @@ -1253,7 +1258,8 @@ def _determine_omega_vector(syslist, omega_in, omega_limits, omega_num):
omega_range_given = False
# Select a default range if none is provided
omega_out = _default_frequency_range(syslist,
number_of_samples=omega_num)
number_of_samples=omega_num,
Hz=Hz)
else:
omega_limits = np.asarray(omega_limits)
if len(omega_limits) != 2:
Expand All @@ -1280,7 +1286,7 @@ def _default_frequency_range(syslist, Hz=None, number_of_samples=None,
----------
syslist : list of LTI
List of linear input/output systems (single system is OK)
Hz : bool
Hz : bool, optional
If True, the limits (first and last value) of the frequencies
are set to full decades in Hz so it fits plotting with logarithmic
scale in Hz otherwise in rad/s. Omega is always returned in rad/sec.
Expand Down Expand Up @@ -1326,7 +1332,7 @@ def _default_frequency_range(syslist, Hz=None, number_of_samples=None,
features_ = np.concatenate((np.abs(sys.pole()),
np.abs(sys.zero())))
# Get rid of poles and zeros at the origin
toreplace = features_ == 0.0
toreplace = np.isclose(features_, 0.0)
if np.any(toreplace):
features_ = features_[~toreplace]
elif sys.isdtime(strict=True):
Expand All @@ -1339,7 +1345,7 @@ def _default_frequency_range(syslist, Hz=None, number_of_samples=None,
# Get rid of poles and zeros on the real axis (imag==0)
# * origin and real < 0
# * at 1.: would result in omega=0. (logaritmic plot!)
toreplace = (features_.imag == 0.0) & (
toreplace = np.isclose(features_.imag, 0.0) & (
(features_.real <= 0.) |
(np.abs(features_.real - 1.0) < 1.e-10))
if np.any(toreplace):
Expand All @@ -1360,15 +1366,13 @@ def _default_frequency_range(syslist, Hz=None, number_of_samples=None,

if Hz:
features /= 2. * math.pi
features = np.log10(features)
lsp_min = np.floor(np.min(features) - feature_periphery_decades)
lsp_max = np.ceil(np.max(features) + feature_periphery_decades)
features = np.log10(features)
lsp_min = np.rint(np.min(features) - feature_periphery_decades)
lsp_max = np.rint(np.max(features) + feature_periphery_decades)
if Hz:
lsp_min += np.log10(2. * math.pi)
lsp_max += np.log10(2. * math.pi)
else:
features = np.log10(features)
lsp_min = np.floor(np.min(features) - feature_periphery_decades)
lsp_max = np.ceil(np.max(features) + feature_periphery_decades)

if freq_interesting:
lsp_min = min(lsp_min, np.log10(min(freq_interesting)))
lsp_max = max(lsp_max, np.log10(max(freq_interesting)))
Expand Down
36 changes: 25 additions & 11 deletions control/tests/nyquist_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,42 +182,56 @@ def test_nyquist_encirclements():
assert _Z(sys) == count + _P(sys)


def test_nyquist_indent():
@pytest.fixture
def indentsys():
# FBS Figure 10.10
s = ct.tf('s')
sys = 3 * (s+6)**2 / (s * (s+1)**2)
# poles: [-1, -1, 0]
s = ct.tf('s')
return 3 * (s+6)**2 / (s * (s+1)**2)


def test_nyquist_indent_default(indentsys):
plt.figure();
count = ct.nyquist_plot(sys)
count = ct.nyquist_plot(indentsys)
plt.title("Pole at origin; indent_radius=default")
assert _Z(sys) == count + _P(sys)
assert _Z(indentsys) == count + _P(indentsys)


def test_nyquist_indent_dont(indentsys):
# first value of default omega vector was 0.1, replaced by 0. for contour
# indent_radius is larger than 0.1 -> no extra quater circle around origin
count, contour = ct.nyquist_plot(sys, plot=False, indent_radius=.1007,
count, contour = ct.nyquist_plot(indentsys,
plot=False,
indent_radius=.1007,
return_contour=True)
np.testing.assert_allclose(contour[0], .1007+0.j)
# second value of omega_vector is larger than indent_radius: not indented
assert np.all(contour.real[2:] == 0.)


def test_nyquist_indent_do(indentsys):
plt.figure();
count, contour = ct.nyquist_plot(sys, indent_radius=0.01,
count, contour = ct.nyquist_plot(indentsys,
indent_radius=0.01,
return_contour=True)
plt.title("Pole at origin; indent_radius=0.01; encirclements = %d" % count)
assert _Z(sys) == count + _P(sys)
assert _Z(indentsys) == count + _P(indentsys)
# indent radius is smaller than the start of the default omega vector
# check that a quarter circle around the pole at origin has been added.
np.testing.assert_allclose(contour[:50].real**2 + contour[:50].imag**2,
0.01**2)


def test_nyquist_indent_left(indentsys):
plt.figure();
count = ct.nyquist_plot(sys, indent_direction='left')
count = ct.nyquist_plot(indentsys, indent_direction='left')
plt.title(
"Pole at origin; indent_direction='left'; encirclements = %d" % count)
assert _Z(sys) == count + _P(sys, indent='left')
assert _Z(indentsys) == count + _P(indentsys, indent='left')


# System with poles on the imaginary axis
def test_nyquist_indent_im():
"""Test system with poles on the imaginary axis."""
sys = ct.tf([1, 1], [1, 0, 1])

# Imaginary poles with standard indentation
Expand Down
4 changes: 2 additions & 2 deletions control/tests/sisotool_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ def test_sisotool(self, tsys):

# Check if the bode_mag line has moved
bode_mag_moved = np.array(
[674.0242, 667.8354, 661.7033, 655.6275, 649.6074, 643.6426,
637.7324, 631.8765, 626.0742, 620.3252])
[69.0065, 68.6749, 68.3448, 68.0161, 67.6889, 67.3631, 67.0388,
66.7159, 66.3944, 66.0743])
assert_array_almost_equal(ax_mag.lines[0].get_data()[1][10:20],
bode_mag_moved, 4)

Expand Down