Skip to content

Commit 4023edd

Browse files
committed
add iosys conversions (mul, rmul, add, radd, sub, rsub) + PEP8 cleanup
1 parent 56cecc0 commit 4023edd

3 files changed

Lines changed: 256 additions & 72 deletions

File tree

control/iosys.py

Lines changed: 139 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from warnings import warn
3333

3434
from .statesp import StateSpace, tf2ss, _convert_to_statespace
35+
from .xferfcn import TransferFunction
3536
from .timeresp import _check_convert_array, _process_time_response, \
3637
TimeResponseData
3738
from .lti import isctime, isdtime, common_timebase
@@ -120,6 +121,9 @@ class for a set of subclasses that are used to implement specific
120121
121122
"""
122123

124+
# Allow ndarray * InputOutputSystem to give IOSystem._rmul_() priority
125+
__array_priority__ = 12 # override ndarray, matrix, SS types
126+
123127
_idCounter = 0
124128

125129
def _name_or_default(self, name=None):
@@ -195,14 +199,19 @@ def __str__(self):
195199

196200
def __mul__(sys2, sys1):
197201
"""Multiply two input/output systems (series interconnection)"""
202+
# Note: order of arguments is flipped so that self = sys2,
203+
# corresponding to the ordering convention of sys2 * sys1
198204

205+
# Convert sys1 to an I/O system if needed
199206
if isinstance(sys1, (int, float, np.number)):
200-
# TODO: Scale the output
201-
raise NotImplemented("Scalar multiplication not yet implemented")
207+
sys1 = LinearIOSystem(StateSpace(
208+
[], [], [], sys1 * np.eye(sys2.ninputs)))
202209

203210
elif isinstance(sys1, np.ndarray):
204-
# TODO: Post-multiply by a matrix
205-
raise NotImplemented("Matrix multiplication not yet implemented")
211+
sys1 = LinearIOSystem(StateSpace([], [], [], sys1))
212+
213+
elif isinstance(sys1, (StateSpace, TransferFunction)):
214+
sys1 = LinearIOSystem(sys1)
206215

207216
elif not isinstance(sys1, InputOutputSystem):
208217
raise TypeError("Unknown I/O system object ", sys1)
@@ -239,42 +248,41 @@ def __mul__(sys2, sys1):
239248

240249
def __rmul__(sys1, sys2):
241250
"""Pre-multiply an input/output systems by a scalar/matrix"""
242-
if isinstance(sys2, InputOutputSystem):
243-
# Both systems are InputOutputSystems => use __mul__
244-
return InputOutputSystem.__mul__(sys2, sys1)
245-
246-
elif isinstance(sys2, (int, float, np.number)):
247-
# TODO: Scale the output
248-
raise NotImplemented("Scalar multiplication not yet implemented")
251+
# Convert sys2 to an I/O system if needed
252+
if isinstance(sys2, (int, float, np.number)):
253+
sys2 = LinearIOSystem(StateSpace(
254+
[], [], [], sys2 * np.eye(sys1.noutputs)))
249255

250256
elif isinstance(sys2, np.ndarray):
251-
# TODO: Post-multiply by a matrix
252-
raise NotImplemented("Matrix multiplication not yet implemented")
257+
sys2 = LinearIOSystem(StateSpace([], [], [], sys2))
253258

254-
elif isinstance(sys2, StateSpace):
255-
# TODO: Should eventuall preserve LinearIOSystem structure
256-
return StateSpace.__mul__(sys2, sys1)
259+
elif isinstance(sys2, (StateSpace, TransferFunction)):
260+
sys2 = LinearIOSystem(sys2)
257261

258-
else:
259-
raise TypeError("Unknown I/O system object ", sys1)
262+
elif not isinstance(sys2, InputOutputSystem):
263+
raise TypeError("Unknown I/O system object ", sys2)
264+
265+
return InputOutputSystem.__mul__(sys2, sys1)
260266

261267
def __add__(sys1, sys2):
262268
"""Add two input/output systems (parallel interconnection)"""
263-
# TODO: Allow addition of scalars and matrices
269+
# Convert sys1 to an I/O system if needed
264270
if isinstance(sys2, (int, float, np.number)):
265-
# TODO: Scale the output
266-
raise NotImplemented("Scalar addition not yet implemented")
271+
sys2 = LinearIOSystem(StateSpace(
272+
[], [], [], sys2 * np.eye(sys1.ninputs)))
267273

268274
elif isinstance(sys2, np.ndarray):
269-
# TODO: Post-multiply by a matrix
270-
raise NotImplemented("Matrix addition not yet implemented")
275+
sys2 = LinearIOSystem(StateSpace([], [], [], sys2))
276+
277+
elif isinstance(sys2, (StateSpace, TransferFunction)):
278+
sys2 = LinearIOSystem(sys2)
271279

272280
elif not isinstance(sys2, InputOutputSystem):
273281
raise TypeError("Unknown I/O system object ", sys2)
274282

275283
# Make sure number of input and outputs match
276284
if sys1.ninputs != sys2.ninputs or sys1.noutputs != sys2.noutputs:
277-
raise ValueError("Can't add systems with different numbers of "
285+
raise ValueError("Can't add systems with incompatible numbers of "
278286
"inputs or outputs.")
279287
ninputs = sys1.ninputs
280288
noutputs = sys1.noutputs
@@ -293,16 +301,87 @@ def __add__(sys1, sys2):
293301
# Return the newly created InterconnectedSystem
294302
return newsys
295303

296-
# TODO: add __radd__ to allow postaddition by scalars and matrices
304+
def __radd__(sys1, sys2):
305+
"""Parallel addition of input/output system to a compatible object."""
306+
# Convert sys2 to an I/O system if needed
307+
if isinstance(sys2, (int, float, np.number)):
308+
sys2 = LinearIOSystem(StateSpace(
309+
[], [], [], sys2 * np.eye(sys1.noutputs)))
310+
311+
elif isinstance(sys2, np.ndarray):
312+
sys2 = LinearIOSystem(StateSpace([], [], [], sys2))
313+
314+
elif isinstance(sys2, (StateSpace, TransferFunction)):
315+
sys2 = LinearIOSystem(sys2)
316+
317+
elif not isinstance(sys2, InputOutputSystem):
318+
raise TypeError("Unknown I/O system object ", sys2)
319+
320+
return InputOutputSystem.__add__(sys2, sys1)
321+
322+
def __sub__(sys1, sys2):
323+
"""Subtract two input/output systems (parallel interconnection)"""
324+
# Convert sys1 to an I/O system if needed
325+
if isinstance(sys2, (int, float, np.number)):
326+
sys2 = LinearIOSystem(StateSpace(
327+
[], [], [], sys2 * np.eye(sys1.ninputs)))
328+
329+
elif isinstance(sys2, np.ndarray):
330+
sys2 = LinearIOSystem(StateSpace([], [], [], sys2))
331+
332+
elif isinstance(sys2, (StateSpace, TransferFunction)):
333+
sys2 = LinearIOSystem(sys2)
334+
335+
elif not isinstance(sys2, InputOutputSystem):
336+
raise TypeError("Unknown I/O system object ", sys2)
337+
338+
# Make sure number of input and outputs match
339+
if sys1.ninputs != sys2.ninputs or sys1.noutputs != sys2.noutputs:
340+
raise ValueError("Can't add systems with incompatible numbers of "
341+
"inputs or outputs.")
342+
ninputs = sys1.ninputs
343+
noutputs = sys1.noutputs
344+
345+
# Create a new system to handle the composition
346+
inplist = [[(0, i), (1, i)] for i in range(ninputs)]
347+
outlist = [[(0, i), (1, i, -1)] for i in range(noutputs)]
348+
newsys = InterconnectedSystem(
349+
(sys1, sys2), inplist=inplist, outlist=outlist)
350+
351+
# If both systems are linear, create LinearICSystem
352+
if isinstance(sys1, StateSpace) and isinstance(sys2, StateSpace):
353+
ss_sys = StateSpace.__sub__(sys1, sys2)
354+
return LinearICSystem(newsys, ss_sys)
355+
356+
# Return the newly created InterconnectedSystem
357+
return newsys
358+
359+
def __rsub__(sys1, sys2):
360+
"""Parallel subtraction of I/O system to a compatible object."""
361+
# Convert sys2 to an I/O system if needed
362+
if isinstance(sys2, (int, float, np.number)):
363+
sys2 = LinearIOSystem(StateSpace(
364+
[], [], [], sys2 * np.eye(sys1.noutputs)))
365+
366+
elif isinstance(sys2, np.ndarray):
367+
sys2 = LinearIOSystem(StateSpace([], [], [], sys2))
368+
369+
elif isinstance(sys2, (StateSpace, TransferFunction)):
370+
sys2 = LinearIOSystem(sys2)
371+
372+
elif not isinstance(sys2, InputOutputSystem):
373+
raise TypeError("Unknown I/O system object ", sys2)
374+
375+
return InputOutputSystem.__sub__(sys2, sys1)
297376

298377
def __neg__(sys):
299378
"""Negate an input/output systems (rescale)"""
300379
if sys.ninputs is None or sys.noutputs is None:
301380
raise ValueError("Can't determine number of inputs or outputs")
302381

382+
# Create a new system to hold the negation
303383
inplist = [(0, i) for i in range(sys.ninputs)]
304384
outlist = [(0, i, -1) for i in range(sys.noutputs)]
305-
# Create a new system to hold the negation
306385
newsys = InterconnectedSystem(
307386
(sys,), dt=sys.dt, inplist=inplist, outlist=outlist)
308387

@@ -667,8 +746,8 @@ class LinearIOSystem(InputOutputSystem, StateSpace):
667746
668747
Parameters
669748
----------
670-
linsys : StateSpace
671-
LTI StateSpace system to be converted
749+
linsys : StateSpace or TransferFunction
750+
LTI system to be converted
672751
inputs : int, list of str or None, optional
673752
Description of the system inputs. This can be given as an integer
674753
count or as a list of strings that name the individual signals. If an
@@ -711,12 +790,16 @@ def __init__(self, linsys, inputs=None, outputs=None, states=None,
711790
states. The new system can be a continuous or discrete time system.
712791
713792
"""
714-
if not isinstance(linsys, StateSpace):
793+
if isinstance(linsys, TransferFunction):
794+
# Convert system to StateSpace
795+
linsys = _convert_to_statespace(linsys)
796+
797+
elif not isinstance(linsys, StateSpace):
715798
raise TypeError("Linear I/O system must be a state space object")
716799

