1507 lines
54 KiB
Python
1507 lines
54 KiB
Python
|
"""xferfcn.py
|
||
|
|
||
|
Transfer function representation and functions.
|
||
|
|
||
|
This file contains the TransferFunction class and also functions
|
||
|
that operate on transfer functions. This is the primary representation
|
||
|
for the python-control library.
|
||
|
"""
|
||
|
|
||
|
# Python 3 compatibility (needs to go here)
|
||
|
from __future__ import print_function
|
||
|
from __future__ import division
|
||
|
|
||
|
"""Copyright (c) 2010 by California Institute of Technology
|
||
|
All rights reserved.
|
||
|
|
||
|
Redistribution and use in source and binary forms, with or without
|
||
|
modification, are permitted provided that the following conditions
|
||
|
are met:
|
||
|
|
||
|
1. Redistributions of source code must retain the above copyright
|
||
|
notice, this list of conditions and the following disclaimer.
|
||
|
|
||
|
2. Redistributions in binary form must reproduce the above copyright
|
||
|
notice, this list of conditions and the following disclaimer in the
|
||
|
documentation and/or other materials provided with the distribution.
|
||
|
|
||
|
3. Neither the name of the California Institute of Technology nor
|
||
|
the names of its contributors may be used to endorse or promote
|
||
|
products derived from this software without specific prior
|
||
|
written permission.
|
||
|
|
||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||
|
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH
|
||
|
OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
||
|
USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
||
|
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||
|
SUCH DAMAGE.
|
||
|
|
||
|
Author: Richard M. Murray
|
||
|
Date: 24 May 09
|
||
|
Revised: Kevin K. Chen, Dec 10
|
||
|
|
||
|
$Id$
|
||
|
|
||
|
"""
|
||
|
|
||
|
# External function declarations
|
||
|
import numpy as np
|
||
|
from numpy import angle, array, empty, finfo, ndarray, ones, \
|
||
|
polyadd, polymul, polyval, roots, sqrt, zeros, squeeze, exp, pi, \
|
||
|
where, delete, real, poly, nonzero
|
||
|
import scipy as sp
|
||
|
from scipy.signal import lti, tf2zpk, zpk2tf, cont2discrete
|
||
|
from copy import deepcopy
|
||
|
from warnings import warn
|
||
|
from itertools import chain
|
||
|
from re import sub
|
||
|
from .lti import LTI, timebaseEqual, timebase, isdtime
|
||
|
|
||
|
__all__ = ['TransferFunction', 'tf', 'ss2tf', 'tfdata']
|
||
|
|
||
|
|
||
|
class TransferFunction(LTI):
|
||
|
|
||
|
"""TransferFunction(num, den[, dt])
|
||
|
|
||
|
A class for representing transfer functions
|
||
|
|
||
|
The TransferFunction class is used to represent systems in transfer
|
||
|
function form.
|
||
|
|
||
|
The main data members are 'num' and 'den', which are 2-D lists of arrays
|
||
|
containing MIMO numerator and denominator coefficients. For example,
|
||
|
|
||
|
>>> num[2][5] = numpy.array([1., 4., 8.])
|
||
|
|
||
|
means that the numerator of the transfer function from the 6th input to the
|
||
|
3rd output is set to s^2 + 4s + 8.
|
||
|
|
||
|
Discrete-time transfer functions are implemented by using the 'dt'
|
||
|
instance variable and setting it to something other than 'None'. If 'dt'
|
||
|
has a non-zero value, then it must match whenever two transfer functions
|
||
|
are combined. If 'dt' is set to True, the system will be treated as a
|
||
|
discrete time system with unspecified sampling time.
|
||
|
|
||
|
The TransferFunction class defines two constants ``s`` and ``z`` that
|
||
|
represent the differentiation and delay operators in continuous and
|
||
|
discrete time. These can be used to create variables that allow algebraic
|
||
|
creation of transfer functions. For example,
|
||
|
|
||
|
>>> s = TransferFunction.s
|
||
|
>>> G = (s + 1)/(s**2 + 2*s + 1)
|
||
|
|
||
|
"""
|
||
|
def __init__(self, *args):
|
||
|
"""TransferFunction(num, den[, dt])
|
||
|
|
||
|
Construct a transfer function.
|
||
|
|
||
|
The default constructor is TransferFunction(num, den), where num and
|
||
|
den are lists of lists of arrays containing polynomial coefficients.
|
||
|
To create a discrete time transfer funtion, use TransferFunction(num,
|
||
|
den, dt) where 'dt' is the sampling time (or True for unspecified
|
||
|
sampling time). To call the copy constructor, call
|
||
|
TransferFunction(sys), where sys is a TransferFunction object
|
||
|
(continuous or discrete).
|
||
|
|
||
|
"""
|
||
|
args = deepcopy(args)
|
||
|
if len(args) == 2:
|
||
|
# The user provided a numerator and a denominator.
|
||
|
(num, den) = args
|
||
|
dt = None
|
||
|
elif len(args) == 3:
|
||
|
# Discrete time transfer function
|
||
|
(num, den, dt) = args
|
||
|
elif len(args) == 1:
|
||
|
# Use the copy constructor.
|
||
|
if not isinstance(args[0], TransferFunction):
|
||
|
raise TypeError("The one-argument constructor can only take \
|
||
|
in a TransferFunction object. Received %s."
|
||
|
% type(args[0]))
|
||
|
num = args[0].num
|
||
|
den = args[0].den
|
||
|
# TODO: not sure this can ever happen since dt is always present
|
||
|
try:
|
||
|
dt = args[0].dt
|
||
|
except NameError: # pragma: no coverage
|
||
|
dt = None
|
||
|
else:
|
||
|
raise ValueError("Needs 1, 2 or 3 arguments; received %i."
|
||
|
% len(args))
|
||
|
|
||
|
num = _clean_part(num)
|
||
|
den = _clean_part(den)
|
||
|
|
||
|
inputs = len(num[0])
|
||
|
outputs = len(num)
|
||
|
|
||
|
# Make sure numerator and denominator matrices have consistent sizes
|
||
|
if inputs != len(den[0]):
|
||
|
raise ValueError(
|
||
|
"The numerator has %i input(s), but the denominator has "
|
||
|
"%i input(s)." % (inputs, len(den[0])))
|
||
|
if outputs != len(den):
|
||
|
raise ValueError(
|
||
|
"The numerator has %i output(s), but the denominator has "
|
||
|
"%i output(s)." % (outputs, len(den)))
|
||
|
|
||
|
# Additional checks/updates on structure of the transfer function
|
||
|
for i in range(outputs):
|
||
|
# Make sure that each row has the same number of columns
|
||
|
if len(num[i]) != inputs:
|
||
|
raise ValueError(
|
||
|
"Row 0 of the numerator matrix has %i elements, but row "
|
||
|
"%i has %i." % (inputs, i, len(num[i])))
|
||
|
if len(den[i]) != inputs:
|
||
|
raise ValueError(
|
||
|
"Row 0 of the denominator matrix has %i elements, but row "
|
||
|
"%i has %i." % (inputs, i, len(den[i])))
|
||
|
|
||
|
# Check for zeros in numerator or denominator
|
||
|
# TODO: Right now these checks are only done during construction.
|
||
|
# It might be worthwhile to think of a way to perform checks if the
|
||
|
# user modifies the transfer function after construction.
|
||
|
for j in range(inputs):
|
||
|
# Check that we don't have any zero denominators.
|
||
|
zeroden = True
|
||
|
for k in den[i][j]:
|
||
|
if k:
|
||
|
zeroden = False
|
||
|
break
|
||
|
if zeroden:
|
||
|
raise ValueError(
|
||
|
"Input %i, output %i has a zero denominator."
|
||
|
% (j + 1, i + 1))
|
||
|
|
||
|
# If we have zero numerators, set the denominator to 1.
|
||
|
zeronum = True
|
||
|
for k in num[i][j]:
|
||
|
if k:
|
||
|
zeronum = False
|
||
|
break
|
||
|
if zeronum:
|
||
|
den[i][j] = ones(1)
|
||
|
|
||
|
LTI.__init__(self, inputs, outputs, dt)
|
||
|
self.num = num
|
||
|
self.den = den
|
||
|
|
||
|
self._truncatecoeff()
|
||
|
|
||
|
def __call__(self, s):
|
||
|
"""Evaluate the system's transfer function for a complex variable
|
||
|
|
||
|
For a SISO transfer function, returns the value of the
|
||
|
transfer function. For a MIMO transfer fuction, returns a
|
||
|
matrix of values evaluated at complex variable s."""
|
||
|
|
||
|
if self.issiso():
|
||
|
# return a scalar
|
||
|
return self.horner(s)[0][0]
|
||
|
else:
|
||
|
# return a matrix
|
||
|
return self.horner(s)
|
||
|
|
||
|
def _truncatecoeff(self):
|
||
|
"""Remove extraneous zero coefficients from num and den.
|
||
|
|
||
|
Check every element of the numerator and denominator matrices, and
|
||
|
truncate leading zeros. For instance, running self._truncatecoeff()
|
||
|
will reduce self.num = [[[0, 0, 1, 2]]] to [[[1, 2]]].
|
||
|
|
||
|
"""
|
||
|
|
||
|
# Beware: this is a shallow copy. This should be okay.
|
||
|
data = [self.num, self.den]
|
||
|
for p in range(len(data)):
|
||
|
for i in range(self.outputs):
|
||
|
for j in range(self.inputs):
|
||
|
# Find the first nontrivial coefficient.
|
||
|
nonzero = None
|
||
|
for k in range(data[p][i][j].size):
|
||
|
if data[p][i][j][k]:
|
||
|
nonzero = k
|
||
|
break
|
||
|
|
||
|
if nonzero is None:
|
||
|
# The array is all zeros.
|
||
|
data[p][i][j] = zeros(1)
|
||
|
else:
|
||
|
# Truncate the trivial coefficients.
|
||
|
data[p][i][j] = data[p][i][j][nonzero:]
|
||
|
[self.num, self.den] = data
|
||
|
|
||
|
def __str__(self, var=None):
|
||
|
"""String representation of the transfer function."""
|
||
|
|
||
|
mimo = self.inputs > 1 or self.outputs > 1
|
||
|
if var is None:
|
||
|
# TODO: replace with standard calls to lti functions
|
||
|
var = 's' if self.dt is None or self.dt == 0 else 'z'
|
||
|
outstr = ""
|
||
|
|
||
|
for i in range(self.inputs):
|
||
|
for j in range(self.outputs):
|
||
|
if mimo:
|
||
|
outstr += "\nInput %i to output %i:" % (i + 1, j + 1)
|
||
|
|
||
|
# Convert the numerator and denominator polynomials to strings.
|
||
|
numstr = _tf_polynomial_to_string(self.num[j][i], var=var)
|
||
|
denstr = _tf_polynomial_to_string(self.den[j][i], var=var)
|
||
|
|
||
|
# Figure out the length of the separating line
|
||
|
dashcount = max(len(numstr), len(denstr))
|
||
|
dashes = '-' * dashcount
|
||
|
|
||
|
# Center the numerator or denominator
|
||
|
if len(numstr) < dashcount:
|
||
|
numstr = (' ' * int(round((dashcount - len(numstr)) / 2)) +
|
||
|
numstr)
|
||
|
if len(denstr) < dashcount:
|
||
|
denstr = (' ' * int(round((dashcount - len(denstr)) / 2)) +
|
||
|
denstr)
|
||
|
|
||
|
outstr += "\n" + numstr + "\n" + dashes + "\n" + denstr + "\n"
|
||
|
|
||
|
# See if this is a discrete time system with specific sampling time
|
||
|
if not (self.dt is None) and type(self.dt) != bool and self.dt > 0:
|
||
|
# TODO: replace with standard calls to lti functions
|
||
|
outstr += "\ndt = " + self.dt.__str__() + "\n"
|
||
|
|
||
|
return outstr
|
||
|
|
||
|
# represent as string, makes display work for IPython
|
||
|
__repr__ = __str__
|
||
|
|
||
|
def _repr_latex_(self, var=None):
|
||
|
"""LaTeX representation of transfer function, for Jupyter notebook"""
|
||
|
|
||
|
mimo = self.inputs > 1 or self.outputs > 1
|
||
|
|
||
|
if var is None:
|
||
|
# ! TODO: replace with standard calls to lti functions
|
||
|
var = 's' if self.dt is None or self.dt == 0 else 'z'
|
||
|
|
||
|
out = ['$$']
|
||
|
|
||
|
if mimo:
|
||
|
out.append(r"\begin{bmatrix}")
|
||
|
|
||
|
for i in range(self.outputs):
|
||
|
for j in range(self.inputs):
|
||
|
# Convert the numerator and denominator polynomials to strings.
|
||
|
numstr = _tf_polynomial_to_string(self.num[i][j], var=var)
|
||
|
denstr = _tf_polynomial_to_string(self.den[i][j], var=var)
|
||
|
|
||
|
numstr = _tf_string_to_latex(numstr, var=var)
|
||
|
denstr = _tf_string_to_latex(denstr, var=var)
|
||
|
|
||
|
out += [r"\frac{", numstr, "}{", denstr, "}"]
|
||
|
|
||
|
if mimo and j < self.outputs - 1:
|
||
|
out.append("&")
|
||
|
|
||
|
if mimo:
|
||
|
out.append(r"\\")
|
||
|
|
||
|
if mimo:
|
||
|
out.append(r" \end{bmatrix}")
|
||
|
|
||
|
# See if this is a discrete time system with specific sampling time
|
||
|
if not (self.dt is None) and type(self.dt) != bool and self.dt > 0:
|
||
|
out += [r"\quad dt = ", str(self.dt)]
|
||
|
|
||
|
out.append("$$")
|
||
|
|
||
|
return ''.join(out)
|
||
|
|
||
|
def __neg__(self):
|
||
|
"""Negate a transfer function."""
|
||
|
|
||
|
num = deepcopy(self.num)
|
||
|
for i in range(self.outputs):
|
||
|
for j in range(self.inputs):
|
||
|
num[i][j] *= -1
|
||
|
|
||
|
return TransferFunction(num, self.den, self.dt)
|
||
|
|
||
|
def __add__(self, other):
|
||
|
"""Add two LTI objects (parallel connection)."""
|
||
|
from .statesp import StateSpace
|
||
|
|
||
|
# Convert the second argument to a transfer function.
|
||
|
if isinstance(other, StateSpace):
|
||
|
other = _convert_to_transfer_function(other)
|
||
|
elif not isinstance(other, TransferFunction):
|
||
|
other = _convert_to_transfer_function(other, inputs=self.inputs,
|
||
|
outputs=self.outputs)
|
||
|
|
||
|
# Check that the input-output sizes are consistent.
|
||
|
if self.inputs != other.inputs:
|
||
|
raise ValueError(
|
||
|
"The first summand has %i input(s), but the second has %i."
|
||
|
% (self.inputs, other.inputs))
|
||
|
if self.outputs != other.outputs:
|
||
|
raise ValueError(
|
||
|
"The first summand has %i output(s), but the second has %i."
|
||
|
% (self.outputs, other.outputs))
|
||
|
|
||
|
# Figure out the sampling time to use
|
||
|
if self.dt is None and other.dt is not None:
|
||
|
dt = other.dt # use dt from second argument
|
||
|
elif (other.dt is None and self.dt is not None) or \
|
||
|
(timebaseEqual(self, other)):
|
||
|
dt = self.dt # use dt from first argument
|
||
|
else:
|
||
|
raise ValueError("Systems have different sampling times")
|
||
|
|
||
|
# Preallocate the numerator and denominator of the sum.
|
||
|
num = [[[] for j in range(self.inputs)] for i in range(self.outputs)]
|
||
|
den = [[[] for j in range(self.inputs)] for i in range(self.outputs)]
|
||
|
|
||
|
for i in range(self.outputs):
|
||
|
for j in range(self.inputs):
|
||
|
num[i][j], den[i][j] = _add_siso(
|
||
|
self.num[i][j], self.den[i][j],
|
||
|
other.num[i][j], other.den[i][j])
|
||
|
|
||
|
return TransferFunction(num, den, dt)
|
||
|
|
||
|
def __radd__(self, other):
|
||
|
"""Right add two LTI objects (parallel connection)."""
|
||
|
return self + other
|
||
|
|
||
|
def __sub__(self, other):
|
||
|
"""Subtract two LTI objects."""
|
||
|
return self + (-other)
|
||
|
|
||
|
def __rsub__(self, other):
|
||
|
"""Right subtract two LTI objects."""
|
||
|
return other + (-self)
|
||
|
|
||
|
def __mul__(self, other):
|
||
|
"""Multiply two LTI objects (serial connection)."""
|
||
|
# Convert the second argument to a transfer function.
|
||
|
if isinstance(other, (int, float, complex, np.number)):
|
||
|
other = _convert_to_transfer_function(other, inputs=self.inputs,
|
||
|
outputs=self.inputs)
|
||
|
else:
|
||
|
other = _convert_to_transfer_function(other)
|
||
|
|
||
|
# Check that the input-output sizes are consistent.
|
||
|
if self.inputs != other.outputs:
|
||
|
raise ValueError(
|
||
|
"C = A * B: A has %i column(s) (input(s)), but B has %i "
|
||
|
"row(s)\n(output(s))." % (self.inputs, other.outputs))
|
||
|
|
||
|
inputs = other.inputs
|
||
|
outputs = self.outputs
|
||
|
|
||
|
# Figure out the sampling time to use
|
||
|
if self.dt is None and other.dt is not None:
|
||
|
dt = other.dt # use dt from second argument
|
||
|
elif (other.dt is None and self.dt is not None) or \
|
||
|
(self.dt == other.dt):
|
||
|
dt = self.dt # use dt from first argument
|
||
|
else:
|
||
|
raise ValueError("Systems have different sampling times")
|
||
|
|
||
|
# Preallocate the numerator and denominator of the sum.
|
||
|
num = [[[0] for j in range(inputs)] for i in range(outputs)]
|
||
|
den = [[[1] for j in range(inputs)] for i in range(outputs)]
|
||
|
|
||
|
# Temporary storage for the summands needed to find the (i, j)th
|
||
|
# element of the product.
|
||
|
num_summand = [[] for k in range(self.inputs)]
|
||
|
den_summand = [[] for k in range(self.inputs)]
|
||
|
|
||
|
# Multiply & add.
|
||
|
for row in range(outputs):
|
||
|
for col in range(inputs):
|
||
|
for k in range(self.inputs):
|
||
|
num_summand[k] = polymul(
|
||
|
self.num[row][k], other.num[k][col])
|
||
|
den_summand[k] = polymul(
|
||
|
self.den[row][k], other.den[k][col])
|
||
|
num[row][col], den[row][col] = _add_siso(
|
||
|
num[row][col], den[row][col],
|
||
|
num_summand[k], den_summand[k])
|
||
|
|
||
|
return TransferFunction(num, den, dt)
|
||
|
|
||
|
def __rmul__(self, other):
|
||
|
"""Right multiply two LTI objects (serial connection)."""
|
||
|
|
||
|
# Convert the second argument to a transfer function.
|
||
|
if isinstance(other, (int, float, complex, np.number)):
|
||
|
other = _convert_to_transfer_function(other, inputs=self.inputs,
|
||
|
outputs=self.inputs)
|
||
|
else:
|
||
|
other = _convert_to_transfer_function(other)
|
||
|
|
||
|
# Check that the input-output sizes are consistent.
|
||
|
if other.inputs != self.outputs:
|
||
|
raise ValueError(
|
||
|
"C = A * B: A has %i column(s) (input(s)), but B has %i "
|
||
|
"row(s)\n(output(s))." % (other.inputs, self.outputs))
|
||
|
|
||
|
inputs = self.inputs
|
||
|
outputs = other.outputs
|
||
|
|
||
|
# Figure out the sampling time to use
|
||
|
if self.dt is None and other.dt is not None:
|
||
|
dt = other.dt # use dt from second argument
|
||
|
elif (other.dt is None and self.dt is not None) \
|
||
|
or (self.dt == other.dt):
|
||
|
dt = self.dt # use dt from first argument
|
||
|
else:
|
||
|
raise ValueError("Systems have different sampling times")
|
||
|
|
||
|
# Preallocate the numerator and denominator of the sum.
|
||
|
num = [[[0] for j in range(inputs)] for i in range(outputs)]
|
||
|
den = [[[1] for j in range(inputs)] for i in range(outputs)]
|
||
|
|
||
|
# Temporary storage for the summands needed to find the
|
||
|
# (i, j)th element
|
||
|
# of the product.
|
||
|
num_summand = [[] for k in range(other.inputs)]
|
||
|
den_summand = [[] for k in range(other.inputs)]
|
||
|
|
||
|
for i in range(outputs): # Iterate through rows of product.
|
||
|
for j in range(inputs): # Iterate through columns of product.
|
||
|
for k in range(other.inputs): # Multiply & add.
|
||
|
num_summand[k] = polymul(other.num[i][k], self.num[k][j])
|
||
|
den_summand[k] = polymul(other.den[i][k], self.den[k][j])
|
||
|
num[i][j], den[i][j] = _add_siso(
|
||
|
num[i][j], den[i][j],
|
||
|
num_summand[k], den_summand[k])
|
||
|
|
||
|
return TransferFunction(num, den, dt)
|
||
|
|
||
|
# TODO: Division of MIMO transfer function objects is not written yet.
|
||
|
def __truediv__(self, other):
|
||
|
"""Divide two LTI objects."""
|
||
|
|
||
|
if isinstance(other, (int, float, complex, np.number)):
|
||
|
other = _convert_to_transfer_function(
|
||
|
other, inputs=self.inputs,
|
||
|
outputs=self.inputs)
|
||
|
else:
|
||
|
other = _convert_to_transfer_function(other)
|
||
|
|
||
|
if (self.inputs > 1 or self.outputs > 1 or
|
||
|
other.inputs > 1 or other.outputs > 1):
|
||
|
raise NotImplementedError(
|
||
|
"TransferFunction.__truediv__ is currently \
|
||
|
implemented only for SISO systems.")
|
||
|
|
||
|
# Figure out the sampling time to use
|
||
|
if self.dt is None and other.dt is not None:
|
||
|
dt = other.dt # use dt from second argument
|
||
|
elif (other.dt is None and self.dt is not None) or \
|
||
|
(self.dt == other.dt):
|
||
|
dt = self.dt # use dt from first argument
|
||
|
else:
|
||
|
raise ValueError("Systems have different sampling times")
|
||
|
|
||
|
num = polymul(self.num[0][0], other.den[0][0])
|
||
|
den = polymul(self.den[0][0], other.num[0][0])
|
||
|
|
||
|
return TransferFunction(num, den, dt)
|
||
|
|
||
|
# TODO: Remove when transition to python3 complete
|
||
|
def __div__(self, other):
|
||
|
return TransferFunction.__truediv__(self, other)
|
||
|
|
||
|
# TODO: Division of MIMO transfer function objects is not written yet.
|
||
|
def __rtruediv__(self, other):
|
||
|
"""Right divide two LTI objects."""
|
||
|
if isinstance(other, (int, float, complex, np.number)):
|
||
|
other = _convert_to_transfer_function(
|
||
|
other, inputs=self.inputs,
|
||
|
outputs=self.inputs)
|
||
|
else:
|
||
|
other = _convert_to_transfer_function(other)
|
||
|
|
||
|
if (self.inputs > 1 or self.outputs > 1 or
|
||
|
other.inputs > 1 or other.outputs > 1):
|
||
|
raise NotImplementedError(
|
||
|
"TransferFunction.__rtruediv__ is currently implemented only "
|
||
|
"for SISO systems.")
|
||
|
|
||
|
return other / self
|
||
|
|
||
|
# TODO: Remove when transition to python3 complete
|
||
|
def __rdiv__(self, other):
|
||
|
return TransferFunction.__rtruediv__(self, other)
|
||
|
|
||
|
def __pow__(self, other):
|
||
|
if not type(other) == int:
|
||
|
raise ValueError("Exponent must be an integer")
|
||
|
if other == 0:
|
||
|
return TransferFunction([1], [1]) # unity
|
||
|
if other > 0:
|
||
|
return self * (self**(other - 1))
|
||
|
if other < 0:
|
||
|
return (TransferFunction([1], [1]) / self) * (self**(other + 1))
|
||
|
|
||
|
def __getitem__(self, key):
|
||
|
key1, key2 = key
|
||
|
|
||
|
# pre-process
|
||
|
if isinstance(key1, int):
|
||
|
key1 = slice(key1, key1 + 1, 1)
|
||
|
if isinstance(key2, int):
|
||
|
key2 = slice(key2, key2 + 1, 1)
|
||
|
# dim1
|
||
|
start1, stop1, step1 = key1.start, key1.stop, key1.step
|
||
|
if step1 is None:
|
||
|
step1 = 1
|
||
|
if start1 is None:
|
||
|
start1 = 0
|
||
|
if stop1 is None:
|
||
|
stop1 = len(self.num)
|
||
|
# dim1
|
||
|
start2, stop2, step2 = key2.start, key2.stop, key2.step
|
||
|
if step2 is None:
|
||
|
step2 = 1
|
||
|
if start2 is None:
|
||
|
start2 = 0
|
||
|
if stop2 is None:
|
||
|
stop2 = len(self.num[0])
|
||
|
|
||
|
num = []
|
||
|
den = []
|
||
|
for i in range(start1, stop1, step1):
|
||
|
num_i = []
|
||
|
den_i = []
|
||
|
for j in range(start2, stop2, step2):
|
||
|
num_i.append(self.num[i][j])
|
||
|
den_i.append(self.den[i][j])
|
||
|
num.append(num_i)
|
||
|
den.append(den_i)
|
||
|
if self.isctime():
|
||
|
return TransferFunction(num, den)
|
||
|
else:
|
||
|
return TransferFunction(num, den, self.dt)
|
||
|
|
||
|
def evalfr(self, omega):
|
||
|
"""Evaluate a transfer function at a single angular frequency.
|
||
|
|
||
|
self._evalfr(omega) returns the value of the transfer function
|
||
|
matrix with input value s = i * omega.
|
||
|
|
||
|
"""
|
||
|
warn("TransferFunction.evalfr(omega) will be deprecated in a "
|
||
|
"future release of python-control; use evalfr(sys, omega*1j) "
|
||
|
"instead", PendingDeprecationWarning)
|
||
|
return self._evalfr(omega)
|
||
|
|
||
|
def _evalfr(self, omega):
|
||
|
"""Evaluate a transfer function at a single angular frequency."""
|
||
|
# TODO: implement for discrete time systems
|
||
|
if isdtime(self, strict=True):
|
||
|
# Convert the frequency to discrete time
|
||
|
dt = timebase(self)
|
||
|
s = exp(1.j * omega * dt)
|
||
|
if np.any(omega * dt > pi):
|
||
|
warn("_evalfr: frequency evaluation above Nyquist frequency")
|
||
|
else:
|
||
|
s = 1.j * omega
|
||
|
|
||
|
return self.horner(s)
|
||
|
|
||
|
def horner(self, s):
|
||
|
"""Evaluate the systems's transfer function for a complex variable
|
||
|
|
||
|
Returns a matrix of values evaluated at complex variable s.
|
||
|
"""
|
||
|
|
||
|
# Preallocate the output.
|
||
|
if getattr(s, '__iter__', False):
|
||
|
out = empty((self.outputs, self.inputs, len(s)), dtype=complex)
|
||
|
else:
|
||
|
out = empty((self.outputs, self.inputs), dtype=complex)
|
||
|
|
||
|
for i in range(self.outputs):
|
||
|
for j in range(self.inputs):
|
||
|
out[i][j] = (polyval(self.num[i][j], s) /
|
||
|
polyval(self.den[i][j], s))
|
||
|
|
||
|
return out
|
||
|
|
||
|
# Method for generating the frequency response of the system
|
||
|
def freqresp(self, omega):
|
||
|
"""Evaluate a transfer function at a list of angular frequencies.
|
||
|
|
||
|
mag, phase, omega = self.freqresp(omega)
|
||
|
|
||
|
reports the value of the magnitude, phase, and angular frequency of
|
||
|
the transfer function matrix evaluated at s = i * omega, where omega
|
||
|
is a list of angular frequencies, and is a sorted version of the input
|
||
|
omega.
|
||
|
|
||
|
"""
|
||
|
|
||
|
# Preallocate outputs.
|
||
|
numfreq = len(omega)
|
||
|
mag = empty((self.outputs, self.inputs, numfreq))
|
||
|
phase = empty((self.outputs, self.inputs, numfreq))
|
||
|
|
||
|
# Figure out the frequencies
|
||
|
omega.sort()
|
||
|
if isdtime(self, strict=True):
|
||
|
dt = timebase(self)
|
||
|
slist = np.array([exp(1.j * w * dt) for w in omega])
|
||
|
if max(omega) * dt > pi:
|
||
|
warn("freqresp: frequency evaluation above Nyquist frequency")
|
||
|
else:
|
||
|
slist = np.array([1j * w for w in omega])
|
||
|
|
||
|
# Compute frequency response for each input/output pair
|
||
|
for i in range(self.outputs):
|
||
|
for j in range(self.inputs):
|
||
|
fresp = (polyval(self.num[i][j], slist) /
|
||
|
polyval(self.den[i][j], slist))
|
||
|
mag[i, j, :] = abs(fresp)
|
||
|
phase[i, j, :] = angle(fresp)
|
||
|
|
||
|
return mag, phase, omega
|
||
|
|
||
|
def pole(self):
|
||
|
"""Compute the poles of a transfer function."""
|
||
|
num, den, denorder = self._common_den()
|
||
|
rts = []
|
||
|
for d, o in zip(den, denorder):
|
||
|
rts.extend(roots(d[:o + 1]))
|
||
|
return np.array(rts)
|
||
|
|
||
|
def zero(self):
|
||
|
"""Compute the zeros of a transfer function."""
|
||
|
if self.inputs > 1 or self.outputs > 1:
|
||
|
raise NotImplementedError(
|
||
|
"TransferFunction.zero is currently only implemented "
|
||
|
"for SISO systems.")
|
||
|
else:
|
||
|
# for now, just give zeros of a SISO tf
|
||
|
return roots(self.num[0][0])
|
||
|
|
||
|
def feedback(self, other=1, sign=-1):
|
||
|
"""Feedback interconnection between two LTI objects."""
|
||
|
other = _convert_to_transfer_function(other)
|
||
|
|
||
|
if (self.inputs > 1 or self.outputs > 1 or
|
||
|
other.inputs > 1 or other.outputs > 1):
|
||
|
# TODO: MIMO feedback
|
||
|
raise NotImplementedError(
|
||
|
"TransferFunction.feedback is currently only implemented "
|
||
|
"for SISO functions.")
|
||
|
|
||
|
# Figure out the sampling time to use
|
||
|
if self.dt is None and other.dt is not None:
|
||
|
dt = other.dt # use dt from second argument
|
||
|
elif (other.dt is None and self.dt is not None) or \
|
||
|
(self.dt == other.dt):
|
||
|
dt = self.dt # use dt from first argument
|
||
|
else:
|
||
|
raise ValueError("Systems have different sampling times")
|
||
|
|
||
|
num1 = self.num[0][0]
|
||
|
den1 = self.den[0][0]
|
||
|
num2 = other.num[0][0]
|
||
|
den2 = other.den[0][0]
|
||
|
|
||
|
num = polymul(num1, den2)
|
||
|
den = polyadd(polymul(den2, den1), -sign * polymul(num2, num1))
|
||
|
|
||
|
return TransferFunction(num, den, dt)
|
||
|
|
||
|
# For MIMO or SISO systems, the analytic expression is
|
||
|
# self / (1 - sign * other * self)
|
||
|
# But this does not work correctly because the state size will be too
|
||
|
# large.
|
||
|
|
||
|
def minreal(self, tol=None):
|
||
|
"""Remove cancelling pole/zero pairs from a transfer function"""
|
||
|
# based on octave minreal
|
||
|
|
||
|
# default accuracy
|
||
|
from sys import float_info
|
||
|
sqrt_eps = sqrt(float_info.epsilon)
|
||
|
|
||
|
# pre-allocate arrays
|
||
|
num = [[[] for j in range(self.inputs)] for i in range(self.outputs)]
|
||
|
den = [[[] for j in range(self.inputs)] for i in range(self.outputs)]
|
||
|
|
||
|
for i in range(self.outputs):
|
||
|
for j in range(self.inputs):
|
||
|
|
||
|
# split up in zeros, poles and gain
|
||
|
newzeros = []
|
||
|
zeros = roots(self.num[i][j])
|
||
|
poles = roots(self.den[i][j])
|
||
|
gain = self.num[i][j][0] / self.den[i][j][0]
|
||
|
|
||
|
# check all zeros
|
||
|
for z in zeros:
|
||
|
t = tol or \
|
||
|
1000 * max(float_info.epsilon, abs(z) * sqrt_eps)
|
||
|
idx = where(abs(z - poles) < t)[0]
|
||
|
if len(idx):
|
||
|
# cancel this zero against one of the poles
|
||
|
poles = delete(poles, idx[0])
|
||
|
else:
|
||
|
# keep this zero
|
||
|
newzeros.append(z)
|
||
|
|
||
|
# poly([]) returns a scalar, but we always want a 1d array
|
||
|
num[i][j] = np.atleast_1d(gain * real(poly(newzeros)))
|
||
|
den[i][j] = np.atleast_1d(real(poly(poles)))
|
||
|
|
||
|
# end result
|
||
|
return TransferFunction(num, den, self.dt)
|
||
|
|
||
|
def returnScipySignalLTI(self):
|
||
|
"""Return a list of a list of scipy.signal.lti objects.
|
||
|
|
||
|
For instance,
|
||
|
|
||
|
>>> out = tfobject.returnScipySignalLTI()
|
||
|
>>> out[3][5]
|
||
|
|
||
|
is a signal.scipy.lti object corresponding to the
|
||
|
transfer function from the 6th input to the 4th output.
|
||
|
|
||
|
"""
|
||
|
|
||
|
# TODO: implement for discrete time systems
|
||
|
if self.dt != 0 and self.dt is not None:
|
||
|
raise NotImplementedError("Function not \
|
||
|
implemented in discrete time")
|
||
|
|
||
|
# Preallocate the output.
|
||
|
out = [[[] for j in range(self.inputs)] for i in range(self.outputs)]
|
||
|
|
||
|
for i in range(self.outputs):
|
||
|
for j in range(self.inputs):
|
||
|
out[i][j] = lti(self.num[i][j], self.den[i][j])
|
||
|
|
||
|
return out
|
||
|
|
||
|
def _common_den(self, imag_tol=None):
|
||
|
"""
|
||
|
Compute MIMO common denominators; return them and adjusted numerators.
|
||
|
|
||
|
This function computes the denominators per input containing all
|
||
|
the poles of sys.den, and reports it as the array den. The
|
||
|
output numerator array num is modified to use the common
|
||
|
denominator for this input/column; the coefficient arrays are also
|
||
|
padded with zeros to be the same size for all num/den.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
imag_tol: float
|
||
|
Threshold for the imaginary part of a root to use in detecting
|
||
|
complex poles
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
num: array
|
||
|
n by n by kd where n = max(sys.outputs,sys.inputs)
|
||
|
kd = max(denorder)+1
|
||
|
Multi-dimensional array of numerator coefficients. num[i,j]
|
||
|
gives the numerator coefficient array for the ith output and jth
|
||
|
input; padded for use in td04ad ('C' option); matches the
|
||
|
denorder order; highest coefficient starts on the left.
|
||
|
|
||
|
den: array
|
||
|
sys.inputs by kd
|
||
|
Multi-dimensional array of coefficients for common denominator
|
||
|
polynomial, one row per input. The array is prepared for use in
|
||
|
slycot td04ad, the first element is the highest-order polynomial
|
||
|
coefficient of s, matching the order in denorder. If denorder <
|
||
|
number of columns in den, the den is padded with zeros.
|
||
|
|
||
|
denorder: array of int, orders of den, one per input
|
||
|
|
||
|
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> num, den, denorder = sys._common_den()
|
||
|
|
||
|
"""
|
||
|
|
||
|
# Machine precision for floats.
|
||
|
eps = finfo(float).eps
|
||
|
real_tol = sqrt(eps * self.inputs * self.outputs)
|
||
|
|
||
|
# The tolerance to use in deciding if a pole is complex
|
||
|
if (imag_tol is None):
|
||
|
imag_tol = 2 * real_tol
|
||
|
|
||
|
# A list to keep track of cumulative poles found as we scan
|
||
|
# self.den[..][..]
|
||
|
poles = [[] for j in range(self.inputs)]
|
||
|
|
||
|
# RvP, new implementation 180526, issue #194
|
||
|
# BG, modification, issue #343, PR #354
|
||
|
|
||
|
# pre-calculate the poles for all num, den
|
||
|
# has zeros, poles, gain, list for pole indices not in den,
|
||
|
# number of poles known at the time analyzed
|
||
|
|
||
|
# do not calculate minreal. Rory's hint .minreal()
|
||
|
poleset = []
|
||
|
for i in range(self.outputs):
|
||
|
poleset.append([])
|
||
|
for j in range(self.inputs):
|
||
|
if abs(self.num[i][j]).max() <= eps:
|
||
|
poleset[-1].append([array([], dtype=float),
|
||
|
roots(self.den[i][j]), 0.0, [], 0])
|
||
|
else:
|
||
|
z, p, k = tf2zpk(self.num[i][j], self.den[i][j])
|
||
|
poleset[-1].append([z, p, k, [], 0])
|
||
|
|
||
|
# collect all individual poles
|
||
|
for j in range(self.inputs):
|
||
|
for i in range(self.outputs):
|
||
|
currentpoles = poleset[i][j][1]
|
||
|
nothave = ones(currentpoles.shape, dtype=bool)
|
||
|
for ip, p in enumerate(poles[j]):
|
||
|
collect = (np.isclose(currentpoles.real, p.real,
|
||
|
atol=real_tol) &
|
||
|
np.isclose(currentpoles.imag, p.imag,
|
||
|
atol=imag_tol) &
|
||
|
nothave)
|
||
|
if np.any(collect):
|
||
|
# mark first found pole as already collected
|
||
|
nothave[nonzero(collect)[0][0]] = False
|
||
|
else:
|
||
|
# remember id of pole not in tf
|
||
|
poleset[i][j][3].append(ip)
|
||
|
for h, c in zip(nothave, currentpoles):
|
||
|
if h:
|
||
|
if abs(c.imag) < imag_tol:
|
||
|
c = c.real
|
||
|
poles[j].append(c)
|
||
|
# remember how many poles now known
|
||
|
poleset[i][j][4] = len(poles[j])
|
||
|
|
||
|
# figure out maximum number of poles, for sizing the den
|
||
|
maxindex = max([len(p) for p in poles])
|
||
|
den = zeros((self.inputs, maxindex + 1), dtype=float)
|
||
|
num = zeros((max(1, self.outputs, self.inputs),
|
||
|
max(1, self.outputs, self.inputs),
|
||
|
maxindex + 1),
|
||
|
dtype=float)
|
||
|
denorder = zeros((self.inputs,), dtype=int)
|
||
|
|
||
|
for j in range(self.inputs):
|
||
|
if not len(poles[j]):
|
||
|
# no poles matching this input; only one or more gains
|
||
|
den[j, 0] = 1.0
|
||
|
for i in range(self.outputs):
|
||
|
num[i, j, 0] = poleset[i][j][2]
|
||
|
else:
|
||
|
# create the denominator matching this input
|
||
|
# coefficients should be padded on right, ending at maxindex
|
||
|
maxindex = len(poles[j])
|
||
|
den[j, :maxindex+1] = poly(poles[j])
|
||
|
denorder[j] = maxindex
|
||
|
|
||
|
# now create the numerator, also padded on the right
|
||
|
for i in range(self.outputs):
|
||
|
# start with the current set of zeros for this output
|
||
|
nwzeros = list(poleset[i][j][0])
|
||
|
# add all poles not found in the original denominator,
|
||
|
# and the ones later added from other denominators
|
||
|
for ip in chain(poleset[i][j][3],
|
||
|
range(poleset[i][j][4], maxindex)):
|
||
|
nwzeros.append(poles[j][ip])
|
||
|
|
||
|
numpoly = poleset[i][j][2] * np.atleast_1d(poly(nwzeros))
|
||
|
# numerator polynomial should be padded on left and right
|
||
|
# ending at maxindex to line up with what td04ad expects.
|
||
|
num[i, j, maxindex+1-len(numpoly):maxindex+1] = numpoly
|
||
|
# print(num[i, j])
|
||
|
|
||
|
return num, den, denorder
|
||
|
|
||
|
def sample(self, Ts, method='zoh', alpha=None):
|
||
|
"""Convert a continuous-time system to discrete time
|
||
|
|
||
|
Creates a discrete-time system from a continuous-time system by
|
||
|
sampling. Multiple methods of conversion are supported.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
Ts : float
|
||
|
Sampling period
|
||
|
method : {"gbt", "bilinear", "euler", "backward_diff",
|
||
|
"zoh", "matched"}
|
||
|
Method to use for sampling:
|
||
|
|
||
|
* gbt: generalized bilinear transformation
|
||
|
* bilinear: Tustin's approximation ("gbt" with alpha=0.5)
|
||
|
* euler: Euler (or forward difference) method ("gbt" with alpha=0)
|
||
|
* backward_diff: Backwards difference ("gbt" with alpha=1.0)
|
||
|
* zoh: zero-order hold (default)
|
||
|
|
||
|
alpha : float within [0, 1]
|
||
|
The generalized bilinear transformation weighting parameter, which
|
||
|
should only be specified with method="gbt", and is ignored
|
||
|
otherwise.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
sysd : StateSpace system
|
||
|
Discrete time system, with sampling rate Ts
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
1. Available only for SISO systems
|
||
|
|
||
|
2. Uses the command `cont2discrete` from `scipy.signal`
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> sys = TransferFunction(1, [1,1])
|
||
|
>>> sysd = sys.sample(0.5, method='bilinear')
|
||
|
|
||
|
"""
|
||
|
if not self.isctime():
|
||
|
raise ValueError("System must be continuous time system")
|
||
|
if not self.issiso():
|
||
|
raise NotImplementedError("MIMO implementation not available")
|
||
|
if method == "matched":
|
||
|
return _c2d_matched(self, Ts)
|
||
|
sys = (self.num[0][0], self.den[0][0])
|
||
|
numd, dend, dt = cont2discrete(sys, Ts, method, alpha)
|
||
|
return TransferFunction(numd[0, :], dend, dt)
|
||
|
|
||
|
def dcgain(self):
|
||
|
"""Return the zero-frequency (or DC) gain
|
||
|
|
||
|
For a continous-time transfer function G(s), the DC gain is G(0)
|
||
|
For a discrete-time transfer function G(z), the DC gain is G(1)
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
gain : ndarray
|
||
|
The zero-frequency gain
|
||
|
"""
|
||
|
if self.isctime():
|
||
|
return self._dcgain_cont()
|
||
|
else:
|
||
|
return self(1)
|
||
|
|
||
|
def _dcgain_cont(self):
|
||
|
"""_dcgain_cont() -> DC gain as matrix or scalar
|
||
|
|
||
|
Special cased evaluation at 0 for continuous-time systems."""
|
||
|
gain = np.empty((self.outputs, self.inputs), dtype=float)
|
||
|
for i in range(self.outputs):
|
||
|
for j in range(self.inputs):
|
||
|
num = self.num[i][j][-1]
|
||
|
den = self.den[i][j][-1]
|
||
|
if den:
|
||
|
gain[i][j] = num / den
|
||
|
else:
|
||
|
if num:
|
||
|
# numerator nonzero: infinite gain
|
||
|
gain[i][j] = np.inf
|
||
|
else:
|
||
|
# numerator is zero too: give up
|
||
|
gain[i][j] = np.nan
|
||
|
return np.squeeze(gain)
|
||
|
|
||
|
|
||
|
# c2d function contributed by Benjamin White, Oct 2012
|
||
|
def _c2d_matched(sysC, Ts):
|
||
|
# Pole-zero match method of continuous to discrete time conversion
|
||
|
szeros, spoles, sgain = tf2zpk(sysC.num[0][0], sysC.den[0][0])
|
||
|
zzeros = [0] * len(szeros)
|
||
|
zpoles = [0] * len(spoles)
|
||
|
pregainnum = [0] * len(szeros)
|
||
|
pregainden = [0] * len(spoles)
|
||
|
for idx, s in enumerate(szeros):
|
||
|
sTs = s * Ts
|
||
|
z = exp(sTs)
|
||
|
zzeros[idx] = z
|
||
|
pregainnum[idx] = 1 - z
|
||
|
for idx, s in enumerate(spoles):
|
||
|
sTs = s * Ts
|
||
|
z = exp(sTs)
|
||
|
zpoles[idx] = z
|
||
|
pregainden[idx] = 1 - z
|
||
|
zgain = np.multiply.reduce(pregainnum) / np.multiply.reduce(pregainden)
|
||
|
gain = sgain / zgain
|
||
|
sysDnum, sysDden = zpk2tf(zzeros, zpoles, gain)
|
||
|
return TransferFunction(sysDnum, sysDden, Ts)
|
||
|
|
||
|
# Utility function to convert a transfer function polynomial to a string
|
||
|
# Borrowed from poly1d library
|
||
|
|
||
|
|
||
|
def _tf_polynomial_to_string(coeffs, var='s'):
|
||
|
"""Convert a transfer function polynomial to a string"""
|
||
|
|
||
|
thestr = "0"
|
||
|
|
||
|
# Compute the number of coefficients
|
||
|
N = len(coeffs) - 1
|
||
|
|
||
|
for k in range(len(coeffs)):
|
||
|
coefstr = '%.4g' % abs(coeffs[k])
|
||
|
if coefstr[-4:] == '0000':
|
||
|
coefstr = coefstr[:-5]
|
||
|
power = (N - k)
|
||
|
if power == 0:
|
||
|
if coefstr != '0':
|
||
|
newstr = '%s' % (coefstr,)
|
||
|
else:
|
||
|
if k == 0:
|
||
|
newstr = '0'
|
||
|
else:
|
||
|
newstr = ''
|
||
|
elif power == 1:
|
||
|
if coefstr == '0':
|
||
|
newstr = ''
|
||
|
elif coefstr == '1':
|
||
|
newstr = var
|
||
|
else:
|
||
|
newstr = '%s %s' % (coefstr, var)
|
||
|
else:
|
||
|
if coefstr == '0':
|
||
|
newstr = ''
|
||
|
elif coefstr == '1':
|
||
|
newstr = '%s^%d' % (var, power,)
|
||
|
else:
|
||
|
newstr = '%s %s^%d' % (coefstr, var, power)
|
||
|
|
||
|
if k > 0:
|
||
|
if newstr != '':
|
||
|
if coeffs[k] < 0:
|
||
|
thestr = "%s - %s" % (thestr, newstr)
|
||
|
else:
|
||
|
thestr = "%s + %s" % (thestr, newstr)
|
||
|
elif (k == 0) and (newstr != '') and (coeffs[k] < 0):
|
||
|
thestr = "-%s" % (newstr,)
|
||
|
else:
|
||
|
thestr = newstr
|
||
|
return thestr
|
||
|
|
||
|
|
||
|
def _tf_string_to_latex(thestr, var='s'):
|
||
|
""" make sure to superscript all digits in a polynomial string
|
||
|
and convert float coefficients in scientific notation
|
||
|
to prettier LaTeX representation """
|
||
|
# TODO: make the multiplication sign configurable
|
||
|
expmul = r' \\times'
|
||
|
thestr = sub(var + r'\^(\d{2,})', var + r'^{\1}', thestr)
|
||
|
thestr = sub(r'[eE]\+0*(\d+)', expmul + r' 10^{\1}', thestr)
|
||
|
thestr = sub(r'[eE]\-0*(\d+)', expmul + r' 10^{-\1}', thestr)
|
||
|
return thestr
|
||
|
|
||
|
|
||
|
def _add_siso(num1, den1, num2, den2):
|
||
|
"""Return num/den = num1/den1 + num2/den2.
|
||
|
|
||
|
Each numerator and denominator is a list of polynomial coefficients.
|
||
|
|
||
|
"""
|
||
|
|
||
|
num = polyadd(polymul(num1, den2), polymul(num2, den1))
|
||
|
den = polymul(den1, den2)
|
||
|
|
||
|
return num, den
|
||
|
|
||
|
|
||
|
def _convert_to_transfer_function(sys, **kw):
|
||
|
"""Convert a system to transfer function form (if needed).
|
||
|
|
||
|
If sys is already a transfer function, then it is returned. If sys is a
|
||
|
state space object, then it is converted to a transfer function and
|
||
|
returned. If sys is a scalar, then the number of inputs and outputs can be
|
||
|
specified manually, as in:
|
||
|
|
||
|
>>> sys = _convert_to_transfer_function(3.) # Assumes inputs = outputs = 1
|
||
|
>>> sys = _convert_to_transfer_function(1., inputs=3, outputs=2)
|
||
|
|
||
|
In the latter example, sys's matrix transfer function is [[1., 1., 1.]
|
||
|
[1., 1., 1.]].
|
||
|
|
||
|
If sys is an array-like type, then it is converted to a constant-gain
|
||
|
transfer function.
|
||
|
|
||
|
>>> sys = _convert_to_transfer_function([[1., 0.], [2., 3.]])
|
||
|
|
||
|
In this example, the numerator matrix will be
|
||
|
[[[1.0], [0.0]], [[2.0], [3.0]]]
|
||
|
and the denominator matrix [[[1.0], [1.0]], [[1.0], [1.0]]]
|
||
|
|
||
|
"""
|
||
|
from .statesp import StateSpace
|
||
|
|
||
|
if isinstance(sys, TransferFunction):
|
||
|
if len(kw):
|
||
|
raise TypeError("If sys is a TransferFunction, " +
|
||
|
"_convertToTransferFunction cannot take keywords.")
|
||
|
|
||
|
return sys
|
||
|
elif isinstance(sys, StateSpace):
|
||
|
|
||
|
if 0 == sys.states:
|
||
|
# Slycot doesn't like static SS->TF conversion, so handle
|
||
|
# it first. Can't join this with the no-Slycot branch,
|
||
|
# since that doesn't handle general MIMO systems
|
||
|
num = [[[sys.D[i, j]] for j in range(sys.inputs)]
|
||
|
for i in range(sys.outputs)]
|
||
|
den = [[[1.] for j in range(sys.inputs)]
|
||
|
for i in range(sys.outputs)]
|
||
|
else:
|
||
|
try:
|
||
|
from slycot import tb04ad
|
||
|
if len(kw):
|
||
|
raise TypeError(
|
||
|
"If sys is a StateSpace, " +
|
||
|
"_convertToTransferFunction cannot take keywords.")
|
||
|
|
||
|
# Use Slycot to make the transformation
|
||
|
# Make sure to convert system matrices to numpy arrays
|
||
|
tfout = tb04ad(
|
||
|
sys.states, sys.inputs, sys.outputs, array(sys.A),
|
||
|
array(sys.B), array(sys.C), array(sys.D), tol1=0.0)
|
||
|
|
||
|
# Preallocate outputs.
|
||
|
num = [[[] for j in range(sys.inputs)]
|
||
|
for i in range(sys.outputs)]
|
||
|
den = [[[] for j in range(sys.inputs)]
|
||
|
for i in range(sys.outputs)]
|
||
|
|
||
|
for i in range(sys.outputs):
|
||
|
for j in range(sys.inputs):
|
||
|
num[i][j] = list(tfout[6][i, j, :])
|
||
|
# Each transfer function matrix row
|
||
|
# has a common denominator.
|
||
|
den[i][j] = list(tfout[5][i, :])
|
||
|
|
||
|
except ImportError:
|
||
|
# If slycot is not available, use signal.lti (SISO only)
|
||
|
if sys.inputs != 1 or sys.outputs != 1:
|
||
|
raise TypeError("No support for MIMO without slycot.")
|
||
|
|
||
|
# Do the conversion using sp.signal.ss2tf
|
||
|
# Note that this returns a 2D array for the numerator
|
||
|
num, den = sp.signal.ss2tf(sys.A, sys.B, sys.C, sys.D)
|
||
|
num = squeeze(num) # Convert to 1D array
|
||
|
den = squeeze(den) # Probably not needed
|
||
|
|
||
|
return TransferFunction(num, den, sys.dt)
|
||
|
|
||
|
elif isinstance(sys, (int, float, complex, np.number)):
|
||
|
if "inputs" in kw:
|
||
|
inputs = kw["inputs"]
|
||
|
else:
|
||
|
inputs = 1
|
||
|
if "outputs" in kw:
|
||
|
outputs = kw["outputs"]
|
||
|
else:
|
||
|
outputs = 1
|
||
|
|
||
|
num = [[[sys] for j in range(inputs)] for i in range(outputs)]
|
||
|
den = [[[1] for j in range(inputs)] for i in range(outputs)]
|
||
|
|
||
|
return TransferFunction(num, den)
|
||
|
|
||
|
# If this is array-like, try to create a constant feedthrough
|
||
|
try:
|
||
|
D = array(sys)
|
||
|
outputs, inputs = D.shape
|
||
|
num = [[[D[i, j]] for j in range(inputs)] for i in range(outputs)]
|
||
|
den = [[[1] for j in range(inputs)] for i in range(outputs)]
|
||
|
return TransferFunction(num, den)
|
||
|
except Exception as e:
|
||
|
print("Failure to assume argument is matrix-like in"
|
||
|
" _convertToTransferFunction, result %s" % e)
|
||
|
|
||
|
raise TypeError("Can't convert given type to TransferFunction system.")
|
||
|
|
||
|
|
||
|
def tf(*args):
|
||
|
"""tf(num, den[, dt])
|
||
|
|
||
|
Create a transfer function system. Can create MIMO systems.
|
||
|
|
||
|
The function accepts either 1, 2, or 3 parameters:
|
||
|
|
||
|
``tf(sys)``
|
||
|
Convert a linear system into transfer function form. Always creates
|
||
|
a new system, even if sys is already a TransferFunction object.
|
||
|
|
||
|
``tf(num, den)``
|
||
|
Create a transfer function system from its numerator and denominator
|
||
|
polynomial coefficients.
|
||
|
|
||
|
If `num` and `den` are 1D array_like objects, the function creates a
|
||
|
SISO system.
|
||
|
|
||
|
To create a MIMO system, `num` and `den` need to be 2D nested lists
|
||
|
of array_like objects. (A 3 dimensional data structure in total.)
|
||
|
(For details see note below.)
|
||
|
|
||
|
``tf(num, den, dt)``
|
||
|
Create a discrete time transfer function system; dt can either be a
|
||
|
positive number indicating the sampling time or 'True' if no
|
||
|
specific timebase is given.
|
||
|
|
||
|
``tf('s')`` or ``tf('z')``
|
||
|
Create a transfer function representing the differential operator
|
||
|
('s') or delay operator ('z').
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
sys: LTI (StateSpace or TransferFunction)
|
||
|
A linear system
|
||
|
num: array_like, or list of list of array_like
|
||
|
Polynomial coefficients of the numerator
|
||
|
den: array_like, or list of list of array_like
|
||
|
Polynomial coefficients of the denominator
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
out: :class:`TransferFunction`
|
||
|
The new linear system
|
||
|
|
||
|
Raises
|
||
|
------
|
||
|
ValueError
|
||
|
if `num` and `den` have invalid or unequal dimensions
|
||
|
TypeError
|
||
|
if `num` or `den` are of incorrect type
|
||
|
|
||
|
See Also
|
||
|
--------
|
||
|
TransferFunction
|
||
|
ss
|
||
|
ss2tf
|
||
|
tf2ss
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
``num[i][j]`` contains the polynomial coefficients of the numerator
|
||
|
for the transfer function from the (j+1)st input to the (i+1)st output.
|
||
|
``den[i][j]`` works the same way.
|
||
|
|
||
|
The list ``[2, 3, 4]`` denotes the polynomial :math:`2s^2 + 3s + 4`.
|
||
|
|
||
|
The special forms ``tf('s')`` and ``tf('z')`` can be used to create
|
||
|
transfer functions for differentiation and unit delays.
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> # Create a MIMO transfer function object
|
||
|
>>> # The transfer function from the 2nd input to the 1st output is
|
||
|
>>> # (3s + 4) / (6s^2 + 5s + 4).
|
||
|
>>> num = [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]]]
|
||
|
>>> den = [[[9., 8., 7.], [6., 5., 4.]], [[3., 2., 1.], [-1., -2., -3.]]]
|
||
|
>>> sys1 = tf(num, den)
|
||
|
|
||
|
>>> # Create a variable 's' to allow algebra operations for SISO systems
|
||
|
>>> s = tf('s')
|
||
|
>>> G = (s + 1)/(s**2 + 2*s + 1)
|
||
|
|
||
|
>>> # Convert a StateSpace to a TransferFunction object.
|
||
|
>>> sys_ss = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.")
|
||
|
>>> sys2 = tf(sys1)
|
||
|
|
||
|
"""
|
||
|
|
||
|
if len(args) == 2 or len(args) == 3:
|
||
|
return TransferFunction(*args)
|
||
|
elif len(args) == 1:
|
||
|
# Look for special cases defining differential/delay operator
|
||
|
if args[0] == 's':
|
||
|
return TransferFunction.s
|
||
|
elif args[0] == 'z':
|
||
|
return TransferFunction.z
|
||
|
|
||
|
from .statesp import StateSpace
|
||
|
sys = args[0]
|
||
|
if isinstance(sys, StateSpace):
|
||
|
return ss2tf(sys)
|
||
|
elif isinstance(sys, TransferFunction):
|
||
|
return deepcopy(sys)
|
||
|
else:
|
||
|
raise TypeError("tf(sys): sys must be a StateSpace or "
|
||
|
"TransferFunction object. It is %s." % type(sys))
|
||
|
else:
|
||
|
raise ValueError("Needs 1 or 2 arguments; received %i." % len(args))
|
||
|
|
||
|
|
||
|
def ss2tf(*args):
|
||
|
"""ss2tf(sys)
|
||
|
|
||
|
Transform a state space system to a transfer function.
|
||
|
|
||
|
The function accepts either 1 or 4 parameters:
|
||
|
|
||
|
``ss2tf(sys)``
|
||
|
Convert a linear system into space system form. Always creates a
|
||
|
new system, even if sys is already a StateSpace object.
|
||
|
|
||
|
``ss2tf(A, B, C, D)``
|
||
|
Create a state space system from the matrices of its state and
|
||
|
output equations.
|
||
|
|
||
|
For details see: :func:`ss`
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
sys: StateSpace
|
||
|
A linear system
|
||
|
A: array_like or string
|
||
|
System matrix
|
||
|
B: array_like or string
|
||
|
Control matrix
|
||
|
C: array_like or string
|
||
|
Output matrix
|
||
|
D: array_like or string
|
||
|
Feedthrough matrix
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
out: TransferFunction
|
||
|
New linear system in transfer function form
|
||
|
|
||
|
Raises
|
||
|
------
|
||
|
ValueError
|
||
|
if matrix sizes are not self-consistent, or if an invalid number of
|
||
|
arguments is passed in
|
||
|
TypeError
|
||
|
if `sys` is not a StateSpace object
|
||
|
|
||
|
See Also
|
||
|
--------
|
||
|
tf
|
||
|
ss
|
||
|
tf2ss
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> A = [[1., -2], [3, -4]]
|
||
|
>>> B = [[5.], [7]]
|
||
|
>>> C = [[6., 8]]
|
||
|
>>> D = [[9.]]
|
||
|
>>> sys1 = ss2tf(A, B, C, D)
|
||
|
|
||
|
>>> sys_ss = ss(A, B, C, D)
|
||
|
>>> sys2 = ss2tf(sys_ss)
|
||
|
|
||
|
"""
|
||
|
|
||
|
from .statesp import StateSpace
|
||
|
if len(args) == 4 or len(args) == 5:
|
||
|
# Assume we were given the A, B, C, D matrix and (optional) dt
|
||
|
return _convert_to_transfer_function(StateSpace(*args))
|
||
|
|
||
|
elif len(args) == 1:
|
||
|
sys = args[0]
|
||
|
if isinstance(sys, StateSpace):
|
||
|
return _convert_to_transfer_function(sys)
|
||
|
else:
|
||
|
raise TypeError(
|
||
|
"ss2tf(sys): sys must be a StateSpace object. It is %s."
|
||
|
% type(sys))
|
||
|
else:
|
||
|
raise ValueError("Needs 1 or 4 arguments; received %i." % len(args))
|
||
|
|
||
|
|
||
|
def tfdata(sys):
|
||
|
"""
|
||
|
Return transfer function data objects for a system
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
sys: LTI (StateSpace, or TransferFunction)
|
||
|
LTI system whose data will be returned
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
(num, den): numerator and denominator arrays
|
||
|
Transfer function coefficients (SISO only)
|
||
|
"""
|
||
|
tf = _convert_to_transfer_function(sys)
|
||
|
|
||
|
return tf.num, tf.den
|
||
|
|
||
|
|
||
|
def _clean_part(data):
|
||
|
"""
|
||
|
Return a valid, cleaned up numerator or denominator
|
||
|
for the TransferFunction class.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
data: numerator or denominator of a transfer function.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
data: list of lists of ndarrays, with int converted to float
|
||
|
"""
|
||
|
valid_types = (int, float, complex, np.number)
|
||
|
valid_collection = (list, tuple, ndarray)
|
||
|
|
||
|
if (isinstance(data, valid_types) or
|
||
|
(isinstance(data, ndarray) and data.ndim == 0)):
|
||
|
# Data is a scalar (including 0d ndarray)
|
||
|
data = [[array([data])]]
|
||
|
elif (isinstance(data, ndarray) and data.ndim == 3 and
|
||
|
isinstance(data[0, 0, 0], valid_types)):
|
||
|
data = [[array(data[i, j])
|
||
|
for j in range(data.shape[1])]
|
||
|
for i in range(data.shape[0])]
|
||
|
elif (isinstance(data, valid_collection) and
|
||
|
all([isinstance(d, valid_types) for d in data])):
|
||
|
data = [[array(data)]]
|
||
|
elif (isinstance(data, (list, tuple)) and
|
||
|
isinstance(data[0], (list, tuple)) and
|
||
|
(isinstance(data[0][0], valid_collection) and
|
||
|
all([isinstance(d, valid_types) for d in data[0][0]]))):
|
||
|
data = list(data)
|
||
|
for j in range(len(data)):
|
||
|
data[j] = list(data[j])
|
||
|
for k in range(len(data[j])):
|
||
|
data[j][k] = array(data[j][k])
|
||
|
else:
|
||
|
# If the user passed in anything else, then it's unclear what
|
||
|
# the meaning is.
|
||
|
raise TypeError(
|
||
|
"The numerator and denominator inputs must be scalars or vectors "
|
||
|
"(for\nSISO), or lists of lists of vectors (for SISO or MIMO).")
|
||
|
|
||
|
# Check for coefficients that are ints and convert to floats
|
||
|
for i in range(len(data)):
|
||
|
for j in range(len(data[i])):
|
||
|
for k in range(len(data[i][j])):
|
||
|
if isinstance(data[i][j][k], (int, np.int)):
|
||
|
data[i][j][k] = float(data[i][j][k])
|
||
|
|
||
|
return data
|
||
|
|
||
|
|
||
|
# Define constants to represent differentiation, unit delay
|
||
|
TransferFunction.s = TransferFunction([1, 0], [1], 0)
|
||
|
TransferFunction.z = TransferFunction([1, 0], [1], True)
|