Re: Do special functions need to have all kwargs/attributes/methods of ufuncs?

Ralf Gommers <[email protected]> Thu, 21 Sep 2023 08:22:41 +0200
Newsgroups gmane.comp.python.scientific.devel
Message-ID <CABL7CQgdc6BMw2yGY44iWFyj4WuR9DhFnR-P1Q4+semAG3D8Dw@mail.gmail.com>
On Wed, Sep 20, 2023 at 10:33 PM Albert Steppi <[email protected]>
wrote:

> It should be straightforward if a little tedious to add support for the
> ufunc attributes like `nin`, `nout`, etc. and ufunc methods like `reduce`,
> `accumulate`, etc. (for numpy and raising an error for backends that don't
> support these methods), if not doing so would require a difficult
> deprecation cycle. I think making something like isinstance(special.ndtr,
> np.ufunc) work would require some pretty ugly hackery, so I'd prefer to at
> least break this.
>

Agreed. Ufuncs are nice, but can also be constraining. E.g., if we'd like
to add a keyword to a function that isn't supported by ufuncs, we have to
be able to change the return type. We reserve the right to move functions
to being ufuncs, and we should also reserve the right to move them away
from it.

The current status is this:

>>> import numpy as np
>>> from scipy import special
>>> ufuncs = []
>>> non_ufuncs = []
>>> for fname in special.__all__:
...     func = getattr(special, fname)
...     if isinstance(func, np.ufunc):
...         ufuncs.append(func)
...     else:
...         non_ufuncs.append(func)
...
>>> len(ufuncs)
232
>>> len(non_ufuncs)
119

It's not only functions that don't accept arrays in the non-ufunc list.
E.g., `logsumexp`, `softmax`, `lambertw`, `multigammaln` and `sinc` aren't
ufuncs (and none have the warning Robert mentions). The docs are a bit
incomplete and misleading - we cannot promise that all functions that
accept arrays are or will be ufuncs, it would for example prevent
implementing any function in pure Python.

I believe functions in special being ufuncs yes or no is mostly leaking of
implementation details - using the ufunc machinery was nice and performant,
but quite a few of the methods don't really make sense as Albert points
out. There's probably more such leakage - for example, `sinc` has
array-function dispatching support. The return type situation is pretty
messy right now, and wholly unintentional:

>>> special.softmax(d)  # not a ufunc, so returns numpy.ndarray
array([0.18242552, 0.81757448])
>>> special.j1(d)       # is a ufunc, so returns a dask array
dask.array<j1, shape=(2,), dtype=float64, chunksize=(2,),
chunktype=numpy.ndarray>
>>> special.sinc(d)     # not a ufunc, but has array-function support ->
dask array
dask.array<sinc, shape=(2,), dtype=float64, chunksize=(2,),
chunktype=numpy.ndarray>
>>> special.airy([1, 2.5])  # is a ufunc, accepts array-like's, returns
numpy.ndarray
(array([0.13529242, 0.01572592]), array([-0.15914744, -0.02625088]),
array([1.20742359, 6.48166074]
), array([0.93243593, 9.42142332]))
>>> special.airy(d)         # is a ufunc but breaks *because* it's a ufunc
for dask arrays
...
TypeError: operand type(s) all returned NotImplemented from
__array_ufunc__(<ufunc 'airy'>, '__call
__', dask.array<array, shape=(2,), dtype=float64, chunksize=(2,),
chunktype=numpy.ndarray>): 'Array
'

This kind of inconsistency with return types for arrays from other array
libraries is one of the key things that the current effort is attempting to
fix.



> I can't see a serious use for `reduce`, `accumulate`, and `reduceat` for
> special functions, but `outer` and `at` could be useful in some cases.
> Based on the documentation Robert Kern linked to, I think it would probably
> be best to support all ufunc attributes and methods.
>
> Albert
>
> On Tue, Sep 19, 2023 at 12:30 PM Robert Kern <[email protected]>
> wrote:
>
>> On Tue, Sep 19, 2023 at 12:00 PM Matt Haberland <[email protected]>
>> wrote:
>>
>>> Yes, the documentation mentions that they are "technically" ufuncs. My
>>> comment was about documentation of the features of ufuncs - the
>>> documentation seems to intentionally hide all ufunc parameters from the
>>> signature. Please see
>>> https://github.com/scipy/scipy/pull/19023#issuecomment-1711949107
>>> for further context.
>>>
>>
>> More out of concision and deduplication than anything else because those
>> parameters are documented in `ufunc` itself.
>>
>> ...
>>
>> That's as much the official docs for this object as what appears on
>> docs.scipy.org.
>>
>> These are ufuncs. They should be ufuncs regardless of the environment
>> variable. If you want to change them from being ufuncs to just being
>> elementwise functions, go through a big deprecation where they are *never*
>> ufuncs regardless of environment variable (i.e. plain functions with the
>> docs.scipy.org signatures and just use the ufunc implementations
>> underneath when the Array API is `numpy`). But I suspect one can also
>> implement a ufunc-like override object that passes through all attribute
>> access and ufunc-only keyword calls to the ufunc object and calls the
>> override elementwise function only when it fits.
>>
>
This would be a useful thing to do either way I think, because it would
make the API more uniform. I'd be inclined to keep only the few methods
that are actually useful, and not the whole API surface of ufuncs. The not
so useful methods and attributes can be deprecated first in a wrapper.
Finally, a wrapper would also help fix the issues with using
`inspect.signature`, `inspect.isroutine` & co, and attributes like
`__module__`.

Cheers,
Ralf

_______________________________________________
SciPy-Dev mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/scipy-dev.python.org/
Member address: [email protected]