From 012d1d0d4b1c7451ec039e85c5c48132224b34db Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 23 Jan 2021 10:39:13 -0800 Subject: [PATCH 1/3] add summation_block + implicit signal interconnection --- control/iosys.py | 213 +++++++++++++++++++++++++++-- control/tests/interconnect_test.py | 131 ++++++++++++++++++ 2 files changed, 335 insertions(+), 9 deletions(-) create mode 100644 control/tests/interconnect_test.py diff --git a/control/iosys.py b/control/iosys.py index 66ca20ebb..8c3418b77 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -36,14 +36,15 @@ import copy from warnings import warn -from .statesp import StateSpace, tf2ss +from .statesp import StateSpace, tf2ss, _convert_to_statespace from .timeresp import _check_convert_array, _process_time_response from .lti import isctime, isdtime, common_timebase from . import config __all__ = ['InputOutputSystem', 'LinearIOSystem', 'NonlinearIOSystem', 'InterconnectedSystem', 'LinearICSystem', 'input_output_response', - 'find_eqpt', 'linearize', 'ss2io', 'tf2io', 'interconnect'] + 'find_eqpt', 'linearize', 'ss2io', 'tf2io', 'interconnect', + 'summation_block'] # Define module default parameter values _iosys_defaults = { @@ -481,9 +482,14 @@ def feedback(self, other=1, sign=-1, params={}): """ # TODO: add conversion to I/O system when needed if not isinstance(other, InputOutputSystem): - raise TypeError("Feedback around I/O system must be I/O system.") - - return new_io_sys + # Try converting to a state space system + try: + other = _convert_to_statespace(other) + except TypeError: + raise TypeError( + "Feedback around I/O system must be an I/O system " + "or convertable to an I/O system.") + other = LinearIOSystem(other) # Make sure systems can be interconnected if self.noutputs != other.ninputs or other.noutputs != self.ninputs: @@ -1846,7 +1852,7 @@ def tf2io(*args, **kwargs): # Function to create an interconnected system -def interconnect(syslist, connections=[], inplist=[], outlist=[], +def interconnect(syslist, connections=None, inplist=[], outlist=[], inputs=None, outputs=None, states=None, params={}, dt=None, name=None): """Interconnect a set of input/output systems. @@ -1893,8 +1899,18 @@ def interconnect(syslist, connections=[], inplist=[], outlist=[], and the special form '-sys.sig' can be used to specify a signal with gain -1. - If omitted, the connection map (matrix) can be specified using the - :func:`~control.InterconnectedSystem.set_connect_map` method. + If omitted, all the `interconnect` function will attempt to create the + interconneciton map by connecting all signals with the same base names + (ignoring the system name). Specifically, for each input signal name + in the list of systems, if that signal name corresponds to the output + signal in any of the systems, it will be connected to that input (with + a summation across all signals if the output name occurs in more than + one system). + + The `connections` keyword can also be set to `False`, which will leave + the connection map empty and it can be specified instead using the + low-level :func:`~control.InterconnectedSystem.set_connect_map` + method. inplist : list of input connections, optional List of connections for how the inputs for the overall system are @@ -1983,7 +1999,7 @@ def interconnect(syslist, connections=[], inplist=[], outlist=[], a warning is generated a copy of the system is created with the name of the new system determined by adding the prefix and suffix strings in config.defaults['iosys.linearized_system_name_prefix'] - and config.defaults['iosys.linearized_system_name_suffix'], with the + and config.defaults['iosys.linearized_system_name_suffix'], with the default being to add the suffix '$copy'$ to the system name. It is possible to replace lists in most of arguments with tuples instead, @@ -2001,6 +2017,78 @@ def interconnect(syslist, connections=[], inplist=[], outlist=[], :class:`~control.InputOutputSystem`. """ + # If connections was not specified, set up default connection list + if connections is None: + # For each system input, look for outputs with the same name + connections = [] + for input_sys in syslist: + for input_name in input_sys.input_index.keys(): + connect = [input_sys.name + "." + input_name] + for output_sys in syslist: + if input_name in output_sys.output_index.keys(): + connect.append(output_sys.name + "." + input_name) + if len(connect) > 1: + connections.append(connect) + elif connections is False: + # Use an empty connections list + connections = [] + + # Process input list + if not isinstance(inplist, (list, tuple)): + inplist = [inplist] + new_inplist = [] + for signal in inplist: + # Check for signal names without a system name + if isinstance(signal, str) and len(signal.split('.')) == 1: + # Get the signal name + name = signal[1:] if signal[0] == '-' else signal + sign = '-' if signal[0] == '-' else "" + + # Look for the signal name as a system input + new_name = None + for sys in syslist: + if name in sys.input_index.keys(): + if new_name is not None: + raise ValueError("signal %s is not unique" % name) + new_name = sign + sys.name + "." + name + + # Make sure we found the name + if new_name is None: + raise ValueError("could not find signal %s" % name) + else: + new_inplist.append(new_name) + else: + new_inplist.append(signal) + inplist = new_inplist + + # Process output list + if not isinstance(outlist, (list, tuple)): + outlist = [outlist] + new_outlist = [] + for signal in outlist: + # Check for signal names without a system name + if isinstance(signal, str) and len(signal.split('.')) == 1: + # Get the signal name + name = signal[1:] if signal[0] == '-' else signal + sign = '-' if signal[0] == '-' else "" + + # Look for the signal name as a system output + new_name = None + for sys in syslist: + if name in sys.output_index.keys(): + if new_name is not None: + raise ValueError("signal %s is not unique" % name) + new_name = sign + sys.name + "." + name + + # Make sure we found the name + if new_name is None: + raise ValueError("could not find signal %s" % name) + else: + new_outlist.append(new_name) + else: + new_outlist.append(signal) + outlist = new_outlist + newsys = InterconnectedSystem( syslist, connections=connections, inplist=inplist, outlist=outlist, inputs=inputs, outputs=outputs, states=states, @@ -2011,3 +2099,110 @@ def interconnect(syslist, connections=[], inplist=[], outlist=[], return LinearICSystem(newsys, None) return newsys + + +# Summation block +def summation_block(inputs, output='y', dimension=None, name=None, prefix='u'): + """Create a summation block as an input/output system. + + This function creates a static input/output system that outputs the sum of + the inputs, potentially with a change in sign for each individual input. + The input/output system that is created by this function can be used as a + component in the :func:`~control.interconnect` function. + + Parameters + ---------- + inputs : int, string or list of strings + Description of the inputs to the summation block. This can be given + as an integer count, a string, or a list of strings. If an integer + count is specified, the names of the input signals will be of the form + `u[i]`. + output : string, optional + Name of the system output. If not specified, the output will be 'y'. + dimension : int, optional + The dimension of the summing block. If the dimension is set to a + positive integer, a multi-input, multi-output summation block will be + created. The input and output signal names will be of the form + `[i]` where `signal` is the input/output signal name specified + by the `inputs` and `output` keywords. Default value is `None`. + name : string, optional + System name (used for specifying signals). If unspecified, a generic + name is generated with a unique integer id. + prefix : string, optional + If `inputs` is an integer, create the names of the states using the + given prefix (default = 'u'). The names of the input will be of the + form `prefix[i]`. + + Returns + ------- + sys : static LinearIOSystem + Linear input/output system object with no states and only a direct + term that implements the summation block. + + """ + # Utility function to parse input and output signal lists + def _parse_list(signals, signame='input', prefix='u'): + # Parse signals, including gains + if isinstance(signals, int): + nsignals = signals + names = ["%s[%d]" % (prefix, i) for i in range(nsignals)] + gains = np.ones((nsignals,)) + elif isinstance(signals, str): + nsignals = 1 + gains = [-1 if signals[0] == '-' else 1] + names = [signals[1:] if signals[0] == '-' else signals] + elif isinstance(signals, list) and \ + all([isinstance(x, str) for x in signals]): + nsignals = len(signals) + gains = np.ones((nsignals,)) + names = [] + for i in range(nsignals): + if signals[i][0] == '-': + gains[i] = -1 + names.append(signals[i][1:]) + else: + names.append(signals[i]) + else: + raise ValueError( + "could not parse %s description '%s'" + % (signame, str(signals))) + + # Return the parsed list + return nsignals, names, gains + + # Read the input list + ninputs, input_names, input_gains = _parse_list( + inputs, signame="input", prefix=prefix) + noutputs, output_names, output_gains = _parse_list( + output, signame="output", prefix='y') + if noutputs > 1: + raise NotImplementedError("vector outputs not yet supported") + + # If the dimension keyword is present, vectorize inputs and outputs + if isinstance(dimension, int) and dimension >= 1: + # Create a new list of input/output names and update parameters + input_names = ["%s[%d]" % (name, dim) + for name in input_names + for dim in range(dimension)] + ninputs = ninputs * dimension + + output_names = ["%s[%d]" % (name, dim) + for name in output_names + for dim in range(dimension)] + noutputs = noutputs * dimension + elif dimension is not None: + raise ValueError( + "unrecognized dimension value '%s'" % str(dimension)) + else: + dimension = 1 + + # Create the direct term + D = np.kron(input_gains * output_gains[0], np.eye(dimension)) + + # Create a linear system of the appropriate size + ss_sys = StateSpace( + np.zeros((0, 0)), np.ones((0, ninputs)), np.ones((noutputs, 0)), D) + + # Create a LinearIOSystem + return LinearIOSystem( + ss_sys, inputs=input_names, outputs=output_names, name=name) diff --git a/control/tests/interconnect_test.py b/control/tests/interconnect_test.py new file mode 100644 index 000000000..dcc588dad --- /dev/null +++ b/control/tests/interconnect_test.py @@ -0,0 +1,131 @@ +"""interconnect_test.py - test input/output interconnect function + +RMM, 22 Jan 2021 + +This set of unit tests covers the various operatons of the interconnect() +function, as well as some of the support functions associated with +interconnect(). + +Note: additional tests are available in iosys_test.py, which focuses on the +raw InterconnectedSystem constructor. This set of unit tests focuses on +functionality implemented in the interconnect() function itself. + +""" + +import pytest + +import numpy as np +import scipy as sp + +import control as ct + +@pytest.mark.parametrize("inputs, output, dimension, D", [ + [1, 1, None, [[1]] ], + ['u', 'y', None, [[1]] ], + [['u'], ['y'], None, [[1]] ], + [2, 1, None, [[1, 1]] ], + [['r', '-y'], ['e'], None, [[1, -1]] ], + [5, 1, None, np.ones((1, 5)) ], + ['u', 'y', 1, [[1]] ], + ['u', 'y', 2, [[1, 0], [0, 1]] ], + [['r', '-y'], ['e'], 2, [[1, 0, -1, 0], [0, 1, 0, -1]] ], +]) +def test_summation_block(inputs, output, dimension, D): + ninputs = 1 if isinstance(inputs, str) else \ + inputs if isinstance(inputs, int) else len(inputs) + sum = ct.summation_block( + inputs=inputs, output=output, dimension=dimension) + dim = 1 if dimension is None else dimension + np.testing.assert_array_equal(sum.A, np.ndarray((0, 0))) + np.testing.assert_array_equal(sum.B, np.ndarray((0, ninputs*dim))) + np.testing.assert_array_equal(sum.C, np.ndarray((dim, 0))) + np.testing.assert_array_equal(sum.D, D) + + +def test_summation_exceptions(): + # Bad input description + with pytest.raises(ValueError, match="could not parse input"): + sumblk = ct.summation_block(None, 'y') + + # Bad output description + with pytest.raises(ValueError, match="could not parse output"): + sumblk = ct.summation_block('u', None) + + # Bad input dimension + with pytest.raises(ValueError, match="unrecognized dimension"): + sumblk = ct.summation_block('u', 'y', dimension=False) + + +def test_interconnect_implicit(): + """Test the use of implicit connections in interconnect()""" + import random + + # System definition + P = ct.ss2io( + ct.rss(2, 1, 1, strictly_proper=True), + inputs='u', outputs='y', name='P') + kp = ct.tf(random.uniform(1, 10), [1]) + ki = ct.tf(random.uniform(1, 10), [1, 0]) + C = ct.tf2io(kp + ki, inputs='e', outputs='u', name='C') + + # Block diagram computation + Tss = ct.feedback(P * C, 1) + + # Construct the interconnection explicitly + Tio_exp = ct.interconnect( + (C, P), + connections = [['P.u', 'C.u'], ['C.e', '-P.y']], + inplist='C.e', outlist='P.y') + + # Compare to bdalg computation + np.testing.assert_almost_equal(Tio_exp.A, Tss.A) + np.testing.assert_almost_equal(Tio_exp.B, Tss.B) + np.testing.assert_almost_equal(Tio_exp.C, Tss.C) + np.testing.assert_almost_equal(Tio_exp.D, Tss.D) + + # Construct the interconnection via a summation block + sumblk = ct.summation_block(inputs=['r', '-y'], output='e', name="sum") + Tio_sum = ct.interconnect( + (C, P, sumblk), inplist=['r'], outlist=['y']) + + np.testing.assert_almost_equal(Tio_sum.A, Tss.A) + np.testing.assert_almost_equal(Tio_sum.B, Tss.B) + np.testing.assert_almost_equal(Tio_sum.C, Tss.C) + np.testing.assert_almost_equal(Tio_sum.D, Tss.D) + + # Setting connections to False should lead to an empty connection map + empty = ct.interconnect( + (C, P, sumblk), connections=False, inplist=['r'], outlist=['y']) + np.testing.assert_array_equal(empty.connect_map, np.zeros((4, 3))) + + # Implicit summation across repeated signals + kp_io = ct.tf2io(kp, inputs='e', outputs='u', name='kp') + ki_io = ct.tf2io(ki, inputs='e', outputs='u', name='ki') + Tio_sum = ct.interconnect( + (kp_io, ki_io, P, sumblk), inplist=['r'], outlist=['y']) + np.testing.assert_almost_equal(Tio_sum.A, Tss.A) + np.testing.assert_almost_equal(Tio_sum.B, Tss.B) + np.testing.assert_almost_equal(Tio_sum.C, Tss.C) + np.testing.assert_almost_equal(Tio_sum.D, Tss.D) + + # Make sure that repeated inplist/outlist names generate an error + # Input not unique + Cbad = ct.tf2io(ct.tf(10, [1, 1]), inputs='r', outputs='x', name='C') + with pytest.raises(ValueError, match="not unique"): + Tio_sum = ct.interconnect( + (Cbad, P, sumblk), inplist=['r'], outlist=['y']) + + # Output not unique + Cbad = ct.tf2io(ct.tf(10, [1, 1]), inputs='e', outputs='y', name='C') + with pytest.raises(ValueError, match="not unique"): + Tio_sum = ct.interconnect( + (Cbad, P, sumblk), inplist=['r'], outlist=['y']) + + # Signal not found + with pytest.raises(ValueError, match="could not find"): + Tio_sum = ct.interconnect( + (C, P, sumblk), inplist=['x'], outlist=['y']) + + with pytest.raises(ValueError, match="could not find"): + Tio_sum = ct.interconnect( + (C, P, sumblk), inplist=['r'], outlist=['x']) From cd3e4b65649b77d4d8a488ecd2a7a718dd5ba540 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sun, 24 Jan 2021 10:01:43 -0800 Subject: [PATCH 2/3] change name to summing_junction + updated documentation and examples --- control/iosys.py | 43 ++++++++++------- control/statesp.py | 4 +- control/tests/interconnect_test.py | 57 +++++++++++++++++++--- doc/control.rst | 1 + doc/iosys.rst | 77 ++++++++++++++++++++++++++++-- 5 files changed, 154 insertions(+), 28 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index 8c3418b77..cb360bad4 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -3,16 +3,11 @@ # RMM, 28 April 2019 # # Additional features to add -# * Improve support for signal names, specially in operator overloads -# - Figure out how to handle "nested" names (icsys.sys[1].x[1]) -# - Use this to implement signal names for operators? # * Allow constant inputs for MIMO input_output_response (w/out ones) # * Add support for constants/matrices as part of operators (1 + P) # * Add unit tests (and example?) for time-varying systems # * Allow time vector for discrete time simulations to be multiples of dt # * Check the way initial outputs for discrete time systems are handled -# * Rename 'connections' as 'conlist' to match 'inplist' and 'outlist'? -# * Allow signal summation in InterconnectedSystem diagrams (via new output?) # """The :mod:`~control.iosys` module contains the @@ -44,7 +39,7 @@ __all__ = ['InputOutputSystem', 'LinearIOSystem', 'NonlinearIOSystem', 'InterconnectedSystem', 'LinearICSystem', 'input_output_response', 'find_eqpt', 'linearize', 'ss2io', 'tf2io', 'interconnect', - 'summation_block'] + 'summing_junction'] # Define module default parameter values _iosys_defaults = { @@ -1982,17 +1977,26 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], Example ------- >>> P = control.LinearIOSystem( - >>> ct.rss(2, 2, 2, strictly_proper=True), name='P') + >>> control.rss(2, 2, 2, strictly_proper=True), name='P') >>> C = control.LinearIOSystem(control.rss(2, 2, 2), name='C') - >>> S = control.InterconnectedSystem( + >>> T = control.interconnect( >>> [P, C], >>> connections = [ - >>> ['P.u[0]', 'C.y[0]'], ['P.u[1]', 'C.y[0]'], + >>> ['P.u[0]', 'C.y[0]'], ['P.u[1]', 'C.y[1]'], >>> ['C.u[0]', '-P.y[0]'], ['C.u[1]', '-P.y[1]']], >>> inplist = ['C.u[0]', 'C.u[1]'], >>> outlist = ['P.y[0]', 'P.y[1]'], >>> ) + For a SISO system, this example can be simplified by using the + :func:`~control.summing_block` function and the ability to automatically + interconnect signals with the same names: + + >>> P = control.tf2io(control.tf(1, [1, 0]), inputs='u', outputs='y') + >>> C = control.tf2io(control.tf(10, [1, 1]), inputs='e', outputs='u') + >>> sumblk = control.summing_junction(inputs=['r', '-y'], output='e') + >>> T = control.interconnect([P, C, sumblk], inplist='r', outlist='y') + Notes ----- If a system is duplicated in the list of systems to be connected, @@ -2101,9 +2105,9 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], return newsys -# Summation block -def summation_block(inputs, output='y', dimension=None, name=None, prefix='u'): - """Create a summation block as an input/output system. +# Summing junction +def summing_junction(inputs, output='y', dimension=None, name=None, prefix='u'): + """Create a summing junction as an input/output system. This function creates a static input/output system that outputs the sum of the inputs, potentially with a change in sign for each individual input. @@ -2113,15 +2117,15 @@ def summation_block(inputs, output='y', dimension=None, name=None, prefix='u'): Parameters ---------- inputs : int, string or list of strings - Description of the inputs to the summation block. This can be given + Description of the inputs to the summing junction. This can be given as an integer count, a string, or a list of strings. If an integer count is specified, the names of the input signals will be of the form `u[i]`. output : string, optional Name of the system output. If not specified, the output will be 'y'. dimension : int, optional - The dimension of the summing block. If the dimension is set to a - positive integer, a multi-input, multi-output summation block will be + The dimension of the summing junction. If the dimension is set to a + positive integer, a multi-input, multi-output summing junction will be created. The input and output signal names will be of the form `[i]` where `signal` is the input/output signal name specified by the `inputs` and `output` keywords. Default value is `None`. @@ -2137,7 +2141,14 @@ def summation_block(inputs, output='y', dimension=None, name=None, prefix='u'): ------- sys : static LinearIOSystem Linear input/output system object with no states and only a direct - term that implements the summation block. + term that implements the summing junction. + + Example + ------- + >>> P = control.tf2io(ct.tf(1, [1, 0]), inputs='u', outputs='y') + >>> C = control.tf2io(ct.tf(10, [1, 1]), inputs='e', outputs='u') + >>> sumblk = control.summing_junction(inputs=['r', '-y'], output='e') + >>> T = control.interconnect((P, C, sumblk), inplist='r', outlist='y') """ # Utility function to parse input and output signal lists diff --git a/control/statesp.py b/control/statesp.py index 0ee83cb7d..cac3ecb39 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -223,7 +223,7 @@ def __init__(self, *args, **kwargs): The default constructor is StateSpace(A, B, C, D), where A, B, C, D are matrices or equivalent objects. To create a discrete time system, - use StateSpace(A, B, C, D, dt) where 'dt' is the sampling time (or + use StateSpace(A, B, C, D, dt) where `dt` is the sampling time (or True for unspecified sampling time). To call the copy constructor, call StateSpace(sys), where sys is a StateSpace object. @@ -231,7 +231,7 @@ def __init__(self, *args, **kwargs): C matrices for rows or columns of zeros. If the zeros are such that a particular state has no effect on the input-output dynamics, then that state is removed from the A, B, and C matrices. If not specified, the - value is read from `config.defaults['statesp.remove_useless_states'] + value is read from `config.defaults['statesp.remove_useless_states']` (default = False). """ diff --git a/control/tests/interconnect_test.py b/control/tests/interconnect_test.py index dcc588dad..34daffb75 100644 --- a/control/tests/interconnect_test.py +++ b/control/tests/interconnect_test.py @@ -30,10 +30,10 @@ ['u', 'y', 2, [[1, 0], [0, 1]] ], [['r', '-y'], ['e'], 2, [[1, 0, -1, 0], [0, 1, 0, -1]] ], ]) -def test_summation_block(inputs, output, dimension, D): +def test_summing_junction(inputs, output, dimension, D): ninputs = 1 if isinstance(inputs, str) else \ inputs if isinstance(inputs, int) else len(inputs) - sum = ct.summation_block( + sum = ct.summing_junction( inputs=inputs, output=output, dimension=dimension) dim = 1 if dimension is None else dimension np.testing.assert_array_equal(sum.A, np.ndarray((0, 0))) @@ -45,15 +45,15 @@ def test_summation_block(inputs, output, dimension, D): def test_summation_exceptions(): # Bad input description with pytest.raises(ValueError, match="could not parse input"): - sumblk = ct.summation_block(None, 'y') + sumblk = ct.summing_junction(None, 'y') # Bad output description with pytest.raises(ValueError, match="could not parse output"): - sumblk = ct.summation_block('u', None) + sumblk = ct.summing_junction('u', None) # Bad input dimension with pytest.raises(ValueError, match="unrecognized dimension"): - sumblk = ct.summation_block('u', 'y', dimension=False) + sumblk = ct.summing_junction('u', 'y', dimension=False) def test_interconnect_implicit(): @@ -83,8 +83,8 @@ def test_interconnect_implicit(): np.testing.assert_almost_equal(Tio_exp.C, Tss.C) np.testing.assert_almost_equal(Tio_exp.D, Tss.D) - # Construct the interconnection via a summation block - sumblk = ct.summation_block(inputs=['r', '-y'], output='e', name="sum") + # Construct the interconnection via a summing junction + sumblk = ct.summing_junction(inputs=['r', '-y'], output='e', name="sum") Tio_sum = ct.interconnect( (C, P, sumblk), inplist=['r'], outlist=['y']) @@ -108,6 +108,17 @@ def test_interconnect_implicit(): np.testing.assert_almost_equal(Tio_sum.C, Tss.C) np.testing.assert_almost_equal(Tio_sum.D, Tss.D) + # TODO: interconnect a MIMO system using implicit connections + # P = control.ss2io( + # control.rss(2, 2, 2, strictly_proper=True), + # input_prefix='u', output_prefix='y', name='P') + # C = control.ss2io( + # control.rss(2, 2, 2), + # input_prefix='e', output_prefix='u', name='C') + # sumblk = control.summing_junction( + # inputs=['r', '-y'], output='e', dimension=2) + # S = control.interconnect([P, C, sumblk], inplist='r', outlist='y') + # Make sure that repeated inplist/outlist names generate an error # Input not unique Cbad = ct.tf2io(ct.tf(10, [1, 1]), inputs='r', outputs='x', name='C') @@ -129,3 +140,35 @@ def test_interconnect_implicit(): with pytest.raises(ValueError, match="could not find"): Tio_sum = ct.interconnect( (C, P, sumblk), inplist=['r'], outlist=['x']) + +def test_interconnect_docstring(): + """Test the examples from the interconnect() docstring""" + + # MIMO interconnection (note: use [C, P] instead of [P, C] for state order) + P = ct.LinearIOSystem( + ct.rss(2, 2, 2, strictly_proper=True), name='P') + C = ct.LinearIOSystem(ct.rss(2, 2, 2), name='C') + T = ct.interconnect( + [C, P], + connections = [ + ['P.u[0]', 'C.y[0]'], ['P.u[1]', 'C.y[1]'], + ['C.u[0]', '-P.y[0]'], ['C.u[1]', '-P.y[1]']], + inplist = ['C.u[0]', 'C.u[1]'], + outlist = ['P.y[0]', 'P.y[1]'], + ) + T_ss = ct.feedback(P * C, ct.ss([], [], [], np.eye(2))) + np.testing.assert_almost_equal(T.A, T_ss.A) + np.testing.assert_almost_equal(T.B, T_ss.B) + np.testing.assert_almost_equal(T.C, T_ss.C) + np.testing.assert_almost_equal(T.D, T_ss.D) + + # Implicit interconnection (note: use [C, P, sumblk] for proper state order) + P = ct.tf2io(ct.tf(1, [1, 0]), inputs='u', outputs='y') + C = ct.tf2io(ct.tf(10, [1, 1]), inputs='e', outputs='u') + sumblk = ct.summing_junction(inputs=['r', '-y'], output='e') + T = ct.interconnect([C, P, sumblk], inplist='r', outlist='y') + T_ss = ct.feedback(P * C, 1) + np.testing.assert_almost_equal(T.A, T_ss.A) + np.testing.assert_almost_equal(T.B, T_ss.B) + np.testing.assert_almost_equal(T.C, T_ss.C) + np.testing.assert_almost_equal(T.D, T_ss.D) diff --git a/doc/control.rst b/doc/control.rst index 80119f691..500f6db3c 100644 --- a/doc/control.rst +++ b/doc/control.rst @@ -144,6 +144,7 @@ Nonlinear system support linearize input_output_response ss2io + summing_junction tf2io flatsys.point_to_point diff --git a/doc/iosys.rst b/doc/iosys.rst index b2ac752af..1b160bad1 100644 --- a/doc/iosys.rst +++ b/doc/iosys.rst @@ -156,7 +156,8 @@ The input to the controller is `u`, consisting of the vector of hare and lynx populations followed by the desired lynx population. To connect the controller to the predatory-prey model, we create an -:class:`~control.InterconnectedSystem`: +:class:`~control.InterconnectedSystem` using the :func:`~control.interconnect` +function: .. code-block:: python @@ -189,13 +190,83 @@ Finally, we simulate the closed loop system: plt.legend(['input']) plt.show(block=False) +Additional features +=================== + +The I/O systems module has a number of other features that can be used to +simplify the creation of interconnected input/output systems. + +Summing junction +---------------- + +The :func:`~control.summing_junction` function can be used to create an +input/output system that takes the sum of an arbitrary number of inputs. For +ezample, to create an input/output system that takes the sum of three inputs, +use the command + +.. code-block:: python + + sumblk = ct.summing_junction(3) + +By default, the name of the inputs will be of the form ``u[i]`` and the output +will be ``y``. This can be changed by giving an explicit list of names:: + + sumblk = ct.summing_junction(inputs=['a', 'b', 'c'], output='d') + +A more typical usage would be to define an input/output system that compares a +reference signal to the output of the process and computes the error:: + + sumblk = ct.summing_junction(inputs=['r', '-y'], output='e') + +Note the use of the minus sign as a means of setting the sign of the input 'y' +to be negative instead of positive. + +It is also possible to define "vector" summing blocks that take +multi-dimensional inputs and produce a multi-dimensional output. For example, +the command + +.. code-block:: python + + sumblk = ct.summing_junction(inputs=['r', '-y'], output='e', dimension=2) + +will produce an input/output block that implements ``e[0] = r[0] - y[0]`` and +``e[1] = r[1] - y[1]``. + +Automatic connections using signal names +---------------------------------------- + +The :func:`~control.interconnect` function allows the interconnection of +multiple systems by using signal names of the form ``sys.signal``. In many +situations, it can be cumbersome to explicitly connect all of the appropriate +inputs and outputs. As an alternative, if the ``connections`` keyword is +omitted, the :func:`~control.interconnect` function will connect all signals +of the same name to each other. This can allow for simplified methods of +interconnecting systems, especially when combined with the +:func:`~control.summing_junction` function. For example, the following code +will create a unity gain, negative feedback system:: + + P = control.tf2io(control.tf(1, [1, 0]), inputs='u', outputs='y') + C = control.tf2io(control.tf(10, [1, 1]), inputs='e', outputs='u') + sumblk = control.summing_junction(inputs=['r', '-y'], output='e') + T = control.interconnect([P, C, sumblk], inplist='r', outlist='y') + +If a signal name appears in multiple outputs then that signal will be summed +when it is interconnected. Similarly, if a signal name appears in multiple +inputs then all systems using that signal name will receive the same input. +The :func:`~control.interconnect` function will generate an error if an signal +listed in ``inplist`` or ``outlist`` (corresponding to the inputs and outputs +of the interconnected system) is not found, but inputs and outputs of +individual systems that are not connected to other systems are left +unconnected (so be careful!). + + Module classes and functions ============================ Input/output system classes --------------------------- .. autosummary:: - + ~control.InputOutputSystem ~control.InterconnectedSystem ~control.LinearICSystem @@ -211,5 +282,5 @@ Input/output system functions ~control.input_output_response ~control.interconnect ~control.ss2io + ~control.summing_junction ~control.tf2io - From 791f7a677db500731697bdde7a82ca391f9a2b37 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Mon, 25 Jan 2021 22:18:55 -0800 Subject: [PATCH 3/3] TRV: docstring typo --- control/iosys.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index cb360bad4..afee5088c 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -1894,8 +1894,8 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], and the special form '-sys.sig' can be used to specify a signal with gain -1. - If omitted, all the `interconnect` function will attempt to create the - interconneciton map by connecting all signals with the same base names + If omitted, the `interconnect` function will attempt to create the + interconnection map by connecting all signals with the same base names (ignoring the system name). Specifically, for each input signal name in the list of systems, if that signal name corresponds to the output signal in any of the systems, it will be connected to that input (with