Merge pull request #1081 from sdahdah/main · python-control/python-control@71bd731

@@ -30,6 +30,7 @@

3030

from scipy.signal import cont2discrete

31313232

from . import config

33+

from . import bdalg

3334

from .exception import ControlMIMONotImplemented, ControlSlycot, slycot_check

3435

from .frdata import FrequencyResponseData

3536

from .iosys import InputOutputSystem, NamedSignal, _process_dt_keyword, \

@@ -572,6 +573,9 @@ def __add__(self, other):

572573573574

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

583587584588

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):

636646637647

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

648662649663

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)

687707688708

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)

692719693720

if not isinstance(other, StateSpace):

694721

return 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+696729

return other * self

697730698731

# 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

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+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: