Forcing multiplication to use __rmul__() instead of Numpy array __mul__() or bypassing the broadcasting
Asked Answered
L

2

5

This question is close to what is asked in Overriding other __rmul__ with your class's __mul__ but I am under the impression that this is a more general problem then only numerical data. Also that is not answered and I really don't want to use the matrix multiplication @ for this operation. Hence, the question.

I do have an object which accepts multiplication with scalars and numerical arrays. As usual, the left multiplication works fine since it is the myobj() methods are used but in the right multiplication, NumPy uses broadcasting rules and gives elementwise results with dtype=object.

This has also the side-effect of not being able to check the size of the array whether the size is compatible or not.

Therefore, the question is

Is there a way to force numpy array to look for the __rmul__() of the other object instead of broadcasting and performing elementwise __mul__()?

In my particular case, the object is a MIMO (multiple-input, multiple-output) transfer function matrix (or filter coefficients matrix if you will) so matrix multiplication has a special meaning in terms of adding and multiplying linear systems. Hence in each entry there is SISO system.

import numpy as np

class myobj():
    def __init__(self):
        pass

    def __mul__(self, other):
        if isinstance(other, type(np.array([0.]))):
            if other.size == 1:
                print('Scalar multiplication')
            else:
                print('Multiplication of arrays')

    def __rmul__(self, other):
        if isinstance(other, type(np.array([0.]))):
            if other.size == 1:
                print('Scalar multiplication')
            else:
                print('Multiplication of arrays')

A = myobj()
a = np.array([[[1+1j]]])  # some generic scalar
B = np.random.rand(3, 3)

With these definitions, the following commands show the undesired behavior.

In [123]: A*a
Scalar multiplication

In [124]: a*A
Out[124]: array([[[None]]], dtype=object)

In [125]: B*A
Out[125]: 
array([[None, None, None],
       [None, None, None],
       [None, None, None]], dtype=object)

In [126]: A*B
Multiplication of arrays

In [127]: 5 * A

In [128]: A.__rmul__(B)  # This is the desired behavior for B*A
Multiplication of arrays
Lamblike answered 19/11, 2016 at 15:8 Comment(3)
In your link @ was mentioned because it is a np.dot like operator, not because it addresses the rmul issue.Pickwickian
Take seriously the discussion about subclassing on the right hand side. Use of mul v rmul is a basis python syntax issue.Pickwickian
@Pickwickian This is also a dot() like operation but I would like to take care of the row vector multiplications in a specialized way. The key point here is the broadcasting. I'm perfectly OK with mul vs rmul since numpy will not be able to see how it can multiply if it doesn't do the broadcasting. See the last example it works with no problem.Lamblike
L
3

By default, NumPy assumes that unknown object (not inheriting from ndarray) are scalars, and it needs to "vectorize" multiplication over each element of any NumPy arrays.

To control the operations yourself, you need to set either __array_priority__ (most backwards compatible) or __array_ufunc__ (NumPy 1.13+ only). For example:

class myworkingobj(myobj):
    __array_priority__ = 1000

A = myworkingobj()
B = np.random.rand(3, 3)
B * A  # Multiplication of arrays
Longe answered 19/6, 2017 at 15:36 Comment(1)
That's indeed great to know. Thank you very much.Lamblike
P
1

I'll try to demonstrate what is going on.

In [494]: B=np.random.rand(3,3)

barebones class:

In [497]: class myobj():
     ...:     pass
     ...: 
In [498]: B*myobj()
...

TypeError: unsupported operand type(s) for *: 'float' and 'myobj'

add a __mul__

In [500]: class myobj():
     ...:     pass
     ...:     def __mul__(self,other):
     ...:         print('myobj mul')
     ...:         return 12.3
     ...: 
In [501]: B*myobj()
...
TypeError: unsupported operand type(s) for *: 'float' and 'myobj'
In [502]: myobj()*B
myobj mul
Out[502]: 12.3

add a rmul:

In [515]: class myobj():
     ...:     pass
     ...:     def __mul__(self,other):
     ...:         print('myobj mul',other)
     ...:         return 12.3
     ...:     def __rmul__(self,other):
     ...:         print('myobj rmul',other)
     ...:         return 4.32
     ...: 
In [516]: B*myobj()
myobj rmul 0.792751549595306
myobj rmul 0.5668783619454384
myobj rmul 0.2196204913660168
myobj rmul 0.5474970289273348
myobj rmul 0.2079367474424587
myobj rmul 0.5374571198848628
myobj rmul 0.35748803226628456
myobj rmul 0.41306113085906715
myobj rmul 0.499598995529441
Out[516]: 
array([[4.32, 4.32, 4.32],
       [4.32, 4.32, 4.32],
       [4.32, 4.32, 4.32]], dtype=object)

B*myobj() is given to B, as B.__mul__(myobj()), which proceeds to perform myobj().__rmul__(i) for each element of B.

In myobj()*B translates to myobj.__mul__(B):

In [517]: myobj()*B
myobj mul [[ 0.79275155  0.56687836  0.21962049]
 [ 0.54749703  0.20793675  0.53745712]
 [ 0.35748803  0.41306113  0.499599  ]]
Out[517]: 12.3

In [518]: myobj().__rmul__(B)
myobj rmul [[ 0.79275155  0.56687836  0.21962049]
 [ 0.54749703  0.20793675  0.53745712]
 [ 0.35748803  0.41306113  0.499599  ]]
Out[518]: 4.32

You can't do anything in myobj to override the translation of B*myobj() to B.__mul__(myobj()). Use functions or methods if you need greater control over operation. It's hard to fight the interpreter.

Pickwickian answered 19/11, 2016 at 16:37 Comment(3)
I think I can't explain properly. I'm not trying to cancel the multiplication of NumPy. I'm trying to stop it from broadcasting to single multiplications. The rest is I'm already pretty familiar withLamblike
That 'broadcasting' is done by B.__mul__. You may have to ubclass ndarray, and over ride the __mul__ method?Pickwickian
Then I have to force users to use the subclass instead of the regular arrays which then defeats the purpose of the whole implementation of mul and rmul.Lamblike

© 2022 - 2024 — McMap. All rights reserved.