Skip to content

Commit 71bbce2

Browse files
authored
Merge pull request #881 from murrayrm/multivariable_interconnect-30Mar2023
Multivariable interconnect functionality
2 parents f7d18f1 + 55e2b55 commit 71bbce2

File tree

13 files changed

+1119
-417
lines changed

13 files changed

+1119
-417
lines changed

control/iosys.py

Lines changed: 505 additions & 297 deletions
Large diffs are not rendered by default.

control/namedio.py

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import numpy as np
99
from copy import deepcopy
1010
from warnings import warn
11+
import re
1112
from . import config
1213

1314
__all__ = ['issiso', 'timebase', 'common_timebase', 'timebaseEqual',
@@ -22,22 +23,25 @@
2223
'namedio.linearized_system_name_suffix': '$linearized',
2324
'namedio.sampled_system_name_prefix': '',
2425
'namedio.sampled_system_name_suffix': '$sampled',
26+
'namedio.indexed_system_name_prefix': '',
27+
'namedio.indexed_system_name_suffix': '$indexed',
2528
'namedio.converted_system_name_prefix': '',
2629
'namedio.converted_system_name_suffix': '$converted',
2730
}
2831

2932

3033
class NamedIOSystem(object):
3134
def __init__(
32-
self, name=None, inputs=None, outputs=None, states=None, **kwargs):
35+
self, name=None, inputs=None, outputs=None, states=None,
36+
input_prefix='u', output_prefix='y', state_prefix='x', **kwargs):
3337

3438
# system name
3539
self.name = self._name_or_default(name)
3640

3741
# Parse and store the number of inputs and outputs
38-
self.set_inputs(inputs)
39-
self.set_outputs(outputs)
40-
self.set_states(states)
42+
self.set_inputs(inputs, prefix=input_prefix)
43+
self.set_outputs(outputs, prefix=output_prefix)
44+
self.set_states(states, prefix=state_prefix)
4145

