Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I add `quantities.Quantity`-behavior without subclassing it by using `__array__`?

quantities.Quantity is a subclass of numpy.ndarray that handles arithmetic and conversions of physical quantities. How can I make use of it's arithmetics without subclassing it? The following approach uses an __array__-method -- but just works 80%, as you can see in the end:

class Numeric(object):
  def __init__(self, signal):
    self.signal = signal
    self._dimensionality = self.signal._dimensionality
    self.dimensionality = self.signal.dimensionality
  def __array__(self):
    return self.signal
  def __mul__(self, obj):
    return self.signal.__mul__(obj)
  def __rmul__(self, obj):
    return self.signal.__rmul__(obj)

With this I can do:

import quantities as pq
import numpy as np

num = Numeric(pq.Quantity([1,2,3], 'mV'))
q = pq.Quantity([2,3,4], 'mV')
n = np.array([3,4,5])

All of the following operations return the correct unit -- except the last, there the unit is missing:

print num * num
# [1 4 9] mV**2
print num * q
# [ 2  6 12] mV**2
print num * n
# [ 3  8 15] mV
print q * num
# [ 2  6 12] mV**2
print n * num
# [ 3  8 15] <------- no unit!

Any idea, what to fix in order to keep the correct unit?

edit: The return type/value of an arithmentic opperation should be equivalent to:

  • num.signal * num.signal
  • num.signal * q
  • num.signal * n
  • q * num.signal
  • n * num.signal # this doesn't work
like image 888
Philipp der Rautenberg Avatar asked Feb 21 '23 06:02

Philipp der Rautenberg


2 Answers

When Python sees x * y here's what happens:

  • if y is a subclass of x --> y.__rmul__(x) is called

otherwise:

  • x.__mul__(y) is called

IF x.__mul__(y) returns NotImplemented (which is different from raise NotImplementedError

  • y.__rmul__(x) is called

So, there are two ways that __rmul__ can be called -- subclass ndarray, or have ndarray not be able to multiply with Numeric.

You are unable to subclass, and apparently ndarray is happy to work with Numeric, so . . .

Thankfully, the numpy folks prepared for situations such as this -- the answer lies in the __array_wrap__ method:

def __array_wrap__(self, out_arr, context=None):
    return type(self.signal)(out_arr, self.dimensionality)

We are using the original signal class, along with the original dimensionality, to create a new signal for the new Numeric object.

The entire bit looks like this:

import quantities as pq
import numpy as np

class Numeric(object):
    def __init__(self, signal):
        self.signal = signal
        self.dimensionality = self.signal.dimensionality
        self._dimensionality = self.signal._dimensionality
    def __array__(self):
        return self.signal
    def __array_wrap__(self, out_arr, context=None):
        return type(self.signal)(out_arr, self.dimensionality)
    def __mul__(self, obj):
        return self.signal.__mul__(obj)
    def __rmul__(self, obj):
        return self.signal.__rmul__(obj)


num = Numeric(pq.Quantity([1,2,3], 'mV'))
q = pq.Quantity([2,3,4], 'mV')
n = np.array([3,4,5])

t = num * num
print type(t), t
t = num * q
print type(t), t
t = num * n
print type(t), t
t = q * num
print type(t), t
t = n * num
print type(t), t

And when run:

<class 'quantities.quantity.Quantity'> [1 4 9] mV**2
<class 'quantities.quantity.Quantity'> [ 2  6 12] mV**2
<class 'quantities.quantity.Quantity'> [ 3  8 15] mV
<class 'quantities.quantity.Quantity'> [ 2  6 12] mV**2
<class 'quantities.quantity.Quantity'> [ 3  8 15] mV
like image 97
Ethan Furman Avatar answered May 01 '23 23:05

Ethan Furman


You need to definine __array_wrap__. See the documentation here.

As a quick example using your example (but not requiring quantities):

class Numeric(object):
  def __init__(self, signal):
    self.signal = signal
  def __array__(self):
    return self.signal
  def __mul__(self, obj):
    return type(self)(self.signal.__mul__(obj))
  def __rmul__(self, obj):
    return type(self)(self.signal.__rmul__(obj))

import numpy as np

num = Numeric(np.arange(10))
n = np.arange(10)

print type(num * n)
print type(n * num)

This yields:

<class '__main__.Numeric'>
<type 'numpy.ndarray'>

If we include __array_wrap__:

class Numeric(object):
  def __init__(self, signal):
    self.signal = signal
  def __array__(self):
    return self.signal
  def __mul__(self, obj):
    return type(self)(self.signal.__mul__(obj))
  def __rmul__(self, obj):
    return type(self)(self.signal.__rmul__(obj))
  def __array_wrap__(self, out_arr, context=None):
    return type(self)(out_arr)

import numpy as np

num = Numeric(np.arange(10))
n = np.arange(10)

print type(num * n)
print type(n * num)

It yields:

<class '__main__.Numeric'>
<class '__main__.Numeric'>

However, I'm still confused as to why you can't just subclass ndarray in the first place... I suspect it would be a lot cleaner in the long run. If you can't, you can't, though.

To fully mimic an ndarray without subclassing ndarray, you're going to need to get very familiar with the details of subclassing them.

like image 41
Joe Kington Avatar answered May 01 '23 23:05

Joe Kington