From aa9ef958c53dcdbf04384784ab5d1cabf42a08b2 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Fri, 21 Jul 2023 14:24:30 -0700 Subject: [PATCH 01/12] fix name of default name for duplcated system in interconnect --- control/nlsys.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control/nlsys.py b/control/nlsys.py index 82b6aeef3..2f8d549e4 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -2132,8 +2132,8 @@ def interconnect( If a system is duplicated in the list of systems to be connected, a warning is generated and a copy of the system is created with the name of the new system determined by adding the prefix and suffix - strings in config.defaults['iosys.linearized_system_name_prefix'] - and config.defaults['iosys.linearized_system_name_suffix'], with the + strings in config.defaults['iosys.duplicate_system_name_prefix'] + and config.defaults['iosys.duplicate_system_name_suffix'], with the default being to add the suffix '$copy' to the system name. In addition to explicit lists of system signals, it is possible to From 5e92bb405e7db4466933d1bf35cbe72065f1c0d7 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Mon, 24 Jul 2023 10:05:16 -0700 Subject: [PATCH 02/12] remove namedio from docstrings --- control/iosys.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control/iosys.py b/control/iosys.py index 99f0e7db6..53cda7d19 100644 --- a/control/iosys.py +++ b/control/iosys.py @@ -539,7 +539,7 @@ def isctime(sys, strict=False): return sys.isctime(strict) -# Utility function to parse nameio keywords +# Utility function to parse iosys keywords def _process_iosys_keywords( keywords={}, defaults={}, static=False, end=False): """Process iosys specification. @@ -611,7 +611,7 @@ def pop_with_default(kw, defval=None, return_list=True): return name, inputs, outputs, states, dt # -# Parse 'dt' in for named I/O system +# Parse 'dt' for I/O system # # The 'dt' keyword is used to set the timebase for a system. Its # processing is a bit unusual: if it is not specified at all, then the From 4e96553e2cf0e7bcb4b562c7347bf64f34f2bb50 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Mon, 24 Jul 2023 12:37:25 -0700 Subject: [PATCH 03/12] add signal_table method to show implicit interconnections --- control/nlsys.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/control/nlsys.py b/control/nlsys.py index 2f8d549e4..041a29e59 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -1001,6 +1001,69 @@ def unused_signals(self): return ({inputs[i][:2]: inputs[i][2] for i in unused_sysinp}, {outputs[i][:2]: outputs[i][2] for i in unused_sysout}) + def signal_table(self, show_names=False): + """Print table of signal names, sources, and destinations. + + Intended primarily for systems that have been connected implicitly + using signal names. + + Parameters + ---------- + show_names : bool (optional) + Instead of printing out the system number, print out the name of + each system. Default is False because system name is not usually + specified when performing implicit interconnection using + :func:`interconnect`. + + Examples + -------- + >>> P = ct.ss(1,1,1,0, inputs='u', outputs='y') + >>> C = ct.tf(10, [.1, 1], inputs='e', outputs='u') + >>> L = ct.interconnect([C, P], inputs='e', outputs='y') + >>> L.signal_table() + signal | source | destination + -------------------------------------------------------------- + e | input | system 0 + u | system 0 | system 1 + y | system 1 | output + """ + + spacing = 26 + print('signal'.ljust(10) + '| source'.ljust(spacing) + '| destination') + print('-'*(10 + spacing * 2)) + + # collect signal labels + signal_labels = [] + for sys in self.syslist: + signal_labels += sys.input_labels + sys.output_labels + signal_labels = set(signal_labels) + + for signal_label in signal_labels: + print(signal_label.ljust(10), end='') + sources = '| ' + dests = '| ' + + # overall interconnected system inputs and outputs + if self.find_input(signal_label) is not None: + sources += 'input' + if self.find_output(signal_label) is not None: + dests += 'output' + + # internal connections + for idx, sys in enumerate(self.syslist): + loc = sys.find_output(signal_label) + if loc is not None: + if not sources.endswith(' '): + sources += ', ' + sources += sys.name if show_names else 'system ' + str(idx) + loc = sys.find_input(signal_label) + if loc is not None: + if not dests.endswith(' '): + dests += ', ' + dests += sys.name if show_names else 'system ' + str(idx) + print(sources.ljust(spacing), end='') + print(dests.ljust(spacing), end='\n') + def _find_inputs_by_basename(self, basename): """Find all subsystem inputs matching basename From b5925c37408d110011b059c73fe23664c1c9bba2 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Mon, 24 Jul 2023 14:03:58 -0700 Subject: [PATCH 04/12] unit test --- control/tests/interconnect_test.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/control/tests/interconnect_test.py b/control/tests/interconnect_test.py index 3a333aef5..9779cbe08 100644 --- a/control/tests/interconnect_test.py +++ b/control/tests/interconnect_test.py @@ -201,6 +201,24 @@ def test_interconnect_docstring(): np.testing.assert_almost_equal(T.C @ T. A @ T.B, T_ss.C @ T_ss.A @ T_ss.B) np.testing.assert_almost_equal(T.D, T_ss.D) +def test_signal_table(capsys): + P = ct.ss(1,1,1,0, inputs='u', outputs='y') + C = ct.tf(10, [.1, 1], inputs='e', outputs='u') + L = ct.interconnect([C, P], inputs='e', outputs='y') + L.signal_table() + captured = capsys.readouterr().out + + # break the following strings separately because the printout order varies + # because signals are stored as a dict + mystrings = \ + ["signal | source | destination", + "-------------------------------------------------------------", + "e | input | system 0", + "u | system 0 | system 1", + "y | system 1 | output"] + + for str_ in mystrings: + assert str_ in captured def test_interconnect_exceptions(): # First make sure the docstring example works From 614de832ad906de71cd29433703b14014439e9d8 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Mon, 24 Jul 2023 15:16:28 -0700 Subject: [PATCH 05/12] add signal_table function, test against show_names --- control/nlsys.py | 37 ++++++++++++++++++++++++++++-- control/tests/interconnect_test.py | 35 +++++++++++++++++++--------- doc/control.rst | 1 + 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/control/nlsys.py b/control/nlsys.py index 041a29e59..80bb7d303 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -31,7 +31,7 @@ __all__ = ['NonlinearIOSystem', 'InterconnectedSystem', 'nlsys', 'input_output_response', 'find_eqpt', 'linearize', - 'interconnect'] + 'interconnect', 'signal_table'] class NonlinearIOSystem(InputOutputSystem): @@ -1020,7 +1020,7 @@ def signal_table(self, show_names=False): >>> P = ct.ss(1,1,1,0, inputs='u', outputs='y') >>> C = ct.tf(10, [.1, 1], inputs='e', outputs='u') >>> L = ct.interconnect([C, P], inputs='e', outputs='y') - >>> L.signal_table() + >>> L.signal_table() # doctest: +SKIP signal | source | destination -------------------------------------------------------------- e | input | system 0 @@ -2563,3 +2563,36 @@ def _convert_static_iosystem(sys): return NonlinearIOSystem( None, lambda t, x, u, params: sys @ u, outputs=sys.shape[0], inputs=sys.shape[1]) + +def signal_table(sys, **kwargs): + """Print table of signal names, sources, and destinations. + + Intended primarily for systems that have been connected implicitly + using signal names. + + Parameters + ---------- + sys : :class:`InterconnectedSystem` + Interconnected system object + show_names : bool (optional) + Instead of printing out the system number, print out the name of + each system. Default is False because system name is not usually + specified when performing implicit interconnection using + :func:`interconnect`. + + Examples + -------- + >>> P = ct.ss(1,1,1,0, inputs='u', outputs='y') + >>> C = ct.tf(10, [.1, 1], inputs='e', outputs='u') + >>> L = ct.interconnect([C, P], inputs='e', outputs='y') + >>> ct.signal_table(L) # doctest: +SKIP + signal | source | destination + -------------------------------------------------------------- + e | input | system 0 + u | system 0 | system 1 + y | system 1 | output + """ + assert isinstance(sys, InterconnectedSystem), "system must be"\ + "an InterconnectedSystem." + + sys.signal_table(**kwargs) diff --git a/control/tests/interconnect_test.py b/control/tests/interconnect_test.py index 9779cbe08..89827db79 100644 --- a/control/tests/interconnect_test.py +++ b/control/tests/interconnect_test.py @@ -201,24 +201,37 @@ def test_interconnect_docstring(): np.testing.assert_almost_equal(T.C @ T. A @ T.B, T_ss.C @ T_ss.A @ T_ss.B) np.testing.assert_almost_equal(T.D, T_ss.D) -def test_signal_table(capsys): - P = ct.ss(1,1,1,0, inputs='u', outputs='y') - C = ct.tf(10, [.1, 1], inputs='e', outputs='u') +@pytest.mark.parametrize("show_names", (True, False)) +def test_signal_table(capsys, show_names): + P = ct.ss(1,1,1,0, inputs='u', outputs='y', name='P') + C = ct.tf(10, [.1, 1], inputs='e', outputs='u', name='C') L = ct.interconnect([C, P], inputs='e', outputs='y') - L.signal_table() - captured = capsys.readouterr().out + L.signal_table(show_names=show_names) + captured_from_method = capsys.readouterr().out + + ct.signal_table(L, show_names=show_names) + captured_from_function = capsys.readouterr().out # break the following strings separately because the printout order varies - # because signals are stored as a dict + # because signal names are stored as a set mystrings = \ ["signal | source | destination", - "-------------------------------------------------------------", - "e | input | system 0", - "u | system 0 | system 1", - "y | system 1 | output"] + "-------------------------------------------------------------"] + if show_names: + mystrings += \ + ["e | input | C", + "u | C | P", + "y | P | output"] + else: + mystrings += \ + ["e | input | system 0", + "u | system 0 | system 1", + "y | system 1 | output"] for str_ in mystrings: - assert str_ in captured + assert str_ in captured_from_method + assert str_ in captured_from_function + def test_interconnect_exceptions(): # First make sure the docstring example works diff --git a/doc/control.rst b/doc/control.rst index a2fb8e69b..c9f8bc97f 100644 --- a/doc/control.rst +++ b/doc/control.rst @@ -36,6 +36,7 @@ System interconnections negate parallel series + signal_table Frequency domain plotting From 91aac8f5394bae7cd8716acf7dd332d7f43e7875 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Mon, 24 Jul 2023 15:57:08 -0700 Subject: [PATCH 06/12] remove **kwargs --- control/nlsys.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control/nlsys.py b/control/nlsys.py index 80bb7d303..b1f43fe39 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -2564,7 +2564,7 @@ def _convert_static_iosystem(sys): None, lambda t, x, u, params: sys @ u, outputs=sys.shape[0], inputs=sys.shape[1]) -def signal_table(sys, **kwargs): +def signal_table(sys, show_names=False): """Print table of signal names, sources, and destinations. Intended primarily for systems that have been connected implicitly @@ -2595,4 +2595,4 @@ def signal_table(sys, **kwargs): assert isinstance(sys, InterconnectedSystem), "system must be"\ "an InterconnectedSystem." - sys.signal_table(**kwargs) + sys.signal_table(show_names=show_names) From 796acad0a56855a103a48d586647869c3e383d4a Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Tue, 25 Jul 2023 10:28:09 -0700 Subject: [PATCH 07/12] switch docstring example to use system names --- control/nlsys.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/control/nlsys.py b/control/nlsys.py index b1f43fe39..2f64c3211 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -1017,15 +1017,15 @@ def signal_table(self, show_names=False): Examples -------- - >>> P = ct.ss(1,1,1,0, inputs='u', outputs='y') - >>> C = ct.tf(10, [.1, 1], inputs='e', outputs='u') + >>> P = ct.ss(1,1,1,0, inputs='u', outputs='y', name='P') + >>> C = ct.tf(10, [.1, 1], inputs='e', outputs='u', name='C') >>> L = ct.interconnect([C, P], inputs='e', outputs='y') - >>> L.signal_table() # doctest: +SKIP + >>> L.signal_table(show_names=True) # doctest: +SKIP signal | source | destination -------------------------------------------------------------- - e | input | system 0 - u | system 0 | system 1 - y | system 1 | output + e | input | C + u | C | P + y | P | output """ spacing = 26 @@ -1053,12 +1053,12 @@ def signal_table(self, show_names=False): for idx, sys in enumerate(self.syslist): loc = sys.find_output(signal_label) if loc is not None: - if not sources.endswith(' '): + if not sources.endswith(', '): sources += ', ' sources += sys.name if show_names else 'system ' + str(idx) loc = sys.find_input(signal_label) if loc is not None: - if not dests.endswith(' '): + if not dests.endswith(', '): dests += ', ' dests += sys.name if show_names else 'system ' + str(idx) print(sources.ljust(spacing), end='') @@ -2582,15 +2582,15 @@ def signal_table(sys, show_names=False): Examples -------- - >>> P = ct.ss(1,1,1,0, inputs='u', outputs='y') - >>> C = ct.tf(10, [.1, 1], inputs='e', outputs='u') + >>> P = ct.ss(1,1,1,0, inputs='u', outputs='y', name='P') + >>> C = ct.tf(10, [.1, 1], inputs='e', outputs='u', name='C') >>> L = ct.interconnect([C, P], inputs='e', outputs='y') - >>> ct.signal_table(L) # doctest: +SKIP + >>> L.signal_table(show_names=True) # doctest: +SKIP signal | source | destination -------------------------------------------------------------- - e | input | system 0 - u | system 0 | system 1 - y | system 1 | output + e | input | C + u | C | P + y | P | output """ assert isinstance(sys, InterconnectedSystem), "system must be"\ "an InterconnectedSystem." From b787a9ef8f44850161c8983d727ad8a2f20018c6 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Tue, 25 Jul 2023 14:58:02 -0700 Subject: [PATCH 08/12] add tests for auto-sum and auto-split --- control/nlsys.py | 16 +++---- control/tests/interconnect_test.py | 77 ++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 16 deletions(-) diff --git a/control/nlsys.py b/control/nlsys.py index 2f64c3211..d3fbc7609 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -1021,14 +1021,14 @@ def signal_table(self, show_names=False): >>> C = ct.tf(10, [.1, 1], inputs='e', outputs='u', name='C') >>> L = ct.interconnect([C, P], inputs='e', outputs='y') >>> L.signal_table(show_names=True) # doctest: +SKIP - signal | source | destination - -------------------------------------------------------------- - e | input | C - u | C | P - y | P | output + signal | source | destination + -------------------------------------------------------------------- + e | input | C + u | C | P + y | P | output """ - spacing = 26 + spacing = 32 print('signal'.ljust(10) + '| source'.ljust(spacing) + '| destination') print('-'*(10 + spacing * 2)) @@ -1053,12 +1053,12 @@ def signal_table(self, show_names=False): for idx, sys in enumerate(self.syslist): loc = sys.find_output(signal_label) if loc is not None: - if not sources.endswith(', '): + if not sources.endswith(' '): sources += ', ' sources += sys.name if show_names else 'system ' + str(idx) loc = sys.find_input(signal_label) if loc is not None: - if not dests.endswith(', '): + if not dests.endswith(' '): dests += ', ' dests += sys.name if show_names else 'system ' + str(idx) print(sources.ljust(spacing), end='') diff --git a/control/tests/interconnect_test.py b/control/tests/interconnect_test.py index 89827db79..675c94402 100644 --- a/control/tests/interconnect_test.py +++ b/control/tests/interconnect_test.py @@ -215,23 +215,84 @@ def test_signal_table(capsys, show_names): # break the following strings separately because the printout order varies # because signal names are stored as a set mystrings = \ - ["signal | source | destination", - "-------------------------------------------------------------"] + ["signal | source | destination", + "------------------------------------------------------------------"] if show_names: mystrings += \ - ["e | input | C", - "u | C | P", - "y | P | output"] + ["e | input | C", + "u | C | P", + "y | P | output"] else: mystrings += \ - ["e | input | system 0", - "u | system 0 | system 1", - "y | system 1 | output"] + ["e | input | system 0", + "u | system 0 | system 1", + "y | system 1 | output"] for str_ in mystrings: assert str_ in captured_from_method assert str_ in captured_from_function + # check auto-sum + P1 = ct.ss(1,1,1,0, inputs='u', outputs='y', name='P1') + P2 = ct.tf(10, [.1, 1], inputs='e', outputs='y', name='P2') + P3 = ct.tf(10, [.1, 1], inputs='x', outputs='y', name='P3') + P = ct.interconnect([P1, P2, P3], inputs=['e', 'u', 'x'], outputs='y') + P.signal_table(show_names=show_names) + captured_from_method = capsys.readouterr().out + + ct.signal_table(P, show_names=show_names) + captured_from_function = capsys.readouterr().out + + mystrings = \ + ["signal | source | destination", + "-------------------------------------------------------------------"] + if show_names: + mystrings += \ + ["u | input | P1", + "e | input | P2", + "x | input | P3", + "y | P1, P2, P3 | output"] + else: + mystrings += \ + ["u | input | system 0", + "e | input | system 1", + "x | input | system 2", + "y | system 0, system 1, system 2 | output"] + + for str_ in mystrings: + assert str_ in captured_from_method + assert str_ in captured_from_function + + # check auto-split + P1 = ct.ss(1,1,1,0, inputs='u', outputs='x', name='P1') + P2 = ct.tf(10, [.1, 1], inputs='u', outputs='y', name='P2') + P3 = ct.tf(10, [.1, 1], inputs='u', outputs='z', name='P3') + P = ct.interconnect([P1, P2, P3], inputs=['u'], outputs=['x','y','z']) + P.signal_table(show_names=show_names) + captured_from_method = capsys.readouterr().out + + ct.signal_table(P, show_names=show_names) + captured_from_function = capsys.readouterr().out + + mystrings = \ + ["signal | source | destination", + "-------------------------------------------------------------------"] + if show_names: + mystrings += \ + ["u | input | P1, P2, P3", + "x | P1 | output ", + "y | P2 | output", + "z | P3 | output"] + else: + mystrings += \ + ["u | input | system 0, system 1, system 2", + "x | system 0 | output ", + "y | system 1 | output", + "z | system 2 | output"] + + for str_ in mystrings: + assert str_ in captured_from_method + assert str_ in captured_from_function def test_interconnect_exceptions(): # First make sure the docstring example works From c2b4480c3d1e285e8a398427352209d89d49428c Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Mon, 14 Aug 2023 11:10:31 -0700 Subject: [PATCH 09/12] rename function to connection_table, add option to change column width --- control/nlsys.py | 51 ++++++++++++++++++------------ control/tests/interconnect_test.py | 14 ++++---- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/control/nlsys.py b/control/nlsys.py index d3fbc7609..05b45b8fc 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -31,7 +31,7 @@ __all__ = ['NonlinearIOSystem', 'InterconnectedSystem', 'nlsys', 'input_output_response', 'find_eqpt', 'linearize', - 'interconnect', 'signal_table'] + 'interconnect', 'connection_table'] class NonlinearIOSystem(InputOutputSystem): @@ -1001,11 +1001,11 @@ def unused_signals(self): return ({inputs[i][:2]: inputs[i][2] for i in unused_sysinp}, {outputs[i][:2]: outputs[i][2] for i in unused_sysout}) - def signal_table(self, show_names=False): - """Print table of signal names, sources, and destinations. + def connection_table(self, show_names=False, column_width=32): + """Print table of connections inside an interconnected system model. - Intended primarily for systems that have been connected implicitly - using signal names. + Intended primarily for :class:`InterconnectedSystems` that have been + connected implicitly using signal names. Parameters ---------- @@ -1014,13 +1014,15 @@ def signal_table(self, show_names=False): each system. Default is False because system name is not usually specified when performing implicit interconnection using :func:`interconnect`. + column_width : int (optional) + Character width of printed columns Examples -------- >>> P = ct.ss(1,1,1,0, inputs='u', outputs='y', name='P') >>> C = ct.tf(10, [.1, 1], inputs='e', outputs='u', name='C') >>> L = ct.interconnect([C, P], inputs='e', outputs='y') - >>> L.signal_table(show_names=True) # doctest: +SKIP + >>> L.connection_table(show_names=True) # doctest: +SKIP signal | source | destination -------------------------------------------------------------------- e | input | C @@ -1028,9 +1030,12 @@ def signal_table(self, show_names=False): y | P | output """ - spacing = 32 - print('signal'.ljust(10) + '| source'.ljust(spacing) + '| destination') - print('-'*(10 + spacing * 2)) + print('signal'.ljust(10) + '| source'.ljust(column_width) + \ + '| destination') + print('-'*(10 + column_width * 2)) + + # TODO: version of this method that is better suited + # to explicitly-connected systems # collect signal labels signal_labels = [] @@ -1061,8 +1066,12 @@ def signal_table(self, show_names=False): if not dests.endswith(' '): dests += ', ' dests += sys.name if show_names else 'system ' + str(idx) - print(sources.ljust(spacing), end='') - print(dests.ljust(spacing), end='\n') + if len(sources) >= column_width: + sources = sources[:column_width - 3] + '.. ' + print(sources.ljust(column_width), end='') + if len(dests) > column_width: + dests = dests[:column_width - 3] + '.. ' + print(dests.ljust(column_width), end='\n') def _find_inputs_by_basename(self, basename): """Find all subsystem inputs matching basename @@ -2018,7 +2027,7 @@ def interconnect( signals are given names, then the forms 'sys.sig' or ('sys', 'sig') are also recognized. Finally, for multivariable systems the signal index can be given as a list, for example '(subsys_i, [inp_j1, ..., - inp_jn])'; as a slice, for example, 'sys.sig[i:j]'; or as a base + inp_jn])'; or as a slice, for example, 'sys.sig[i:j]'; or as a base name `sys.sig` (which matches `sys.sig[i]`). Similarly, each output-spec should describe an output signal from @@ -2235,8 +2244,7 @@ def interconnect( 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: + if connections is False and not any((inplist, outlist, inputs, outputs)): # user has disabled auto-connect, and supplied neither input # nor output mappings; assume they know what they're doing check_unused = False @@ -2564,11 +2572,11 @@ def _convert_static_iosystem(sys): None, lambda t, x, u, params: sys @ u, outputs=sys.shape[0], inputs=sys.shape[1]) -def signal_table(sys, show_names=False): - """Print table of signal names, sources, and destinations. +def connection_table(sys, show_names=False, column_width=32): + """Print table of connections inside an interconnected system model. - Intended primarily for systems that have been connected implicitly - using signal names. + Intended primarily for :class:`InterconnectedSystems` that have been + connected implicitly using signal names. Parameters ---------- @@ -2579,13 +2587,16 @@ def signal_table(sys, show_names=False): each system. Default is False because system name is not usually specified when performing implicit interconnection using :func:`interconnect`. + column_width : int (optional) + Character width of printed columns + Examples -------- >>> P = ct.ss(1,1,1,0, inputs='u', outputs='y', name='P') >>> C = ct.tf(10, [.1, 1], inputs='e', outputs='u', name='C') >>> L = ct.interconnect([C, P], inputs='e', outputs='y') - >>> L.signal_table(show_names=True) # doctest: +SKIP + >>> L.connection_table(show_names=True) # doctest: +SKIP signal | source | destination -------------------------------------------------------------- e | input | C @@ -2595,4 +2606,4 @@ def signal_table(sys, show_names=False): assert isinstance(sys, InterconnectedSystem), "system must be"\ "an InterconnectedSystem." - sys.signal_table(show_names=show_names) + sys.connection_table(show_names=show_names) diff --git a/control/tests/interconnect_test.py b/control/tests/interconnect_test.py index 675c94402..e57567333 100644 --- a/control/tests/interconnect_test.py +++ b/control/tests/interconnect_test.py @@ -202,14 +202,14 @@ def test_interconnect_docstring(): np.testing.assert_almost_equal(T.D, T_ss.D) @pytest.mark.parametrize("show_names", (True, False)) -def test_signal_table(capsys, show_names): +def test_connection_table(capsys, show_names): P = ct.ss(1,1,1,0, inputs='u', outputs='y', name='P') C = ct.tf(10, [.1, 1], inputs='e', outputs='u', name='C') L = ct.interconnect([C, P], inputs='e', outputs='y') - L.signal_table(show_names=show_names) + L.connection_table(show_names=show_names) captured_from_method = capsys.readouterr().out - ct.signal_table(L, show_names=show_names) + ct.connection_table(L, show_names=show_names) captured_from_function = capsys.readouterr().out # break the following strings separately because the printout order varies @@ -237,10 +237,10 @@ def test_signal_table(capsys, show_names): P2 = ct.tf(10, [.1, 1], inputs='e', outputs='y', name='P2') P3 = ct.tf(10, [.1, 1], inputs='x', outputs='y', name='P3') P = ct.interconnect([P1, P2, P3], inputs=['e', 'u', 'x'], outputs='y') - P.signal_table(show_names=show_names) + P.connection_table(show_names=show_names) captured_from_method = capsys.readouterr().out - ct.signal_table(P, show_names=show_names) + ct.connection_table(P, show_names=show_names) captured_from_function = capsys.readouterr().out mystrings = \ @@ -268,10 +268,10 @@ def test_signal_table(capsys, show_names): P2 = ct.tf(10, [.1, 1], inputs='u', outputs='y', name='P2') P3 = ct.tf(10, [.1, 1], inputs='u', outputs='z', name='P3') P = ct.interconnect([P1, P2, P3], inputs=['u'], outputs=['x','y','z']) - P.signal_table(show_names=show_names) + P.connection_table(show_names=show_names) captured_from_method = capsys.readouterr().out - ct.signal_table(P, show_names=show_names) + ct.connection_table(P, show_names=show_names) captured_from_function = capsys.readouterr().out mystrings = \ From 58e1b601e84c2943a5a62eb42767a50da4627498 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Mon, 14 Aug 2023 12:07:33 -0700 Subject: [PATCH 10/12] added field to interconnectedSystem and LinearICSystem to keep track of whether connection was explicit or implicit and issue a warning in connection_table if explicit --- control/nlsys.py | 36 ++++++++++++++++++------------ control/statesp.py | 3 ++- control/tests/interconnect_test.py | 30 ++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/control/nlsys.py b/control/nlsys.py index 05b45b8fc..44eba2962 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -589,11 +589,14 @@ class InterconnectedSystem(NonlinearIOSystem): """ def __init__(self, syslist, connections=None, inplist=None, outlist=None, - params=None, warn_duplicate=None, **kwargs): + params=None, warn_duplicate=None, connection_type=None, + **kwargs): """Create an I/O system from a list of systems + connection info.""" from .statesp import _convert_to_statespace from .xferfcn import TransferFunction + self.connection_type = connection_type # explicit, implicit, or None + # Convert input and output names to lists if they aren't already if inplist is not None and not isinstance(inplist, list): inplist = [inplist] @@ -1034,8 +1037,10 @@ def connection_table(self, show_names=False, column_width=32): '| destination') print('-'*(10 + column_width * 2)) - # TODO: version of this method that is better suited - # to explicitly-connected systems + # TODO: update this method for explicitly-connected systems + if not self.connection_type == 'implicit': + warn('connection_table only gives useful output for implicitly-'\ + 'connected systems') # collect signal labels signal_labels = [] @@ -2239,6 +2244,7 @@ def interconnect( dt = kwargs.pop('dt', None) # bypass normal 'dt' processing name, inputs, outputs, states, _ = _process_iosys_keywords(kwargs) + connection_type = None # explicit, implicit, or None if not check_unused and (ignore_inputs or ignore_outputs): raise ValueError('check_unused is False, but either ' @@ -2249,8 +2255,10 @@ def interconnect( # 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 was not specified, assume implicit interconnection. + # set up default connection list if connections is None: + connection_type = 'implicit' # For each system input, look for outputs with the same name connections = [] for input_sys in syslist: @@ -2262,17 +2270,17 @@ def interconnect( if len(connect) > 1: connections.append(connect) - auto_connect = True - elif connections is False: check_unused = False # Use an empty connections list connections = [] - elif isinstance(connections, list) and \ - all([isinstance(cnxn, (str, tuple)) for cnxn in connections]): - # Special case where there is a single connection - connections = [connections] + else: + connection_type = 'explicit' + if isinstance(connections, list) and \ + all([isinstance(cnxn, (str, tuple)) for cnxn in connections]): + # Special case where there is a single connection + connections = [connections] # If inplist/outlist is not present, try using inputs/outputs instead inplist_none, outlist_none = False, False @@ -2507,7 +2515,7 @@ def interconnect( syslist, connections=connections, inplist=inplist, outlist=outlist, inputs=inputs, outputs=outputs, states=states, params=params, dt=dt, name=name, warn_duplicate=warn_duplicate, - **kwargs) + connection_type=connection_type, **kwargs) # See if we should add any signals if add_unused: @@ -2528,7 +2536,7 @@ def interconnect( syslist, connections=connections, inplist=inplist, outlist=outlist, inputs=inputs, outputs=outputs, states=states, params=params, dt=dt, name=name, warn_duplicate=warn_duplicate, - **kwargs) + connection_type=connection_type, **kwargs) # check for implicitly dropped signals if check_unused: @@ -2536,7 +2544,7 @@ def interconnect( # If all subsystems are linear systems, maintain linear structure if all([isinstance(sys, StateSpace) for sys in newsys.syslist]): - return LinearICSystem(newsys, None) + newsys = LinearICSystem(newsys, None, connection_type=connection_type) return newsys @@ -2606,4 +2614,4 @@ def connection_table(sys, show_names=False, column_width=32): assert isinstance(sys, InterconnectedSystem), "system must be"\ "an InterconnectedSystem." - sys.connection_table(show_names=show_names) + sys.connection_table(show_names=show_names, column_width=column_width) diff --git a/control/statesp.py b/control/statesp.py index 362945ad6..38dd2388d 100644 --- a/control/statesp.py +++ b/control/statesp.py @@ -1459,7 +1459,7 @@ class LinearICSystem(InterconnectedSystem, StateSpace): """ - def __init__(self, io_sys, ss_sys=None): + def __init__(self, io_sys, ss_sys=None, connection_type=None): # # Because this is a "hybrid" object, the initialization proceeds in # stages. We first create an empty InputOutputSystem of the @@ -1483,6 +1483,7 @@ def __init__(self, io_sys, ss_sys=None): self.input_map = io_sys.input_map self.output_map = io_sys.output_map self.params = io_sys.params + self.connection_type = connection_type # If we didnt' get a state space system, linearize the full system if ss_sys is None: diff --git a/control/tests/interconnect_test.py b/control/tests/interconnect_test.py index e57567333..a37b18eec 100644 --- a/control/tests/interconnect_test.py +++ b/control/tests/interconnect_test.py @@ -276,7 +276,7 @@ def test_connection_table(capsys, show_names): mystrings = \ ["signal | source | destination", - "-------------------------------------------------------------------"] + "-------------------------------------------------------------------"] if show_names: mystrings += \ ["u | input | P1, P2, P3", @@ -294,6 +294,34 @@ def test_connection_table(capsys, show_names): assert str_ in captured_from_method assert str_ in captured_from_function + # check change column width + P.connection_table(show_names=show_names, column_width=20) + captured_from_method = capsys.readouterr().out + + ct.connection_table(P, show_names=show_names, column_width=20) + captured_from_function = capsys.readouterr().out + + mystrings = \ + ["signal | source | destination", + "------------------------------------------------"] + if show_names: + mystrings += \ + ["u | input | P1, P2, P3", + "x | P1 | output ", + "y | P2 | output", + "z | P3 | output"] + else: + mystrings += \ + ["u | input | system 0, syste.. ", + "x | system 0 | output ", + "y | system 1 | output", + "z | system 2 | output"] + + for str_ in mystrings: + assert str_ in captured_from_method + assert str_ in captured_from_function + + def test_interconnect_exceptions(): # First make sure the docstring example works P = ct.tf(1, [1, 0], input='u', output='y') From 36379972b9f538990f72ab079f70c89c71576d58 Mon Sep 17 00:00:00 2001 From: Sawyer Fuller Date: Mon, 14 Aug 2023 12:55:38 -0700 Subject: [PATCH 11/12] added connection_table to documentation in control.rst --- doc/control.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/control.rst b/doc/control.rst index c9f8bc97f..96714bf7d 100644 --- a/doc/control.rst +++ b/doc/control.rst @@ -36,7 +36,7 @@ System interconnections negate parallel series - signal_table + connection_table Frequency domain plotting From 5c23f1a64dfe8c2927a27c96966a5616152318a6 Mon Sep 17 00:00:00 2001 From: Richard Murray Date: Sat, 16 Sep 2023 08:17:54 -0700 Subject: [PATCH 12/12] change "(optional)" to ", optional" per numpydoc --- control/nlsys.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/control/nlsys.py b/control/nlsys.py index 44eba2962..fd6e207fc 100644 --- a/control/nlsys.py +++ b/control/nlsys.py @@ -395,7 +395,7 @@ def dynamics(self, t, x, u, params=None): current state u : array_like input - params : dict (optional) + params : dict, optional system parameter values Returns @@ -436,7 +436,7 @@ def output(self, t, x, u, params=None): current state u : array_like input - params : dict (optional) + params : dict, optional system parameter values Returns @@ -1012,13 +1012,13 @@ def connection_table(self, show_names=False, column_width=32): Parameters ---------- - show_names : bool (optional) + show_names : bool, optional Instead of printing out the system number, print out the name of each system. Default is False because system name is not usually specified when performing implicit interconnection using :func:`interconnect`. - column_width : int (optional) - Character width of printed columns + column_width : int, optional + Character width of printed columns. Examples -------- @@ -2590,13 +2590,13 @@ def connection_table(sys, show_names=False, column_width=32): ---------- sys : :class:`InterconnectedSystem` Interconnected system object - show_names : bool (optional) + show_names : bool, optional Instead of printing out the system number, print out the name of each system. Default is False because system name is not usually specified when performing implicit interconnection using :func:`interconnect`. - column_width : int (optional) - Character width of printed columns + column_width : int, optional + Character width of printed columns. Examples