Skip to content

Commit 8b85747

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

File tree

5 files changed

+159
-20
lines changed

5 files changed

+159
-20
lines changed

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy