Skip to content

Commit 98ec00f

Browse files
samlafbnavigator
andauthored
Fixed InterconnectedSystems name bugs. (#400)
* Made sure interconnectedsystems keeps names of parts. * Fixed signal names for InterconnectedSystems. * Fixed some naming convention problems and added unit tests for naming conventions. * Removed for loops from tests to get better error messages Co-authored-by: Ben Greiner <code@bnavigator.de>
1 parent 6ede92e commit 98ec00f

File tree

2 files changed

+248
-105
lines changed

2 files changed

+248
-105
lines changed

control/iosys.py

Lines changed: 111 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ class for a set of subclasses that are used to implement specific
7676
Parameter values for the systems. Passed to the evaluation functions
7777
for the system as default values, overriding internal defaults.
7878
name : string, optional
79-
System name (used for specifying signals)
79+
System name (used for specifying signals). If unspecified, a generic
80+
name <sys[id]> is generated with a unique integer id.
8081
8182
Attributes
8283
----------
@@ -108,6 +109,14 @@ class for a set of subclasses that are used to implement specific
108109
The default is to return the entire system state.
109110
110111
"""
112+
113+
idCounter = 0
114+
def name_or_default(self, name=None):
115+
if name is None:
116+
name = "sys[{}]".format(InputOutputSystem.idCounter)
117+
InputOutputSystem.idCounter += 1
118+
return name
119+
111120
def __init__(self, inputs=None, outputs=None, states=None, params={},
112121
dt=None, name=None):
113122
"""Create an input/output system.
@@ -143,7 +152,8 @@ def __init__(self, inputs=None, outputs=None, states=None, params={},
143152
functions for the system as default values, overriding internal
144153
defaults.
145154
name : string, optional
146-
System name (used for specifying signals)
155+
System name (used for specifying signals). If unspecified, a generic
156+
name <sys[id]> is generated with a unique integer id.
147157
148158
Returns
149159
-------
@@ -152,9 +162,9 @@ def __init__(self, inputs=None, outputs=None, states=None, params={},
152162
153163
"""
154164
# Store the input arguments
155-
self.params = params.copy() # default parameters
156-
self.dt = dt # timebase
157-
self.name = name # system name
165+
self.params = params.copy() # default parameters
166+
self.dt = dt # timebase
167+
self.name = self.name_or_default(name) # system name
158168

159169
# Parse and store the number of inputs, outputs, and states
160170
self.set_inputs(inputs)
@@ -204,29 +214,19 @@ def __mul__(sys2, sys1):
204214
if dt is False:
205215
raise ValueError("System timebases are not compabile")
206216

217+
inplist = [(0,i) for i in range(sys1.ninputs)]
218+
outlist = [(1,i) for i in range(sys2.noutputs)]
207219
# Return the series interconnection between the systems
208-
newsys = InterconnectedSystem((sys1, sys2))
220+
newsys = InterconnectedSystem((sys1, sys2), inplist=inplist, outlist=outlist)
209221

210-
# Set up the connecton map
222+
# Set up the connection map manually
211223
newsys.set_connect_map(np.block(
212224
[[np.zeros((sys1.ninputs, sys1.noutputs)),
213225
np.zeros((sys1.ninputs, sys2.noutputs))],
214226
[np.eye(sys2.ninputs, sys1.noutputs),
215227
np.zeros((sys2.ninputs, sys2.noutputs))]]
216228
))
217229

218-
# Set up the input map
219-
newsys.set_input_map(np.concatenate(
220-
(np.eye(sys1.ninputs), np.zeros((sys2.ninputs, sys1.ninputs))),
221-
axis=0))
222-
# TODO: set up input names
223-
224-
# Set up the output map
225-
newsys.set_output_map(np.concatenate(
226-
(np.zeros((sys2.noutputs, sys1.noutputs)), np.eye(sys2.noutputs)),
227-
axis=1))
228-
# TODO: set up output names
229-
230230
# Return the newly created system
231231
return newsys
232232

@@ -271,18 +271,10 @@ def __add__(sys1, sys2):
271271
ninputs = sys1.ninputs
272272
noutputs = sys1.noutputs
273273

274+
inplist = [[(0,i),(1,i)] for i in range(ninputs)]
275+
outlist = [[(0,i),(1,i)] for i in range(noutputs)]
274276
# Create a new system to handle the composition
275-
newsys = InterconnectedSystem((sys1, sys2))
276-
277-
# Set up the input map
278-
newsys.set_input_map(np.concatenate(
279-
(np.eye(ninputs), np.eye(ninputs)), axis=0))
280-
# TODO: set up input names
281-
282-
# Set up the output map
283-
newsys.set_output_map(np.concatenate(
284-
(np.eye(noutputs), np.eye(noutputs)), axis=1))
285-
# TODO: set up output names
277+
newsys = InterconnectedSystem((sys1, sys2), inplist=inplist, outlist=outlist)
286278

287279
# Return the newly created system
288280
return newsys
@@ -301,16 +293,10 @@ def __neg__(sys):
301293
if sys.ninputs is None or sys.noutputs is None:
302294
raise ValueError("Can't determine number of inputs or outputs")
303295

296+
inplist = [(0,i) for i in range(sys.ninputs)]
297+
outlist = [(0,i,-1) for i in range(sys.noutputs)]
304298
# Create a new system to hold the negation
305-
newsys = InterconnectedSystem((sys,), dt=sys.dt)
306-
307-
# Set up the input map (identity)
308-
newsys.set_input_map(np.eye(sys.ninputs))
309-
# TODO: set up input names
310-
311-
# Set up the output map (negate the output)
312-
newsys.set_output_map(-np.eye(sys.noutputs))
313-
# TODO: set up output names
299+
newsys = InterconnectedSystem((sys,), dt=sys.dt, inplist=inplist, outlist=outlist)
314300

315301
# Return the newly created system
316302
return newsys
@@ -482,29 +468,20 @@ def feedback(self, other=1, sign=-1, params={}):
482468
if dt is False:
483469
raise ValueError("System timebases are not compabile")
484470

471+
inplist = [(0,i) for i in range(self.ninputs)]
472+
outlist = [(0,i) for i in range(self.noutputs)]
485473
# Return the series interconnection between the systems
486-
newsys = InterconnectedSystem((self, other), params=params, dt=dt)
474+
newsys = InterconnectedSystem((self, other), inplist=inplist, outlist=outlist,
475+
params=params, dt=dt)
487476

488-
# Set up the connecton map
477+
# Set up the connecton map manually
489478
newsys.set_connect_map(np.block(
490479
[[np.zeros((self.ninputs, self.noutputs)),
491480
sign * np.eye(self.ninputs, other.noutputs)],
492481
[np.eye(other.ninputs, self.noutputs),
493482
np.zeros((other.ninputs, other.noutputs))]]
494483
))
495484

496-
# Set up the input map
497-
newsys.set_input_map(np.concatenate(
498-
(np.eye(self.ninputs), np.zeros((other.ninputs, self.ninputs))),
499-
axis=0))
500-
# TODO: set up input names
501-
502-
# Set up the output map
503-
newsys.set_output_map(np.concatenate(
504-
(np.eye(self.noutputs), np.zeros((self.noutputs, other.noutputs))),
505-
axis=1))
506-
# TODO: set up output names
507-
508485
# Return the newly created system
509486
return newsys
510487

@@ -564,9 +541,11 @@ def linearize(self, x0, u0, t=0, params={}, eps=1e-6):
564541
linsys = StateSpace(A, B, C, D, self.dt, remove_useless=False)
565542
return LinearIOSystem(linsys)
566543

567-
def copy(self):
544+
def copy(self, newname=None):
568545
"""Make a copy of an input/output system."""
569-
return copy.copy(self)
546+
newsys = copy.copy(self)
547+
newsys.name = self.name_or_default("copy of " + self.name if not newname else newname)
548+
return newsys
570549

571550

572551
class LinearIOSystem(InputOutputSystem, StateSpace):
@@ -610,7 +589,8 @@ def __init__(self, linsys, inputs=None, outputs=None, states=None,
610589
functions for the system as default values, overriding internal
611590
defaults.
612591
name : string, optional
613-
System name (used for specifying signals)
592+
System name (used for specifying signals). If unspecified, a generic
593+
name <sys[id]> is generated with a unique integer id.
614594
615595
Returns
616596
-------
@@ -728,7 +708,8 @@ def __init__(self, updfcn, outfcn=None, inputs=None, outputs=None,
728708
* dt = True Discrete time with unspecified sampling time
729709
730710
name : string, optional
731-
System name (used for specifying signals).
711+
System name (used for specifying signals). If unspecified, a generic
712+
name <sys[id]> is generated with a unique integer id.
732713
733714
Returns
734715
-------
@@ -808,10 +789,13 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[],
808789
syslist : array_like of InputOutputSystems
809790
The list of input/output systems to be connected
810791
811-
connections : tuple of connection specifications, optional
792+
connections : list of tuple of connection specifications, optional
812793
Description of the internal connections between the subsystems.
813-
Each element of the tuple describes an input to one of the
814-
subsystems. The entries are are of the form:
794+
795+
[connection1, connection2, ...]
796+
797+
Each connection is a tuple that describes an input to one of the
798+
subsystems. The entries are of the form:
815799
816800
(input-spec, output-spec1, output-spec2, ...)
817801
@@ -835,10 +819,15 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[],
835819
If omitted, the connection map (matrix) can be specified using the
836820
:func:`~control.InterconnectedSystem.set_connect_map` method.
837821
838-
inplist : tuple of input specifications, optional
822+
inplist : List of tuple of input specifications, optional
839823
List of specifications for how the inputs for the overall system
840824
are mapped to the subsystem inputs. The input specification is
841-
the same as the form defined in the connection specification.
825+
similar to the form defined in the connection specification, except
826+
that connections do not specify an input-spec, since these are
827+
the system inputs. The entries are thus of the form:
828+
829+
(output-spec1, output-spec2, ...)
830+
842831
Each system input is added to the input for the listed subsystem.
843832
844833
If omitted, the input map can be specified using the
@@ -847,14 +836,31 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[],
847836
outlist : tuple of output specifications, optional
848837
List of specifications for how the outputs for the subsystems are
849838
mapped to overall system outputs. The output specification is the
850-
same as the form defined in the connection specification
839+
same as the form defined in the inplist specification
851840
(including the optional gain term). Numbered outputs must be
852841
chosen from the list of subsystem outputs, but named outputs can
853842
also be contained in the list of subsystem inputs.
854843
855844
If omitted, the output map can be specified using the
856845
`set_output_map` method.
857846
847+
inputs : int, list of str or None, optional
848+
Description of the system inputs. This can be given as an integer
849+
count or as a list of strings that name the individual signals.
850+
If an integer count is specified, the names of the signal will be
851+
of the form `s[i]` (where `s` is one of `u`, `y`, or `x`). If
852+
this parameter is not given or given as `None`, the relevant
853+
quantity will be determined when possible based on other
854+
information provided to functions using the system.
855+
856+
outputs : int, list of str or None, optional
857+
Description of the system outputs. Same format as `inputs`.
858+
859+
states : int, list of str, or None, optional
860+
Description of the system states. Same format as `inputs`, except
861+
the state names will be of the form '<subsys_name>.<state_name>',
862+
for each subsys in syslist and each state_name of each subsys.
863+
858864
params : dict, optional
859865
Parameter values for the systems. Passed to the evaluation
860866
functions for the system as default values, overriding internal
@@ -871,7 +877,8 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[],
871877
* dt = True Discrete time with unspecified sampling time
872878
873879
name : string, optional
874-
System name (used for specifying signals).
880+
System name (used for specifying signals). If unspecified, a generic
881+
name <sys[id]> is generated with a unique integer id.
875882
876883
"""
877884
# Convert input and output names to lists if they aren't already
@@ -885,8 +892,9 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[],
885892
nstates = 0; self.state_offset = []
886893
ninputs = 0; self.input_offset = []
887894
noutputs = 0; self.output_offset = []
888-
system_count = 0
889-
for sys in syslist:
895+
sysobj_name_dct = {}
896+
sysname_count_dct = {}
897+
for sysidx, sys in enumerate(syslist):
890898
# Make sure time bases are consistent
891899
# TODO: Use lti._find_timebase() instead?
892900
if dt is None and sys.dt is not None:
@@ -912,36 +920,44 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[],
912920
ninputs += sys.ninputs
913921
noutputs += sys.noutputs
914922

915-
# Store the index to the system for later retrieval
916-
# TODO: look for duplicated system names
917-
self.syslist_index[sys.name] = system_count
918-
system_count += 1
919-
920-
# Check for duplicate systems or duplicate names
921-
sysobj_list = []
922-
sysname_list = []
923-
for sys in syslist:
924-
if sys in sysobj_list:
925-
warn("Duplicate object found in system list: %s" % str(sys))
926-
elif sys.name is not None and sys.name in sysname_list:
927-
warn("Duplicate name found in system list: %s" % sys.name)
928-
sysobj_list.append(sys)
929-
sysname_list.append(sys.name)
923+
# Check for duplicate systems or duplicate names
924+
# Duplicates are renamed sysname_1, sysname_2, etc.
925+
if sys in sysobj_name_dct:
926+
sys = sys.copy()
927+
warn("Duplicate object found in system list: %s. Making a copy" % str(sys))
928+
if sys.name is not None and sys.name in sysname_count_dct:
929+
count = sysname_count_dct[sys.name]
930+
sysname_count_dct[sys.name] += 1
931+
sysname = sys.name + "_" + str(count)
932+
sysobj_name_dct[sys] = sysname
933+
self.syslist_index[sysname] = sysidx
934+
warn("Duplicate name found in system list. Renamed to {}".format(sysname))
935+
else:
936+
sysname_count_dct[sys.name] = 1
937+
sysobj_name_dct[sys] = sys.name
938+
self.syslist_index[sys.name] = sysidx
939+
940+
if states is None:
941+
states = []
942+
for sys, sysname in sysobj_name_dct.items():
943+
states += [sysname + '.' + statename for statename in sys.state_index.keys()]
930944

931945
# Create the I/O system
932946
super(InterconnectedSystem, self).__init__(
933947
inputs=len(inplist), outputs=len(outlist),
934-
states=nstates, params=params, dt=dt)
948+
states=states, params=params, dt=dt, name=name)
935949

936950
# If input or output list was specified, update it
937-
nsignals, self.input_index = \
938-
self._process_signal_list(inputs, prefix='u')
939-
if nsignals is not None and len(inplist) != nsignals:
940-
raise ValueError("Wrong number/type of inputs given.")
941-
nsignals, self.output_index = \
942-
self._process_signal_list(outputs, prefix='y')
943-
if nsignals is not None and len(outlist) != nsignals:
944-
raise ValueError("Wrong number/type of outputs given.")
951+
if inputs is not None:
952+
nsignals, self.input_index = \
953+
self._process_signal_list(inputs, prefix='u')
954+
if nsignals is not None and len(inplist) != nsignals:
955+
raise ValueError("Wrong number/type of inputs given.")
956+
if outputs is not None:
957+
nsignals, self.output_index = \
958+
self._process_signal_list(outputs, prefix='y')
959+
if nsignals is not None and len(outlist) != nsignals:
960+
raise ValueError("Wrong number/type of outputs given.")
945961

946962
# Convert the list of interconnections to a connection map (matrix)
947963
self.connect_map = np.zeros((ninputs, noutputs))
@@ -960,9 +976,11 @@ def __init__(self, syslist, connections=[], inplist=[], outlist=[],
960976

961977
# Convert the output list to a matrix: maps subsystems to system
962978
self.output_map = np.zeros((self.noutputs, noutputs + ninputs))
963-
for index in range(len(outlist)):
964-
ylist_index, gain = self._parse_output_spec(outlist[index])
965-
self.output_map[index, ylist_index] = gain
979+
for index, outspec in enumerate(outlist):
980+
if isinstance(outspec, (int, str, tuple)): outspec = [outspec]
981+
for spec in outspec:
982+
ylist_index, gain = self._parse_output_spec(spec)
983+
self.output_map[index, ylist_index] = gain
966984

967985
# Save the parameters for the system
968986
self.params = params.copy()

0 commit comments

Comments
 (0)