Repository: python-control/python-control
Pull request #793 (2022-11-17 05:14:38)

fix error when an IOSystem is combined with a TransferFunction system
Fixes a bug in which system and system name were not being handled correctly when a `TransferFunction` system is combined with other linear systems using `interconnect`. 


Patch:
diff --git a/control/iosys.py b/control/iosys.py
index 2bb445bd..df75f3b5 100644
--- a/control/iosys.py
+++ b/control/iosys.py
@@ -890,7 +890,7 @@ class InterconnectedSystem(InputOutputSystem):
             kwargs, defaults, end=True)
 
         # Initialize the system list and index
-        self.syslist = syslist
+        self.syslist = list(syslist) # insure modifications can be made
         self.syslist_index = {}
 
         # Initialize the input, output, and state counts, indices
@@ -903,12 +903,12 @@ class InterconnectedSystem(InputOutputSystem):
         sysname_count_dct = {}
 
         # Go through the system list and keep track of counts, offsets
-        for sysidx, sys in enumerate(syslist):
+        for sysidx, sys in enumerate(self.syslist):
             # If we were passed a SS or TF system, convert to LinearIOSystem
             if isinstance(sys, (StateSpace, TransferFunction)) and \
                not isinstance(sys, LinearIOSystem):
-                sys = LinearIOSystem(sys)
-                syslist[sysidx] = sys
+                sys = LinearIOSystem(sys, name=sys.name)
+                self.syslist[sysidx] = sys
 
             # Make sure time bases are consistent
             dt = common_timebase(dt, sys.dt)
@@ -2850,12 +2850,12 @@ def interconnect(syslist, connections=None, inplist=None, outlist=None,
         inputs=inputs, outputs=outputs, states=states,
         params=params, dt=dt, name=name, warn_duplicate=warn_duplicate)
 
-    # check for implicity dropped signals
+    # check for implicitly dropped signals
     if check_unused:
         newsys.check_unused_signals(ignore_inputs, ignore_outputs)
 
     # If all subsystems are linear systems, maintain linear structure
-    if all([isinstance(sys, LinearIOSystem) for sys in syslist]):
+    if all([isinstance(sys, LinearIOSystem) for sys in newsys.syslist]):
         return LinearICSystem(newsys, None)
 
     return newsys


Test patch:
diff --git a/control/tests/interconnect_test.py b/control/tests/interconnect_test.py
index 3b99adc6..2c29aeac 100644
--- a/control/tests/interconnect_test.py
+++ b/control/tests/interconnect_test.py
@@ -230,3 +230,25 @@ def test_string_inputoutput():
 
     P_s2 = ct.interconnect([P1_iosys, P2_iosys], inputs=['u1'], output='y2')
     assert P_s2.output_index == {'y2' : 0}
+
+def test_linear_interconnect():
+    tf_ctrl = ct.tf(1, (10.1, 1), inputs='e', outputs='u')
+    tf_plant = ct.tf(1, (10.1, 1), inputs='u', outputs='y')
+    ss_ctrl = ct.ss(1, 2, 1, 2, inputs='e', outputs='u')
+    ss_plant = ct.ss(1, 2, 1, 2, inputs='u', outputs='y')
+    nl_ctrl = ct.NonlinearIOSystem(
+        lambda t, x, u, params: x*x, 
+        lambda t, x, u, params: u*x, states=1, inputs='e', outputs='u')
+    nl_plant = ct.NonlinearIOSystem(
+        lambda t, x, u, params: x*x, 
+        lambda t, x, u, params: u*x, states=1, inputs='u', outputs='y')
+
+    assert isinstance(ct.interconnect((tf_ctrl, tf_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
+    assert isinstance(ct.interconnect((ss_ctrl, ss_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
+    assert isinstance(ct.interconnect((tf_ctrl, ss_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
+    assert isinstance(ct.interconnect((ss_ctrl, tf_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
+    
+    assert ~isinstance(ct.interconnect((nl_ctrl, ss_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
+    assert ~isinstance(ct.interconnect((nl_ctrl, tf_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
+    assert ~isinstance(ct.interconnect((ss_ctrl, nl_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
+    assert ~isinstance(ct.interconnect((tf_ctrl, nl_plant), inputs='e', outputs='y'), ct.LinearIOSystem)
\ No newline at end of file
diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py
index 8a6ed816..7d3b9fee 100644
--- a/control/tests/iosys_test.py
+++ b/control/tests/iosys_test.py
@@ -1454,6 +1454,11 @@ def test_linear_interconnection():
         inputs = ('u[0]', 'u[1]'),
         outputs = ('y[0]', 'y[1]'),
         name = 'sys2')
+    tf_siso = ct.tf(1, [0.1, 1])
+    ss_siso = ct.ss(1, 2, 1, 1)
+    nl_siso = ios.NonlinearIOSystem(
+        lambda t, x, u, params: x*x, 
+        lambda t, x, u, params: u*x, states=1, inputs=1, outputs=1)
 
     # Create a "regular" InterconnectedSystem
     nl_connect = ios.interconnect(
@@ -1500,6 +1505,18 @@ def test_linear_interconnection():
     np.testing.assert_array_almost_equal(io_connect.C, ss_connect.C)
     np.testing.assert_array_almost_equal(io_connect.D, ss_connect.D)
 
+    # make sure interconnections of linear systems are linear and 
+    # if a nonlinear system is included then system is nonlinear
+    assert isinstance(ss_siso*ss_siso, ios.LinearIOSystem)
+    assert isinstance(tf_siso*ss_siso, ios.LinearIOSystem)
+    assert isinstance(ss_siso*tf_siso, ios.LinearIOSystem)
+    assert ~isinstance(ss_siso*nl_siso, ios.LinearIOSystem)
+    assert ~isinstance(nl_siso*ss_siso, ios.LinearIOSystem)
+    assert ~isinstance(nl_siso*nl_siso, ios.LinearIOSystem)
+    assert ~isinstance(tf_siso*nl_siso, ios.LinearIOSystem)
+    assert ~isinstance(nl_siso*tf_siso, ios.LinearIOSystem)
+    assert ~isinstance(nl_siso*nl_siso, ios.LinearIOSystem)
+    
 
 def predprey(t, x, u, params={}):
     """Predator prey dynamics"""