Repository: python-control/python-control
Pull request #790 (2022-11-13 07:26:12)

fix _isstatic() to use nstates==0
This PR fixes the issues identified in PR #785 regarding the way that `StateSpace._isstatic` was defining a static system.  New version requires `nstates == 0`.


Patch:
diff --git a/control/statesp.py b/control/statesp.py
index a1fd84b2..a2fa3dd7 100644
--- a/control/statesp.py
+++ b/control/statesp.py
@@ -326,7 +326,7 @@ class StateSpace(LTI):
             D = np.zeros((C.shape[0], B.shape[1]))
         D = _ssmatrix(D)
 
-        # Matrices definining the linear system
+        # Matrices defining the linear system
         self.A = A
         self.B = B
         self.C = C
@@ -346,9 +346,8 @@ class StateSpace(LTI):
             defaults = args[0] if len(args) == 1 else \
                 {'inputs': D.shape[1], 'outputs': D.shape[0],
                  'states': A.shape[0]}
-            static = (A.size == 0)
             name, inputs, outputs, states, dt = _process_namedio_keywords(
-                kwargs, defaults, static=static, end=True)
+                kwargs, defaults, static=(A.size == 0), end=True)
 
             # Initialize LTI (NamedIOSystem) object
             super().__init__(
@@ -1484,11 +1483,6 @@ class StateSpace(LTI):
             return (self.C @ x).reshape((-1,)) \
                 + (self.D @ u).reshape((-1,))  # return as row vector
 
-    def _isstatic(self):
-        """True if and only if the system has no dynamics, that is,
-        if A and B are zero. """
-        return not np.any(self.A) and not np.any(self.B)
-
 
 # TODO: add discrete time check
 def _convert_to_statespace(sys):


Test patch:
diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py
index 693be979..8a6ed816 100644
--- a/control/tests/iosys_test.py
+++ b/control/tests/iosys_test.py
@@ -41,6 +41,9 @@ class TestIOSys:
             [[-1, 1], [0, -2]], [[0, 1], [1, 0]],
             [[1, 0], [0, 1]], np.zeros((2, 2)))
 
+        # Create a static gain linear system
+        T.staticgain = ct.StateSpace([], [], [], 1)
+
         # Create simulation parameters
         T.T = np.linspace(0, 10, 100)
         T.U = np.sin(T.T)
@@ -51,7 +54,7 @@ class TestIOSys:
     def test_linear_iosys(self, tsys):
         # Create an input/output system from the linear system
         linsys = tsys.siso_linsys
-        iosys = ios.LinearIOSystem(linsys)
+        iosys = ios.LinearIOSystem(linsys).copy()
 
         # Make sure that the right hand side matches linear system
         for x, u in (([0, 0], 0), ([1, 0], 0), ([0, 1], 0), ([0, 0], 1)):
@@ -66,6 +69,11 @@ class TestIOSys:
         np.testing.assert_array_almost_equal(lti_t, ios_t)
         np.testing.assert_allclose(lti_y, ios_y, atol=0.002, rtol=0.)
 
+        # Make sure that a static linear system has dt=None
+        # and otherwise dt is as specified
+        assert ios.LinearIOSystem(tsys.staticgain).dt is None
+        assert ios.LinearIOSystem(tsys.staticgain, dt=.1).dt == .1
+
     def test_tf2io(self, tsys):
         # Create a transfer function from the state space system
         linsys = tsys.siso_linsys
diff --git a/control/tests/statesp_test.py b/control/tests/statesp_test.py
index e97584fb..6fcdade2 100644
--- a/control/tests/statesp_test.py
+++ b/control/tests/statesp_test.py
@@ -399,25 +399,6 @@ class TestStateSpace:
             mag, phase, omega = sys.freqresp(true_omega)
             np.testing.assert_almost_equal(mag, true_mag)
 
-    def test__isstatic(self):
-        A0 = np.zeros((2,2))
-        A1 = A0.copy()
-        A1[0,1] = 1.1
-        B0 = np.zeros((2,1))
-        B1 = B0.copy()
-        B1[0,0] = 1.3
-        C0 = A0
-        C1 = np.eye(2)
-        D0 = 0
-        D1 = np.ones((2,1))
-        assert StateSpace(A0, B0, C1, D1)._isstatic()
-        assert not StateSpace(A1, B0, C1, D1)._isstatic()
-        assert not StateSpace(A0, B1, C1, D1)._isstatic()
-        assert not StateSpace(A1, B1, C1, D1)._isstatic()
-        assert StateSpace(A0, B0, C0, D0)._isstatic()
-        assert StateSpace(A0, B0, C0, D1)._isstatic()
-        assert StateSpace(A0, B0, C1, D0)._isstatic()
-
     @slycotonly
     def test_minreal(self):
         """Test a minreal model reduction."""
@@ -1160,6 +1141,20 @@ class TestLinfnorm:
         np.testing.assert_allclose(fpeak, reffpeak)
 
 
+@pytest.mark.parametrize("args, static", [
+    (([], [], [], 1), True),       # ctime, empty state
+    (([], [], [], 1, 1), True),    # dtime, empty state
+    ((0, 0, 0, 1), False),         # ctime, unused state
+    ((-1, 0, 0, 1), False),        # ctime, exponential decay
+    ((-1, 0, 0, 0), False),        # ctime, no input, no output
+    ((0, 0, 0, 1, 1), False),      # dtime, integrator
+    ((1, 0, 0, 1, 1), False),      # dtime, unused state
+    ((0, 0, 0, 1, None), False),   # unspecified, unused state
+])
+def test_isstatic(args, static):
+    sys = ct.StateSpace(*args)
+    assert sys._isstatic() == static
+
 # Make sure that using params for StateSpace objects generates a warning
 def test_params_warning():
     sys = StateSpace(-1, 1, 1, 0)
@@ -1169,4 +1164,3 @@ def test_params_warning():
 
     with pytest.warns(UserWarning, match="params keyword ignored"):
         sys.output(0, [0], [0], {'k': 5})
-