@@ -1398,7 +1398,164 @@ def set_output_map(self, output_map):
13981398 self .noutputs = output_map .shape [0 ]
13991399
14001400
1401+ def unused_signals (self ):
1402+ """Find unused subsystem inputs and outputs
1403+
1404+ Returns
1405+ -------
1406+
1407+ unused_inputs : dict
1408+
1409+ A mapping from tuple of indices (isys, isig) to string
1410+ '{sys}.{sig}', for all unused subsystem inputs.
1411+
1412+ unused_outputs : dict
1413+
1414+ A mapping from tuple of indices (isys, isig) to string
1415+ '{sys}.{sig}', for all unused subsystem outputs.
1416+
1417+ """
1418+ used_sysinp_via_inp = np .nonzero (self .input_map )[0 ]
1419+ used_sysout_via_out = np .nonzero (self .output_map )[1 ]
1420+ used_sysinp_via_con , used_sysout_via_con = np .nonzero (self .connect_map )
1421+
1422+ used_sysinp = set (used_sysinp_via_inp ) | set (used_sysinp_via_con )
1423+ used_sysout = set (used_sysout_via_out ) | set (used_sysout_via_con )
1424+
1425+ nsubsysinp = sum (sys .ninputs for sys in self .syslist )
1426+ nsubsysout = sum (sys .noutputs for sys in self .syslist )
1427+
1428+ unused_sysinp = sorted (set (range (nsubsysinp )) - used_sysinp )
1429+ unused_sysout = sorted (set (range (nsubsysout )) - used_sysout )
1430+
1431+ inputs = [(isys ,isig , f'{ sys .name } .{ sig } ' )
1432+ for isys , sys in enumerate (self .syslist )
1433+ for sig , isig in sys .input_index .items ()]
1434+
1435+ outputs = [(isys ,isig ,f'{ sys .name } .{ sig } ' )
1436+ for isys , sys in enumerate (self .syslist )
1437+ for sig , isig in sys .output_index .items ()]
1438+
1439+ return ({inputs [i ][:2 ]:inputs [i ][2 ]
1440+ for i in unused_sysinp },
1441+ {outputs [i ][:2 ]:outputs [i ][2 ]
1442+ for i in unused_sysout })
1443+
1444+
1445+ def _find_inputs_by_basename (self , basename ):
1446+ """Find all subsystem inputs matching basename
1447+
1448+ Returns
1449+ -------
1450+ Mapping from (isys, isig) to '{sys}.{sig}'
1451+
1452+ """
1453+ return {(isys , isig ) : f'{ sys .name } .{ basename } '
1454+ for isys , sys in enumerate (self .syslist )
1455+ for sig , isig in sys .input_index .items ()
1456+ if sig == (basename )}
1457+
1458+
1459+ def _find_outputs_by_basename (self , basename ):
1460+ """Find all subsystem outputs matching basename
1461+
1462+ Returns
1463+ -------
1464+ Mapping from (isys, isig) to '{sys}.{sig}'
1465+
1466+ """
1467+ return {(isys , isig ) : f'{ sys .name } .{ basename } '
1468+ for isys , sys in enumerate (self .syslist )
1469+ for sig , isig in sys .output_index .items ()
1470+ if sig == (basename )}
1471+
1472+
1473+ def check_unused_signals (self , ignore_inputs = None , ignore_outputs = None ):
1474+ """Check for unused subsystem inputs and outputs
1475+
1476+ If any unused inputs or outputs are found, emit a warning.
1477+
1478+ Parameters
1479+ ----------
1480+ ignore_inputs : list of input-spec
1481+ Subsystem inputs known to be unused. input-spec can be any of:
1482+ 'sig', 'sys.sig', (isys, isig), ('sys', isig)
1483+
1484+ If the 'sig' form is used, all subsystem inputs with that
1485+ name are considered ignored.
1486+
1487+ ignore_outputs : list of output-spec
1488+ Subsystem outputs known to be unused. output-spec can be any of:
1489+ 'sig', 'sys.sig', (isys, isig), ('sys', isig)
1490+
1491+ If the 'sig' form is used, all subsystem outputs with that
1492+ name are considered ignored.
1493+
1494+ """
1495+
1496+ if ignore_inputs is None :
1497+ ignore_inputs = []
1498+
1499+ if ignore_outputs is None :
1500+ ignore_outputs = []
1501+
1502+ unused_inputs , unused_outputs = self .unused_signals ()
1503+
1504+ # (isys, isig) -> signal-spec
1505+ ignore_input_map = {}
1506+ for ignore_input in ignore_inputs :
1507+ if isinstance (ignore_input , str ) and '.' not in ignore_input :
1508+ ignore_idxs = self ._find_inputs_by_basename (ignore_input )
1509+ if not ignore_idxs :
1510+ raise ValueError (f"Couldn't find ignored input { ignore_input } in subsystems" )
1511+ ignore_input_map .update (ignore_idxs )
1512+ else :
1513+ ignore_input_map [self ._parse_signal (ignore_input , 'input' )[:2 ]] = ignore_input
1514+
1515+ # (isys, isig) -> signal-spec
1516+ ignore_output_map = {}
1517+ for ignore_output in ignore_outputs :
1518+ if isinstance (ignore_output , str ) and '.' not in ignore_output :
1519+ ignore_found = self ._find_outputs_by_basename (ignore_output )
1520+ if not ignore_found :
1521+ raise ValueError (f"Couldn't find ignored output { ignore_output } in subsystems" )
1522+ ignore_output_map .update (ignore_found )
1523+ else :
1524+ ignore_output_map [self ._parse_signal (ignore_output , 'output' )[:2 ]] = ignore_output
1525+
1526+ dropped_inputs = set (unused_inputs ) - set (ignore_input_map )
1527+ dropped_outputs = set (unused_outputs ) - set (ignore_output_map )
1528+
1529+ used_ignored_inputs = set (ignore_input_map ) - set (unused_inputs )
1530+ used_ignored_outputs = set (ignore_output_map ) - set (unused_outputs )
1531+
1532+ if dropped_inputs :
1533+ msg = ('Unused input(s) in InterconnectedSystem: '
1534+ + '; ' .join (f'{ inp } ={ unused_inputs [inp ]} '
1535+ for inp in dropped_inputs ))
1536+ warn (msg )
1537+
1538+ if dropped_outputs :
1539+ msg = ('Unused output(s) in InterconnectedSystem: '
1540+ + '; ' .join (f'{ out } : { unused_outputs [out ]} '
1541+ for out in dropped_outputs ))
1542+ warn (msg )
1543+
1544+ if used_ignored_inputs :
1545+ msg = ('Input(s) specified as ignored is (are) used: '
1546+ + '; ' .join (f'{ inp } : { ignore_input_map [inp ]} '
1547+ for inp in used_ignored_inputs ))
1548+ warn (msg )
1549+
1550+ if used_ignored_outputs :
1551+ msg = ('Output(s) specified as ignored is (are) used: '
1552+ + '; ' .join (f'{ out } ={ ignore_output_map [out ]} '
1553+ for out in used_ignored_outputs ))
1554+ warn (msg )
1555+
1556+
14011557class LinearICSystem (InterconnectedSystem , LinearIOSystem ):
1558+
14021559 """Interconnection of a set of linear input/output systems.
14031560
14041561 This class is used to implement a system that is an interconnection of
@@ -2020,7 +2177,9 @@ def tf2io(*args, **kwargs):
20202177# Function to create an interconnected system
20212178def interconnect (syslist , connections = None , inplist = [], outlist = [],
20222179 inputs = None , outputs = None , states = None ,
2023- params = {}, dt = None , name = None , ** kwargs ):
2180+ params = {}, dt = None , name = None ,
2181+ check_unused = True , ignore_inputs = None , ignore_outputs = None ,
2182+ ** kwargs ):
20242183 """Interconnect a set of input/output systems.
20252184
20262185 This function creates a new system that is an interconnection of a set of
@@ -2145,6 +2304,32 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[],
21452304 System name (used for specifying signals). If unspecified, a generic
21462305 name <sys[id]> is generated with a unique integer id.
21472306
2307+ check_unused : bool
2308+ If True, check for unused sub-system signals. This check is
2309+ not done if connections is False, and neither input nor output
2310+ mappings are specified.
2311+
2312+ ignore_inputs : list of input-spec
2313+ A list of sub-system inputs known not to be connected. This is
2314+ *only* used in checking for unused signals, and does not
2315+ disable use of the input.
2316+
2317+ Besides the usual input-spec forms (see `connections`), an
2318+ input-spec can be just the signal base name, in which case all
2319+ signals from all sub-systems with that base name are
2320+ considered ignored.
2321+
2322+ ignore_outputs : list of output-spec
2323+ A list of sub-system outputs known not to be connected. This
2324+ is *only* used in checking for unused signals, and does not
2325+ disable use of the output.
2326+
2327+ Besides the usual output-spec forms (see `connections`), an
2328+ output-spec can be just the signal base name, in which all
2329+ outputs from all sub-systems with that base name are
2330+ considered ignored.
2331+
2332+
21482333 Example
21492334 -------
21502335 >>> P = control.LinearIOSystem(
@@ -2199,6 +2384,17 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[],
21992384 inputs = _parse_signal_parameter (inputs , 'input' , kwargs )
22002385 outputs = _parse_signal_parameter (outputs , 'output' , kwargs , end = True )
22012386
2387+ if not check_unused and (ignore_inputs or ignore_outputs ):
2388+ raise ValueError ('check_unused is False, but either '
2389+ + 'ignore_inputs or ignore_outputs non-empty' )
2390+
2391+ if (connections is False
2392+ and not inplist and not outlist
2393+ and not inputs and not outputs ):
2394+ # user has disabled auto-connect, and supplied neither input
2395+ # nor output mappings; assume they know what they're doing
2396+ check_unused = False
2397+
22022398 # If connections was not specified, set up default connection list
22032399 if connections is None :
22042400 # For each system input, look for outputs with the same name
@@ -2211,7 +2407,11 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[],
22112407 connect .append (output_sys .name + "." + input_name )
22122408 if len (connect ) > 1 :
22132409 connections .append (connect )
2410+
2411+ auto_connect = True
2412+
22142413 elif connections is False :
2414+ check_unused = False
22152415 # Use an empty connections list
22162416 connections = []
22172417
@@ -2282,6 +2482,10 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[],
22822482 inputs = inputs , outputs = outputs , states = states ,
22832483 params = params , dt = dt , name = name )
22842484
2485+
2486+ # check for implicity dropped signals
2487+ if check_unused :
2488+ newsys .check_unused_signals (ignore_inputs , ignore_outputs )
22852489 # If all subsystems are linear systems, maintain linear structure
22862490 if all ([isinstance (sys , LinearIOSystem ) for sys in syslist ]):
22872491 return LinearICSystem (newsys , None )
0 commit comments