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
3631import copy
3732from warnings import warn
3833
39- from .statesp import StateSpace , tf2ss
34+ from .statesp import StateSpace , tf2ss , _convert_to_statespace
4035from .timeresp import _check_convert_array , _process_time_response
4136from .lti import isctime , isdtime , common_timebase
4237from . 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 )
0 commit comments