nebius/SWE-rebench-V2-PRs huggingface dataset:
Repository: python-control/python-control
Pull request #798 (2022-11-24 13:16:14)

Fix find_eqpt when y0 is None
When `y0` is `None`, `dy` in the root function should not be calculated (since it tries to subtract `NoneType`).  In general, when `y0` is `None`, the evaluation of the output function is not necessary.


Patch:
diff --git a/control/iosys.py b/control/iosys.py
index 9b33d216..a32978b5 100644
--- a/control/iosys.py
+++ b/control/iosys.py
@@ -2138,10 +2138,15 @@ def find_eqpt(sys, x0, u0=None, y0=None, t=0, params=None,
             dx = sys._rhs(t, x, u) - dx0
             if dtime:
                 dx -= x           # TODO: check
-            dy = sys._out(t, x, u) - y0
 
-            # Map the results into the constrained variables
-            return np.concatenate((dx[deriv_vars], dy[output_vars]), axis=0)
+            # If no y0 is given, don't evaluate the output function
+            if y0 is None:
+                return dx[deriv_vars]
+            else:
+                dy = sys._out(t, x, u) - y0
+
+                # Map the results into the constrained variables
+                return np.concatenate((dx[deriv_vars], dy[output_vars]), axis=0)
 
         # Set the initial condition for the root finding algorithm
         z0 = np.concatenate((x[state_vars], u[input_vars]), axis=0)


Test patch:
diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py
index 7d3b9fee..003c43d0 100644
--- a/control/tests/iosys_test.py
+++ b/control/tests/iosys_test.py
@@ -822,6 +822,17 @@ class TestIOSys:
         np.testing.assert_array_almost_equal(
             nlsys_full._rhs(0, xeq, ueq)[-4:], np.zeros((4,)), decimal=5)
 
+        # The same test as previous, but now all constraints are in the state vector
+        nlsys_full = ios.NonlinearIOSystem(pvtol_full, None)
+        xeq, ueq, result = ios.find_eqpt(
+            nlsys_full, [0, 0, 0.1, 0.1, 0, 0], [0.01, 4*9.8],
+            idx=[2, 3, 4, 5], ix=[0, 1, 2, 3], return_result=True)
+        assert result.success
+        np.testing.assert_array_almost_equal(
+            nlsys_full._out(0, xeq, ueq)[[2, 3]], [0.1, 0.1], decimal=5)
+        np.testing.assert_array_almost_equal(
+            nlsys_full._rhs(0, xeq, ueq)[-4:], np.zeros((4,)), decimal=5)
+
         # Fix one input and vary the other
         nlsys_full = ios.NonlinearIOSystem(pvtol_full, None)
         xeq, ueq, result = ios.find_eqpt(
Repository: python-control/python-control
Pull request #996 (2024-04-23 11:39:03)

Fix nyquist plotting from FrequencyResponseData
This PR resolves the issue #992, by adding a check for FrequencyResponseData during plotting of a nyquist plot and setting omega if not yet set correctly


Patch:
diff --git a/control/freqplot.py b/control/freqplot.py
index 961f499b..2f0bcea2 100644
--- a/control/freqplot.py
+++ b/control/freqplot.py
@@ -2468,6 +2468,14 @@ def _determine_omega_vector(syslist, omega_in, omega_limits, omega_num,
     """
     omega_range_given = True
 
+    if omega_in is None:
+        for sys in syslist:
+            if isinstance(sys, FrequencyResponseData):
+                # FRD already has predetermined frequencies
+                if omega_in is not None and not np.all(omega_in == sys.omega):
+                    raise ValueError("List of FrequencyResponseData systems can only have a single frequency range between them")
+                omega_in = sys.omega
+
     if omega_in is None:
         if omega_limits is None:
             omega_range_given = False


Test patch:
diff --git a/control/tests/frd_test.py b/control/tests/frd_test.py
index 98712198..25ecc5e2 100644
--- a/control/tests/frd_test.py
+++ b/control/tests/frd_test.py
@@ -192,6 +192,8 @@ class TestFRD:
         # plt.savefig('/dev/null', format='svg')
         plt.figure(2)
         freqplot.nyquist(f1, f1.omega)
+        plt.figure(3)
+        freqplot.nyquist(f1)
         # plt.savefig('/dev/null', format='svg')
 
     @slycotonly
Repository: python-control/python-control
Pull request #1003 (2024-05-13 01:44:28)

process legacy rlocus keyword k->gains
As [requested ](https://github.com/python-control/python-control/pull/999#issuecomment-2105836061) by @bnavigator in review of #999.


Patch:
diff --git a/control/rlocus.py b/control/rlocus.py
index 281fed08..dab21f4a 100644
--- a/control/rlocus.py
+++ b/control/rlocus.py
@@ -160,7 +160,8 @@ def root_locus_plot(
     from .pzmap import pole_zero_plot
 
     # Legacy parameters
-    gains = config._process_legacy_keyword(kwargs, 'kvect', 'gains', gains)
+    for oldkey in ['kvect', 'k']:
+        gains = config._process_legacy_keyword(kwargs, oldkey, 'gains', gains)
 
     # Set default parameters
     grid = config._get_param('rlocus', 'grid', grid, _rlocus_defaults)


Test patch:
diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py
index 0b3e868e..15eb67d9 100644
--- a/control/tests/rlocus_test.py
+++ b/control/tests/rlocus_test.py
@@ -181,10 +181,11 @@ def test_root_locus_plots(sys, grid, xlim, ylim, interactive):
 
 
 # Test deprecated keywords
-def test_root_locus_legacy():
+@pytest.mark.parametrize("keyword", ["kvect", "k"])
+def test_root_locus_legacy(keyword):
     sys = ct.rss(2, 1, 1)
-    with pytest.warns(DeprecationWarning, match="'kvect' is deprecated"):
-        ct.root_locus_plot(sys, kvect=[0, 1, 2])
+    with pytest.warns(DeprecationWarning, match=f"'{keyword}' is deprecated"):
+        ct.root_locus_plot(sys, **{keyword: [0, 1, 2]})
 
 
 # Generate plots used in documentation
Repository: python-control/python-control
Pull request #1023 (2024-07-03 18:44:35)

Fix impuse response, input data only contains one input[0]==1 in each trace
Computation of impulse response was correct, but input in impulse response was not.


Patch:
diff --git a/control/timeresp.py b/control/timeresp.py
index 81b2030b..d4db8381 100644
--- a/control/timeresp.py
+++ b/control/timeresp.py
@@ -1960,7 +1960,7 @@ def impulse_response(sys, T=None, input=None, output=None, T_num=None,
         yout[:, inpidx, :] = response.y if output is None \
             else response.y[output]
         xout[:, inpidx, :] = response.x
-        uout[:, inpidx, :] = U[i]
+        uout[:, inpidx, :] = U if input is None else U[i]
 
     # Figure out if the system is SISO or not
     issiso = sys.issiso() or (input is not None and output is not None)


Test patch:
diff --git a/control/tests/timeresp_test.py b/control/tests/timeresp_test.py
index bdbbb3e8..73032c0a 100644
--- a/control/tests/timeresp_test.py
+++ b/control/tests/timeresp_test.py
@@ -536,6 +536,32 @@ class TestTimeresp:
         sysdt = sys.sample(dt, 'impulse')
         np.testing.assert_array_almost_equal(impulse_response(sys, t)[1],
                                              impulse_response(sysdt, t)[1])
+        
+    def test_discrete_time_impulse_input(self):
+        # discrete time impulse input, Only one active input for each trace
+        A = [[.5, 0.25],[.0, .5]]
+        B = [[1., 0,],[0., 1.]]
+        C = [[1., 0.],[0., 1.]]
+        D = [[0., 0.],[0., 0.]]
+        dt = True
+        sysd = ct.ss(A,B,C,D, dt=dt)
+        response = ct.impulse_response(sysd,T=dt*3)
+
+        Uexpected = np.zeros((2,2,4), dtype=float).astype(object)
+        Uexpected[0,0,0] = 1./dt
+        Uexpected[1,1,0] = 1./dt
+
+        np.testing.assert_array_equal(response.inputs,Uexpected)
+
+        dt = 0.5
+        sysd = ct.ss(A,B,C,D, dt=dt)
+        response = ct.impulse_response(sysd,T=dt*3)
+
+        Uexpected = np.zeros((2,2,4), dtype=float).astype(object)
+        Uexpected[0,0,0] = 1./dt
+        Uexpected[1,1,0] = 1./dt
+
+        np.testing.assert_array_equal(response.inputs,Uexpected)
 
     @pytest.mark.parametrize("tsystem", ["siso_ss1"], indirect=True)
     def test_impulse_response_warnD(self, tsystem):
Repository: python-control/python-control
Pull request #1051 (2024-10-17 13:04:02)

Improve code style, markov
Improve code style, markov:

- Add space after commas


Patch:
diff --git a/control/modelsimp.py b/control/modelsimp.py
index d2eadc4c..be994937 100644
--- a/control/modelsimp.py
+++ b/control/modelsimp.py
@@ -667,21 +667,21 @@ def markov(*args, m=None, transpose=False, dt=None, truncate=False):
 
     # Set up the full problem
     # Create matrix of (shifted) inputs
-    UUT = np.zeros((p*m,(l)))
+    UUT = np.zeros((p*m, l))
     for i in range(m):
         # Shift previous column down and keep zeros at the top
-        UUT[i*p:(i+1)*p,i:] = Umat[:,:l-i]
+        UUT[i*p:(i+1)*p, i:] = Umat[:, :l-i]
 
     # Truncate first t=0 or t=m time steps, transpose the problem for lsq
-    YY = Ymat[:,t:].T
-    UU = UUT[:,t:].T
+    YY = Ymat[:, t:].T
+    UU = UUT[:, t:].T
     
     # Solve for the Markov parameters from  YY = UU @ H.T
     HT, _, _, _ = np.linalg.lstsq(UU, YY, rcond=None)
     H = HT.T/dt # scaling
 
-    H = H.reshape(q,m,p) # output, time*input -> output, time, input
-    H = H.transpose(0,2,1) # output, input, time
+    H = H.reshape(q, m, p) # output, time*input -> output, time, input
+    H = H.transpose(0, 2, 1) # output, input, time
 
     # for siso return a 1D array instead of a 3D array
     if q == 1 and p == 1:


Test patch:
diff --git a/control/tests/docstrings_test.py b/control/tests/docstrings_test.py
index 28bd0f38..70a0cc3c 100644
--- a/control/tests/docstrings_test.py
+++ b/control/tests/docstrings_test.py
@@ -39,7 +39,7 @@ function_docstring_hash = {
     control.ss2tf:                      '48ff25d22d28e7b396e686dd5eb58831',
     control.tf:                         '53a13f4a7f75a31c81800e10c88730ef',
     control.tf2ss:                      '086a3692659b7321c2af126f79f4bc11',
-    control.markov:                     '753309de348132ef238e78ac756412c1',
+    control.markov:                     'eda7c4635bbb863ae6659e574285d356',
     control.gangof4:                    '0e52eb6cf7ce024f9a41f3ae3ebf04f7',
 }
Repository: python-control/python-control
Pull request #1065 (2024-11-23 02:58:33)

allow root locus maps with only 1 point
Fixes issue #1063, including a unit test that catches the previous error.



Patch:
diff --git a/control/pzmap.py b/control/pzmap.py
index 7b0f8d09..400db280 100644
--- a/control/pzmap.py
+++ b/control/pzmap.py
@@ -607,14 +607,16 @@ def _compute_root_locus_limits(response):
         # Find the local maxima of root locus curve
         xpeaks = np.where(
             np.diff(np.abs(locus.real)) < 0, locus.real[0:-1], 0)
-        xlim = [
-            min(xlim[0], np.min(xpeaks) * rho),
-            max(xlim[1], np.max(xpeaks) * rho)
-        ]
+        if xpeaks.size > 0:
+            xlim = [
+                min(xlim[0], np.min(xpeaks) * rho),
+                max(xlim[1], np.max(xpeaks) * rho)
+            ]
 
         ypeaks = np.where(
             np.diff(np.abs(locus.imag)) < 0, locus.imag[0:-1], 0)
-        ylim = max(ylim, np.max(ypeaks) * rho)
+        if ypeaks.size > 0:
+            ylim = max(ylim, np.max(ypeaks) * rho)
 
     if isctime(dt=response.dt):
         # Adjust the limits to include some space around features
diff --git a/control/rlocus.py b/control/rlocus.py
index 189c2ccd..54c67299 100644
--- a/control/rlocus.py
+++ b/control/rlocus.py
@@ -56,10 +56,9 @@ def root_locus_map(sysdata, gains=None):
     Returns
     -------
     rldata : PoleZeroData or list of PoleZeroData
-        Root locus data object(s) corresponding to the .  The loci of
-        the root locus diagram are available in the array
-        `rldata.loci`, indexed by the gain index and the locus index,
-        and the gains are in the array `rldata.gains`.
+        Root locus data object(s).  The loci of the root locus diagram are
+        available in the array `rldata.loci`, indexed by the gain index and
+        the locus index, and the gains are in the array `rldata.gains`.
 
     Notes
     -----


Test patch:
diff --git a/control/tests/rlocus_test.py b/control/tests/rlocus_test.py
index a62bc742..52546a42 100644
--- a/control/tests/rlocus_test.py
+++ b/control/tests/rlocus_test.py
@@ -287,3 +287,18 @@ if __name__ == "__main__":
 
     # Run tests that generate plots for the documentation
     test_root_locus_documentation(savefigs=True)
+
+
+# https://github.com/python-control/python-control/issues/1063
+def test_rlocus_singleton():
+    # Generate a root locus map for a singleton
+    L = ct.tf([1, 1], [1, 2, 3])
+    rldata = ct.root_locus_map(L, 1)
+    np.testing.assert_equal(rldata.gains, np.array([1]))
+    assert rldata.loci.shape == (1, 2)
+
+    # Generate the root locus plot (no loci)
+    cplt = rldata.plot()
+    assert len(cplt.lines[0, 0]) == 1      # poles (one set of markers)
+    assert len(cplt.lines[0, 1]) == 1      # zeros
+    assert len(cplt.lines[0, 2]) == 2      # loci (two 0-length lines)
Repository: python-control/python-control
Pull request #1104 (2025-01-28 05:29:08)

Add test for Nyquist evaluation at a pole
This PR addresses #1103 by adding a check and raising an exception if the Nyquist contour includes a pole.  In this case the Nyquist curve is not well defined and hence an exception is raised.  (As noted in #1103, this case was already raising an exception, but not quite for the right reason.)

Unit test added that covers the situation.



Patch:
diff --git a/control/freqplot.py b/control/freqplot.py
index 3bc3a0c2..456431f3 100644
--- a/control/freqplot.py
+++ b/control/freqplot.py
@@ -1447,6 +1447,12 @@ def nyquist_response(
         else:
             contour = np.exp(splane_contour * sys.dt)
 
+        # Make sure we don't try to evaluate at a pole
+        if isinstance(sys, (StateSpace, TransferFunction)):
+            if any([pole in contour for pole in sys.poles()]):
+                raise RuntimeError(
+                    "attempt to evaluate at a pole; indent required")
+
         # Compute the primary curve
         resp = sys(contour)
 


Test patch:
diff --git a/control/tests/nyquist_test.py b/control/tests/nyquist_test.py
index 0d6907b6..8d6fd856 100644
--- a/control/tests/nyquist_test.py
+++ b/control/tests/nyquist_test.py
@@ -517,6 +517,15 @@ def test_nyquist_frd():
     warnings.resetwarnings()
 
 
+def test_no_indent_pole():
+    s = ct.tf('s')
+    sys = ((1 + 5/s)/(1 + 0.5/s))**2   # Double-Lag-Compensator
+
+    with pytest.raises(RuntimeError, match="evaluate at a pole"):
+        resp = ct.nyquist_response(
+            sys, warn_encirclements=False, indent_direction='none')
+
+
 if __name__ == "__main__":
     #
     # Interactive mode: generate plots for manual viewing
Repository: python-control/python-control
Pull request #1111 (2025-02-01 11:41:02)

Handle scalar `timeresp` in `solve_flat_ocp`
Fixes gh-1110.


Patch:
diff --git a/control/flatsys/flatsys.py b/control/flatsys/flatsys.py
index 7d76b9d7..5818d118 100644
--- a/control/flatsys/flatsys.py
+++ b/control/flatsys/flatsys.py
@@ -721,8 +721,7 @@ def solve_flat_ocp(
 
     # Process final time
     timepts = np.atleast_1d(timepts)
-    Tf = timepts[-1]
-    T0 = timepts[0] if len(timepts) > 1 else T0
+    T0 = timepts[0] if len(timepts) > 1 else 0
 
     # Process keyword arguments
     if trajectory_constraints is None:


Test patch:
diff --git a/control/tests/flatsys_test.py b/control/tests/flatsys_test.py
index a12bf148..5b66edaf 100644
--- a/control/tests/flatsys_test.py
+++ b/control/tests/flatsys_test.py
@@ -452,6 +452,29 @@ class TestFlatSys:
         np.testing.assert_almost_equal(x_const, x_nlconst)
         np.testing.assert_almost_equal(u_const, u_nlconst)
 
+    def test_solve_flat_ocp_scalar_timepts(self):
+        # scalar timepts gives expected result
+        f = fs.LinearFlatSystem(ct.ss(ct.tf([1],[1,1])))
+
+        def terminal_cost(x, u):
+            return (x-5).dot(x-5)+u.dot(u)
+
+        traj1 = fs.solve_flat_ocp(f, [0, 1], x0=[23],
+                                  terminal_cost=terminal_cost)
+
+        traj2 = fs.solve_flat_ocp(f, 1, x0=[23],
+                                  terminal_cost=terminal_cost)
+
+        teval = np.linspace(0, 1, 101)
+
+        r1 = traj1.response(teval)
+        r2 = traj2.response(teval)
+
+        np.testing.assert_array_equal(r1.x, r2.x)
+        np.testing.assert_array_equal(r1.y, r2.y)
+        np.testing.assert_array_equal(r1.u, r2.u)
+
+
     def test_bezier_basis(self):
         bezier = fs.BezierFamily(4)
         time = np.linspace(0, 1, 100)