Repository: python-control/python-control
Pull request #863 (2023-02-17 13:03:30)

Solve #862 and #864: bode_plot phase wrapping incorrect for multiple systems
Function argument clobbered by local assignment. Use local variable instead.


Patch:
diff --git a/control/freqplot.py b/control/freqplot.py
index d3490685..a749c3f6 100644
--- a/control/freqplot.py
+++ b/control/freqplot.py
@@ -276,18 +276,21 @@ def bode_plot(syslist, omega=None,
             if initial_phase is None:
                 # Start phase in the range 0 to -360 w/ initial phase = -180
                 # If wrap_phase is true, use 0 instead (phase \in (-pi, pi])
-                initial_phase = -math.pi if wrap_phase is not True else 0
+                initial_phase_value = -math.pi if wrap_phase is not True else 0
             elif isinstance(initial_phase, (int, float)):
                 # Allow the user to override the default calculation
                 if deg:
-                    initial_phase = initial_phase/180. * math.pi
+                    initial_phase_value = initial_phase/180. * math.pi
+                else:
+                    initial_phase_value = initial_phase
+
             else:
                 raise ValueError("initial_phase must be a number.")
 
             # Shift the phase if needed
-            if abs(phase[0] - initial_phase) > math.pi:
+            if abs(phase[0] - initial_phase_value) > math.pi:
                 phase -= 2*math.pi * \
-                    round((phase[0] - initial_phase) / (2*math.pi))
+                    round((phase[0] - initial_phase_value) / (2*math.pi))
 
             # Phase wrapping
             if wrap_phase is False:
@@ -1021,9 +1024,11 @@ def nyquist_plot(
             # Plot the scaled sections of the curve (changing linestyle)
             x_scl = np.ma.masked_where(scale_mask, resp.real)
             y_scl = np.ma.masked_where(scale_mask, resp.imag)
-            plt.plot(
-                x_scl * (1 + curve_offset), y_scl * (1 + curve_offset),
-                primary_style[1], color=c, **kwargs)
+            if x_scl.count() >= 1 and y_scl.count() >= 1:
+                plt.plot(
+                    x_scl * (1 + curve_offset),
+                    y_scl * (1 + curve_offset),
+                    primary_style[1], color=c, **kwargs)
 
             # Plot the primary curve (invisible) for setting arrows
             x, y = resp.real.copy(), resp.imag.copy()
@@ -1041,10 +1046,11 @@ def nyquist_plot(
                 # Plot the regular and scaled segments
                 plt.plot(
                     x_reg, -y_reg, mirror_style[0], color=c, **kwargs)
-                plt.plot(
-                    x_scl * (1 - curve_offset),
-                    -y_scl * (1 - curve_offset),
-                    mirror_style[1], color=c, **kwargs)
+                if x_scl.count() >= 1 and y_scl.count() >= 1:
+                    plt.plot(
+                        x_scl * (1 - curve_offset),
+                        -y_scl * (1 - curve_offset),
+                        mirror_style[1], color=c, **kwargs)
 
                 # Add the arrows (on top of an invisible contour)
                 x, y = resp.real.copy(), resp.imag.copy()


Test patch:
diff --git a/control/tests/freqresp_test.py b/control/tests/freqresp_test.py
index 573fd635..9fc52112 100644
--- a/control/tests/freqresp_test.py
+++ b/control/tests/freqresp_test.py
@@ -375,6 +375,18 @@ def test_phase_wrap(TF, wrap_phase, min_phase, max_phase):
     assert(max(phase) <= max_phase)
 
 
+def test_phase_wrap_multiple_systems():
+    sys_unstable = ctrl.zpk([],[1,1], gain=1)
+
+    mag, phase, omega = ctrl.bode(sys_unstable, plot=False)
+    assert(np.min(phase) >= -2*np.pi)
+    assert(np.max(phase) <= -1*np.pi)
+
+    mag, phase, omega = ctrl.bode((sys_unstable, sys_unstable), plot=False)
+    assert(np.min(phase) >= -2*np.pi)
+    assert(np.max(phase) <= -1*np.pi)
+
+
 def test_freqresp_warn_infinite():
     """Test evaluation warnings for transfer functions w/ pole at the origin"""
     sys_finite = ctrl.tf([1], [1, 0.01])