3030from scipy .signal import cont2discrete
3131
3232from . import config
33+ from . import bdalg
3334from .exception import ControlMIMONotImplemented , ControlSlycot , slycot_check
3435from .frdata import FrequencyResponseData
3536from .iosys import InputOutputSystem , NamedSignal , _process_dt_keyword , \
@@ -572,6 +573,9 @@ def __add__(self, other):
572573
573574 elif isinstance (other , np .ndarray ):
574575 other = np .atleast_2d (other )
576+ # Special case for SISO
577+ if self .issiso ():
578+ self = np .ones_like (other ) * self
575579 if self .ninputs != other .shape [0 ]:
576580 raise ValueError ("array has incompatible shape" )
577581 A , B , C = self .A , self .B , self .C
@@ -582,6 +586,12 @@ def __add__(self, other):
582586 return NotImplemented # let other.__rmul__ handle it
583587
584588 else :
589+ # Promote SISO object to compatible dimension
590+ if self .issiso () and not other .issiso ():
591+ self = np .ones ((other .noutputs , other .ninputs )) * self
592+ elif not self .issiso () and other .issiso ():
593+ other = np .ones ((self .noutputs , self .ninputs )) * other
594+
585595 # Check to make sure the dimensions are OK
586596 if ((self .ninputs != other .ninputs ) or
587597 (self .noutputs != other .noutputs )):
@@ -636,6 +646,10 @@ def __mul__(self, other):
636646
637647 elif isinstance (other , np .ndarray ):
638648 other = np .atleast_2d (other )
649+ # Special case for SISO
650+ if self .issiso ():
651+ self = bdalg .append (* ([self ] * other .shape [0 ]))
652+ # Dimension check after broadcasting
639653 if self .ninputs != other .shape [0 ]:
640654 raise ValueError ("array has incompatible shape" )
641655 A , C = self .A , self .C
@@ -647,6 +661,12 @@ def __mul__(self, other):
647661 return NotImplemented # let other.__rmul__ handle it
648662
649663 else :
664+ # Promote SISO object to compatible dimension
665+ if self .issiso () and not other .issiso ():
666+ self = bdalg .append (* ([self ] * other .noutputs ))
667+ elif not self .issiso () and other .issiso ():
668+ other = bdalg .append (* ([other ] * self .ninputs ))
669+
650670 # Check to make sure the dimensions are OK
651671 if self .ninputs != other .noutputs :
652672 raise ValueError (
@@ -686,23 +706,67 @@ def __rmul__(self, other):
686706 return StateSpace (self .A , B , self .C , D , self .dt )
687707
688708 elif isinstance (other , np .ndarray ):
689- C = np .atleast_2d (other ) @ self .C
690- D = np .atleast_2d (other ) @ self .D
709+ other = np .atleast_2d (other )
710+ # Special case for SISO transfer function
711+ if self .issiso ():
712+ self = bdalg .append (* ([self ] * other .shape [1 ]))
713+ # Dimension check after broadcasting
714+ if self .noutputs != other .shape [1 ]:
715+ raise ValueError ("array has incompatible shape" )
716+ C = other @ self .C
717+ D = other @ self .D
691718 return StateSpace (self .A , self .B , C , D , self .dt )
692719
693720 if not isinstance (other , StateSpace ):
694721 return NotImplemented
695722
723+ # Promote SISO object to compatible dimension
724+ if self .issiso () and not other .issiso ():
725+ self = bdalg .append (* ([self ] * other .ninputs ))
726+ elif not self .issiso () and other .issiso ():
727+ other = bdalg .append (* ([other ] * self .noutputs ))
728+
696729 return other * self
697730
698731 # TODO: general __truediv__ requires descriptor system support
699732 def __truediv__ (self , other ):
700733 """Division of state space systems by TFs, FRDs, scalars, and arrays"""
701- if not isinstance (other , (LTI , InputOutputSystem )):
702- return self * (1 / other )
703- else :
734+ # Let ``other.__rtruediv__`` handle it
735+ try :
736+ return self * (1 / other )
737+ except ValueError :
704738 return NotImplemented
705739
740+ def __rtruediv__ (self , other ):
741+ """Division by state space system"""
742+ return other * self ** - 1
743+
744+ def __pow__ (self , other ):
745+ """Power of a state space system"""
746+ if not type (other ) == int :
747+ raise ValueError ("Exponent must be an integer" )
748+ if self .ninputs != self .noutputs :
749+ # System must have same number of inputs and outputs
750+ return NotImplemented
751+ if other < - 1 :
752+ return (self ** - 1 )** (- other )
753+ elif other == - 1 :
754+ try :
755+ Di = scipy .linalg .inv (self .D )
756+ except scipy .linalg .LinAlgError :
757+ # D matrix must be nonsingular
758+ return NotImplemented
759+ Ai = self .A - self .B @ Di @ self .C
760+ Bi = self .B @ Di
761+ Ci = - Di @ self .C
762+ return StateSpace (Ai , Bi , Ci , Di , self .dt )
763+ elif other == 0 :
764+ return StateSpace ([], [], [], np .eye (self .ninputs ), self .dt )
765+ elif other == 1 :
766+ return self
767+ elif other > 1 :
768+ return self * (self ** (other - 1 ))
769+
706770 def __call__ (self , x , squeeze = None , warn_infinite = True ):
707771 """Evaluate system's frequency response at complex frequencies.
708772
@@ -1107,7 +1171,7 @@ def minreal(self, tol=0.0):
11071171 A , B , C , nr = tb01pd (self .nstates , self .ninputs , self .noutputs ,
11081172 self .A , B , C , tol = tol )
11091173 return StateSpace (A [:nr , :nr ], B [:nr , :self .ninputs ],
1110- C [:self .noutputs , :nr ], self .D )
1174+ C [:self .noutputs , :nr ], self .D , self . dt )
11111175 except ImportError :
11121176 raise TypeError ("minreal requires slycot tb01pd" )
11131177 else :
0 commit comments