717800
# Look for 'input' and 'output' parameter name variants
718801
inputs = _parse_signal_parameter(inputs, 'input', kwargs)
719-
outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True)
802+
outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True)
720803

721804
# Create the I/O system object
722805
super(LinearIOSystem, self).__init__(
@@ -837,7 +920,7 @@ def __init__(self, updfcn, outfcn=None, inputs=None, outputs=None,
837920
"""Create a nonlinear I/O system given update and output functions."""
838921
# Look for 'input' and 'output' parameter name variants
839922
inputs = _parse_signal_parameter(inputs, 'input', kwargs)
840-
outputs = _parse_signal_parameter(outputs, 'output', kwargs)
923+
outputs = _parse_signal_parameter(outputs, 'output', kwargs)
841924

842925
# Store the update and output functions
843926
self.updfcn = updfcn
@@ -1399,13 +1482,12 @@ def set_output_map(self, output_map):
13991482
self.output_map = output_map
14001483
self.noutputs = output_map.shape[0]
14011484

1402-
14031485
def unused_signals(self):
14041486
"""Find unused subsystem inputs and outputs
14051487
14061488
Returns
14071489
-------
1408-
1490+
14091491
unused_inputs : dict
14101492
14111493
A mapping from tuple of indices (isys, isig) to string
@@ -1430,66 +1512,61 @@ def unused_signals(self):
14301512
unused_sysinp = sorted(set(range(nsubsysinp)) - used_sysinp)
14311513
unused_sysout = sorted(set(range(nsubsysout)) - used_sysout)
14321514

1433-
inputs = [(isys,isig, f'{sys.name}.{sig}')
1515+
inputs = [(isys, isig, f'{sys.name}.{sig}')
14341516
for isys, sys in enumerate(self.syslist)
14351517
for sig, isig in sys.input_index.items()]
14361518

1437-
outputs = [(isys,isig,f'{sys.name}.{sig}')
1519+
outputs = [(isys, isig, f'{sys.name}.{sig}')
14381520
for isys, sys in enumerate(self.syslist)
14391521
for sig, isig in sys.output_index.items()]
14401522

1441-
return ({inputs[i][:2]:inputs[i][2]
1442-
for i in unused_sysinp},
1443-
{outputs[i][:2]:outputs[i][2]
1444-
for i in unused_sysout})
1445-
1523+
return ({inputs[i][:2]: inputs[i][2] for i in unused_sysinp},
1524+
{outputs[i][:2]: outputs[i][2] for i in unused_sysout})
14461525

14471526
def _find_inputs_by_basename(self, basename):
14481527
"""Find all subsystem inputs matching basename
14491528
14501529
Returns
14511530
-------
1452-
Mapping from (isys, isig) to '{sys}.{sig}'
1531+
Mapping from (isys, isig) to '{sys}.{sig}'
14531532
14541533
"""
1455-
return {(isys, isig) : f'{sys.name}.{basename}'
1534+
return {(isys, isig): f'{sys.name}.{basename}'
14561535
for isys, sys in enumerate(self.syslist)
14571536
for sig, isig in sys.input_index.items()
14581537
if sig == (basename)}
14591538

1460-
14611539
def _find_outputs_by_basename(self, basename):
14621540
"""Find all subsystem outputs matching basename
14631541
14641542
Returns
14651543
-------
1466-
Mapping from (isys, isig) to '{sys}.{sig}'
1544+
Mapping from (isys, isig) to '{sys}.{sig}'
14671545
14681546
"""
1469-
return {(isys, isig) : f'{sys.name}.{basename}'
1547+
return {(isys, isig): f'{sys.name}.{basename}'
14701548
for isys, sys in enumerate(self.syslist)
14711549
for sig, isig in sys.output_index.items()
14721550
if sig == (basename)}
14731551

1474-
14751552
def check_unused_signals(self, ignore_inputs=None, ignore_outputs=None):
14761553
"""Check for unused subsystem inputs and outputs
14771554
14781555
If any unused inputs or outputs are found, emit a warning.
1479-
1556+
14801557
Parameters
14811558
----------
14821559
ignore_inputs : list of input-spec
14831560
Subsystem inputs known to be unused. input-spec can be any of:
14841561
'sig', 'sys.sig', (isys, isig), ('sys', isig)
1485-
1562+
14861563
If the 'sig' form is used, all subsystem inputs with that
14871564
name are considered ignored.
14881565
14891566
ignore_outputs : list of output-spec
14901567
Subsystem outputs known to be unused. output-spec can be any of:
14911568
'sig', 'sys.sig', (isys, isig), ('sys', isig)
1492-
1569+
14931570
If the 'sig' form is used, all subsystem outputs with that
14941571
name are considered ignored.
14951572
@@ -1509,27 +1586,31 @@ def check_unused_signals(self, ignore_inputs=None, ignore_outputs=None):
15091586
if isinstance(ignore_input, str) and '.' not in ignore_input:
15101587
ignore_idxs = self._find_inputs_by_basename(ignore_input)
15111588
if not ignore_idxs:
1512-
raise ValueError(f"Couldn't find ignored input {ignore_input} in subsystems")
1589+
raise ValueError(f"Couldn't find ignored input "
1590+
"{ignore_input} in subsystems")
15131591
ignore_input_map.update(ignore_idxs)
15141592
else:
1515-
ignore_input_map[self._parse_signal(ignore_input, 'input')[:2]] = ignore_input
1593+
ignore_input_map[self._parse_signal(
1594+
ignore_input, 'input')[:2]] = ignore_input
15161595

15171596
# (isys, isig) -> signal-spec
15181597
ignore_output_map = {}
15191598
for ignore_output in ignore_outputs:
15201599
if isinstance(ignore_output, str) and '.' not in ignore_output:
15211600
ignore_found = self._find_outputs_by_basename(ignore_output)
15221601
if not ignore_found:
1523-
raise ValueError(f"Couldn't find ignored output {ignore_output} in subsystems")
1602+
raise ValueError(f"Couldn't find ignored output "
1603+
"{ignore_output} in subsystems")
15241604
ignore_output_map.update(ignore_found)
15251605
else:
1526-
ignore_output_map[self._parse_signal(ignore_output, 'output')[:2]] = ignore_output
1606+
ignore_output_map[self._parse_signal(
1607+
ignore_output, 'output')[:2]] = ignore_output
15271608

15281609
dropped_inputs = set(unused_inputs) - set(ignore_input_map)
15291610
dropped_outputs = set(unused_outputs) - set(ignore_output_map)
15301611

15311612
used_ignored_inputs = set(ignore_input_map) - set(unused_inputs)
1532-
used_ignored_outputs = set(ignore_output_map) - set(unused_outputs)
1613+
used_ignored_outputs = set(ignore_output_map) - set(unused_outputs)
15331614

15341615
if dropped_inputs:
15351616
msg = ('Unused input(s) in InterconnectedSystem: '
@@ -2407,7 +2488,7 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[],
24072488
"""
24082489
# Look for 'input' and 'output' parameter name variants
24092490
inputs = _parse_signal_parameter(inputs, 'input', kwargs)
2410-
outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True)
2491+
outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True)
24112492

24122493
if not check_unused and (ignore_inputs or ignore_outputs):
24132494
raise ValueError('check_unused is False, but either '
@@ -2507,7 +2588,6 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[],
25072588
inputs=inputs, outputs=outputs, states=states,
25082589
params=params, dt=dt, name=name)
25092590

2510-
25112591
# check for implicity dropped signals
25122592
if check_unused:
25132593
newsys.check_unused_signals(ignore_inputs, ignore_outputs)
@@ -2598,7 +2678,7 @@ def _parse_list(signals, signame='input', prefix='u'):
25982678

25992679
# Look for 'input' and 'output' parameter name variants
26002680
inputs = _parse_signal_parameter(inputs, 'input', kwargs)
2601-
output = _parse_signal_parameter(output, 'outputs', kwargs, end=True)
2681+
output = _parse_signal_parameter(output, 'outputs', kwargs, end=True)
26022682

26032683
# Default values for inputs and output
26042684
if inputs is None:
@@ -2623,8 +2703,8 @@ def _parse_list(signals, signame='input', prefix='u'):
26232703
ninputs = ninputs * dimension
26242704

26252705
output_names = ["%s[%d]" % (name, dim)
2626-
for name in output_names
2627-
for dim in range(dimension)]
2706+
for name in output_names
2707+
for dim in range(dimension)]
26282708
noutputs = noutputs * dimension
26292709
elif dimension is not None:
26302710
raise ValueError(

0 commit comments

Comments
 (0)