Skip to content

Commit 8b85747

Browse files
committed
allow renaming of system/signal names in bdalg functions
1 parent 0fab739 commit 8b85747

5 files changed

Lines changed: 159 additions & 20 deletions

File tree

control/bdalg.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,19 @@
5454
"""
5555

5656
from functools import reduce
57-
import numpy as np
5857
from warnings import warn
59-
from . import xferfcn as tf
60-
from . import statesp as ss
58+
59+
import numpy as np
60+
6161
from . import frdata as frd
62+
from . import statesp as ss
63+
from . import xferfcn as tf
6264
from .iosys import InputOutputSystem
6365

6466
__all__ = ['series', 'parallel', 'negate', 'feedback', 'append', 'connect']
6567

6668

67-
def series(sys1, *sysn):
69+
def series(sys1, *sysn, **kwargs):
6870
r"""series(sys1, sys2, [..., sysn])
6971
7072
Return the series connection (`sysn` \* ...\ \*) `sys2` \* `sys1`.
@@ -117,10 +119,12 @@ def series(sys1, *sysn):
117119
(2, 1, 5)
118120
119121
"""
120-
return reduce(lambda x, y: y * x, sysn, sys1)
122+
sys = reduce(lambda x, y: y * x, sysn, sys1)
123+
sys.update_names(**kwargs)
124+
return sys
121125

122126

123-
def parallel(sys1, *sysn):
127+
def parallel(sys1, *sysn, **kwargs):
124128
r"""parallel(sys1, sys2, [..., sysn])
125129
126130
Return the parallel connection `sys1` + `sys2` (+ ...\ + `sysn`).
@@ -171,10 +175,11 @@ def parallel(sys1, *sysn):
171175
(3, 4, 7)
172176
173177
"""
174-
return reduce(lambda x, y: x + y, sysn, sys1)
175-
178+
sys = reduce(lambda x, y: x + y, sysn, sys1)
179+
sys.update_names(**kwargs)
180+
return sys
176181

177-
def negate(sys):
182+
def negate(sys, **kwargs):
178183
"""
179184
Return the negative of a system.
180185
@@ -208,11 +213,12 @@ def negate(sys):
208213
np.float64(-2.0)
209214
210215
"""
211-
return -sys
216+
sys = -sys
217+
sys.update_names(**kwargs)
218+
return sys
212219

213220
#! TODO: expand to allow sys2 default to work in MIMO case?
214-
#! TODO: allow renaming of signals (for all bdalg operations)
215-
def feedback(sys1, sys2=1, sign=-1):
221+
def feedback(sys1, sys2=1, sign=-1, **kwargs):
216222
"""Feedback interconnection between two I/O systems.
217223
218224
Parameters
@@ -261,7 +267,7 @@ def feedback(sys1, sys2=1, sign=-1):
261267
# Allow anything with a feedback function to call that function
262268
# TODO: rewrite to allow __rfeedback__
263269
try:
264-
return sys1.feedback(sys2, sign)
270+
return sys1.feedback(sys2, sign, **kwargs)
265271
except (AttributeError, TypeError):
266272
pass
267273

@@ -284,9 +290,11 @@ def feedback(sys1, sys2=1, sign=-1):
284290
else:
285291
sys1 = ss._convert_to_statespace(sys1)
286292

287-
return sys1.feedback(sys2, sign)
293+
sys = sys1.feedback(sys2, sign)
294+
sys.update_names(**kwargs)
295+
return sys
288296

