From 58d8cd6e4fe77fd5a18f91c791532e3e259b1949 Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Tue, 4 Jan 2022 20:09:23 +0100 Subject: [PATCH 1/4] round to nearest integer for default omega --- control/freqplot.py | 16 +++++++--------- control/tests/sisotool_test.py | 4 ++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 881ec93dd..7225afe97 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -1326,7 +1326,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): @@ -1339,7 +1339,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): @@ -1360,15 +1360,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))) diff --git a/control/tests/sisotool_test.py b/control/tests/sisotool_test.py index 6b8c6d148..d5e9dd013 100644 --- a/control/tests/sisotool_test.py +++ b/control/tests/sisotool_test.py @@ -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) From 19801e6deac560e72ee9593f81be2d24e8df14e6 Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Tue, 4 Jan 2022 21:07:35 +0100 Subject: [PATCH 2/4] split up nyquist indent tests --- control/tests/nyquist_test.py | 36 ++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/control/tests/nyquist_test.py b/control/tests/nyquist_test.py index 4667c6219..c77d94c86 100644 --- a/control/tests/nyquist_test.py +++ b/control/tests/nyquist_test.py @@ -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 From c44b901d3af030187d4c2c75dbee75f2bedcf29a Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Tue, 4 Jan 2022 21:08:06 +0100 Subject: [PATCH 3/4] passthrough Hz parameter for omega vector --- control/freqplot.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 7225afe97..18b9a4485 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -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 @@ -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. @@ -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) @@ -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. @@ -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 ------- @@ -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: @@ -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. From 021372f4cd821907fee913ceef6d46af899a9bcd Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Tue, 4 Jan 2022 21:37:10 +0100 Subject: [PATCH 4/4] docstring punctuation --- control/freqplot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control/freqplot.py b/control/freqplot.py index 18b9a4485..a8324e06e 100644 --- a/control/freqplot.py +++ b/control/freqplot.py @@ -1237,7 +1237,7 @@ 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 + 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. @@ -1286,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. optional + 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.