Skip to content

Commit 1e12a51

Browse files
committed
add warning for nyquist_plot() when Nyquist criterion isn't met
1 parent 18e997c commit 1e12a51

File tree

2 files changed

+35
-9
lines changed

2 files changed

+35
-9
lines changed

control/freqplot.py

+23
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import matplotlib.pyplot as plt
4848
import numpy as np
4949
import warnings
50+
from math import nan
5051

5152
from .ctrlutil import unwrap
5253
from .bdalg import feedback
@@ -805,6 +806,28 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None,
805806
phase = -unwrap(np.angle(resp + 1))
806807
count = int(np.round(np.sum(np.diff(phase)) / np.pi, 0))
807808

809+
#
810+
# Make sure that the enciriclements match the Nyquist criterion
811+
#
812+
# If the user specifies the frequency points to use, it is possible
813+
# to miss enciriclements, so we check here to make sure that the
814+
# Nyquist criterion is actually satisfied.
815+
#
816+
if isinstance(sys, (StateSpace, TransferFunction)):
817+
P = (sys.pole().real > 0).sum() if indent_direction == 'right' \
818+
else (sys.pole().real >= 0).sum()
819+
Z = (sys.feedback().pole().real >= 0).sum()
820+
if Z != count + P:
821+
warnings.warn(
822+
"number of encirclements does not match Nyquist criterion;"
823+
" check frequency range and indent radius/direction",
824+
UserWarning, stacklevel=2)
825+
elif indent_direction == 'none' and any(sys.pole().real == 0):
826+
warnings.warn(
827+
"system has pure imaginary poles but indentation is"
828+
" turned off; results may be meaningless",
829+
RuntimeWarning, stacklevel=2)
830+
808831
counts.append(count)
809832
contours.append(contour)
810833

control/tests/nyquist_test.py

+12-9
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,10 @@ def test_nyquist_basic():
9898
count, contour_indented = ct.nyquist_plot(
9999
sys, np.linspace(1e-4, 1e2, 100), return_contour=True)
100100
assert not all(contour_indented.real == 0)
101-
count, contour = ct.nyquist_plot(
102-
sys, np.linspace(1e-4, 1e2, 100), return_contour=True,
103-
indent_direction='none')
101+
with pytest.warns(UserWarning, match="encirclements does not match"):
102+
count, contour = ct.nyquist_plot(
103+
sys, np.linspace(1e-4, 1e2, 100), return_contour=True,
104+
indent_direction='none')
104105
np.testing.assert_almost_equal(contour, 1j*np.linspace(1e-4, 1e2, 100))
105106

106107
# Nyquist plot with poles at the origin, omega unspecified
@@ -166,10 +167,11 @@ def test_nyquist_fbs_examples():
166167

167168
plt.figure()
168169
plt.title("Figure 10.10: L(s) = 3 (s+6)^2 / (s (s+1)^2) [zoom]")
169-
count = ct.nyquist_plot(sys, omega_limits=[1.5, 1e3])
170-
# Frequency limits for zoom give incorrect encirclement count
171-
# assert _Z(sys) == count + _P(sys)
172-
assert count == -1
170+
with pytest.warns(UserWarning, match="encirclements does not match"):
171+
count = ct.nyquist_plot(sys, omega_limits=[1.5, 1e3])
172+
# Frequency limits for zoom give incorrect encirclement count
173+
# assert _Z(sys) == count + _P(sys)
174+
assert count == -1
173175

174176

175177
@pytest.mark.parametrize("arrows", [
@@ -276,8 +278,9 @@ def test_nyquist_indent_im():
276278

277279
# Imaginary poles with no indentation
278280
plt.figure();
279-
count = ct.nyquist_plot(
280-
sys, np.linspace(0, 1e3, 1000), indent_direction='none')
281+
with pytest.warns(UserWarning, match="encirclements does not match"):
282+
count = ct.nyquist_plot(
283+
sys, np.linspace(0, 1e3, 1000), indent_direction='none')
281284
plt.title(
282285
"Imaginary poles; indent_direction='none'; encirclements = %d" % count)
283286
assert _Z(sys) == count + _P(sys)

0 commit comments

Comments
 (0)