Skip to content

Commit 2c132c9

Browse files
Merge pull request #536 from murrayrm/iosys_updates
I/O system updates
2 parents cc67807 + 2538a06 commit 2c132c9

File tree

2 files changed

+123
-36
lines changed

2 files changed

+123
-36
lines changed

control/iosys.py

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,7 @@ class LinearIOSystem(InputOutputSystem, StateSpace):
616616
617617
"""
618618
def __init__(self, linsys, inputs=None, outputs=None, states=None,
619-
name=None):
619+
name=None, **kwargs):
620620
"""Create an I/O system from a state space linear system.
621621
622622
Converts a :class:`~control.StateSpace` system into an
@@ -662,6 +662,10 @@ def __init__(self, linsys, inputs=None, outputs=None, states=None,
662662
if not isinstance(linsys, StateSpace):
663663
raise TypeError("Linear I/O system must be a state space object")
664664

665+
# Look for 'input' and 'output' parameter name variants
666+
inputs = _parse_signal_parameter(inputs, 'input', kwargs)
667+
outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True)
668+
665669
# Create the I/O system object
666670
super(LinearIOSystem, self).__init__(
667671
inputs=linsys.ninputs, outputs=linsys.noutputs,
@@ -711,8 +715,7 @@ class NonlinearIOSystem(InputOutputSystem):
711715
712716
"""
713717
def __init__(self, updfcn, outfcn=None, inputs=None, outputs=None,
714-
states=None, params={},
715-
name=None, **kwargs):
718+
states=None, params={}, name=None, **kwargs):
716719
"""Create a nonlinear I/O system given update and output functions.
717720
718721
Creates an :class:`~control.InputOutputSystem` for a nonlinear system
@@ -779,17 +782,25 @@ def __init__(self, updfcn, outfcn=None, inputs=None, outputs=None,
779782
Nonlinear system represented as an input/output system.
780783
781784
"""
785+
# Look for 'input' and 'output' parameter name variants
786+
inputs = _parse_signal_parameter(inputs, 'input', kwargs)
787+
outputs = _parse_signal_parameter(outputs, 'output', kwargs)
788+
782789
# Store the update and output functions
783790
self.updfcn = updfcn
784791
self.outfcn = outfcn
785792

786793
# Initialize the rest of the structure
787-
dt = kwargs.get('dt', config.defaults['control.default_dt'])
794+
dt = kwargs.pop('dt', config.defaults['control.default_dt'])
788795
super(NonlinearIOSystem, self).__init__(
789796
inputs=inputs, outputs=outputs, states=states,
790797
params=params, dt=dt, name=name
791798
)
792799

800+
# Make sure all input arguments got parsed
801+
if kwargs:
802+
raise TypeError("unknown parameters %s" % kwargs)
803+
793804
# Check to make sure arguments are consistent
794805
if updfcn is None:
795806
if self.nstates is None:
@@ -874,7 +885,7 @@ class InterconnectedSystem(InputOutputSystem):
874885
"""
875886
def __init__(self, syslist, connections=[], inplist=[], outlist=[],
876887
inputs=None, outputs=None, states=None,
877-
params={}, dt=None, name=None):
888+
params={}, dt=None, name=None, **kwargs):
878889
"""Create an I/O system from a list of systems + connection info.
879890
880891
The InterconnectedSystem class is used to represent an input/output
@@ -886,6 +897,10 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[],
886897
See :func:`~control.interconnect` for a list of parameters.
887898
888899
"""
900+
# Look for 'input' and 'output' parameter name variants
901+
inputs = _parse_signal_parameter(inputs, 'input', kwargs)
902+
outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True)
903+
889904
# Convert input and output names to lists if they aren't already
890905
if not isinstance(inplist, (list, tuple)):
891906
inplist = [inplist]
@@ -1850,6 +1865,15 @@ def linearize(sys, xeq, ueq=[], t=0, params={}, **kw):
18501865
return sys.linearize(xeq, ueq, t=t, params=params, **kw)
18511866

18521867

