Merge pull request #1081 from sdahdah/main · python-control/python-control@71bd731
@@ -30,6 +30,7 @@
3030from scipy.signal import cont2discrete
31313232from . 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):
572573573574elif isinstance(other, np.ndarray):
574575other = np.atleast_2d(other)
576+# Special case for SISO
577+if self.issiso():
578+self = np.ones_like(other) * self
575579if self.ninputs != other.shape[0]:
576580raise ValueError("array has incompatible shape")
577581A, B, C = self.A, self.B, self.C
@@ -582,6 +586,12 @@ def __add__(self, other):
582586return NotImplemented # let other.__rmul__ handle it
583587584588else:
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
586596if ((self.ninputs != other.ninputs) or
587597 (self.noutputs != other.noutputs)):
@@ -636,6 +646,10 @@ def __mul__(self, other):
636646637647elif isinstance(other, np.ndarray):
638648other = 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
639653if self.ninputs != other.shape[0]:
640654raise ValueError("array has incompatible shape")
641655A, C = self.A, self.C
@@ -647,6 +661,12 @@ def __mul__(self, other):
647661return NotImplemented # let other.__rmul__ handle it
648662649663else:
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
651671if self.ninputs != other.noutputs:
652672raise ValueError(
@@ -686,23 +706,67 @@ def __rmul__(self, other):
686706return StateSpace(self.A, B, self.C, D, self.dt)
687707688708elif 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
691718return StateSpace(self.A, self.B, C, D, self.dt)
692719693720if not isinstance(other, StateSpace):
694721return NotImplemented
695722723+# 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+696729return other * self
697730698731# TODO: general __truediv__ requires descriptor system support
699732def __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:
704738return NotImplemented
705739740+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+706770def __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):
11071171A, B, C, nr = tb01pd(self.nstates, self.ninputs, self.noutputs,
11081172self.A, B, C, tol=tol)
11091173return StateSpace(A[:nr, :nr], B[:nr, :self.ninputs],
1110-C[:self.noutputs, :nr], self.D)
1174+C[:self.noutputs, :nr], self.D, self.dt)
11111175except ImportError:
11121176raise TypeError("minreal requires slycot tb01pd")
11131177else: