Skip to content

Commit f95dc54

Browse files
committed
add iosys conversions (mul, rmul, add, radd, sub, rsub) + PEP8 cleanup
1 parent 5155dd5 commit f95dc54

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
from .lti import isctime, isdtime, common_timebase
3738
from . import config
@@ -119,6 +120,9 @@ class for a set of subclasses that are used to implement specific
119120
120121
"""
121122

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

124128
def _name_or_default(self, name=None):
@@ -194,14 +198,19 @@ def __str__(self):
194198

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

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

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

206215
elif not isinstance(sys1, InputOutputSystem):
207216
raise TypeError("Unknown I/O system object ", sys1)
@@ -238,42 +247,41 @@ def __mul__(sys2, sys1):
238247

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

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

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

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

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

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

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

274282
# Make sure number of input and outputs match
275283
if sys1.ninputs != sys2.ninputs or sys1.noutputs != sys2.noutputs:
276-
raise ValueError("Can't add systems with different numbers of "
284+
raise ValueError("Can't add systems with incompatible numbers of "
277285
"inputs or outputs.")
278286
ninputs = sys1.ninputs
279287
noutputs = sys1.noutputs
@@ -292,16 +300,87 @@ def __add__(sys1, sys2):
292300
# Return the newly created InterconnectedSystem
293301
return newsys
294302

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

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

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

@@ -666,8 +745,8 @@ class LinearIOSystem(InputOutputSystem, StateSpace):
666745
667746
Parameters
668747
----------
669-
linsys : StateSpace
670-
LTI StateSpace system to be converted
748+
linsys : StateSpace or TransferFunction
749+
LTI system to be converted
671750
inputs : int, list of str or None, optional
672751
Description of the system inputs. This can be given as an integer
673752
count or as a list of strings that name the individual signals. If an
@@ -710,12 +789,16 @@ def __init__(self, linsys, inputs=None, outputs=None, states=None,
710789
states. The new system can be a continuous or discrete time system.
711790
712791
"""
713-
if not isinstance(linsys, StateSpace):
792+
if isinstance(linsys, TransferFunction):
793+
# Convert system to StateSpace
794+
linsys = _convert_to_statespace(linsys)
795+
796+
elif not isinstance(linsys, StateSpace):
714797
raise TypeError("Linear I/O system must be a state space object")
715798

716799
# Look for 'input' and 'output' parameter name variants
717800
inputs = _parse_signal_parameter(inputs, 'input', kwargs)
718-
outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True)
801+
outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True)
719802

720803
# Create the I/O system object
721804
super(LinearIOSystem, self).__init__(
@@ -836,7 +919,7 @@ def __init__(self, updfcn, outfcn=None, inputs=None, outputs=None,
836919
"""Create a nonlinear I/O system given update and output functions."""
837920
# Look for 'input' and 'output' parameter name variants
838921
inputs = _parse_signal_parameter(inputs, 'input', kwargs)
839-
outputs = _parse_signal_parameter(outputs, 'output', kwargs)
922+
outputs = _parse_signal_parameter(outputs, 'output', kwargs)
840923

841924
# Store the update and output functions
842925
self.updfcn = updfcn
@@ -1397,13 +1480,12 @@ def set_output_map(self, output_map):
13971480
self.output_map = output_map
13981481
self.noutputs = output_map.shape[0]
13991482

1400-
14011483
def unused_signals(self):
14021484
"""Find unused subsystem inputs and outputs
14031485
14041486
Returns
14051487
-------
1406-
1488+
14071489
unused_inputs : dict
14081490
14091491
A mapping from tuple of indices (isys, isig) to string
@@ -1428,66 +1510,61 @@ def unused_signals(self):
14281510
unused_sysinp = sorted(set(range(nsubsysinp)) - used_sysinp)
14291511
unused_sysout = sorted(set(range(nsubsysout)) - used_sysout)
14301512

1431-
inputs = [(isys,isig, f'{sys.name}.{sig}')
1513+
inputs = [(isys, isig, f'{sys.name}.{sig}')
14321514
for isys, sys in enumerate(self.syslist)
14331515
for sig, isig in sys.input_index.items()]
14341516

1435-
outputs = [(isys,isig,f'{sys.name}.{sig}')
1517+
outputs = [(isys, isig, f'{sys.name}.{sig}')
14361518
for isys, sys in enumerate(self.syslist)
14371519
for sig, isig in sys.output_index.items()]
14381520

1439-
return ({inputs[i][:2]:inputs[i][2]
1440-
for i in unused_sysinp},
1441-
{outputs[i][:2]:outputs[i][2]
1442-
for i in unused_sysout})
1443-
1521+
return ({inputs[i][:2]: inputs[i][2] for i in unused_sysinp},
1522+
{outputs[i][:2]: outputs[i][2] for i in unused_sysout})
14441523

14451524
def _find_inputs_by_basename(self, basename):
14461525
"""Find all subsystem inputs matching basename
14471526
14481527
Returns
14491528
-------
1450-
Mapping from (isys, isig) to '{sys}.{sig}'
1529+
Mapping from (isys, isig) to '{sys}.{sig}'
14511530
14521531
"""
1453-
return {(isys, isig) : f'{sys.name}.{basename}'
1532+
return {(isys, isig): f'{sys.name}.{basename}'
14541533
for isys, sys in enumerate(self.syslist)
14551534
for sig, isig in sys.input_index.items()
14561535
if sig == (basename)}
14571536

1458-
14591537
def _find_outputs_by_basename(self, basename):
14601538
"""Find all subsystem outputs matching basename
14611539
14621540
Returns
14631541
-------
1464-
Mapping from (isys, isig) to '{sys}.{sig}'
1542+
Mapping from (isys, isig) to '{sys}.{sig}'
14651543
14661544
"""
1467-
return {(isys, isig) : f'{sys.name}.{basename}'
1545+
return {(isys, isig): f'{sys.name}.{basename}'
14681546
for isys, sys in enumerate(self.syslist)
14691547
for sig, isig in sys.output_index.items()
14701548
if sig == (basename)}
14711549

1472-
14731550
def check_unused_signals(self, ignore_inputs=None, ignore_outputs=None):
14741551
"""Check for unused subsystem inputs and outputs
14751552
14761553
If any unused inputs or outputs are found, emit a warning.
1477-
1554+
14781555
Parameters
14791556
----------
14801557
ignore_inputs : list of input-spec
14811558
Subsystem inputs known to be unused. input-spec can be any of:
14821559
'sig', 'sys.sig', (isys, isig), ('sys', isig)
1483-
1560+
14841561
If the 'sig' form is used, all subsystem inputs with that
14851562
name are considered ignored.
14861563
14871564
ignore_outputs : list of output-spec
14881565
Subsystem outputs known to be unused. output-spec can be any of:
14891566
'sig', 'sys.sig', (isys, isig), ('sys', isig)
1490-
1567+
14911568
If the 'sig' form is used, all subsystem outputs with that
14921569
name are considered ignored.
14931570
@@ -1507,27 +1584,31 @@ def check_unused_signals(self, ignore_inputs=None, ignore_outputs=None):
15071584
if isinstance(ignore_input, str) and '.' not in ignore_input:
15081585
ignore_idxs = self._find_inputs_by_basename(ignore_input)
15091586
if not ignore_idxs:
1510-
raise ValueError(f"Couldn't find ignored input {ignore_input} in subsystems")
1587+
raise ValueError(f"Couldn't find ignored input "
1588+
"{ignore_input} in subsystems")
15111589
ignore_input_map.update(ignore_idxs)
15121590
else:
1513-
ignore_input_map[self._parse_signal(ignore_input, 'input')[:2]] = ignore_input
1591+
ignore_input_map[self._parse_signal(
1592+
ignore_input, 'input')[:2]] = ignore_input
15141593

15151594
# (isys, isig) -> signal-spec
15161595
ignore_output_map = {}
15171596
for ignore_output in ignore_outputs:
15181597
if isinstance(ignore_output, str) and '.' not in ignore_output:
15191598
ignore_found = self._find_outputs_by_basename(ignore_output)
15201599
if not ignore_found:
1521-
raise ValueError(f"Couldn't find ignored output {ignore_output} in subsystems")
1600+
raise ValueError(f"Couldn't find ignored output "
1601+
"{ignore_output} in subsystems")
15221602
ignore_output_map.update(ignore_found)
15231603
else:
1524-
ignore_output_map[self._parse_signal(ignore_output, 'output')[:2]] = ignore_output
1604+
ignore_output_map[self._parse_signal(
1605+
ignore_output, 'output')[:2]] = ignore_output
15251606

15261607
dropped_inputs = set(unused_inputs) - set(ignore_input_map)
15271608
dropped_outputs = set(unused_outputs) - set(ignore_output_map)
15281609

15291610
used_ignored_inputs = set(ignore_input_map) - set(unused_inputs)
1530-
used_ignored_outputs = set(ignore_output_map) - set(unused_outputs)
1611+
used_ignored_outputs = set(ignore_output_map) - set(unused_outputs)
15311612

15321613
if dropped_inputs:
15331614
msg = ('Unused input(s) in InterconnectedSystem: '
@@ -2382,7 +2463,7 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[],
23822463
"""
23832464
# Look for 'input' and 'output' parameter name variants
23842465
inputs = _parse_signal_parameter(inputs, 'input', kwargs)
2385-
outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True)
2466+
outputs = _parse_signal_parameter(outputs, 'output', kwargs, end=True)
23862467

23872468
if not check_unused and (ignore_inputs or ignore_outputs):
23882469
raise ValueError('check_unused is False, but either '
@@ -2482,7 +2563,6 @@ def interconnect(syslist, connections=None, inplist=[], outlist=[],
24822563
inputs=inputs, outputs=outputs, states=states,
24832564
params=params, dt=dt, name=name)
24842565

2485-
24862566
# check for implicity dropped signals
24872567
if check_unused:
24882568
newsys.check_unused_signals(ignore_inputs, ignore_outputs)
@@ -2573,7 +2653,7 @@ def _parse_list(signals, signame='input', prefix='u'):
25732653

25742654
# Look for 'input' and 'output' parameter name variants
25752655
inputs = _parse_signal_parameter(inputs, 'input', kwargs)
2576-
output = _parse_signal_parameter(output, 'outputs', kwargs, end=True)
2656+
output = _parse_signal_parameter(output, 'outputs', kwargs, end=True)
25772657

25782658
# Default values for inputs and output
25792659
if inputs is None:
@@ -2598,8 +2678,8 @@ def _parse_list(signals, signame='input', prefix='u'):
25982678
ninputs = ninputs * dimension
25992679

26002680
output_names = ["%s[%d]" % (name, dim)
2601-
for name in output_names
2602-
for dim in range(dimension)]
2681+
for name in output_names
2682+
for dim in range(dimension)]
26032683
noutputs = noutputs * dimension
26042684
elif dimension is not None:
26052685
raise ValueError(

0 commit comments

Comments
 (0)