Skip to content

Commit 78b3349

Browse files
committed
Check for unused subsystem signals in InterconnectedSystem
Add capability to check for unused signals in InterconnectedSystem; this check is invoked by default by `interconnect`.
1 parent 5ab0a1c commit 78b3349

2 files changed

Lines changed: 342 additions & 1 deletion

File tree

control/iosys.py

Lines changed: 216 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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+
14011557
class 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
20212178
def 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,43 @@ 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 not input and output
2310+
mappings are specified.
2311+
2312+
ignore_inputs : list of input-spec
2313+
2314+
A list of sub-system known not to be connected. This is
2315+
*only* used in checking for unused signals, and does not
2316+
disable use of the input.
2317+
2318+
Besides the usual input-spec forms (see `connections`), an
2319+
input-spec can be a string give just the signal name, as for inpu
2320+
2321+
ignore_inputs : list of input-spec
2322+
2323+
A list of sub-system inputs known not to be connected. This is
2324+
*only* used in checking for unused signals, and does not
2325+
disable use of the input.
2326+
2327+
Besides the usual input-spec forms (see `connections`), an
2328+
input-spec can be just the signal base name, in which case all
2329+
signals from all sub-systems with that base name are
2330+
considered ignored.
2331+
2332+
ignore_outputs : list of output-spec
2333+
2334+
A list of sub-system outputs known not to be connected. This
2335+
is *only* used in checking for unused signals, and does not
2336+
disable use of the output.
2337+
2338+
Besides the usual output-spec forms (see `connections`), an
2339+
output-spec can be just the signal base name, in which all
2340+
outputs from all sub-systems with that base name are
2341+
considered ignored.
2342+
2343+
21482344
Example
21492345
-------
21502346
>>> P = control.LinearIOSystem(
@@ -2199,6 +2395,17 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[],
21992395
inputs = _parse_signal_parameter(inputs, 'input', kwargs)
22002396
outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True)
22012397

2398+
if not check_unused and (ignore_inputs or ignore_outputs):
2399+
raise ValueError('check_unused is False, but either '
2400+
+ 'ignore_inputs or ignore_outputs non-empty')
2401+
2402+
if (connections is False
2403+
and not inplist and not outlist
2404+
and not inputs and not outputs):
2405+
# user has disabled auto-connect, and supplied neither input
2406+
# nor output mappings; assume they know what they're doing
2407+
check_unused = False
2408+
22022409
# If connections was not specified, set up default connection list
22032410
if connections is None:
22042411
# For each system input, look for outputs with the same name
@@ -2211,7 +2418,11 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[],
22112418
connect.append(output_sys.name + "." + input_name)
22122419
if len(connect) > 1:
22132420
connections.append(connect)
2421+
2422+
auto_connect = True
2423+
22142424
elif connections is False:
2425+
check_unused = False
22152426
# Use an empty connections list
22162427
connections = []
22172428

@@ -2282,6 +2493,10 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[],
22822493
inputs=inputs, outputs=outputs, states=states,
22832494
params=params, dt=dt, name=name)
22842495

2496+
2497+
# check for implicity dropped signals
2498+
if check_unused:
2499+
newsys.check_unused_signals(ignore_inputs, ignore_outputs)
22852500
# If all subsystems are linear systems, maintain linear structure
22862501
if all([isinstance(sys, LinearIOSystem) for sys in syslist]):
22872502
return LinearICSystem(newsys, None)

