3636import copy
3737from warnings import warn
3838
39- from .statesp import StateSpace , tf2ss
39+ from .statesp import StateSpace , tf2ss , _convert_to_statespace
4040from .timeresp import _check_convert_array , _process_time_response
4141from .lti import isctime , isdtime , common_timebase
4242from . 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 )
0 commit comments