numpy / numpy/numpy

Trapz dx doesn't trigger array_function mechanism

Open
#18,902 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

00 - Bug
Dominant language
Python
Stars
32.8k
Forks
12.8k
Avg merge
1d 7h
Merged PRs (30d)
197

Description

It seems that when using numpy's trapz numpy.trapzwith the __array_function__ mechanism, only y and x are caught, but not dx, with numpy's signature numpy.trapz(y, x=None, dx=1.0, axis=-1).

Reproducing code example:

Here is a sample code that creates a numerical-labelled object, basically a value (scalar or array) and a label as a string.
A wrapped version of numpy trapz is created and registered, so that anytime numpy recieves a NumericalLabelled in a trapz call, it relies on the wrapped version. But it seems that the wrapped version is never called when only dx is a NumericalLabelled. I would expect it is called as soon as any of the input is of the type NumericalLabelled.

import numpy as np

HANDLED_FUNCTIONS = {}

class NumericalLabeled():
    def __init__(self, value, label=""):
        self.value = value
        self.label = label
        
    def __repr__(self):
        return "NumericalLabelled<"+str(self.value) + "," + self.label+">"
    
    def __array_function__(self, func, types, args, kwargs):
        print("Got into array function")
        if func not in HANDLED_FUNCTIONS:
            return NotImplemented
        return HANDLED_FUNCTIONS[func](*args, **kwargs)
    
def make_numericallabelled(x, label=""):
    """
    Helper function to cast anything into a NumericalLabelled object.
    """
    if isinstance(x, NumericalLabeled):
        return x
    else:
        return NumericalLabeled(x, label=label)
    
# Numpy functions            
# Override functions - used with __array_function__
def implements(np_function):
    def decorator(func):
        HANDLED_FUNCTIONS[np_function] = func
        return func
    return decorator    
    
@implements(np.trapz)
def np_trapz(q, x=None, dx=1, **kwargs):
    """
    Numpy's trapz wrapper for NumericalLabelled.
    """
    # first convert q into a NumericalLabelled to use `q.value` 
    q = make_numericallabelled(q)
    if x is None:    
        # using dx.value and dx.label
        dx = make_numericallabelled(dx, label="dx")
        return NumericalLabeled(np.trapz(q.value, dx=dx.value, x=None, **kwargs),
                                q.label + dx.label,
                    )
    else:
        # using x/value and x.label
        x = make_numericallabelled(x, label="x")
        return NumericalLabeled(np.trapz(q.value, x=x.value, **kwargs),
                                q.label + x.label,
                    )

def main():
    # create a scalar to use as dx
    half = NumericalLabeled(0.5, "half")
    # create an array to use as x
    x = NumericalLabeled(np.arange(5), "x")
    # then 
    # this works
    print(np.trapz(NumericalLabeled(np.arange(5), "a")))
    # this also works
    print(np.trapz(np.arange(5), x=x))
    # but not this
    print(np.trapz(np.arange(5), dx=half))
    # TypeError: unsupported operand type(s) for *: 'NumericalLabeled' and 'int'
main()
Error message:
Got into array function
NumericalLabelled<8.0,adx>
Got into array function
NumericalLabelled<8.0,x>

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-30-3401bee8c135> in <module>
     68     np.trapz(np.arange(5), dx=half)
     69     # TypeError: unsupported operand type(s) for *: 'NumericalLabeled' and 'int'
---> 70 main()

<ipython-input-30-3401bee8c135> in main()
     66     np.trapz(np.arange(5), x=x)
     67     # but not this
---> 68     np.trapz(np.arange(5), dx=half)
     69     # TypeError: unsupported operand type(s) for *: 'NumericalLabeled' and 'int'
     70 main()

<__array_function__ internals> in trapz(*args, **kwargs)

/opt/anaconda3/lib/python3.8/site-packages/numpy/lib/function_base.py in trapz(y, x, dx, axis)
   4161     slice2[axis] = slice(None, -1)
   4162     try:
-> 4163         ret = (d * (y[tuple(slice1)] + y[tuple(slice2)]) / 2.0).sum(axis)
   4164     except ValueError:
   4165         # Operations didn't work, cast to ndarray

TypeError: unsupported operand type(s) for *: 'NumericalLabeled' and 'int'
NumPy/Python version information:

1.20.0 3.8.5 (default, Sep 4 2020, 02:22:02) [Clang 10.0.0 ]

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Run the reproducing example with numpy.trapz and a NumericalLabeled object passed as dx. Then inspect numpy/lib/function_base.py at the trapz entry point and the array_function dispatch behavior; done means dx triggers the registered wrapper like y and x and the example no longer raises the TypeError.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
data
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.