Multiply Scipy.lti Transfer Functions
Solution 1:
Interestingly, Scipy does not seem to supply that functionality. An alternative is converting the LTI system into a Sympy rational function. Sympy allows you to easily expand and cancel polynomials:
from IPython.display import display
from scipy import signal
import sympy as sy
sy.init_printing() # LaTeX like pretty printing for IPythondeflti_to_sympy(lsys, symplify=True):
""" Convert Scipy's LTI instance to Sympy expression """
s = sy.Symbol('s')
G = sy.Poly(lsys.num, s) / sy.Poly(lsys.den, s)
return sy.simplify(G) if symplify else G
defsympy_to_lti(xpr, s=sy.Symbol('s')):
""" Convert Sympy transfer function polynomial to Scipy LTI """
num, den = sy.simplify(xpr).as_numer_denom() # expressions
p_num_den = sy.poly(num, s), sy.poly(den, s) # polynomials
c_num_den = [sy.expand(p).all_coeffs() for p in p_num_den] # coefficients
l_num, l_den = [sy.lambdify((), c)() for c in c_num_den] # convert to floatsreturn signal.lti(l_num, l_den)
pG, pH, pGH, pIGH = sy.symbols("G, H, GH, IGH") # only needed for displaying# Sample systems:
lti_G = signal.lti([1], [1, 2])
lti_H = signal.lti([2], [1, 0, 3])
# convert to Sympy:
Gs, Hs = lti_to_sympy(lti_G), lti_to_sympy(lti_H)
print("Converted LTI expressions:")
display(sy.Eq(pG, Gs))
display(sy.Eq(pH, Hs))
print("Multiplying Systems:")
GHs = sy.simplify(Gs*Hs).expand() # make sure polynomials are canceled and expanded
display(sy.Eq(pGH, GHs))
print("Closing the loop:")
IGHs = sy.simplify(GHs / (1+GHs)).expand()
display(sy.Eq(pIGH, IGHs))
print("Back to LTI:")
lti_IGH = sympy_to_lti(IGHs)
print(lti_IGH)
The output is:
Solution 2:
Depending on your definition of "easy", you should consider deriving your own class from lti
, implementing the necessary algebraic operations on your transfer functions. This is probably the most elegant approach.
Here's my take on the subject:
from __future__ import division
from scipy.signal.ltisys import TransferFunction as TransFun
from numpy import polymul,polyadd
classltimul(TransFun):
def__neg__(self):
return ltimul(-self.num,self.den)
def__floordiv__(self,other):
# can't make sense of integer division right nowreturnNotImplementeddef__mul__(self,other):
iftype(other) in [int, float]:
return ltimul(self.num*other,self.den)
eliftype(other) in [TransFun, ltimul]:
numer = polymul(self.num,other.num)
denom = polymul(self.den,other.den)
return ltimul(numer,denom)
def__truediv__(self,other):
iftype(other) in [int, float]:
return ltimul(self.num,self.den*other)
iftype(other) in [TransFun, ltimul]:
numer = polymul(self.num,other.den)
denom = polymul(self.den,other.num)
return ltimul(numer,denom)
def__rtruediv__(self,other):
iftype(other) in [int, float]:
return ltimul(other*self.den,self.num)
iftype(other) in [TransFun, ltimul]:
numer = polymul(self.den,other.num)
denom = polymul(self.num,other.den)
return ltimul(numer,denom)
def__add__(self,other):
iftype(other) in [int, float]:
return ltimul(polyadd(self.num,self.den*other),self.den)
iftype(other) in [TransFun, type(self)]:
numer = polyadd(polymul(self.num,other.den),polymul(self.den,other.num))
denom = polymul(self.den,other.den)
return ltimul(numer,denom)
def__sub__(self,other):
iftype(other) in [int, float]:
return ltimul(polyadd(self.num,-self.den*other),self.den)
iftype(other) in [TransFun, type(self)]:
numer = polyadd(polymul(self.num,other.den),-polymul(self.den,other.num))
denom = polymul(self.den,other.den)
return ltimul(numer,denom)
def__rsub__(self,other):
iftype(other) in [int, float]:
return ltimul(polyadd(-self.num,self.den*other),self.den)
iftype(other) in [TransFun, type(self)]:
numer = polyadd(polymul(other.num,self.den),-polymul(other.den,self.num))
denom = polymul(self.den,other.den)
return ltimul(numer,denom)
# sheer laziness: symmetric behaviour for commutative operators
__rmul__ = __mul__
__radd__ = __add__
This defines the ltimul
class, which is lti
plus addition, multiplication, division, subtraction, and negation; binary ones also defined for integers and floats as partners.
I tested it for the example of Dietrich:
G_s = ltimul([1], [1, 2])
H_s = ltimul([2], [1, 0, 3])
print(G_s*H_s)
print(G_s*H_s/(1+G_s*H_s))
While GH
is nicely equal to
ltimul(
array([ 2.]),
array([ 1., 2., 3., 6.])
)
the final result for GH/(1+GH) is less pretty:
ltimul(
array([ 2., 4., 6., 12.]),
array([ 1., 4., 10., 26., 37., 42., 48.])
)
Since I'm not very familiar with transfer functions, I'm not sure how likely it is that this gives the same result as the sympy-based solution due to some simplifications missing from this one. I find it suspicious that already lti
behaves unexpectedly: lti([1,2],[1,2])
doesn't simplify its arguments, even though I'd suspect this function to be constant 1. So I'd rather not guess the correctness of this final result.
Anyway, the main message is inheritance itself, so possible bugs in the above implementation hopefully pose only a minor inconvenience. I'm also quite unfamiliar with class definitions, so it's possible that I didn't follow best practices in the above.
I eventually rewrote the above after @ochurlaud pointed out, that my original only worked for Python 2. The reason is that the /
operation is implemented by __div__
/__rdiv__
in Python 2 (and is the ambiguous "classical division"). In Python 3, however, there is a distinction between /
(true division) and //
(floor division), and they call __truediv__
and __floordiv__
(and their "right" counterparts), respectively. The __future__
import first in the line of the above code triggers the proper Python 3 behaviour even on Python 2, so the above works on both Python versions. Since floor (integer) division doesn't make much sense for our class, we explicitly signal that it can't do anything with //
(unless the other operand implements it).
One could also easily define the respective __iadd__
, __idiv__
etc. in-place operations for +=
, /=
etc., respectively.
Post a Comment for "Multiply Scipy.lti Transfer Functions"