Skip to content

Commit 012d1d0

Browse files
committed
add summation_block + implicit signal interconnection
1 parent 1502d38 commit 012d1d0

2 files changed

Lines changed: 335 additions & 9 deletions

File tree

control/iosys.py

Lines changed: 204 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,15 @@
3636
import copy
3737
from warnings import warn
3838

39-
from .statesp import StateSpace, tf2ss
39+
from .statesp import StateSpace, tf2ss, _convert_to_statespace
4040
from .timeresp import _check_convert_array, _process_time_response
4141
from .lti import isctime, isdtime, common_timebase
4242
from . import config
4343

4444
__all__ = ['InputOutputSystem', 'LinearIOSystem', 'NonlinearIOSystem',
4545
'InterconnectedSystem', 'LinearICSystem', 'input_output_response',
46-
'find_eqpt', 'linearize', 'ss2io', 'tf2io', 'interconnect']
46+
'find_eqpt', 'linearize', 'ss2io', 'tf2io', 'interconnect',
47+
'summation_block']
4748

4849
# Define module default parameter values
4950
_iosys_defaults = {
@@ -481,9 +482,14 @@ def feedback(self, other=1, sign=-1, params={}):
481482
"""
482483
# TODO: add conversion to I/O system when needed
483484
if not isinstance(other, InputOutputSystem):
484-
raise TypeError("Feedback around I/O system must be I/O system.")
485-
486-
return new_io_sys
485+
# Try converting to a state space system
486+
try:
487+
other = _convert_to_statespace(other)
488+
except TypeError:
489+
raise TypeError(
490+
"Feedback around I/O system must be an I/O system "
491+
"or convertable to an I/O system.")
492+
other = LinearIOSystem(other)
487493

488494
# Make sure systems can be interconnected
489495
if self.noutputs != other.ninputs or other.noutputs != self.ninputs:
@@ -1846,7 +1852,7 @@ def tf2io(*args, **kwargs):
18461852

18471853

18481854
# Function to create an interconnected system
1849-
def interconnect(syslist, connections=[], inplist=[], outlist=[],
1855+
def interconnect(syslist, connections=None, inplist=[], outlist=[],
18501856
inputs=None, outputs=None, states=None,
18511857
params={}, dt=None, name=None):
18521858
"""Interconnect a set of input/output systems.
@@ -1893,8 +1899,18 @@ def interconnect(syslist, connections=[], inplist=[], outlist=[],
18931899
and the special form '-sys.sig' can be used to specify a signal with
18941900
gain -1.
18951901
1896-
If omitted, the connection map (matrix) can be specified using the
1897-
:func:`~control.InterconnectedSystem.set_connect_map` method.
1902+
If omitted, all the `interconnect` function will attempt to create the
1903+
interconneciton map by connecting all signals with the same base names
1904+
(ignoring the system name). Specifically, for each input signal name
1905+
in the list of systems, if that signal name corresponds to the output
1906+
signal in any of the systems, it will be connected to that input (with
1907+
a summation across all signals if the output name occurs in more than
1908+
one system).
1909+
1910+
The `connections` keyword can also be set to `False`, which will leave
1911+
the connection map empty and it can be specified instead using the
1912+
low-level :func:`~control.InterconnectedSystem.set_connect_map`
1913+
method.
18981914
18991915
inplist : list of input connections, optional
19001916
List of connections for how the inputs for the overall system are
@@ -1983,7 +1999,7 @@ def interconnect(syslist, connections=[], inplist=[], outlist=[],
19831999
a warning is generated a copy of the system is created with the
19842000
name of the new system determined by adding the prefix and suffix
19852001
strings in config.defaults['iosys.linearized_system_name_prefix']
1986-
and config.defaults['iosys.linearized_system_name_suffix'], with the
2002+
and config.defaults['iosys.linearized_system_name_suffix'], with the
19872003
default being to add the suffix '$copy'$ to the system name.
19882004
19892005
It is possible to replace lists in most of arguments with tuples instead,
@@ -2001,6 +2017,78 @@ def interconnect(syslist, connections=[], inplist=[], outlist=[],
20012017
:class:`~control.InputOutputSystem`.
20022018
20032019
"""
2020+
# If connections was not specified, set up default connection list
2021+
if connections is None:
2022+
# For each system input, look for outputs with the same name
2023+
connections = []
2024+
for input_sys in syslist:
2025+
for input_name in input_sys.input_index.keys():
2026+
connect = [input_sys.name + "." + input_name]
2027+
for output_sys in syslist:
2028+
if input_name in output_sys.output_index.keys():
2029+
connect.append(output_sys.name + "." + input_name)
2030+
if len(connect) > 1:
2031+
connections.append(connect)
2032+
elif connections is False:
2033+
# Use an empty connections list
2034+
connections = []
2035+
2036+
# Process input list
2037+
if not isinstance(inplist, (list, tuple)):
2038+
inplist = [inplist]
2039+
new_inplist = []
2040+
for signal in inplist:
2041+
# Check for signal names without a system name
2042+
if isinstance(signal, str) and len(signal.split('.')) == 1:
2043+
# Get the signal name
2044+
name = signal[1:] if signal[0] == '-' else signal
2045+
sign = '-' if signal[0] == '-' else ""
2046+
2047+
# Look for the signal name as a system input
2048+
new_name = None
2049+
for sys in syslist:
2050+
if name in sys.input_index.keys():
2051+
if new_name is not None:
2052+
raise ValueError("signal %s is not unique" % name)
2053+
new_name = sign + sys.name + "." + name
2054+
2055+
# Make sure we found the name
2056+
if new_name is None:
2057+
raise ValueError("could not find signal %s" % name)
2058+
else:
2059+
new_inplist.append(new_name)
2060+
else:
2061+
new_inplist.append(signal)
2062+
inplist = new_inplist
2063+
2064+
# Process output list
2065+
if not isinstance(outlist, (list, tuple)):
2066+
outlist = [outlist]
2067+
new_outlist = []
2068+
for signal in outlist:
2069+
# Check for signal names without a system name
2070+
if isinstance(signal, str) and len(signal.split('.')) == 1:
2071+
# Get the signal name
2072+
name = signal[1:] if signal[0] == '-' else signal
2073+
sign = '-' if signal[0] == '-' else ""
2074+
2075+
# Look for the signal name as a system output
2076+
new_name = None
2077+
for sys in syslist:
2078+
if name in sys.output_index.keys():
2079+
if new_name is not None:
2080+
raise ValueError("signal %s is not unique" % name)
2081+
new_name = sign + sys.name + "." + name
2082+
2083+
# Make sure we found the name
2084+
if new_name is None:
2085+
raise ValueError("could not find signal %s" % name)
2086+
else:
2087+
new_outlist.append(new_name)
2088+
else:
2089+
new_outlist.append(signal)
2090+
outlist = new_outlist
2091+
20042092
newsys = InterconnectedSystem(
20052093
syslist, connections=connections, inplist=inplist, outlist=outlist,
20062094
inputs=inputs, outputs=outputs, states=states,
@@ -2011,3 +2099,110 @@ def interconnect(syslist, connections=[], inplist=[], outlist=[],
20112099
return LinearICSystem(newsys, None)
20122100

20132101
return newsys
2102+
2103+
2104+
# Summation block
2105+
def summation_block(inputs, output='y', dimension=None, name=None, prefix='u'):
2106+
"""Create a summation block as an input/output system.
2107+
2108+
This function creates a static input/output system that outputs the sum of
2109+
the inputs, potentially with a change in sign for each individual input.
2110+
The input/output system that is created by this function can be used as a
2111+
component in the :func:`~control.interconnect` function.
2112+
2113+
Parameters
2114+
----------
2115+
inputs : int, string or list of strings
2116+
Description of the inputs to the summation block. This can be given
2117+
as an integer count, a string, or a list of strings. If an integer
2118+
count is specified, the names of the input signals will be of the form
2119+
`u[i]`.
2120+
output : string, optional
2121+
Name of the system output. If not specified, the output will be 'y'.
2122+
dimension : int, optional
2123+
The dimension of the summing block. If the dimension is set to a
2124+
positive integer, a multi-input, multi-output summation block will be
2125+
created. The input and output signal names will be of the form
2126+
`<signal>[i]` where `signal` is the input/output signal name specified
2127+
by the `inputs` and `output` keywords. Default value is `None`.
2128+
name : string, optional
2129+
System name (used for specifying signals). If unspecified, a generic
2130+
name <sys[id]> is generated with a unique integer id.
2131+
prefix : string, optional
2132+
If `inputs` is an integer, create the names of the states using the
2133+
given prefix (default = 'u'). The names of the input will be of the
2134+
form `prefix[i]`.
2135+
2136+
Returns
2137+
-------
2138+
sys : static LinearIOSystem
2139+
Linear input/output system object with no states and only a direct
2140+
term that implements the summation block.
2141+
2142+
"""
2143+
# Utility function to parse input and output signal lists
2144+
def _parse_list(signals, signame='input', prefix='u'):
2145+
# Parse signals, including gains
2146+
if isinstance(signals, int):
2147+
nsignals = signals
2148+
names = ["%s[%d]" % (prefix, i) for i in range(nsignals)]
2149+
gains = np.ones((nsignals,))
2150+
elif isinstance(signals, str):
2151+
nsignals = 1
2152+
gains = [-1 if signals[0] == '-' else 1]
2153+
names = [signals[1:] if signals[0] == '-' else signals]
2154+
elif isinstance(signals, list) and \
2155+
all([isinstance(x, str) for x in signals]):
2156+
nsignals = len(signals)
2157+
gains = np.ones((nsignals,))
2158+
names = []
2159+
for i in range(nsignals):
2160+
if signals[i][0] == '-':
2161+
gains[i] = -1
2162+
names.append(signals[i][1:])
2163+
else:
2164+
names.append(signals[i])
2165+
else:
2166+
raise ValueError(
2167+
"could not parse %s description '%s'"
2168+
% (signame, str(signals)))
2169+
2170+
# Return the parsed list
2171+
return nsignals, names, gains
2172+
2173+
# Read the input list
2174+
ninputs, input_names, input_gains = _parse_list(
2175+
inputs, signame="input", prefix=prefix)
2176+
noutputs, output_names, output_gains = _parse_list(
2177+
output, signame="output", prefix='y')
2178+
if noutputs > 1:
2179+
raise NotImplementedError("vector outputs not yet supported")
2180+
2181+
# If the dimension keyword is present, vectorize inputs and outputs
2182+
if isinstance(dimension, int) and dimension >= 1:
2183+
# Create a new list of input/output names and update parameters
2184+
input_names = ["%s[%d]" % (name, dim)
2185+
for name in input_names
2186+
for dim in range(dimension)]
2187+
ninputs = ninputs * dimension
2188+
2189+
output_names = ["%s[%d]" % (name, dim)
2190+
for name in output_names
2191+
for dim in range(dimension)]
2192+
noutputs = noutputs * dimension
2193+
elif dimension is not None:
2194+
raise ValueError(
2195+
"unrecognized dimension value '%s'" % str(dimension))
2196+
else:
2197+
dimension = 1
2198+
2199+
# Create the direct term
2200+
D = np.kron(input_gains * output_gains[0], np.eye(dimension))
2201+
2202+
# Create a linear system of the appropriate size
2203+
ss_sys = StateSpace(
2204+
np.zeros((0, 0)), np.ones((0, ninputs)), np.ones((noutputs, 0)), D)
2205+
2206+
# Create a LinearIOSystem
2207+
return LinearIOSystem(
2208+
ss_sys, inputs=input_names, outputs=output_names, name=name)

control/tests/interconnect_test.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"""interconnect_test.py - test input/output interconnect function
2+
3+
RMM, 22 Jan 2021
4+
5+
This set of unit tests covers the various operatons of the interconnect()
6+
function, as well as some of the support functions associated with
7+
interconnect().
8+
9+
Note: additional tests are available in iosys_test.py, which focuses on the
10+
raw InterconnectedSystem constructor. This set of unit tests focuses on
11+
functionality implemented in the interconnect() function itself.
12+
13+
"""
14+
15+
import pytest
16+
17+
import numpy as np
18+
import scipy as sp
19+
20+
import control as ct
21+
22+
@pytest.mark.parametrize("inputs, output, dimension, D", [
23+
[1, 1, None, [[1]] ],
24+
['u', 'y', None, [[1]] ],
25+
[['u'], ['y'], None, [[1]] ],
26+
[2, 1, None, [[1, 1]] ],
27+
[['r', '-y'], ['e'], None, [[1, -1]] ],
28+
[5, 1, None, np.ones((1, 5)) ],
29+
['u', 'y', 1, [[1]] ],
30+
['u', 'y', 2, [[1, 0], [0, 1]] ],
31+
[['r', '-y'], ['e'], 2, [[1, 0, -1, 0], [0, 1, 0, -1]] ],
32+
])
33+
def test_summation_block(inputs, output, dimension, D):
34+
ninputs = 1 if isinstance(inputs, str) else \
35+
inputs if isinstance(inputs, int) else len(inputs)
36+
sum = ct.summation_block(
37+
inputs=inputs, output=output, dimension=dimension)
38+
dim = 1 if dimension is None else dimension
39+
np.testing.assert_array_equal(sum.A, np.ndarray((0, 0)))
40+
np.testing.assert_array_equal(sum.B, np.ndarray((0, ninputs*dim)))
41+
np.testing.assert_array_equal(sum.C, np.ndarray((dim, 0)))
42+
np.testing.assert_array_equal(sum.D, D)
43+
44+
45+
def test_summation_exceptions():
46+
# Bad input description
47+
with pytest.raises(ValueError, match="could not parse input"):
48+
sumblk = ct.summation_block(None, 'y')
49+
50+
# Bad output description
51+
with pytest.raises(ValueError, match="could not parse output"):
52+
sumblk = ct.summation_block('u', None)
53+
54+
# Bad input dimension
55+
with pytest.raises(ValueError, match="unrecognized dimension"):
56+
sumblk = ct.summation_block('u', 'y', dimension=False)
57+
58+
59+
def test_interconnect_implicit():
60+
"""Test the use of implicit connections in interconnect()"""
61+
import random
62+
63+
# System definition
64+
P = ct.ss2io(
65+
ct.rss(2, 1, 1, strictly_proper=True),
66+
inputs='u', outputs='y', name='P')
67+
kp = ct.tf(random.uniform(1, 10), [1])
68+
ki = ct.tf(random.uniform(1, 10), [1, 0])
69+
C = ct.tf2io(kp + ki, inputs='e', outputs='u', name='C')
70+
71+
# Block diagram computation
72+
Tss = ct.feedback(P * C, 1)
73+
74+
# Construct the interconnection explicitly
75+
Tio_exp = ct.interconnect(
76+
(C, P),
77+
connections = [['P.u', 'C.u'], ['C.e', '-P.y']],
78+
inplist='C.e', outlist='P.y')
79+
80+
# Compare to bdalg computation
81+
np.testing.assert_almost_equal(Tio_exp.A, Tss.A)
82+
np.testing.assert_almost_equal(Tio_exp.B, Tss.B)
83+
np.testing.assert_almost_equal(Tio_exp.C, Tss.C)
84+
np.testing.assert_almost_equal(Tio_exp.D, Tss.D)
85+
86+
# Construct the interconnection via a summation block
87+
sumblk = ct.summation_block(inputs=['r', '-y'], output='e', name="sum")
88+
Tio_sum = ct.interconnect(
89+
(C, P, sumblk), inplist=['r'], outlist=['y'])
90+
91+
np.testing.assert_almost_equal(Tio_sum.A, Tss.A)
92+
np.testing.assert_almost_equal(Tio_sum.B, Tss.B)
93+
np.testing.assert_almost_equal(Tio_sum.C, Tss.C)
94+
np.testing.assert_almost_equal(Tio_sum.D, Tss.D)
95+
96+
# Setting connections to False should lead to an empty connection map
97+
empty = ct.interconnect(
98+
(C, P, sumblk), connections=False, inplist=['r'], outlist=['y'])
99+
np.testing.assert_array_equal(empty.connect_map, np.zeros((4, 3)))
100+
101+
# Implicit summation across repeated signals
102+
kp_io = ct.tf2io(kp, inputs='e', outputs='u', name='kp')
103+
ki_io = ct.tf2io(ki, inputs='e', outputs='u', name='ki')
104+
Tio_sum = ct.interconnect(
105+
(kp_io, ki_io, P, sumblk), inplist=['r'], outlist=['y'])
106+
np.testing.assert_almost_equal(Tio_sum.A, Tss.A)
107+
np.testing.assert_almost_equal(Tio_sum.B, Tss.B)
108+
np.testing.assert_almost_equal(Tio_sum.C, Tss.C)
109+
np.testing.assert_almost_equal(Tio_sum.D, Tss.D)
110+
111+
# Make sure that repeated inplist/outlist names generate an error
112+
# Input not unique
113+
Cbad = ct.tf2io(ct.tf(10, [1, 1]), inputs='r', outputs='x', name='C')
114+
with pytest.raises(ValueError, match="not unique"):
115+
Tio_sum = ct.interconnect(
116+
(Cbad, P, sumblk), inplist=['r'], outlist=['y'])
117+
118+
# Output not unique
119+
Cbad = ct.tf2io(ct.tf(10, [1, 1]), inputs='e', outputs='y', name='C')
120+
with pytest.raises(ValueError, match="not unique"):
121+
Tio_sum = ct.interconnect(
122+
(Cbad, P, sumblk), inplist=['r'], outlist=['y'])
123+
124+
# Signal not found
125+
with pytest.raises(ValueError, match="could not find"):
126+
Tio_sum = ct.interconnect(
127+
(C, P, sumblk), inplist=['x'], outlist=['y'])
128+
129+
with pytest.raises(ValueError, match="could not find"):
130+
Tio_sum = ct.interconnect(
131+
(C, P, sumblk), inplist=['r'], outlist=['x'])

0 commit comments

Comments
 (0)