-
-
Notifications
You must be signed in to change notification settings - Fork 10.9k
WIP: For testing, optionally preserve 0-D arrays in operations #29067
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Signed-off-by: Sebastian Berg <sebastianb@nvidia.com>
Many remaining ones are around object arrays returning arguably better (less "NumPy") results, but probably not all :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seberg - overall, this looks great, and I think it also helps to see why it is good not to be too strict about every result becoming arrays.
Also, it is nice to see the array converter that you implemented coming to the fore. That said, the old_scalar
argument does not look nice - as asked inline, what exactly goes wrong without that?
@@ -404,7 +403,11 @@ static PyMethodDef array_converter_methods[] = { | |||
METH_FASTCALL | METH_KEYWORDS, NULL}, | |||
{"wrap", | |||
(PyCFunction)array_converter_wrap, | |||
METH_FASTCALL | METH_KEYWORDS, NULL}, | |||
METH_FASTCALL | METH_KEYWORDS, | |||
"Apply array-wrap. Supports `to_scalar=None/True/False` and " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand the docstring!
|
||
/* | ||
* all_inputs_were_scalars is used to decide if 0-D results should be | ||
* unpacked to a scalar. But, `np.matmul(vector, vector)` should do this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also np.vecdot
* unpack. (unless keepdims=True, since that behave like a normal ufunc) | ||
* So we pretend inputs were scalars... | ||
* | ||
* TODO: We may need a way to customize that at some point. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For gufunc, whether an argument is scalar would seem to have the logical condition that there are no outer dimensions being iterated over. The effect would be the same as what you have here, but perhaps easier to describe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, maybe that is a better way to describe "no outer dimensions in the result".
Unfortunately, that is also a nice way to explain why we don't like this behavior :). I.e. the whole point of this is that it means the number of outer dimensions (zero or more) has never an impact on the result type!
Unfortunately, unlike arr.sum()
, vecdot(x, y)
doesn't have a way to signal that we should return a scalar (by default).
Maybe out=...
has to be the one way unfortunately... Or maybe vecdot
should return arrays. Also, we could (and probably should!) make it so that if axes=
is passed we never return a scalar (mirroring arr.sum(0)
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see... I guess my initial sense would be to just always return arrays, at least for this trial... Do many tests break?
Note that axes=None
(or axis=None
) might still be a possibility - currently, that errors, so we can change it to mean "default axes, but return scalar if possible". Of course, that then prevents actually passing in meaningful axes and getting a scalar out. Also, it really is a hack.
Instead, maybe we should have an equivalent of out=...
that makes explicit one does want a scalar if possible, say, out=()
(or out=((), ())
for multiple outputs)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead, maybe we should have an equivalent of out=... that makes explicit one does want a scalar if possible, say, out=() (or out=((), ()) for multiple outputs)
Maybe, I omitted multiple outputs for ...
and I think it is better here as well. If we are going to go a step further here, I am wondering if even a return_scalar=True/False/None
may not be better, though (and remove out=...
again).
Maybe the main thing is if vec @ vec
can change behavior. Because if we are willing to gamble on that, it may also be OK to just ask users to do np.matmul(vec, vec)[()]
for starters. Or a more obvious arr.get_element()
EDIT: Part of me wonders if such a change might be OK to uncouple even. Also, we could allow gufuncs to opt out (always return arrays).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My sense would indeed be to uncouple, and just see if this causes big problems. I somewhat doubt it -- gufunc
s are just not used as much and generally scalars are not really expected. (I definitely don't like the idea of a special keyword argument.)
assert type(np.sum(np.float32(2.5), axis=None)) is np.float32 | ||
assert type(np.max(np.float32(2.5), axis=None)) is np.float32 | ||
assert type(np.min(np.float32(2.5), axis=None)) is np.float32 | ||
# TODO: In a sense should return an array, but this is axis=0 is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, arguably axis=0
on a 0-D array should just fail!
More relevantly here, are you sure you want to insist this returns a scalar? I guess just from being an operation on scalars only?
numpy/lib/_arraysetops_impl.py
Outdated
|
||
return _in1d(ar1, ar2, assume_unique, invert, kind=kind) | ||
dt = conv.result_type() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dt
does not seem to be used.
numpy/lib/_nanfunctions_impl.py
Outdated
a = np.asanyarray(a) | ||
conv = _array_converter(a, q) | ||
a, q_arr = conv.as_arrays(pyscalars="convert") | ||
|
||
if a.dtype.kind == "c": | ||
raise TypeError("a must be an array of real numbers") | ||
|
||
# Use dtype of array if possible (e.g., if q is a python int or float). | ||
if isinstance(q, (int, float)) and a.dtype.kind == "f": | ||
q = np.asanyarray(q, dtype=a.dtype) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might as well change it to np.asarray
here, like in the regular quantile
.
@@ -1160,7 +1160,8 @@ def compare(x, y): | |||
if not issubdtype(z.dtype, number): | |||
z = z.astype(np.float64) # handle object arrays | |||
|
|||
return z < 1.5 * 10.0**(-decimal) | |||
# the float64 ensures at least double precision for the comparison. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this actually matter for this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah 3 linalg tests or so fail otherwise. But maybe should check whether the problem is really here or not.
numpy/_core/multiarray.pyi
Outdated
@@ -1289,3 +1289,6 @@ def nested_iters( | |||
casting: _CastingKind = ..., | |||
buffersize: SupportsIndex = ..., | |||
) -> tuple[nditer, ...]: ... | |||
|
|||
def _get_preserve_0d_arrays() -> bool: ... | |||
def _set_preserve_0d_arrays(state: bool, /) -> bool: ... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add CR
Isn't subclasses always the right answer? Wouldn't |
Kind of, although here we already ignored array-wrap, which should be OK. But basically, array-wrap would be passed |
Your earlier answer left me confused - when exactly does this happen? (only with |
The initial comment was just wrong, sorry. We always potentially ignored Right now, more tests than I expected are failing with the env variable set, though. |
OK, things are a bit better now. Should pass without env var. The main problems currently:
|
Co-authored-by: Joren Hammudoglu <jhammudoglu@gmail.com>
This is a WIP, it works well enough for trying, but won't pass tests with environment variable set and has some smaller issues still.
We discussed with @mdhaber and others also to pursue removing the annoyance that 0-D arrays tend to be converted to scalars. This PR implements it, but hides it behind:
NUMPY_PRESERVE_0D_ARRAYS=1
np._get_preserve_0d_arrays()
np._set_preserve_0d_arrays()
All for testing and not thread/context-safe. We probably could do that but it isn't needed for testing. (I wouldn't want context use for indexing but it should be fine for anything else.)
This takes my "minimal change" approach, larger approaches were discussed, but the amount of test failures downstream didn't make it appealing on first sight. The changes are:
quantile(arr, q)
, onlyq
dictates the result shape and thus onlyq
is relevant.)arr1d[0]
is unchanged.arr[..., 0]
orarr[0, ...]
means that indexing already has a way to avoid getting scalars.arr.sum()
, i.e.axis=None
is unchanged and returns scalars. Butarr.sum(0)
always returns arrays. The reason is that the first currently always returns a scalar, while the latter returns an(N-1)
dimensional array or a scalar if 0-D.axis=-1
, I think, but typical reductions useaxis=None
.axis=None
is special enough, although I admit it feels a bit different from the indexing...
use. (which may make sense as one lists all axis, while the other lists axes to be removed)I would want to go with the above in a first merge, but we can discuss adding additional switches, e.g. for the behavior of reductions with
axis=None
. Indexingarr[()]
could also be changed, but if we discuss this, we may want to start with adding something likearr.get_element(idx)
first (becausearr.item()
, seems a bit awkward maybe).Things to do here:
nanfuncs
callnp.asarray()
early on and need to fixed if they are to return scalars for scalar inputs.This PR does "fix" that some functions return Python objects rather than NumPy scalars when inputs have
dtype=object
. I think this is OK, but it is a subtle change.