Skip to content

Commit 122672a

Browse files
authored
Merge pull request #517 from murrayrm/sumblk_implicit_interconnect
add summing junction + implicit signal interconnection
2 parents eb146a6 + 791f7a6 commit 122672a

File tree

5 files changed

+474
-22
lines changed

5 files changed

+474
-22
lines changed

control/iosys.py

Lines changed: 223 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,11 @@
33
# RMM, 28 April 2019
44
#
55
# Additional features to add
6-
# * Improve support for signal names, specially in operator overloads
7-
# - Figure out how to handle "nested" names (icsys.sys[1].x[1])
8-
# - Use this to implement signal names for operators?
96
# * Allow constant inputs for MIMO input_output_response (w/out ones)
107
# * Add support for constants/matrices as part of operators (1 + P)
118
# * Add unit tests (and example?) for time-varying systems
129
# * Allow time vector for discrete time simulations to be multiples of dt
1310
# * Check the way initial outputs for discrete time systems are handled
14-
# * Rename 'connections' as 'conlist' to match 'inplist' and 'outlist'?
15-
# * Allow signal summation in InterconnectedSystem diagrams (via new output?)
1611
#
1712

1813
"""The :mod:`~control.iosys` module contains the
@@ -36,14 +31,15 @@
3631
import copy
3732
from warnings import warn
3833

39-
from .statesp import StateSpace, tf2ss
34+
from .statesp import StateSpace, tf2ss, _convert_to_statespace
4035
from .timeresp import _check_convert_array, _process_time_response
4136
from .lti import isctime, isdtime, common_timebase
4237
from . import config
4338

4439
__all__ = ['InputOutputSystem', 'LinearIOSystem', 'NonlinearIOSystem',
4540
'InterconnectedSystem', 'LinearICSystem', 'input_output_response',
46-
'find_eqpt', 'linearize', 'ss2io', 'tf2io', 'interconnect']
41+
'find_eqpt', 'linearize', 'ss2io', 'tf2io', 'interconnect',
42+
'summing_junction']
4743

4844
# Define module default parameter values
4945
_iosys_defaults = {
@@ -481,9 +477,14 @@ def feedback(self, other=1, sign=-1, params={}):
481477
"""
482478
# TODO: add conversion to I/O system when needed
483479
if not isinstance(other, InputOutputSystem):
484-
raise TypeError("Feedback around I/O system must be I/O system.")
485-
486-
return new_io_sys
480+
# Try converting to a state space system
481+
try:
482+
other = _convert_to_statespace(other)
483+
except TypeError:
484+
raise TypeError(
485+
"Feedback around I/O system must be an I/O system "
486+
"or convertable to an I/O system.")
487+
other = LinearIOSystem(other)
487488

488489
# Make sure systems can be interconnected
489490
if self.noutputs != other.ninputs or other.noutputs != self.ninputs:
@@ -1846,7 +1847,7 @@ def tf2io(*args, **kwargs):
18461847

18471848