1868+
# Utility function to parse a signal parameter
1869+
def _parse_signal_parameter(value, name, kwargs, end=False):
1870+
if value is None and name in kwargs:
1871+
value = list(kwargs.pop(name))
1872+
if end and kwargs:
1873+
raise TypeError("unknown parameters %s" % kwargs)
1874+
return value
1875+
1876+
18531877
def _find_size(sysval, vecval):
18541878
"""Utility function to find the size of a system parameter
18551879
@@ -1889,7 +1913,7 @@ def tf2io(*args, **kwargs):
18891913
# Function to create an interconnected system
18901914
def interconnect(syslist, connections=None, inplist=[], outlist=[],
18911915
inputs=None, outputs=None, states=None,
1892-
params={}, dt=None, name=None):
1916+
params={}, dt=None, name=None, **kwargs):
18931917
"""Interconnect a set of input/output systems.
18941918
18951919
This function creates a new system that is an interconnection of a set of
@@ -2035,7 +2059,7 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[],
20352059
>>> P = control.tf2io(control.tf(1, [1, 0]), inputs='u', outputs='y')
20362060
>>> C = control.tf2io(control.tf(10, [1, 1]), inputs='e', outputs='u')
20372061
>>> sumblk = control.summing_junction(inputs=['r', '-y'], output='e')
2038-
>>> T = control.interconnect([P, C, sumblk], inplist='r', outlist='y')
2062+
>>> T = control.interconnect([P, C, sumblk], input='r', output='y')
20392063
20402064
Notes
20412065
-----
@@ -2060,7 +2084,14 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[],
20602084
treated as both a :class:`~control.StateSpace` system as well as an
20612085
:class:`~control.InputOutputSystem`.
20622086
2087+
The `input` and `output` keywords can be used instead of `inputs` and
2088+
`outputs`, for more natural naming of SISO systems.
2089+
20632090
"""
2091+
# Look for 'input' and 'output' parameter name variants
2092+
inputs = _parse_signal_parameter(inputs, 'input', kwargs)
2093+
outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True)
2094+
20642095
# If connections was not specified, set up default connection list
20652096
if connections is None:
20662097
# For each system input, look for outputs with the same name
@@ -2077,30 +2108,36 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[],
20772108
# Use an empty connections list
20782109
connections = []
20792110

2111+
# If inplist/outlist is not present, try using inputs/outputs instead
2112+
if not inplist and inputs is not None:
2113+
inplist = list(inputs)
2114+
if not outlist and outputs is not None:
2115+
outlist = list(outputs)
2116+
20802117
# Process input list
20812118
if not isinstance(inplist, (list, tuple)):
20822119
inplist = [inplist]
20832120
new_inplist = []
20842121
for signal in inplist:
2122+
# Create an empty connection and append to inplist
2123+
connection = []
2124+
20852125
# Check for signal names without a system name
20862126
if isinstance(signal, str) and len(signal.split('.')) == 1:
20872127
# Get the signal name
20882128
name = signal[1:] if signal[0] == '-' else signal
20892129
sign = '-' if signal[0] == '-' else ""
20902130

20912131
# Look for the signal name as a system input
2092-
new_name = None
20932132
for sys in syslist:
20942133
if name in sys.input_index.keys():
2095-
if new_name is not None:
2096-
raise ValueError("signal %s is not unique" % name)
2097-
new_name = sign + sys.name + "." + name
2134+
connection.append(sign + sys.name + "." + name)
20982135

20992136
# Make sure we found the name
2100-
if new_name is None:
2137+
if len(connection) == 0:
21012138
raise ValueError("could not find signal %s" % name)
21022139
else:
2103-
new_inplist.append(new_name)
2140+
new_inplist.append(connection)
21042141
else:
21052142
new_inplist.append(signal)
21062143
inplist = new_inplist
@@ -2110,25 +2147,25 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[],
21102147
outlist = [outlist]
21112148
new_outlist = []
21122149
for signal in outlist:
2150+
# Create an empty connection and append to inplist
2151+
connection = []
2152+
21132153
# Check for signal names without a system name
21142154
if isinstance(signal, str) and len(signal.split('.')) == 1:
21152155
# Get the signal name
21162156
name = signal[1:] if signal[0] == '-' else signal
21172157
sign = '-' if signal[0] == '-' else ""
21182158

21192159
# Look for the signal name as a system output
2120-
new_name = None
21212160
for sys in syslist:
21222161
if name in sys.output_index.keys():
2123-
if new_name is not None:
2124-
raise ValueError("signal %s is not unique" % name)
2125-
new_name = sign + sys.name + "." + name
2162+
connection.append(sign + sys.name + "." + name)
21262163

21272164
# Make sure we found the name
2128-
if new_name is None:
2165+
if len(connection) == 0:
21292166
raise ValueError("could not find signal %s" % name)
21302167
else:
2131-
new_outlist.append(new_name)
2168+
new_outlist.append(connection)
21322169
else:
21332170
new_outlist.append(signal)
21342171
outlist = new_outlist
@@ -2146,7 +2183,9 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[],
21462183

21472184

21482185
# Summing junction
2149-
def summing_junction(inputs, output='y', dimension=None, name=None, prefix='u'):
2186+
def summing_junction(
2187+
inputs=None, output=None, dimension=None, name=None,
2188+
prefix='u', **kwargs):
21502189
"""Create a summing junction as an input/output system.
21512190
21522191
This function creates a static input/output system that outputs the sum of
@@ -2185,10 +2224,10 @@ def summing_junction(inputs, output='y', dimension=None, name=None, prefix='u'):
21852224
21862225
Example
21872226
-------
2188-
>>> P = control.tf2io(ct.tf(1, [1, 0]), inputs='u', outputs='y')
2189-
>>> C = control.tf2io(ct.tf(10, [1, 1]), inputs='e', outputs='u')
2227+
>>> P = control.tf2io(ct.tf(1, [1, 0]), input='u', output='y')
2228+
>>> C = control.tf2io(ct.tf(10, [1, 1]), input='e', output='u')
21902229
>>> sumblk = control.summing_junction(inputs=['r', '-y'], output='e')
2191-
>>> T = control.interconnect((P, C, sumblk), inplist='r', outlist='y')
2230+
>>> T = control.interconnect((P, C, sumblk), input='r', output='y')
21922231
21932232
"""
21942233
# Utility function to parse input and output signal lists
@@ -2221,6 +2260,16 @@ def _parse_list(signals, signame='input', prefix='u'):
22212260
# Return the parsed list
22222261
return nsignals, names, gains
22232262

2263+
# Look for 'input' and 'output' parameter name variants
2264+
inputs = _parse_signal_parameter(inputs, 'input', kwargs)
2265+
output = _parse_signal_parameter(output, 'outputs', kwargs, end=True)
2266+
2267+
# Default values for inputs and output
2268+
if inputs is None:
2269+
raise TypeError("input specification is required")
2270+
if output is None:
2271+
output = 'y'
2272+
22242273
# Read the input list
22252274
ninputs, input_names, input_gains = _parse_list(
22262275
inputs, signame="input", prefix=prefix)

control/tests/interconnect_test.py

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ def test_summing_junction(inputs, output, dimension, D):
4545
def test_summation_exceptions():
4646
# Bad input description
4747
with pytest.raises(ValueError, match="could not parse input"):
48-
sumblk = ct.summing_junction(None, 'y')
48+
sumblk = ct.summing_junction(np.pi, 'y')
4949

5050
# Bad output description
5151
with pytest.raises(ValueError, match="could not parse output"):
52-
sumblk = ct.summing_junction('u', None)
52+
sumblk = ct.summing_junction('u', np.pi)
5353

5454
# Bad input dimension
5555
with pytest.raises(ValueError, match="unrecognized dimension"):
@@ -119,18 +119,22 @@ def test_interconnect_implicit():
119119
# inputs=['r', '-y'], output='e', dimension=2)
120120
# S = control.interconnect([P, C, sumblk], inplist='r', outlist='y')
121121

122-
# Make sure that repeated inplist/outlist names generate an error
123-
# Input not unique
124-
Cbad = ct.tf2io(ct.tf(10, [1, 1]), inputs='r', outputs='x', name='C')
125-
with pytest.raises(ValueError, match="not unique"):
126-
Tio_sum = ct.interconnect(
127-
(Cbad, P, sumblk), inplist=['r'], outlist=['y'])
122+
# Make sure that repeated inplist/outlist names work
123+
pi_io = ct.interconnect(
124+
(kp_io, ki_io), inplist=['e'], outlist=['u'])
125+
pi_ss = ct.tf2ss(kp + ki)
126+
np.testing.assert_almost_equal(pi_io.A, pi_ss.A)
127+
np.testing.assert_almost_equal(pi_io.B, pi_ss.B)
128+
np.testing.assert_almost_equal(pi_io.C, pi_ss.C)
129+
np.testing.assert_almost_equal(pi_io.D, pi_ss.D)
128130

129-
# Output not unique
130-
Cbad = ct.tf2io(ct.tf(10, [1, 1]), inputs='e', outputs='y', name='C')
131-
with pytest.raises(ValueError, match="not unique"):
132-
Tio_sum = ct.interconnect(
133-
(Cbad, P, sumblk), inplist=['r'], outlist=['y'])
131+
# Default input and output lists, along with singular versions
132+
Tio_sum = ct.interconnect(
133+
(kp_io, ki_io, P, sumblk), input='r', output='y')
134+
np.testing.assert_almost_equal(Tio_sum.A, Tss.A)
135+
np.testing.assert_almost_equal(Tio_sum.B, Tss.B)
136+
np.testing.assert_almost_equal(Tio_sum.C, Tss.C)
137+
np.testing.assert_almost_equal(Tio_sum.D, Tss.D)
134138

135139
# Signal not found
136140
with pytest.raises(ValueError, match="could not find"):
@@ -172,3 +176,37 @@ def test_interconnect_docstring():
172176
np.testing.assert_almost_equal(T.B, T_ss.B)
173177
np.testing.assert_almost_equal(T.C, T_ss.C)
174178
np.testing.assert_almost_equal(T.D, T_ss.D)
179+
180+
181+
def test_interconnect_exceptions():
182+
# First make sure the docstring example works
183+
P = ct.tf2io(ct.tf(1, [1, 0]), input='u', output='y')
184+
C = ct.tf2io(ct.tf(10, [1, 1]), input='e', output='u')
185+
sumblk = ct.summing_junction(inputs=['r', '-y'], output='e')
186+
T = ct.interconnect((P, C, sumblk), input='r', output='y')
187+
assert (T.ninputs, T.noutputs, T.nstates) == (1, 1, 2)
188+
189+
# Unrecognized arguments
190+
# LinearIOSystem
191+
with pytest.raises(TypeError, match="unknown parameter"):
192+
P = ct.LinearIOSystem(ct.rss(2, 1, 1), output_name='y')
193+
194+
# Interconnect
195+
with pytest.raises(TypeError, match="unknown parameter"):
196+
T = ct.interconnect((P, C, sumblk), input_name='r', output='y')
197+
198+
# Interconnected system
199+
with pytest.raises(TypeError, match="unknown parameter"):
200+
T = ct.InterconnectedSystem((P, C, sumblk), input_name='r', output='y')
201+
202+
# NonlinearIOSytem
203+
with pytest.raises(TypeError, match="unknown parameter"):
204+
nlios = ct.NonlinearIOSystem(
205+
None, lambda t, x, u, params: u*u, input_count=1, output_count=1)
206+
207+
# Summing junction
208+
with pytest.raises(TypeError, match="input specification is required"):
209+
sumblk = ct.summing_junction()
210+
211+
with pytest.raises(TypeError, match="unknown parameter"):
212+
sumblk = ct.summing_junction(input_count=2, output_count=2)

0 commit comments

Comments
 (0)