289-
def append(*sys):
297+
def append(*sys, **kwargs):
290298
"""append(sys1, sys2, [..., sysn])
291299
292300
Group LTI state space models by appending their inputs and outputs.
@@ -327,6 +335,7 @@ def append(*sys):
327335
s1 = ss._convert_to_statespace(sys[0])
328336
for s in sys[1:]:
329337
s1 = s1.append(s)
338+
s1.update_names(**kwargs)
330339
return s1
331340

332341
def connect(sys, Q, inputv, outputv):

control/iosys.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
# FrequencyResponseData, InterconnectedSystem and other similar classes
77
# that allow naming of signals.
88

9-
import numpy as np
9+
import re
1010
from copy import deepcopy
1111
from warnings import warn
12-
import re
12+
13+
import numpy as np
14+
1315
from . import config
1416

1517
__all__ = ['InputOutputSystem', 'issiso', 'timebase', 'common_timebase',
@@ -366,6 +368,44 @@ def find_states(self, name_list):
366368
lambda self: list(self.state_index.keys()), # getter
367369
set_states) # setter
368370

371+
def update_names(self, **kwargs):
372+
"""Update signal and system names for an I/O system.
373+
374+
Parameters
375+
----------
376+
inputs : int, list of str, or None
377+
Description of the system inputs.
378+
outputs : int, list of str, or None
379+
Description of the system outputs.
380+
states : int, list of str, or None
381+
Description of the system states.
382+
383+
"""
384+
self.name = kwargs.pop('name', self.name)
385+
if kwargs.get('inputs', None):
386+
ninputs, input_index = _process_signal_list(
387+
kwargs.pop('inputs'), prefix=kwargs.pop('input_prefix', 'u'))
388+
if self.ninputs and self.ninputs != ninputs:
389+
raise ValueError("number of inputs does not match system size")
390+
self.input_index = input_index
391+
if kwargs.get('outputs', None):
392+
noutputs, output_index = _process_signal_list(
393+
kwargs.pop('outputs'), prefix=kwargs.pop('output_prefix', 'y'))
394+
if self.noutputs and self.noutputs != noutputs:
395+
raise ValueError("number of outputs does not match system size")
396+
self.output_index = output_index
397+
if kwargs.get('states', None):
398+
nstates, state_index = _process_signal_list(
399+
kwargs.pop('states'), prefix=kwargs.pop('state_prefix', 'x'))
400+
if self.nstates != nstates:
401+
raise ValueError("number of states does not match system size")
402+
self.state_index = state_index
403+
404+
# Make sure we processed all of the arguments
405+
if kwargs:
406+
raise TypeError("unrecognized keywords: ", str(kwargs))
407+
408+
369409
def isctime(self, strict=False):
370410
"""
371411
Check to see if a system is a continuous-time system.
@@ -825,6 +865,7 @@ def _process_labels(labels, name, default):
825865
#
826866
import re
827867

868+
828869
def _parse_spec(syslist, spec, signame, dictname=None):
829870
"""Parse a signal specification, returning system and signal index."""
830871

control/tests/bdalg_test.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,3 +316,49 @@ def testConnect(self, tsys):
316316
connect(sys, Q, [2], [1, 0])
317317
with pytest.raises(IndexError):
318318
connect(sys, Q, [2], [1, -1])
319+
320+
321+
@pytest.mark.parametrize(
322+
"op, nsys, ninputs, noutputs, nstates", [
323+
(ctrl.series, 2, 1, 1, 4),
324+
(ctrl.parallel, 2, 1, 1, 4),
325+
(ctrl.feedback, 2, 1, 1, 4),
326+
(ctrl.append, 2, 2, 2, 4),
327+
(ctrl.negate, 1, 1, 1, 2),
328+
])
329+
def test_bdalg_update_names(op, nsys, ninputs, noutputs, nstates):
330+
syslist = [ctrl.rss(2, 1, 1), ctrl.rss(2, 1, 1)]
331+
inputs = ['in1', 'in2']
332+
outputs = ['out1', 'out2']
333+
states = ['x1', 'x2', 'x3', 'x4']
334+
335+
newsys = op(
336+
*syslist[:nsys], name='newsys', inputs=inputs[:ninputs],
337+
outputs=outputs[:noutputs], states=states[:nstates])
338+
assert newsys.name == 'newsys'
339+
assert newsys.ninputs == ninputs
340+
assert newsys.input_labels == inputs[:ninputs]
341+
assert newsys.noutputs == noutputs
342+
assert newsys.output_labels == outputs[:noutputs]
343+
assert newsys.nstates == nstates
344+
assert newsys.state_labels == states[:nstates]
345+
346+
347+
def test_bdalg_udpate_names_errors():
348+
sys1 = ctrl.rss(2, 1, 1)
349+
sys2 = ctrl.rss(2, 1, 1)
350+
351+
with pytest.raises(ValueError, match="number of inputs does not match"):
352+
sys = ctrl.series(sys1, sys2, inputs=2)
353+
354+
with pytest.raises(ValueError, match="number of outputs does not match"):
355+
sys = ctrl.series(sys1, sys2, outputs=2)
356+
357+
with pytest.raises(ValueError, match="number of states does not match"):
358+
sys = ctrl.series(sys1, sys2, states=2)
359+
360+
with pytest.raises(ValueError, match="number of states does not match"):
361+
sys = ctrl.series(ctrl.tf(sys1), ctrl.tf(sys2), states=2)
362+
363+
with pytest.raises(TypeError, match="unrecognized keywords"):
364+
sys = ctrl.series(sys1, sys2, dt=1)

