Skip to content

Commit ce20b24

Browse files
Merge pull request #652 from roryyorke/rory/check-interconnect-signals
Check for unused subsystem signals in InterconnectedSystem
2 parents affe4d3 + faf3145 commit ce20b24

2 files changed

Lines changed: 342 additions & 1 deletion

File tree

control/iosys.py

Lines changed: 205 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,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)

control/tests/iosys_test.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"""
1010

1111
from __future__ import print_function
12+
import re
1213

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

0 commit comments

Comments
 (0)