4246
# Process timebase: if not given use default, but allow None as value
4347
self.dt = _process_dt_keyword(kwargs)
@@ -56,6 +60,9 @@ def _name_or_default(self, name=None, prefix_suffix_name=None):
5660
if name is None:
5761
name = "sys[{}]".format(NamedIOSystem._idCounter)
5862
NamedIOSystem._idCounter += 1
63+
elif re.match(r".*\..*", name):
64+
raise ValueError(f"invalid system name '{name}' ('.' not allowed)")
65+
5966
prefix = "" if prefix_suffix_name is None else config.defaults[
6067
'namedio.' + prefix_suffix_name + '_system_name_prefix']
6168
suffix = "" if prefix_suffix_name is None else config.defaults[
@@ -64,7 +71,6 @@ def _name_or_default(self, name=None, prefix_suffix_name=None):
6471

6572
# Check if system name is generic
6673
def _generic_name_check(self):
67-
import re
6874
return re.match(r'^sys\[\d*\]$', self.name) is not None
6975

7076
#
@@ -106,6 +112,39 @@ def __str__(self):
106112
def _find_signal(self, name, sigdict):
107113
return sigdict.get(name, None)
108114

115+
# Find a list of signals by name, index, or pattern
116+
def _find_signals(self, name_list, sigdict):
117+
if not isinstance(name_list, (list, tuple)):
118+
name_list = [name_list]
119+
120+
index_list = []
121+
for name in name_list:
122+
# Look for signal ranges (slice-like or base name)
123+
ms = re.match(r'([\w$]+)\[([\d]*):([\d]*)\]$', name) # slice
124+
mb = re.match(r'([\w$]+)$', name) # base
125+
if ms:
126+
base = ms.group(1)
127+
start = None if ms.group(2) == '' else int(ms.group(2))
128+
stop = None if ms.group(3) == '' else int(ms.group(3))
129+
for var in sigdict:
130+
# Find variables that match
131+
msig = re.match(r'([\w$]+)\[([\d]+)\]$', var)
132+
if msig and msig.group(1) == base and \
133+
(start is None or int(msig.group(2)) >= start) and \
134+
(stop is None or int(msig.group(2)) < stop):
135+
index_list.append(sigdict.get(var))
136+
elif mb and sigdict.get(name, None) is None:
137+
# Try to use name as a base name
138+
for var in sigdict:
139+
msig = re.match(name + r'\[([\d]+)\]$', var)
140+
if msig:
141+
index_list.append(sigdict.get(var))
142+
else:
143+
index_list.append(sigdict.get(name, None))
144+
145+
return None if len(index_list) == 0 or \
146+
any([idx is None for idx in index_list]) else index_list
147+
109148
def _copy_names(self, sys, prefix="", suffix="", prefix_suffix_name=None):
110149
"""copy the signal and system name of sys. Name is given as a keyword
111150
in case a specific name (e.g. append 'linearized') is desired. """
@@ -151,7 +190,6 @@ def copy(self, name=None, use_prefix_suffix=True):
151190
return newsys
152191

153192
def set_inputs(self, inputs, prefix='u'):
154-
155193
"""Set the number/names of the system inputs.
156194
157195
Parameters
@@ -175,6 +213,10 @@ def find_input(self, name):
175213
"""Find the index for an input given its name (`None` if not found)"""
176214
return self.input_index.get(name, None)
177215

216+
def find_inputs(self, name_list):
217+
"""Return list of indices matching input spec (`None` if not found)"""
218+
return self._find_signals(name_list, self.input_index)
219+
178220
# Property for getting and setting list of input signals
179221
input_labels = property(
180222
lambda self: list(self.input_index.keys()), # getter
@@ -204,6 +246,10 @@ def find_output(self, name):
204246
"""Find the index for an output given its name (`None` if not found)"""
205247
return self.output_index.get(name, None)
206248

249+
def find_outputs(self, name_list):
250+
"""Return list of indices matching output spec (`None` if not found)"""
251+
return self._find_signals(name_list, self.output_index)
252+
207253
# Property for getting and setting list of output signals
208254
output_labels = property(
209255
lambda self: list(self.output_index.keys()), # getter
@@ -227,12 +273,16 @@ def set_states(self, states, prefix='x'):
227273
228274
"""
229275
self.nstates, self.state_index = \
230-
_process_signal_list(states, prefix=prefix)
276+
_process_signal_list(states, prefix=prefix, allow_dot=True)
231277

232278
def find_state(self, name):
233279
"""Find the index for a state given its name (`None` if not found)"""
234280
return self.state_index.get(name, None)
235281

282+
def find_states(self, name_list):
283+
"""Return list of indices matching state spec (`None` if not found)"""
284+
return self._find_signals(name_list, self.state_index)
285+
236286
# Property for getting and setting list of state signals
237287
state_labels = property(
238288
lambda self: list(self.state_index.keys()), # getter
@@ -578,7 +628,7 @@ def _process_dt_keyword(keywords, defaults={}, static=False):
578628

579629

580630
# Utility function to parse a list of signals
581-
def _process_signal_list(signals, prefix='s'):
631+
def _process_signal_list(signals, prefix='s', allow_dot=False):
582632
if signals is None:
583633
# No information provided; try and make it up later
584634
return None, {}
@@ -589,10 +639,17 @@ def _process_signal_list(signals, prefix='s'):
589639

590640
elif isinstance(signals, str):
591641
# Single string given => single signal with given name
642+
if not allow_dot and re.match(r".*\..*", signals):
643+
raise ValueError(
644+
f"invalid signal name '{signals}' ('.' not allowed)")
592645
return 1, {signals: 0}
593646

594647
elif all(isinstance(s, str) for s in signals):
595648
# Use the list of strings as the signal names
649+
for signal in signals:
650+
if not allow_dot and re.match(r".*\..*", signal):
651+
raise ValueError(
652+
f"invalid signal name '{signal}' ('.' not allowed)")
596653
return len(signals), {signals[i]: i for i in range(len(signals))}
597654

598655
else:

control/statesp.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,10 +1294,15 @@ def __getitem__(self, indices):
12941294
"""Array style access"""
12951295
if len(indices) != 2:
12961296
raise IOError('must provide indices of length 2 for state space')
1297-
i = indices[0]
1298-
j = indices[1]
1299-
return StateSpace(self.A, self.B[:, j], self.C[i, :],
1300-
self.D[i, j], self.dt)
1297+
outdx = indices[0] if isinstance(indices[0], list) else [indices[0]]
1298+
inpdx = indices[1] if isinstance(indices[1], list) else [indices[1]]
1299+
sysname = config.defaults['namedio.indexed_system_name_prefix'] + \
1300+
self.name + config.defaults['namedio.indexed_system_name_suffix']
1301+
return StateSpace(
1302+
self.A, self.B[:, inpdx], self.C[outdx, :], self.D[outdx, inpdx],
1303+
self.dt, name=sysname,
1304+
inputs=[self.input_labels[i] for i in list(inpdx)],
1305+
outputs=[self.output_labels[i] for i in list(outdx)])
13011306

13021307
def sample(self, Ts, method='zoh', alpha=None, prewarp_frequency=None,
13031308
name=None, copy_names=True, **kwargs):

control/tests/config_test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,18 @@ def test_get_param_last(self):
301301

302302
assert ct.config._get_param(
303303
'config', 'second', kwargs, pop=True, last=True) == 2
304+
305+
def test_system_indexing(self):
306+
# Default renaming
307+
sys = ct.TransferFunction(
308+
[ [ [1], [2], [3]], [ [3], [4], [5]] ],
309+
[ [[1, 2], [1, 3], [1, 4]], [[1, 4], [1, 5], [1, 6]] ], 0.5)
310+
sys1 = sys[1:, 1:]
311+
assert sys1.name == sys.name + '$indexed'
312+
313+
# Reset the format
314+
ct.config.set_defaults(
315+
'namedio', indexed_system_name_prefix='PRE',
316+
indexed_system_name_suffix='POST')
317+
sys2 = sys[1:, 1:]
318+
assert sys2.name == 'PRE' + sys.name + 'POST'

0 commit comments

Comments
 (0)