8000 Merge pull request #925 from sawyerbfuller/connections · python-control/python-control@405bf77 · GitHub
[go: up one dir, main page]

Skip to content

Commit 405bf77

Browse files
authored
Merge pull request #925 from sawyerbfuller/connections
2 parents 42c6fb1 + 5c23f1a commit 405bf77

File tree

5 files changed

+259
-22
lines changed

5 files changed

+259
-22
lines changed

control/iosys.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ def isctime(sys, strict=False):
539539
return sys.isctime(strict)
540540

541541

542-
# Utility function to parse nameio keywords
542+
# Utility function to parse iosys keywords
543543
def _process_iosys_keywords(
544544
keywords={}, defaults={}, static=False, end=False):
545545
"""Process iosys specification.
@@ -611,7 +611,7 @@ def pop_with_default(kw, defval=None, return_list=True):
611611
return name, inputs, outputs, states, dt
612612

613613
#
614-
# Parse 'dt' in for named I/O system
614+
# Parse 'dt' for I/O system
615615
#
616616
# The 'dt' keyword is used to set the timebase for a system. Its
617617
# processing is a bit unusual: if it is not specified at all, then the

control/nlsys.py

Lines changed: 134 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
__all__ = ['NonlinearIOSystem', 'InterconnectedSystem', 'nlsys',
3333
'input_output_response', 'find_eqpt', 'linearize',
34-
'interconnect']
34+
'interconnect', 'connection_table']
3535

3636

3737
class NonlinearIOSystem(InputOutputSystem):
@@ -395,7 +395,7 @@ def dynamics(self, t, x, u, params=None):
395395
current state
396396
u : array_like
397397
input
398-
params : dict (optional)
398+
params : dict, optional
399399
system parameter values
400400
401401
Returns
@@ -436,7 +436,7 @@ def output(self, t, x, u, params=None):
436436
current state
437437
u : array_like
438438
input
439-
params : dict (optional)
439+
params : dict, optional
440440
system parameter values
441441
442442
Returns
@@ -589,11 +589,14 @@ class InterconnectedSystem(NonlinearIOSystem):
589589
590590
"""
591591
def __init__(self, syslist, connections=None, inplist=None, outlist=None,
592-
params=None, warn_duplicate=None, **kwargs):
592+
params=None, warn_duplicate=None, connection_type=None,
593+
**kwargs):
593594
"""Create an I/O system from a list of systems + connection info."""
594595
from .statesp import _convert_to_statespace
595596
from .xferfcn import TransferFunction
596597

598+
self.connection_type = connection_type # explicit, implicit, or None
599+
597600
# Convert input and output names to lists if they aren't already
598601
if inplist is not None and not isinstance(inplist, list):
599602
inplist = [inplist]
@@ -1001,6 +1004,80 @@ def unused_signals(self):
10011004
return ({inputs[i][:2]: inputs[i][2] for i in unused_sysinp},
10021005
{outputs[i][:2]: outputs[i][2] for i in unused_sysout})
10031006

