You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TransferFunction doesn't have the __array_priority = 11 class attribute that StateSpace and FrequencyResponseData do. Is this on purpose? See below for results of array * tf, which results in an array (dtype object), and array * ss, which results in a SS object. I found this while investigating the behaviour of bdalg.connect in relation with #421
Should multiplication of scalar transfer functions by arrays be special-cased so that, e.g., with scalar tf k, k * eye(5) gives a 5x5 TF with identical entries on the diagonal? I think this sort of convention is used in literature, but it might be tricky to get something consistent here (e.g., should 1-input 1-output state-space systems do something analogous?; should one do general broadcasting?)
(control-dev) rory@rory-latitude:~/src/python-control$ ipython
Python 3.8.5 | packaged by conda-forge | (default, Jul 31 2020, 02:39:48)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.17.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: !git describe
0.8.3-62-g6ede92e
In [2]: import control
In [3]: import numpy as np
In [4]: np.array([[1]]) * control.tf([1], [1,1])
Out[4]: array([[TransferFunction(array([1.]), array([1, 1]))]], dtype=object)
In [5]: np.array([[1]]) * control.ss([[-1]], [[1]], [[1]], [[0]])
Out[5]: StateSpace(array([[-1.]]), array([[1.]]), array([[1.]]), array([[0.]]))
As much as possible, it seems like all LTI objects should behave the same with respect to various operations. So it shouldn't matter if you have a TransferFunction or a StateSpace system of an LTI system.
It also seems to me that if a control system object (LTI or InputOutputSystem) is in an expression where you would expect to get back a control system object, things should work as expected. This basically prioritizes the control objects over numpy objects (I think).
For the specific case above, I would have expected to get a TransferFunction back. I'd be interested to hear what others things about this.
I think that the result of a mathematical operation between two mathematical objects should have a type that is a superset of the two types (like casting an int into a float when multiplying by a float). If you think of a transfer function as a frequency-dependent gain, then I agree that the output should be a transfer function too.
For the purposes of being able to use bdalg.connect with TransferFunctions, it's probably sufficient just to add the __array_priority__ entry to the class in question. However, I wondered about general type conversion in python-control; see below.
For the operations +, -, *, /, I would expect the following priority of "type conversion", where conversions are always to earlier entries (so, e.g., FRD + StateSpace should give type FRD). For FRD, this is based on the idea that it's possible, and least in principle, to go from any of the others to FRD type, but the opposite is not possible (or, at least, not straightforward).
FRD
LinearIOSystem
StateSpace
TransferFunction
numpy.ndarray / Python float
However, some command-line experimentation found this is not the case. Results below (script after that), showing the result type of lopnd op ropnd. "op" = operator, "lopnd" is left-hand operand, "ropnd" is right-hand operand; "E" is error. The type names are hopefully obvious except perhaps lio = LinearIOSystem, arr = numpy.ndarray, and flt = Python's built-in float type.
Not all operations are defined (many "E" s), and the tables are frequently not symmetric about the diagonal.
Division is a special case: it probably only makes sense to divide by a tf or frd. (And frd / anything also makes sense).
We don't support matmul at all; ignore that part of the table.
ropnd
ss
tf
frd
lio
arr
flt
op
lopnd
add
ss
ss
ss
E
ss
ss
ss
tf
tf
tf
E
tf
tf
tf
frd
E
E
frd
E
E
E
lio
E
E
E
lio
E
E
arr
ss
arr
E
E
arr
arr
flt
ss
tf
E
E
arr
flt
sub
ss
ss
ss
E
ss
ss
ss
tf
tf
tf
E
tf
tf
tf
frd
E
E
frd
E
E
E
lio
E
E
E
lio
E
E
arr
ss
arr
E
E
arr
arr
flt
ss
tf
E
E
arr
flt
mul
ss
ss
ss
E
E
ss
ss
tf
tf
tf
E
tf
tf
tf
frd
E
E
frd
E
E
frd
lio
lio
E
E
lio
E
E
arr
ss
arr
E
E
arr
arr
flt
ss
tf
frd
E
arr
flt
truediv
ss
E
tf
E
E
E
E
tf
tf
tf
E
tf
tf
tf
frd
E
E
frd
E
E
frd
lio
E
tf
E
E
E
E
arr
E
arr
E
E
arr
arr
flt
E
tf
frd
E
arr
flt
matmul
ss
E
E
E
E
E
E
tf
E
E
E
E
E
E
frd
E
E
E
E
E
E
lio
E
E
E
E
E
E
arr
E
E
E
E
arr
E
flt
E
E
E
E
E
E
from itertools import product
import operator
import numpy as np
import pandas as pd
import control as ct
type_db = [
# name class instance
('ss', ct.StateSpace, ct.ss([[-1]],[[1]],[[1]],[[0]])),
('tf', ct.TransferFunction, ct.tf([1],[0.5, 1])),
('frd', ct.FrequencyResponseData, ct.frd([10+0j, 9 + 1j, 8 + 2j], [1,2,3])),
('lio', ct.LinearIOSystem, ct.LinearIOSystem(ct.ss([[-1]],[[5]],[[5]],[[0]]))),
('arr', np.ndarray, np.array([[2.0]])),
('flt', float, 3.0),
]
klass_to_name = { ncf[1]: ncf[0]
for ncf in type_db }
typenames = [ncf[0] for ncf in type_db]
opnames = ['add', 'sub', 'mul', 'truediv', 'matmul']
def frame_per_op(op):
resultss = [list()
for _ in type_db]
for lh, results in zip(type_db, resultss):
lopnd = lh[2]
for rh in type_db:
ropnd = rh[2]
try:
result = klass_to_name[type(op(lopnd, ropnd))]
except Exception:
result = 'E'
results.append(result)
return pd.DataFrame(resultss,
columns=pd.Index(typenames, name='ropnd'),
index=pd.Index(typenames, name='lopnd'))
ops = [getattr(operator, opname)
for opname in opnames]
df = pd.concat([frame_per_op(op)
for op in ops],
keys=opnames,
names=['op'])
LinearIOSystem and InterconnectedSystems are both subclasses of InputOutputSystem. Any operation with an LTI system that can be converted to a StateSpace system can (and probably should) be promoted to an InputOutput system. Note that you can't combine an FRD system with an InputOutputSystem since the former is has (only) a frequency domain representation and later has (only) a state space representation.
Activity
murrayrm commentedon Aug 22, 2020
My two cents:
As much as possible, it seems like all
LTIobjects should behave the same with respect to various operations. So it shouldn't matter if you have aTransferFunctionor aStateSpacesystem of an LTI system.It also seems to me that if a control system object (
LTIorInputOutputSystem) is in an expression where you would expect to get back a control system object, things should work as expected. This basically prioritizes thecontrolobjects overnumpyobjects (I think).For the specific case above, I would have expected to get a
TransferFunctionback. I'd be interested to hear what others things about this.sawyerbfuller commentedon Aug 22, 2020
I think that the result of a mathematical operation between two mathematical objects should have a type that is a superset of the two types (like casting an int into a float when multiplying by a float). If you think of a transfer function as a frequency-dependent gain, then I agree that the output should be a transfer function too.
roryyorke commentedon Sep 5, 2020
For the purposes of being able to use
bdalg.connectwith TransferFunctions, it's probably sufficient just to add the__array_priority__entry to the class in question. However, I wondered about general type conversion in python-control; see below.For the operations +, -, *, /, I would expect the following priority of "type conversion", where conversions are always to earlier entries (so, e.g., FRD + StateSpace should give type FRD). For FRD, this is based on the idea that it's possible, and least in principle, to go from any of the others to FRD type, but the opposite is not possible (or, at least, not straightforward).
However, some command-line experimentation found this is not the case. Results below (script after that), showing the result type of
lopnd op ropnd. "op" = operator, "lopnd" is left-hand operand, "ropnd" is right-hand operand; "E" is error. The type names are hopefully obvious except perhaps lio =LinearIOSystem, arr =numpy.ndarray, and flt = Python's built-infloattype.Not all operations are defined (many "E" s), and the tables are frequently not symmetric about the diagonal.
Division is a special case: it probably only makes sense to divide by a tf or frd. (And frd / anything also makes sense).
We don't support matmul at all; ignore that part of the table.
sawyerbfuller commentedon Sep 8, 2020
This is great. I knew that ss/float gave an error and was thinking about a PR, but this shows that there are a few holes to fill.
This hierarchy makes sense to me. Should a
nonlineariosystemsit at the top, beside FRD systems?roryyorke commentedon Sep 12, 2020
That seems right; and, operations trying to combine these two should fail. There's also
InterconnectedSystem, I'm not sure how that fits in.murrayrm commentedon Sep 12, 2020
LinearIOSystemandInterconnectedSystemsare both subclasses ofInputOutputSystem. Any operation with anLTIsystem that can be converted to aStateSpacesystem can (and probably should) be promoted to anInputOutputsystem. Note that you can't combine anFRDsystem with anInputOutputSystemsince the former is has (only) a frequency domain representation and later has (only) a state space representation.