diff --git a/control/freqplot.py b/control/freqplot.py index 881ec93dd..a8324e06e 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. @@ -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): @@ -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): @@ -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))) 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 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)