control/tests/iosys_test.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2122,7 +2122,7 @@ def test_find_size():
21222122
lambda t, x, u, params: -x + u,
21232123
lambda t, x, u, params: x[:1],
21242124
inputs=2, states=2)
2125-
2125+
21262126
with pytest.raises(ValueError, match="inconsistent .* size of X0"):
21272127
resp = ct.input_output_response(sys, timepts, [0, 1], X0=[0, 0, 1])
21282128

@@ -2135,4 +2135,35 @@ def test_find_size():
21352135
lambda t, x, u, params: x[:1],
21362136
inputs=2, states=2, outputs=2)
21372137
resp = ct.input_output_response(sys, timepts, [0, 1], X0=[0, 0])
2138-
2138+
2139+
2140+
def test_update_names():
2141+
sys = ct.rss(['x1', 'x2'], 2, 2)
2142+
sys.update_names(
2143+
name='new', states=2, inputs=['u1', 'u2'],
2144+
outputs=2, output_prefix='yy')
2145+
assert sys.name == 'new'
2146+
assert sys.ninputs == 2
2147+
assert sys.input_labels == ['u1', 'u2']
2148+
assert sys.ninputs == 2
2149+
assert sys.output_labels == ['yy[0]', 'yy[1]']
2150+
assert sys.state_labels == ['x[0]', 'x[1]']
2151+
2152+
# Generate some error conditions
2153+
with pytest.raises(ValueError, match="number of inputs does not match"):
2154+
sys.update_names(inputs=3)
2155+
2156+
with pytest.raises(ValueError, match="number of outputs does not match"):
2157+
sys.update_names(outputs=3)
2158+
2159+
with pytest.raises(ValueError, match="number of states does not match"):
2160+
sys.update_names(states=3)
2161+
2162+
with pytest.raises(ValueError, match="number of states does not match"):
2163+
ct.tf(sys).update_names(states=2)
2164+
2165+
with pytest.raises(TypeError, match="unrecognized keywords"):
2166+
sys.update_names(dt=1)
2167+
2168+
with pytest.raises(TypeError, match=".* takes 1 positional argument"):
2169+
sys.update_names(5)

control/tests/kwargs_test.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,25 @@ def test_kwarg_search(module, prefix):
9292