18481849
# Function to create an interconnected system
1849-
def interconnect(syslist, connections=[], inplist=[], outlist=[],
1850+
def interconnect(syslist, connections=None, inplist=[], outlist=[],
18501851
inputs=None, outputs=None, states=None,
18511852
params={}, dt=None, name=None):
18521853
"""Interconnect a set of input/output systems.
@@ -1893,8 +1894,18 @@ def interconnect(syslist, connections=[], inplist=[], outlist=[],
18931894
and the special form '-sys.sig' can be used to specify a signal with
18941895
gain -1.
18951896
1896-
If omitted, the connection map (matrix) can be specified using the
1897-
:func:`~control.InterconnectedSystem.set_connect_map` method.
1897+
If omitted, the `interconnect` function will attempt to create the
1898+
interconnection map by connecting all signals with the same base names
1899+
(ignoring the system name). Specifically, for each input signal name
1900+
in the list of systems, if that signal name corresponds to the output
1901+
signal in any of the systems, it will be connected to that input (with
1902+
a summation across all signals if the output name occurs in more than
1903+
one system).
1904+
1905+
The `connections` keyword can also be set to `False`, which will leave
1906+
the connection map empty and it can be specified instead using the
1907+
low-level :func:`~control.InterconnectedSystem.set_connect_map`
1908+
method.
18981909
18991910
inplist : list of input connections, optional
19001911
List of connections for how the inputs for the overall system are
@@ -1966,24 +1977,33 @@ def interconnect(syslist, connections=[], inplist=[], outlist=[],
19661977
Example
19671978
-------
19681979
>>> P = control.LinearIOSystem(
1969-
>>> ct.rss(2, 2, 2, strictly_proper=True), name='P')
1980+
>>> control.rss(2, 2, 2, strictly_proper=True), name='P')
19701981
>>> C = control.LinearIOSystem(control.rss(2, 2, 2), name='C')
1971-
>>> S = control.InterconnectedSystem(
1982+
>>> T = control.interconnect(
19721983
>>> [P, C],
19731984
>>> connections = [
1974-
>>> ['P.u[0]', 'C.y[0]'], ['P.u[1]', 'C.y[0]'],
1985+
>>> ['P.u[0]', 'C.y[0]'], ['P.u[1]', 'C.y[1]'],
19751986
>>> ['C.u[0]', '-P.y[0]'], ['C.u[1]', '-P.y[1]']],
19761987
>>> inplist = ['C.u[0]', 'C.u[1]'],
19771988
>>> outlist = ['P.y[0]', 'P.y[1]'],
19781989
>>> )
19791990
1991+
For a SISO system, this example can be simplified by using the
1992+
:func:`~control.summing_block` function and the ability to automatically
1993+
interconnect signals with the same names:
1994+
1995+
>>> P = control.tf2io(control.tf(1, [1, 0]), inputs='u', outputs='y')
1996+
>>> C = control.tf2io(control.tf(10, [1, 1]), inputs='e', outputs='u')
1997+
>>> sumblk = control.summing_junction(inputs=['r', '-y'], output='e')
1998+
>>> T = control.interconnect([P, C, sumblk], inplist='r', outlist='y')
1999+
19802000
Notes
19812001
-----
19822002
If a system is duplicated in the list of systems to be connected,
19832003
a warning is generated a copy of the system is created with the
19842004
name of the new system determined by adding the prefix and suffix
19852005
strings in config.defaults['iosys.linearized_system_name_prefix']
1986-
and config.defaults['iosys.linearized_system_name_suffix'], with the
2006+
and config.defaults['iosys.linearized_system_name_suffix'], with the
19872007
default being to add the suffix '$copy'$ to the system name.
19882008
19892009
It is possible to replace lists in most of arguments with tuples instead,
@@ -2001,6 +2021,78 @@ def interconnect(syslist, connections=[], inplist=[], outlist=[],
20012021
:class:`~control.InputOutputSystem`.
20022022
20032023
"""
2024+
# If connections was not specified, set up default connection list
2025+
if connections is None:
2026+
# For each system input, look for outputs with the same name
2027+
connections = []
2028+
for input_sys in syslist:
2029+
for input_name in input_sys.input_index.keys():
2030+
connect = [input_sys.name + "." + input_name]
2031+
for output_sys in syslist:
2032+
if input_name in output_sys.output_index.keys():
2033+
connect.append(output_sys.name + "." + input_name)
2034+
if len(connect) > 1:
2035+
connections.append(connect)
2036+
elif connections is False:
2037+
# Use an empty connections list
2038+
connections = []
2039+
2040+
# Process input list
2041+
if not isinstance(inplist, (list, tuple)):
2042+
inplist = [inplist]
2043+
new_inplist = []
2044+
for signal in inplist:
2045+
# Check for signal names without a system name
2046+
if isinstance(signal, str) and len(signal.split('.')) == 1:
2047+
# Get the signal name
2048+
name = signal[1:] if signal[0] == '-' else signal
2049+
sign = '-' if signal[0] == '-' else ""
2050+
2051+
# Look for the signal name as a system input
2052+
new_name = None
2053+
for sys in syslist:
2054+
if name in sys.input_index.keys():
2055+
if new_name is not None:
2056+
raise ValueError("signal %s is not unique" % name)
2057+
new_name = sign + sys.name + "." + name
2058+
2059+
# Make sure we found the name
2060+
if new_name is None:
2061+
raise ValueError("could not find signal %s" % name)
2062+
else:
2063+
new_inplist.append(new_name)
2064+
else:
2065+
new_inplist.append(signal)
2066+
inplist = new_inplist
2067+
2068+
# Process output list
2069+
if not isinstance(outlist, (list, tuple)):
2070+
outlist = [outlist]
2071+
new_outlist = []
2072+
for signal in outlist:
2073+
# Check for signal names without a system name
2074+
if isinstance(signal, str) and len(signal.split('.')) == 1:
2075+
# Get the signal name
2076+
name = signal[1:] if signal[0] == '-' else signal
2077+
sign = '-' if signal[0] == '-' else ""
2078+
2079+
# Look for the signal name as a system output
2080+
new_name = None
2081+
for sys in syslist:
2082+
if name in sys.output_index.keys():
2083+
if new_name is not None:
2084+
raise ValueError("signal %s is not unique" % name)
2085+
new_name = sign + sys.name + "." + name
2086+
2087+
# Make sure we found the name
2088+
if new_name is None:
2089+
raise ValueError("could not find signal %s" % name)
2090+
else:
2091+
new_outlist.append(new_name)
2092+
else:
2093+
new_outlist.append(signal)
2094+
outlist = new_outlist
2095+
20042096
newsys = InterconnectedSystem(
20052097
syslist, connections=connections, inplist=inplist, outlist=outlist,
20062098
inputs=inputs, outputs=outputs, states=states,
@@ -2011,3 +2103,117 @@ def interconnect(syslist, connections=[], inplist=[], outlist=[],
20112103
return LinearICSystem(newsys, None)
20122104

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

control/statesp.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,15 +223,15 @@ def __init__(self, *args, **kwargs):
223223
224224
The default constructor is StateSpace(A, B, C, D), where A, B, C, D
225225
are matrices or equivalent objects. To create a discrete time system,
226-
use StateSpace(A, B, C, D, dt) where 'dt' is the sampling time (or
226+
use StateSpace(A, B, C, D, dt) where `dt` is the sampling time (or
227227
True for unspecified sampling time). To call the copy constructor,
228228
call StateSpace(sys), where sys is a StateSpace object.
229229
230230
The `remove_useless_states` keyword can be used to scan the A, B, and
231231
C matrices for rows or columns of zeros. If the zeros are such that a
232232
particular state has no effect on the input-output dynamics, then that
233233
state is removed from the A, B, and C matrices. If not specified, the
234-
value is read from `config.defaults['statesp.remove_useless_states']
234+
value is read from `config.defaults['statesp.remove_useless_states']`
235235
(default = False).
236236
237237
"""

0 commit comments

Comments
 (0)