From 78b3349ff791750994bd094a96892b20825b95f1 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 11 Sep 2021 12:12:12 +0200 Subject: [PATCH 1/3] Check for unused subsystem signals in InterconnectedSystem Add capability to check for unused signals in InterconnectedSystem; this check is invoked by default by `interconnect`. --- control/iosys.py | 217 +++++++++++++++++++++++++++++++++++- control/tests/iosys_test.py | 126 +++++++++++++++++++++ 2 files changed, 342 insertions(+), 1 deletion(-) diff --git a/control/iosys.py b/control/iosys.py index 479039c3d..c10f1696e 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -1398,7 +1398,164 @@ def set_output_map(self, output_map): self.noutputs = output_map.shape[0] + def unused_signals(self): + """Find unused subsystem inputs and outputs + + Returns + ------- + + unused_inputs : dict + + A mapping from tuple of indices (isys, isig) to string + '{sys}.{sig}', for all unused subsystem inputs. + + unused_outputs : dict + + A mapping from tuple of indices (isys, isig) to string + '{sys}.{sig}', for all unused subsystem outputs. + + """ + used_sysinp_via_inp = np.nonzero(self.input_map)[0] + used_sysout_via_out = np.nonzero(self.output_map)[1] + used_sysinp_via_con, used_sysout_via_con = np.nonzero(self.connect_map) + + used_sysinp = set(used_sysinp_via_inp) | set(used_sysinp_via_con) + used_sysout = set(used_sysout_via_out) | set(used_sysout_via_con) + + nsubsysinp = sum(sys.ninputs for sys in self.syslist) + nsubsysout = sum(sys.noutputs for sys in self.syslist) + + unused_sysinp = sorted(set(range(nsubsysinp)) - used_sysinp) + unused_sysout = sorted(set(range(nsubsysout)) - used_sysout) + + inputs = [(isys,isig, f'{sys.name}.{sig}') + for isys, sys in enumerate(self.syslist) + for sig, isig in sys.input_index.items()] + + outputs = [(isys,isig,f'{sys.name}.{sig}') + for isys, sys in enumerate(self.syslist) + for sig, isig in sys.output_index.items()] + + return ({inputs[i][:2]:inputs[i][2] + for i in unused_sysinp}, + {outputs[i][:2]:outputs[i][2] + for i in unused_sysout}) + + + def _find_inputs_by_basename(self, basename): + """Find all subsystem inputs matching basename + + Returns + ------- + Mapping from (isys, isig) to '{sys}.{sig}' + + """ + return {(isys, isig) : f'{sys.name}.{basename}' + for isys, sys in enumerate(self.syslist) + for sig, isig in sys.input_index.items() + if sig == (basename)} + + + def _find_outputs_by_basename(self, basename): + """Find all subsystem outputs matching basename + + Returns + ------- + Mapping from (isys, isig) to '{sys}.{sig}' + + """ + return {(isys, isig) : f'{sys.name}.{basename}' + for isys, sys in enumerate(self.syslist) + for sig, isig in sys.output_index.items() + if sig == (basename)} + + + def check_unused_signals(self, ignore_inputs=None, ignore_outputs=None): + """Check for unused subsystem inputs and outputs + + If any unused inputs or outputs are found, emit a warning. + + Parameters + ---------- + ignore_inputs : list of input-spec + Subsystem inputs known to be unused. input-spec can be any of: + 'sig', 'sys.sig', (isys, isig), ('sys', isig) + + If the 'sig' form is used, all subsystem inputs with that + name are considered ignored. + + ignore_outputs : list of output-spec + Subsystem outputs known to be unused. output-spec can be any of: + 'sig', 'sys.sig', (isys, isig), ('sys', isig) + + If the 'sig' form is used, all subsystem outputs with that + name are considered ignored. + + """ + + if ignore_inputs is None: + ignore_inputs = [] + + if ignore_outputs is None: + ignore_outputs = [] + + unused_inputs, unused_outputs = self.unused_signals() + + # (isys, isig) -> signal-spec + ignore_input_map = {} + for ignore_input in ignore_inputs: + if isinstance(ignore_input, str) and '.' not in ignore_input: + ignore_idxs = self._find_inputs_by_basename(ignore_input) + if not ignore_idxs: + raise ValueError(f"Couldn't find ignored input {ignore_input} in subsystems") + ignore_input_map.update(ignore_idxs) + else: + ignore_input_map[self._parse_signal(ignore_input, 'input')[:2]] = ignore_input + + # (isys, isig) -> signal-spec + ignore_output_map = {} + for ignore_output in ignore_outputs: + if isinstance(ignore_output, str) and '.' not in ignore_output: + ignore_found = self._find_outputs_by_basename(ignore_output) + if not ignore_found: + raise ValueError(f"Couldn't find ignored output {ignore_output} in subsystems") + ignore_output_map.update(ignore_found) + else: + ignore_output_map[self._parse_signal(ignore_output, 'output')[:2]] = ignore_output + + dropped_inputs = set(unused_inputs) - set(ignore_input_map) + dropped_outputs = set(unused_outputs) - set(ignore_output_map) + + used_ignored_inputs = set(ignore_input_map) - set(unused_inputs) + used_ignored_outputs = set(ignore_output_map) - set(unused_outputs) + + if dropped_inputs: + msg = ('Unused input(s) in InterconnectedSystem: ' + + '; '.join(f'{inp}={unused_inputs[inp]}' + for inp in dropped_inputs)) + warn(msg) + + if dropped_outputs: + msg = ('Unused output(s) in InterconnectedSystem: ' + + '; '.join(f'{out} : {unused_outputs[out]}' + for out in dropped_outputs)) + warn(msg) + + if used_ignored_inputs: + msg = ('Input(s) specified as ignored is (are) used: ' + + '; '.join(f'{inp} : {ignore_input_map[inp]}' + for inp in used_ignored_inputs)) + warn(msg) + + if used_ignored_outputs: + msg = ('Output(s) specified as ignored is (are) used: ' + + '; '.join(f'{out}={ignore_output_map[out]}' + for out in used_ignored_outputs)) + warn(msg) + + class LinearICSystem(InterconnectedSystem, LinearIOSystem): + """Interconnection of a set of linear input/output systems. This class is used to implement a system that is an interconnection of @@ -2020,7 +2177,9 @@ def tf2io(*args, **kwargs): # Function to create an interconnected system def interconnect(syslist, connections=None, inplist=[], outlist=[], inputs=None, outputs=None, states=None, - params={}, dt=None, name=None, **kwargs): + params={}, dt=None, name=None, + check_unused=True, ignore_inputs=None, ignore_outputs=None, + **kwargs): """Interconnect a set of input/output systems. 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=[], System name (used for specifying signals). If unspecified, a generic name is generated with a unique integer id. + check_unused : bool + If True, check for unused sub-system signals. This check is + not done if connections is False, and not input and output + mappings are specified. + + ignore_inputs : list of input-spec + + A list of sub-system known not to be connected. This is + *only* used in checking for unused signals, and does not + disable use of the input. + + Besides the usual input-spec forms (see `connections`), an + input-spec can be a string give just the signal name, as for inpu + + ignore_inputs : list of input-spec + + A list of sub-system inputs known not to be connected. This is + *only* used in checking for unused signals, and does not + disable use of the input. + + Besides the usual input-spec forms (see `connections`), an + input-spec can be just the signal base name, in which case all + signals from all sub-systems with that base name are + considered ignored. + + ignore_outputs : list of output-spec + + A list of sub-system outputs known not to be connected. This + is *only* used in checking for unused signals, and does not + disable use of the output. + + Besides the usual output-spec forms (see `connections`), an + output-spec can be just the signal base name, in which all + outputs from all sub-systems with that base name are + considered ignored. + + Example ------- >>> P = control.LinearIOSystem( @@ -2199,6 +2395,17 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], inputs = _parse_signal_parameter(inputs, 'input', kwargs) outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True) + if not check_unused and (ignore_inputs or ignore_outputs): + raise ValueError('check_unused is False, but either ' + + 'ignore_inputs or ignore_outputs non-empty') + + if (connections is False + and not inplist and not outlist + and not inputs and not outputs): + # user has disabled auto-connect, and supplied neither input + # nor output mappings; assume they know what they're doing + check_unused = False + # If connections was not specified, set up default connection list if connections is None: # For each system input, look for outputs with the same name @@ -2211,7 +2418,11 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], connect.append(output_sys.name + "." + input_name) if len(connect) > 1: connections.append(connect) + + auto_connect = True + elif connections is False: + check_unused = False # Use an empty connections list connections = [] @@ -2282,6 +2493,10 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], inputs=inputs, outputs=outputs, states=states, params=params, dt=dt, name=name) + + # check for implicity dropped signals + if check_unused: + newsys.check_unused_signals(ignore_inputs, ignore_outputs) # If all subsystems are linear systems, maintain linear structure if all([isinstance(sys, LinearIOSystem) for sys in syslist]): return LinearICSystem(newsys, None) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index 8acd83632..cd70ab396 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -1396,3 +1396,129 @@ def secord_update(t, x, u, params={}): def secord_output(t, x, u, params={}): """Second order system dynamics output""" return np.array([x[0]]) + + +def test_interconnect_unused_input(): + # test that warnings about unused inputs are reported, or not, + # as required + g = ct.LinearIOSystem(ct.ss(-1,1,1,0), + inputs=['u'], + outputs=['y'], + name='g') + + s = ct.summing_junction(inputs=['r','-y','-n'], + outputs=['e'], + name='s') + + k = ct.LinearIOSystem(ct.ss(0,10,2,0), + inputs=['e'], + outputs=['u'], + name='k') + + with pytest.warns(UserWarning, match=r"Unused input\(s\) in InterconnectedSystem"): + h = ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y']) + + with pytest.warns(None) as record: + # no warning if output explicitly ignored, various argument forms + h = ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_inputs=['n']) + + h = ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_inputs=['s.n']) + + # no warning if auto-connect disabled + h = ct.interconnect([g,s,k], + connections=False) + + #https://docs.pytest.org/en/6.2.x/warnings.html#recwarn + assert not record + + # warn if explicity ignored input in fact used + with pytest.warns(UserWarning, match=r"Input\(s\) specified as ignored is \(are\) used:") as record: + h = ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_inputs=['u','n']) + + with pytest.warns(UserWarning, match=r"Input\(s\) specified as ignored is \(are\) used:") as record: + h = ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_inputs=['k.e','n']) + + # error if ignored signal doesn't exist + with pytest.raises(ValueError): + h = ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_inputs=['v']) + + +def test_interconnect_unused_output(): + # test that warnings about ignored outputs are reported, or not, + # as required + g = ct.LinearIOSystem(ct.ss(-1,1,[[1],[-1]],[[0],[1]]), + inputs=['u'], + outputs=['y','dy'], + name='g') + + s = ct.summing_junction(inputs=['r','-y'], + outputs=['e'], + name='s') + + k = ct.LinearIOSystem(ct.ss(0,10,2,0), + inputs=['e'], + outputs=['u'], + name='k') + + with pytest.warns(UserWarning, match=r"Unused output\(s\) in InterconnectedSystem:") as record: + h = ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y']) + print(record.list[0]) + + + # no warning if output explicitly ignored + with pytest.warns(None) as record: + h = ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_outputs=['dy']) + + h = ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_outputs=['g.dy']) + + # no warning if auto-connect disabled + h = ct.interconnect([g,s,k], + connections=False) + + #https://docs.pytest.org/en/6.2.x/warnings.html#recwarn + assert not record + + # warn if explicity ignored output in fact used + with pytest.warns(UserWarning, match=r"Output\(s\) specified as ignored is \(are\) used:"): + h = ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_outputs=['dy','u']) + + with pytest.warns(UserWarning, match=r"Output\(s\) specified as ignored is \(are\) used:"): + h = ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_outputs=['dy', ('k.u')]) + + # error if ignored signal doesn't exist + with pytest.raises(ValueError): + h = ct.interconnect([g,s,k], + inputs=['r'], + outputs=['y'], + ignore_outputs=['v']) From 2224ea522585717fbb3a894d3cb2dd19d26a377a Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sat, 11 Sep 2021 13:02:28 +0200 Subject: [PATCH 2/3] Handle matrix warnings in test_interconnect_unused_{input,output} Ignore warnings with match string from conftest.py's `matrixfilter` warning filter. --- control/tests/iosys_test.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/control/tests/iosys_test.py b/control/tests/iosys_test.py index cd70ab396..ba56fcea3 100644 --- a/control/tests/iosys_test.py +++ b/control/tests/iosys_test.py @@ -9,6 +9,7 @@ """ from __future__ import print_function +import re import numpy as np import pytest @@ -1437,7 +1438,13 @@ def test_interconnect_unused_input(): connections=False) #https://docs.pytest.org/en/6.2.x/warnings.html#recwarn - assert not record + for r in record: + # strip out matrix warnings + if re.match(r'.*matrix subclass', str(r.message)): + continue + print(r.message) + pytest.fail(f'Unexpected warning: {r.message}') + # warn if explicity ignored input in fact used with pytest.warns(UserWarning, match=r"Input\(s\) specified as ignored is \(are\) used:") as record: @@ -1481,7 +1488,6 @@ def test_interconnect_unused_output(): h = ct.interconnect([g,s,k], inputs=['r'], outputs=['y']) - print(record.list[0]) # no warning if output explicitly ignored @@ -1501,7 +1507,12 @@ def test_interconnect_unused_output(): connections=False) #https://docs.pytest.org/en/6.2.x/warnings.html#recwarn - assert not record + for r in record: + # strip out matrix warnings + if re.match(r'.*matrix subclass', str(r.message)): + continue + print(r.message) + pytest.fail(f'Unexpected warning: {r.message}') # warn if explicity ignored output in fact used with pytest.warns(UserWarning, match=r"Output\(s\) specified as ignored is \(are\) used:"): From faf3145753cce81f0fac9bd07030e043e1d30084 Mon Sep 17 00:00:00 2001 From: Rory Yorke Date: Sun, 12 Sep 2021 05:34:04 +0200 Subject: [PATCH 3/3] Fix doc string for interconnect --- control/iosys.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index c10f1696e..876a90ccf 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -2306,20 +2306,10 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], check_unused : bool If True, check for unused sub-system signals. This check is - not done if connections is False, and not input and output + not done if connections is False, and neither input nor output mappings are specified. ignore_inputs : list of input-spec - - A list of sub-system known not to be connected. This is - *only* used in checking for unused signals, and does not - disable use of the input. - - Besides the usual input-spec forms (see `connections`), an - input-spec can be a string give just the signal name, as for inpu - - ignore_inputs : list of input-spec - A list of sub-system inputs known not to be connected. This is *only* used in checking for unused signals, and does not disable use of the input. @@ -2330,7 +2320,6 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[], considered ignored. ignore_outputs : list of output-spec - A list of sub-system outputs known not to be connected. This is *only* used in checking for unused signals, and does not disable use of the output. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy