3232from warnings import warn
3333
3434from .statesp import StateSpace , tf2ss , _convert_to_statespace
35+ from .xferfcn import TransferFunction
3536from .timeresp import _check_convert_array , _process_time_response
3637from .lti import isctime , isdtime , common_timebase
3738from . 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