9393
@pytest.mark.parametrize(
9494
"function, nsssys, ntfsys, moreargs, kwargs",
95-
[(control.dlqe, 1, 0, ([[1]], [[1]]), {}),
95+
[(control.append, 2, 0, (), {}),
96+
(control.dlqe, 1, 0, ([[1]], [[1]]), {}),
9697
(control.dlqr, 1, 0, ([[1, 0], [0, 1]], [[1]]), {}),
9798
(control.drss, 0, 0, (2, 1, 1), {}),
99+
(control.feedback, 2, 0, (), {}),
98100
(control.flatsys.flatsys, 1, 0, (), {}),
99101
(control.input_output_response, 1, 0, ([0, 1, 2], [1, 1, 1]), {}),
100102
(control.lqe, 1, 0, ([[1]], [[1]]), {}),
101103
(control.lqr, 1, 0, ([[1, 0], [0, 1]], [[1]]), {}),
102104
(control.linearize, 1, 0, (0, 0), {}),
105+
(control.negate, 1, 0, (), {}),
103106
(control.nlsys, 0, 0, (lambda t, x, u, params: np.array([0]),), {}),
107+
(control.parallel, 2, 0, (), {}),
104108
(control.pzmap, 1, 0, (), {}),
105109
(control.rlocus, 0, 1, (), {}),
106110
(control.root_locus, 0, 1, (), {}),
107111
(control.root_locus_plot, 0, 1, (), {}),
108112
(control.rss, 0, 0, (2, 1, 1), {}),
113+
(control.series, 2, 0, (), {}),
109114
(control.set_defaults, 0, 0, ('control',), {'default_dt': True}),
110115
(control.ss, 0, 0, (0, 0, 0, 0), {'dt': 1}),
111116
(control.ss2io, 1, 0, (), {}),
@@ -122,6 +127,7 @@ def test_kwarg_search(module, prefix):
122127
(control.LTI, 0, 0, (),
123128
{'inputs': 1, 'outputs': 1, 'states': 1}),
124129
(control.flatsys.LinearFlatSystem, 1, 0, (), {}),
130+
(control.InputOutputSystem.update_names, 1, 0, (), {}),
125131
(control.NonlinearIOSystem.linearize, 1, 0, (0, 0), {}),
126132
(control.StateSpace.sample, 1, 0, (0.1,), {}),
127133
(control.StateSpace, 0, 0,
@@ -232,6 +238,7 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo):
232238
#
233239

234240
kwarg_unittest = {
241+
'append': test_unrecognized_kwargs,
235242
'bode': test_response_plot_kwargs,
236243
'bode_plot': test_response_plot_kwargs,
237244
'create_estimator_iosystem': stochsys_test.test_estimator_errors,
@@ -242,6 +249,7 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo):
242249
'dlqe': test_unrecognized_kwargs,
243250
'dlqr': test_unrecognized_kwargs,
244251
'drss': test_unrecognized_kwargs,
252+
'feedback': test_unrecognized_kwargs,
245253
'flatsys.flatsys': test_unrecognized_kwargs,
246254
'frd': frd_test.TestFRD.test_unrecognized_keyword,
247255
'gangof4': test_matplotlib_kwargs,
@@ -252,19 +260,22 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo):
252260
'linearize': test_unrecognized_kwargs,
253261
'lqe': test_unrecognized_kwargs,
254262
'lqr': test_unrecognized_kwargs,
263+
'negate': test_unrecognized_kwargs,
255264
'nichols_plot': test_matplotlib_kwargs,
256265
'nichols': test_matplotlib_kwargs,
257266
'nlsys': test_unrecognized_kwargs,
258267
'nyquist': test_matplotlib_kwargs,
259268
'nyquist_response': test_response_plot_kwargs,
260269
'nyquist_plot': test_matplotlib_kwargs,
261270
'phase_plane_plot': test_matplotlib_kwargs,
271+
'parallel': test_unrecognized_kwargs,
262272
'pole_zero_plot': test_unrecognized_kwargs,
263273
'pzmap': test_unrecognized_kwargs,
264274
'rlocus': test_unrecognized_kwargs,
265275
'root_locus': test_unrecognized_kwargs,
266276
'root_locus_plot': test_unrecognized_kwargs,
267277
'rss': test_unrecognized_kwargs,
278+
'series': test_unrecognized_kwargs,
268279
'set_defaults': test_unrecognized_kwargs,
269280
'singular_values_plot': test_matplotlib_kwargs,
270281
'ss': test_unrecognized_kwargs,
@@ -292,6 +303,7 @@ def test_response_plot_kwargs(data_fcn, plot_fcn, mimo):
292303
'DescribingFunctionResponse.plot':
293304
descfcn_test.test_describing_function_exceptions,
294305
'InputOutputSystem.__init__': test_unrecognized_kwargs,
306+
'InputOutputSystem.update_names': test_unrecognized_kwargs,
295307
'LTI.__init__': test_unrecognized_kwargs,
296308
'flatsys.LinearFlatSystem.__init__': test_unrecognized_kwargs,
297309
'NonlinearIOSystem.linearize': test_unrecognized_kwargs,

0 commit comments

Comments
 (0)