control/tests/iosys_test.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,3 +1396,129 @@ def secord_update(t, x, u, params={}):
13961396
def secord_output(t, x, u, params={}):
13971397
"""Second order system dynamics output"""
13981398
return np.array([x[0]])
1399+
1400+
1401+
def test_interconnect_unused_input():
1402+
# test that warnings about unused inputs are reported, or not,
1403+
# as required
1404+
g = ct.LinearIOSystem(ct.ss(-1,1,1,0),
1405+
inputs=['u'],
1406+
outputs=['y'],
1407+
name='g')
1408+
1409+
s = ct.summing_junction(inputs=['r','-y','-n'],
1410+
outputs=['e'],
1411+
name='s')
1412+
1413+
k = ct.LinearIOSystem(ct.ss(0,10,2,0),
1414+
inputs=['e'],
1415+
outputs=['u'],
1416+
name='k')
1417+
1418+
with pytest.warns(UserWarning, match=r"Unused input\(s\) in InterconnectedSystem"):
1419+
h = ct.interconnect([g,s,k],
1420+
inputs=['r'],
1421+
outputs=['y'])
1422+
1423+
with pytest.warns(None) as record:
1424+
# no warning if output explicitly ignored, various argument forms
1425+
h = ct.interconnect([g,s,k],
1426+
inputs=['r'],
1427+
outputs=['y'],
1428+
ignore_inputs=['n'])
1429+
1430+
h = ct.interconnect([g,s,k],
1431+
inputs=['r'],
1432+
outputs=['y'],
1433+
ignore_inputs=['s.n'])
1434+
1435+
# no warning if auto-connect disabled
1436+
h = ct.interconnect([g,s,k],
1437+
connections=False)
1438+
1439+
#https://docs.pytest.org/en/6.2.x/warnings.html#recwarn
1440+
assert not record
1441+
1442+
# warn if explicity ignored input in fact used
1443+
with pytest.warns(UserWarning, match=r"Input\(s\) specified as ignored is \(are\) used:") as record:
1444+
h = ct.interconnect([g,s,k],
1445+
inputs=['r'],
1446+
outputs=['y'],
1447+
ignore_inputs=['u','n'])
1448+
1449+
with pytest.warns(UserWarning, match=r"Input\(s\) specified as ignored is \(are\) used:") as record:
1450+
h = ct.interconnect([g,s,k],
1451+
inputs=['r'],
1452+
outputs=['y'],
1453+
ignore_inputs=['k.e','n'])
1454+
1455+
# error if ignored signal doesn't exist
1456+
with pytest.raises(ValueError):
1457+
h = ct.interconnect([g,s,k],
1458+
inputs=['r'],
1459+
outputs=['y'],
1460+
ignore_inputs=['v'])
1461+
1462+
1463+
def test_interconnect_unused_output():
1464+
# test that warnings about ignored outputs are reported, or not,
1465+
# as required
1466+
g = ct.LinearIOSystem(ct.ss(-1,1,[[1],[-1]],[[0],[1]]),
1467+
inputs=['u'],
1468+
outputs=['y','dy'],
1469+
name='g')
1470+
1471+
s = ct.summing_junction(inputs=['r','-y'],
1472+
outputs=['e'],
1473+
name='s')
1474+
1475+
k = ct.LinearIOSystem(ct.ss(0,10,2,0),
1476+
inputs=['e'],
1477+
outputs=['u'],
1478+
name='k')
1479+
1480+
with pytest.warns(UserWarning, match=r"Unused output\(s\) in InterconnectedSystem:") as record:
1481+
h = ct.interconnect([g,s,k],
1482+
inputs=['r'],
1483+
outputs=['y'])
1484+
print(record.list[0])
1485+
1486+
1487+
# no warning if output explicitly ignored
1488+
with pytest.warns(None) as record:
1489+
h = ct.interconnect([g,s,k],
1490+
inputs=['r'],
1491+
outputs=['y'],
1492+
ignore_outputs=['dy'])
1493+
1494+
h = ct.interconnect([g,s,k],
1495+
inputs=['r'],
1496+
outputs=['y'],
1497+
ignore_outputs=['g.dy'])
1498+
1499+
# no warning if auto-connect disabled
1500+
h = ct.interconnect([g,s,k],
1501+
connections=False)
1502+
1503+
#https://docs.pytest.org/en/6.2.x/warnings.html#recwarn
1504+
assert not record
1505+
1506+
# warn if explicity ignored output in fact used
1507+
with pytest.warns(UserWarning, match=r"Output\(s\) specified as ignored is \(are\) used:"):
1508+
h = ct.interconnect([g,s,k],
1509+
inputs=['r'],
1510+
outputs=['y'],
1511+
ignore_outputs=['dy','u'])
1512+
1513+
with pytest.warns(UserWarning, match=r"Output\(s\) specified as ignored is \(are\) used:"):
1514+
h = ct.interconnect([g,s,k],
1515+
inputs=['r'],
1516+
outputs=['y'],
1517+
ignore_outputs=['dy', ('k.u')])
1518+
1519+
# error if ignored signal doesn't exist
1520+
with pytest.raises(ValueError):
1521+
h = ct.interconnect([g,s,k],
1522+
inputs=['r'],
1523+
outputs=['y'],
1524+
ignore_outputs=['v'])

0 commit comments

Comments
 (0)