Skip to content

Commit

Permalink
Merge pull request #21634 from akshanshbhatt/pr_transferfunc
Browse files Browse the repository at this point in the history
Improve `TransferFunction` docs and add `to_expr`
  • Loading branch information
namannimmo10 committed Jun 22, 2021
2 parents 531993a + d7bb3b2 commit 6a57794
Show file tree
Hide file tree
Showing 2 changed files with 220 additions and 18 deletions.
193 changes: 179 additions & 14 deletions sympy/physics/control/lti.py
@@ -1,7 +1,7 @@
from sympy import Basic, Mul, Pow, degree, Symbol, expand, cancel, Expr, exp, roots
from sympy import Basic, Mul, Pow, degree, Symbol, expand, cancel, Expr, roots
from sympy.core.evalf import EvalfMixin
from sympy.core.logic import fuzzy_and
from sympy.core.numbers import Integer
from sympy.core.numbers import Integer, ComplexInfinity
from sympy.core.sympify import sympify, _sympify
from sympy.polys import Poly, rootof
from sympy.series import limit
Expand All @@ -19,15 +19,69 @@ def _roots(poly, var):


class TransferFunction(Basic, EvalfMixin):
"""
r"""
A class for representing LTI (Linear, time-invariant) systems that can be strictly described
by ratio of polynomials in the Laplace Transform complex variable. The arguments
by ratio of polynomials in the Laplace transform complex variable. The arguments
are ``num``, ``den``, and ``var``, where ``num`` and ``den`` are numerator and
denominator polynomials of the ``TransferFunction`` respectively, and the third argument is
a complex variable of the Laplace transform used by these polynomials of the transfer function.
``num`` and ``den`` can be either polynomials or numbers, whereas ``var``
has to be a Symbol.
Explanation
===========
Generally, a dynamical system representing a physical model can be described in terms of Linear
Ordinary Differential Equations like -
$\small{b_{m}y^{\left(m\right)}+b_{m-1}y^{\left(m-1\right)}+\dots+b_{1}y^{\left(1\right)}+b_{0}y=
a_{n}x^{\left(n\right)}+a_{n-1}x^{\left(n-1\right)}+\dots+a_{1}x^{\left(1\right)}+a_{0}x}$
Here, $x$ is the input signal and $y$ is the output signal and superscript on both is the order of derivative
(not exponent). Derivative is taken with respect to the independent variable, $t$. Also, generally $m$ is greater
than $n$.
It is not feasible to analyse the properties of such systems in their native form therefore, we use
mathematical tools like Laplace transform to get a better perspective. Taking the Laplace transform
of both the sides in the equation (at zero initial conditions), we get -
$\small{\mathcal{L}[b_{m}y^{\left(m\right)}+b_{m-1}y^{\left(m-1\right)}+\dots+b_{1}y^{\left(1\right)}+b_{0}y]=
\mathcal{L}[a_{n}x^{\left(n\right)}+a_{n-1}x^{\left(n-1\right)}+\dots+a_{1}x^{\left(1\right)}+a_{0}x]}$
Using the linearity property of Laplace transform and also considering zero initial conditions
(i.e. $\small{y(0^{-}) = 0}$, $\small{y'(0^{-}) = 0}$ and so on), the equation
above gets translated to -
$\small{b_{m}\mathcal{L}[y^{\left(m\right)}]+\dots+b_{1}\mathcal{L}[y^{\left(1\right)}]+b_{0}\mathcal{L}[y]=
a_{n}\mathcal{L}[x^{\left(n\right)}]+\dots+a_{1}\mathcal{L}[x^{\left(1\right)}]+a_{0}\mathcal{L}[x]}$
Now, applying Derivative property of Laplace transform,
$\small{b_{m}s^{m}\mathcal{L}[y]+\dots+b_{1}s\mathcal{L}[y]+b_{0}\mathcal{L}[y]=
a_{n}s^{n}\mathcal{L}[x]+\dots+a_{1}s\mathcal{L}[x]+a_{0}\mathcal{L}[x]}$
Here, the superscript on $s$ is **exponent**. Note that the zero initial conditions assumption, mentioned above, is very important
and cannot be ignored otherwise the dynamical system cannot be considered time-independent and the simplified equation above
cannot be reached.
Collecting $\mathcal{L}[y]$ and $\mathcal{L}[x]$ terms from both the sides and taking the ratio
$\frac{ \mathcal{L}\left\{y\right\} }{ \mathcal{L}\left\{x\right\} }$, we get the typical rational form of transfer
function.
The numerator of the transfer function is, therefore, the Laplace transform of the output signal
(The signals are represented as functions of time) and similarly, the denominator
of the transfer function is the Laplace transform of the input signal. It is also a convention
to denote the input and output signal's Laplace transform with capital alphabets like shown below.
$H(s) = \frac{Y(s)}{X(s)} = \frac{ \mathcal{L}\left\{y(t)\right\} }{ \mathcal{L}\left\{x(t)\right\} }$
$s$, also known as complex frequency, is a complex variable in the Laplace domain. It corresponds to the
equivalent variable $t$, in the time domain. Transfer functions are sometimes also referred to as the Laplace
transform of the system's impulse response. Transfer function, $H$, is represented as a rational
function in $s$ like,
$H(s) =\ \frac{a_{n}s^{n}+a_{n-1}s^{n-1}+\dots+a_{1}s+a_{0}}{b_{m}s^{m}+b_{m-1}s^{m-1}+\dots+b_{1}s+b_{0}}$
Parameters
==========
Expand All @@ -43,9 +97,8 @@ class TransferFunction(Basic, EvalfMixin):
======
TypeError
When ``var`` is not a Symbol or when ``num`` or ``den`` is not
a number or a polynomial. Also, when ``num`` or ``den`` has
a time delay term.
When ``var`` is not a Symbol or when ``num`` or ``den`` is not a
number or a polynomial.
ValueError
When ``den`` is zero.
Expand Down Expand Up @@ -144,25 +197,111 @@ class TransferFunction(Basic, EvalfMixin):
Feedback, Series, Parallel
References
==========
.. [1] https://en.wikipedia.org/wiki/Transfer_function
.. [2] https://en.wikipedia.org/wiki/Laplace_transform
"""
def __new__(cls, num, den, var):
num, den = _sympify(num), _sympify(den)

if not isinstance(var, Symbol):
raise TypeError("Variable input must be a Symbol.")

if den == 0:
raise ValueError("TransferFunction can't have a zero denominator.")

if (((isinstance(num, Expr) and num.has(Symbol) and not num.has(exp)) or num.is_number) and
((isinstance(den, Expr) and den.has(Symbol) and not den.has(exp)) or den.is_number)):
obj = super().__new__(cls, num, den, var)
obj._num = num
obj._den = den
obj._var = var
return obj
if (((isinstance(num, Expr) and num.has(Symbol)) or num.is_number) and
((isinstance(den, Expr) and den.has(Symbol)) or den.is_number)):
obj = super(TransferFunction, cls).__new__(cls, num, den, var)
obj._num = num
obj._den = den
obj._var = var
return obj

else:
raise TypeError("Unsupported type for numerator or denominator of TransferFunction.")

@classmethod
def from_rational_expression(cls, expr, var=None):
r"""
Creates a new ``TransferFunction`` efficiently from a rational expression.
Parameters
==========
expr : Expr, Number
The rational expression representing the ``TransferFunction``.
var : Symbol, optional
Complex variable of the Laplace transform used by the
polynomials of the transfer function.
Raises
======
ValueError
When ``expr`` is of type ``Number`` and optional parameter ``var``
is not passed.
When ``expr`` has more than one variables and an optional parameter
``var`` is not passed.
ZeroDivisionError
When denominator of ``expr`` is zero or it has ``ComplexInfinity``
in its numerator.
Examples
========
>>> from sympy.abc import s, p, a
>>> from sympy.physics.control.lti import TransferFunction
>>> expr1 = (s + 5)/(3*s**2 + 2*s + 1)
>>> tf1 = TransferFunction.from_rational_expression(expr1)
>>> tf1
TransferFunction(s + 5, 3*s**2 + 2*s + 1, s)
>>> expr2 = (a*p**3 - a*p**2 + s*p)/(p + a**2) # Expr with more than one variables
>>> tf2 = TransferFunction.from_rational_expression(expr2, p)
>>> tf2
TransferFunction(a*p**3 - a*p**2 + p*s, a**2 + p, p)
In case of conflict between two or more variables in a expression, SymPy will
raise a ``ValueError``, if ``var`` is not passed by the user.
>>> tf = TransferFunction.from_rational_expression((a + a*s)/(s**2 + s + 1))
Traceback (most recent call last):
...
ValueError: Conflicting values found for positional argument `var` ({a, s}). Specify it manually.
This can be corrected by specifying the ``var`` parameter manually.
>>> tf = TransferFunction.from_rational_expression((a + a*s)/(s**2 + s + 1), s)
>>> tf
TransferFunction(a*s + a, s**2 + s + 1, s)
``var`` also need to be specified when ``expr`` is a ``Number``
>>> tf3 = TransferFunction.from_rational_expression(10, s)
>>> tf3
TransferFunction(10, 1, s)
"""
expr = _sympify(expr)
if var is None:
_free_symbols = expr.free_symbols
_len_free_symbols = len(_free_symbols)
if _len_free_symbols == 1:
var = list(_free_symbols)[0]
elif _len_free_symbols == 0:
raise ValueError("Positional argument `var` not found in the TransferFunction defined. Specify it manually.")
else:
raise ValueError("Conflicting values found for positional argument `var` ({}). Specify it manually.".format(_free_symbols))

_num, _den = expr.as_numer_denom()
if _den == 0 or _num.has(ComplexInfinity):
raise ZeroDivisionError("TransferFunction can't have a zero denominator.")
return cls(_num, _den, var)

@property
def num(self):
"""
Expand Down Expand Up @@ -526,6 +665,32 @@ def is_biproper(self):
"""
return degree(self.num, self.var) == degree(self.den, self.var)

def to_expr(self):
"""
Converts a ``TransferFunction`` object to SymPy Expr.
Examples
========
>>> from sympy.abc import s, p, a, b
>>> from sympy.physics.control.lti import TransferFunction
>>> from sympy.core.expr import Expr
>>> tf1 = TransferFunction(s, a*s**2 + 1, s)
>>> tf1.to_expr()
s/(a*s**2 + 1)
>>> isinstance(_, Expr)
True
>>> tf2 = TransferFunction(1, (p + 3*b)*(b - p), p)
>>> tf2.to_expr()
1/((b - p)*(3*b + p))
>>> tf3 = TransferFunction((s - 2)*(s - 3), (s - 1)*(s - 2)*(s - 3), s)
>>> tf3.to_expr()
((s - 3)*(s - 2))/(((s - 3)*(s - 2)*(s - 1)))
"""

return Mul(self.num, Pow(self.den, -1, evaluate=False), evaluate=False)


class Series(Basic):
"""
Expand Down
45 changes: 41 additions & 4 deletions sympy/physics/control/tests/test_lti.py
@@ -1,9 +1,9 @@
from sympy import symbols, Matrix, factor, Function, simplify, exp, pi, oo, I, \
Rational, sqrt, CRootOf
from sympy import symbols, Matrix, factor, Function, simplify, exp, oo, I, \
Rational, sqrt, CRootOf, S, Mul, Pow, Add
from sympy.physics.control.lti import TransferFunction, Series, Parallel, Feedback
from sympy.testing.pytest import raises

a, x, b, s, g, d, p, k, a0, a1, a2, b0, b1, b2 = symbols('a, x, b, s, g, d, p, k, a0:3, b0:3')
a, x, b, s, g, d, p, k, a0, a1, a2, b0, b1, b2, tau = symbols('a, x, b, s, g, d, p, k, a0:3, b0:3, tau')


def test_TransferFunction_construction():
Expand Down Expand Up @@ -97,14 +97,42 @@ def test_TransferFunction_construction():
raises(ValueError, lambda: TransferFunction(0, 0, s))

raises(TypeError, lambda: TransferFunction(Matrix([1, 2, 3]), s, s))
raises(TypeError, lambda: TransferFunction(s**pi*exp(s), s, s))

raises(TypeError, lambda: TransferFunction(s**2 + 2*s - 1, s + 3, 3))
raises(TypeError, lambda: TransferFunction(p + 1, 5 - p, 4))
raises(TypeError, lambda: TransferFunction(3, 4, 8))


def test_TransferFunction_functions():
# classmethod from_rational_expression
expr_1 = Mul(0, Pow(s, -1, evaluate=False), evaluate=False)
expr_2 = s/0
expr_3 = (p*s**2 + 5*s)/(s + 1)**3
expr_4 = 6
expr_5 = ((2 + 3*s)*(5 + 2*s))/((9 + 3*s)*(5 + 2*s**2))
expr_6 = (9*s**4 + 4*s**2 + 8)/((s + 1)*(s + 9))
tf = TransferFunction(s + 1, s**2 + 2, s)
delay = exp(-s/tau)
expr_7 = delay*tf.to_expr()
H1 = TransferFunction.from_rational_expression(expr_7, s)
H2 = TransferFunction(s + 1, (s**2 + 2)*exp(s/tau), s)
expr_8 = Add(2, 3*s/(s**2 + 1), evaluate=False)

assert TransferFunction.from_rational_expression(expr_1) == TransferFunction(0, s, s)
raises(ZeroDivisionError, lambda: TransferFunction.from_rational_expression(expr_2))
raises(ValueError, lambda: TransferFunction.from_rational_expression(expr_3))
assert TransferFunction.from_rational_expression(expr_3, s) == TransferFunction((p*s**2 + 5*s), (s + 1)**3, s)
assert TransferFunction.from_rational_expression(expr_3, p) == TransferFunction((p*s**2 + 5*s), (s + 1)**3, p)
raises(ValueError, lambda: TransferFunction.from_rational_expression(expr_4))
assert TransferFunction.from_rational_expression(expr_4, s) == TransferFunction(6, 1, s)
assert TransferFunction.from_rational_expression(expr_5, s) == \
TransferFunction((2 + 3*s)*(5 + 2*s), (9 + 3*s)*(5 + 2*s**2), s)
assert TransferFunction.from_rational_expression(expr_6, s) == \
TransferFunction((9*s**4 + 4*s**2 + 8), (s + 1)*(s + 9), s)
assert H1 == H2
assert TransferFunction.from_rational_expression(expr_8, s) == \
TransferFunction(2*s**2 + 3*s + 2, s**2 + 1, s)

# explicitly cancel poles and zeros.
tf0 = TransferFunction(s**5 + s**3 + s, s - s**2, s)
a = TransferFunction(-(s**4 + s**2 + 1), s - 1, s)
Expand Down Expand Up @@ -263,6 +291,15 @@ def test_TransferFunction_functions():
assert tf7.xreplace({s: k}) == TransferFunction(a0*k**p + a1*p**k, a2*p - k, k)
assert tf7.subs(s, k) == TransferFunction(a0*s**p + a1*p**s, a2*p - s, s)

# Conversion to Expr with to_expr()
tf8 = TransferFunction(a0*s**5 + 5*s**2 + 3, s**6 - 3, s)
tf9 = TransferFunction((5 + s), (5 + s)*(6 + s), s)
tf10 = TransferFunction(0, 1, s)
tf11 = TransferFunction(1, 1, s)
assert tf8.to_expr() == Mul((a0*s**5 + 5*s**2 + 3), Pow((s**6 - 3), -1, evaluate=False), evaluate=False)
assert tf9.to_expr() == Mul((s + 5), Pow((5 + s)*(6 + s), -1, evaluate=False), evaluate=False)
assert tf10.to_expr() == Mul(S(0), Pow(1, -1, evaluate=False), evaluate=False)
assert tf11.to_expr() == Mul(S(1), Pow(1, -1, evaluate=False), evaluate=False)

def test_TransferFunction_addition_and_subtraction():
tf1 = TransferFunction(s + 6, s - 5, s)
Expand Down

0 comments on commit 6a57794

Please sign in to comment.