Repository navigation
numpy.ndarray * control.TransferFunction gives array object #459
Description
Activity
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.Reacted by Ben Greiner-
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.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).
- 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-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.
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'])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?This hierarchy makes sense to me. Should a nonlineariosystem sit at the top, beside FRD systems?
That seems right; and, operations trying to combine these two should fail. There's also
InterconnectedSystem, I'm not sure how that fits in.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.- linked a pull request that will close this issueTransferFunction array priority plus system type conversion checking #498
on Jan 5, 2021
TransferFunctiondoesn't have the__array_priority = 11class attribute thatStateSpaceandFrequencyResponseDatado. Is this on purpose? See below for results ofarray * tf, which results in an array (dtype object), andarray * ss, which results in a SS object. I found this while investigating the behaviour ofbdalg.connectin relation with #421Should 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?)