Repository: python-control/python-control

Issue:
"Interconnect" input specification.
Hi,

In this code I want "interconnect" to build a system with just one input "sys.u1". It tries to create two inputs and as a result catches an exception. Am I getting it wrong?

```
import numpy as np
import control as ct

f = np.array([[-35, 1], [-225, -5]])
j = np.array([[0], [5]])
g = np.array([[-965], [-8775]])
cobs = np.eye(2)
dobs = np.zeros((2, 2))

a = np.array([[0, 1, 0], [0, -1, 1], [0, 0, -5]])
b = np.array([[0], [0], [5]])
c = np.array([[1, 0, 0]])
d = 0

plant = ct.ss(a, b, c, d,
              inputs=["u1"], outputs=["y1"], name="plant")
observer = ct.ss(f, np.hstack((g, j)), cobs, dobs,
                 inputs=["u1", "u2"], outputs=["y1", "y2"], name="observer")

sys = ct.interconnect([plant, observer],
                      connections=[["observer.u1", "plant.y1"]],
                      inplist=[["plant.u1", "observer.u2"]],
                      outlist=[["plant.y1"], ["observer.y1"], ["observer.y2"]],
                      inputs=["u1"],
                      outputs=["y1", "y2", "y3"],
                      debug=True)
```


Discussion:
Looks like a bug in the way input (and output) lists are handled.  The code is not catching the fact that a single input is going to multiple subsystems.

Fix coming in a bit.



Patch:
diff --git a/control/nlsys.py b/control/nlsys.py
index 358c4b125..a14de1d9c 100644
--- a/control/nlsys.py
+++ b/control/nlsys.py
@@ -2086,10 +2086,8 @@ def interconnect(
 
     inplist : list of input connections, optional
         List of connections for how the inputs for the overall system are
-        mapped to the subsystem inputs.  The input specification is similar to
-        the form defined in the connection specification, except that
-        connections do not specify an input-spec, since these are the system
-        inputs. The entries for a connection are thus of the form:
+        mapped to the subsystem inputs.  The entries for a connection are
+        of the form:
 
             [input-spec1, input-spec2, ...]
 
@@ -2102,11 +2100,10 @@ def interconnect(
 
     outlist : list of output connections, optional
         List of connections for how the outputs from the subsystems are
-        mapped to overall system outputs.  The output connection
-        description is the same as the form defined in the inplist
-        specification (including the optional gain term).  Numbered outputs
-        must be chosen from the list of subsystem outputs, but named
-        outputs can also be contained in the list of subsystem inputs.
+        mapped to overall system outputs.  The entries for a connection are
+        of the form:
+
+            [output-spec1, output-spec2, ...]
 
         If an output connection contains more than one signal specification,
         then those signals are added together (multiplying by the any gain
@@ -2219,7 +2216,7 @@ def interconnect(
     ...     inplist=['C'], outlist=['P'])
 
     A feedback system can also be constructed using the
-    :func:`~control.summing_block` function and the ability to
+    :func:`~control.summing_junction` function and the ability to
     automatically interconnect signals with the same names:
 
     >>> P = ct.tf(1, [1, 0], inputs='u', outputs='y')
@@ -2425,15 +2422,22 @@ def interconnect(
             elif not found_system:
                 raise ValueError("could not find signal %s" % sname)
         else:
-            # Regular signal specification
-            if not isinstance(connection, list):
-                dprint(f"  converting item to list")
-                connection = [connection]
-            for spec in connection:
-                isys, indices, gain = _parse_spec(syslist, spec, 'input')
+            if isinstance(connection, list):
+                # Passed a list => create input map
+                dprint(f"  detected input list")
+                signal_list = []
+                for spec in connection:
+                    isys, indices, gain = _parse_spec(syslist, spec, 'input')
+                    for isig in indices:
+                        signal_list.append((isys, isig, gain))
+                        dprint(f"    adding input {(isys, isig, gain)} to list")
+                new_inplist.append(signal_list)
+            else:
+                # Passed a single signal name => add individual input(s)
+                isys, indices, gain = _parse_spec(syslist, connection, 'input')
                 for isig in indices:
-                    dprint(f"  adding input {(isys, isig, gain)}")
                     new_inplist.append((isys, isig, gain))
+                    dprint(f"  adding input {(isys, isig, gain)}")
     inplist, inputs = new_inplist, new_inputs
     dprint(f"  {inplist=}\n  {inputs=}")
 
@@ -2499,17 +2503,15 @@ def interconnect(
             elif not found_system:
                 raise ValueError("could not find signal %s" % sname)
         else:
-            # Regular signal specification
-            if not isinstance(connection, list):
-                dprint(f"  converting item to list")
-                connection = [connection]
-            for spec in connection:
+            # Utility function to find named output or input signal
+            def _find_output_or_input_signal(spec):
+                signal_list = []
                 try:
                     # First trying looking in the output signals
                     osys, indices, gain = _parse_spec(syslist, spec, 'output')
                     for osig in indices:
                         dprint(f"  adding output {(osys, osig, gain)}")
-                        new_outlist.append((osys, osig, gain))
+                        signal_list.append((osys, osig, gain))
                 except ValueError:
                     # If not, see if we can find it in inputs
                     isys, indices, gain = _parse_spec(
@@ -2518,9 +2520,21 @@ def interconnect(
                     for isig in indices:
                         # Use string form to allow searching input list
                         dprint(f"  adding input {(isys, isig, gain)}")
-                        new_outlist.append(
+                        signal_list.append(
                             (syslist[isys].name,
                              syslist[isys].input_labels[isig], gain))
+                return signal_list
+                
+            if isinstance(connection, list):
+                # Passed a list => create input map
+                dprint(f"  detected output list")
+                signal_list = []
+                for spec in connection:
+                    signal_list += _find_output_or_input_signal(spec)
+                new_outlist.append(signal_list)
+            else:
+                new_outlist += _find_output_or_input_signal(connection)
+                
     outlist, outputs = new_outlist, new_outputs
     dprint(f"  {outlist=}\n  {outputs=}")
 


Test patch:
diff --git a/control/tests/interconnect_test.py b/control/tests/interconnect_test.py
index f4b0c59a8..604488ca5 100644
--- a/control/tests/interconnect_test.py
+++ b/control/tests/interconnect_test.py
@@ -689,3 +689,19 @@ def test_interconnect_params():
     timepts = np.linspace(0, 10)
     resp = ct.input_output_response(sys, timepts, 0, params={'a': -1})
     assert resp.states[0, -1].item() < 2 * math.exp(-10)
+
+
+# Bug identified in issue #1015
+def test_parallel_interconnect():
+    sys1 = ct.rss(2, 1, 1, name='S1')
+    sys2 = ct.rss(2, 1, 1, name='S2')
+
+    sys_bd = sys1 + sys2
+    sys_ic = ct.interconnect(
+        [sys1, sys2],
+        inplist=[['S1.u[0]', 'S2.u[0]']],
+        outlist=[['S1.y[0]', 'S2.y[0]']])
+    np.testing.assert_allclose(sys_bd.A, sys_ic.A)
+    np.testing.assert_allclose(sys_bd.B, sys_ic.B)
+    np.testing.assert_allclose(sys_bd.C, sys_ic.C)
+    np.testing.assert_allclose(sys_bd.D, sys_ic.D)