1007+
def connection_table(self, show_names=False, column_width=32):
1008+
"""Print table of connections inside an interconnected system model.
1009+
1010+
Intended primarily for :class:`InterconnectedSystems` that have been
1011+
connected implicitly using signal names.
1012+
1013+
Parameters
1014+
----------
1015+
show_names : bool, optional
1016+
Instead of printing out the system number, print out the name of
1017+
each system. Default is False because system name is not usually
1018+
specified when performing implicit interconnection using
1019+
:func:`interconnect`.
1020+
column_width : int, optional
1021+
Character width of printed columns.
1022+
1023+
Examples
1024+
--------
1025+
>>> P = ct.ss(1,1,1,0, inputs='u', outputs='y', name='P')
1026+
>>> C = ct.tf(10, [.1, 1], inputs='e', outputs='u', name='C')
1027+
>>> L = ct.interconnect([C, P], inputs='e', outputs='y')
1028+
>>> L.connection_table(show_names=True) # doctest: +SKIP
1029+
signal | source | destination
1030+
--------------------------------------------------------------------
1031+
e | input | C
1032+
u | C | P
1033+
y | P | output
1034+
"""
1035+
1036+
print('signal'.ljust(10) + '| source'.ljust(column_width) + \
1037+
'| destination')
1038+
print('-'*(10 + column_width * 2))
1039+
1040+
# TODO: update this method for explicitly-connected systems
1041+
if not self.connection_type == 'implicit':
1042+
warn('connection_table only gives useful output for implicitly-'\
1043+
'connected systems')
1044+
1045+
# collect signal labels
1046+
signal_labels = []
1047+
for sys in self.syslist:
1048+
signal_labels += sys.input_labels + sys.output_labels
1049+
signal_labels = set(signal_labels)
1050+
1051+
for signal_label in signal_labels:
1052+
print(signal_label.ljust(10), end='')
1053+
sources = '| '
1054+
dests = '| '
1055+
1056+
# overall interconnected system inputs and outputs
1057+
if self.find_input(signal_label) is not None:
1058+
sources += 'input'
1059+
if self.find_output(signal_label) is not None:
1060+
dests += 'output'
1061+
1062+
# internal connections
1063+
for idx, sys in enumerate(self.syslist):
1064+
loc = sys.find_output(signal_label)
1065+
if loc is not None:
1066+
if not sources.endswith(' '):
1067+
sources += ', '
1068+
sources += sys.name if show_names else 'system ' + str(idx)
1069+
loc = sys.find_input(signal_label)
1070+
if loc is not None:
1071+
if not dests.endswith(' '):
1072+
dests += ', '
1073+
dests += sys.name if show_names else 'system ' + str(idx)
1074+
if len(sources) >= column_width:
1075+
sources = sources[:column_width - 3] + '.. '
1076+
print(sources.ljust(column_width), end='')
1077+
if len(dests) > column_width:
1078+
dests = dests[:column_width - 3] + '.. '
1079+
print(dests.ljust(column_width), end='\n')
1080+
10041081
def _find_inputs_by_basename(self, basename):
10051082
"""Find all subsystem inputs matching basename
10061083
@@ -1955,7 +2032,7 @@ def interconnect(
19552032
signals are given names, then the forms 'sys.sig' or ('sys', 'sig')
19562033
are also recognized. Finally, for multivariable systems the signal
19572034
index can be given as a list, for example '(subsys_i, [inp_j1, ...,
1958-
inp_jn])'; as a slice, for example, 'sys.sig[i:j]'; or as a base
2035+
inp_jn])'; or as a slice, for example, 'sys.sig[i:j]'; or as a base
19592036
name `sys.sig` (which matches `sys.sig[i]`).
19602037
19612038
Similarly, each output-spec should describe an output signal from
@@ -2132,8 +2209,8 @@ def interconnect(
21322209
If a system is duplicated in the list of systems to be connected,
21332210
a warning is generated and a copy of the system is created with the
21342211
name of the new system determined by adding the prefix and suffix
2135-
strings in config.defaults['iosys.linearized_system_name_prefix']
2136-
and config.defaults['iosys.linearized_system_name_suffix'], with the
2212+
strings in config.defaults['iosys.duplicate_system_name_prefix']
2213+
and config.defaults['iosys.duplicate_system_name_suffix'], with the
21372214
default being to add the suffix '$copy' to the system name.
21382215
21392216
In addition to explicit lists of system signals, it is possible to
@@ -2167,19 +2244,21 @@ def interconnect(
21672244

21682245
dt = kwargs.pop('dt', None) # bypass normal 'dt' processing
21692246
name, inputs, outputs, states, _ = _process_iosys_keywords(kwargs)
2247+
connection_type = None # explicit, implicit, or None
21702248

21712249
if not check_unused and (ignore_inputs or ignore_outputs):
21722250
raise ValueError('check_unused is False, but either '
21732251
+ 'ignore_inputs or ignore_outputs non-empty')
21742252

2175-
if connections is False and not inplist and not outlist \
2176-
and not inputs and not outputs:
2253+
if connections is False and not any((inplist, outlist, inputs, outputs)):
21772254
# user has disabled auto-connect, and supplied neither input
21782255
# nor output mappings; assume they know what they're doing
21792256
check_unused = False
21802257

2181-
# If connections was not specified, set up default connection list
2258+
# If connections was not specified, assume implicit interconnection.
2259+
# set up default connection list
21822260
if connections is None:
2261+
connection_type = 'implicit'
21832262
# For each system input, look for outputs with the same name
21842263
connections = []
21852264
for input_sys in syslist:
@@ -2191,17 +2270,17 @@ def interconnect(
21912270
if len(connect) > 1:
21922271
connections.append(connect)
21932272

2194-
auto_connect = True
2195-
21962273
elif connections is False:
21972274
check_unused = False
21982275
# Use an empty connections list
21992276
connections = []
22002277

2201-
elif isinstance(connections, list) and \
2202-
all([isinstance(cnxn, (str, tuple)) for cnxn in connections]):
2203-
# Special case where there is a single connection
2204-
connections = [connections]
2278+
else:
2279+
connection_type = 'explicit'
2280+
if isinstance(connections, list) and \
2281+
all([isinstance(cnxn, (str, tuple)) for cnxn in connections]):
2282+
# Special case where there is a single connection
2283+
connections = [connections]
22052284

22062285
# If inplist/outlist is not present, try using inputs/outputs instead
22072286
inplist_none, outlist_none = False, False
@@ -2436,7 +2515,7 @@ def interconnect(
24362515
syslist, connections=connections, inplist=inplist,
24372516
outlist=outlist, inputs=inputs, outputs=outputs, states=states,
24382517
params=params, dt=dt, name=name, warn_duplicate=warn_duplicate,
2439-
**kwargs)
2518+
connection_type=connection_type, **kwargs)
24402519

24412520
# See if we should add any signals
24422521
if add_unused:
@@ -2457,15 +2536,15 @@ def interconnect(
24572536
syslist, connections=connections, inplist=inplist,
24582537
outlist=outlist, inputs=inputs, outputs=outputs, states=states,
24592538
params=params, dt=dt, name=name, warn_duplicate=warn_duplicate,
2460-
**kwargs)
2539+
connection_type=connection_type, **kwargs)
24612540

24622541
# check for implicitly dropped signals
24632542
if check_unused:
24642543
newsys.check_unused_signals(ignore_inputs, ignore_outputs)
24652544

24662545
# If all subsystems are linear systems, maintain linear structure
24672546
if all([isinstance(sys, StateSpace) for sys in newsys.syslist]):
2468-
return LinearICSystem(newsys, None)
2547+
newsys = LinearICSystem(newsys, None, connection_type=connection_type)
24692548

24702549
return newsys
24712550

@@ -2500,3 +2579,39 @@ def _convert_static_iosystem(sys):
25002579
return NonlinearIOSystem(
25012580
None, lambda t, x, u, params: sys @ u,
25022581
outputs=sys.shape[0], inputs=sys.shape[1])
2582+
2583+
def connection_table(sys, show_names=False, column_width=32):
2584+
"""Print table of connections inside an interconnected system model.
2585+
2586+
Intended primarily for :class:`InterconnectedSystems` that have been
2587+
connected implicitly using signal names.
2588+
2589+
Parameters
2590+
----------
2591+
sys : :class:`InterconnectedSystem`
2592+
Interconnected system object
2593+
show_names : bool, optional
2594+
Instead of printing out the system number, print out the name of
2595+
each system. Default is False because system name is not usually
2596+
specified when performing implicit interconnection using
2597+
:func:`interconnect`.
2598+
column_width : int, optional
2599+
Character width of printed columns.
2600+
2601+
2602+
Examples
2603+
--------
2604+
>>> P = ct.ss(1,1,1,0, inputs='u', outputs='y', name='P')
2605+
>>> C = ct.tf(10, [.1, 1], inputs='e', outputs='u', name='C')
2606+
>>> L = ct.interconnect([C, P], inputs='e', outputs='y')
2607+
>>> L.connection_table(show_names=True) # doctest: +SKIP
2608+
signal | source | destination
2609+
--------------------------------------------------------------
2610+
e | input | C
2611+
u | C | P
2612+
y | P | output
2613+
"""
2614+
assert isinstance(sys, InterconnectedSystem), "system must be"\
2615+
"an InterconnectedSystem."
2616+
2617+
sys.connection_table(show_names=show_names, column_width=column_width)

control/statesp.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1459,7 +1459,7 @@ class LinearICSystem(InterconnectedSystem, StateSpace):
14591459
14601460
"""
14611461

1462-
def __init__(self, io_sys, ss_sys=None):
1462+
def __init__(self, io_sys, ss_sys=None, connection_type=None):
14631463
#
14641464
# Because this is a "hybrid" object, the initialization proceeds in
14651465
# stages. We first create an empty InputOutputSystem of the
@@ -1483,6 +1483,7 @@ def __init__(self, io_sys, ss_sys=None):
14831483
self.input_map = io_sys.input_map
14841484
self.output_map = io_sys.output_map
14851485
self.params = io_sys.params
1486+
self.connection_type = connection_type
14861487

14871488
# If we didnt' get a state space system, linearize the full system
14881489
if ss_sys is None:

0 commit comments

Comments
 (0)
0