Skip to content

numpy.ndarray * control.TransferFunction gives array object #459

Description

@roryyorke

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.]]))

Activity

  1. murrayrm commented on Aug 22, 2020

    @murrayrm
    Member

    My two cents:

    • 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.

  2. sawyerbfuller commented on Aug 22, 2020

    @sawyerbfuller
    Contributor

    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.

  3. roryyorke commented on Sep 5, 2020

    @roryyorke
    ContributorAuthor

    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).

    1. FRD
    2. LinearIOSystem
    3. StateSpace
    4. TransferFunction
    5. 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'])
    
  4. sawyerbfuller commented on Sep 8, 2020

    @sawyerbfuller
    Contributor

    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 nonlineariosystem sit at the top, beside FRD systems?

  5. roryyorke commented on Sep 12, 2020

    @roryyorke
    ContributorAuthor

    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.

  6. murrayrm commented on Sep 12, 2020

    @murrayrm
    Member

    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.

  7. added this to the 0.9.0 milestone on Dec 26, 2020
  8. self-assigned this
    on Jan 4, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions