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 , \
3637 TimeResponseData
3738from .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