Repository navigation
Arithmetic between real arrays and Python complex scalars (revisited) #841
Description
The array API standard is now clear (in Type Promotion Rules):
This means that following does not have defined behavior:
x = xp.asarray([1., 2., 3.])
x + 1jHowever, the behavior is defined consistently in NumPy, CuPy, PyTorch, jax.numpy1, Dask array, and Tensorflow2 in the intuitive way: the real and imaginary component dtypes of the complex output array match the dtype of the input array.
This issue was discussed explicitly in gh-478 beginning with #478 (comment). I see several comments that seem supportive of allowing this operation, e.g.
I would hope it at least works for Python scalars.
x + 0jseems like the simplest and most obvious way to convert a real array to a complex one.
The comment I see against it (#478 (comment)) is
I know it's very common to do this (also *
1.0to convert integer to real). However, it doesn't seem like great practice irrespective of the decision here - there's an explicitastypefunction for converting to other dtypes.
It looked like the summary comments suggested that this would be allowed, but it was not specifically addressed in the PR that closed the issue. (See postscript for more information.)
I thought it might help to add some perspective on how this impacts a developer translating code to the standard: when tests including an operation like
x + 1j
begin to fail with array_api_strict only, the developer is faced with the choice of skipping the array_api_strict tests or figuring out what is wrong and changing the code to something like:
xp.astype(x, xp.result_type(x.dtype, xp.complex64)) + 1jIf they also want it to be able to preserve the lower precision types supported by some libraries (e.g. for NumPy), there would be additional hurdles. I tested for a few libraries, and array API standard compatible version of the code also increases the execution time notably for small arrays. Individually, the inconvenience, complexity, and overhead are small, but they add up. I am excited about adding array API support to SciPy, but not everyone is supportive, and performance regressions and complicated-looking diffs can make it difficult to garner support.
I would suggest that the operation be defined, even if it means adding an exception to the simply stated rules for array/Python arithmetic.
The more specific language about this not being defined was added in gh-513, which provided the justification:
Given that current guidance requires converting a scalar to the array data type, to accommodate complex scalars and real-valued arrays, we'd need to specify casting rules for complex to real,
I think that is referring to the language:

That guidance was added in gh-74, but I can't trace the origin further. Perhaps there can be exceptions to that rule?
Footnotes
-
With NumPy experimental behavior (https://github.com/data-apis/array-api/issues/478#issuecomment-1272631595). Vanilla Tensorflow doesn't seem to support the other array-scalar operations that are defined, but already this was not deemed to be sufficient reason to prevent their inclusion in the standard (https://github.com/data-apis/array-api/issues/478#issuecomment-1270409660). ↩

Activity
asmeurer commentedon Sep 11, 2024
I still agree that this should be supported. We should be careful to not confuse downcasting a complex array (or scalar) to real float, which is indeed not supported and for very good reasons, with upcasting a real float to complex.
In fact, real and complex floating-point arrays are already supported to promote together:
So it seems odd that this doesn't work for Python scalars.
The piece of text that @mdhaber quoted at the top of the issue seems to imply that there is ambiguity whether the resulting array should be complex64 or complex128. But I think this is already pretty well spelled out for the Python
floatcase https://data-apis.org/array-api/latest/API_specification/type_promotion.html#mixing-arrays-with-python-scalarsi.e.,
float32_array + python_floatgivesfloat32andfloat64_array + python_floatgivesfloat64. In other words, the corresponding rule should be that afloat32_array + python_complexgives acomplex64array andfloat64_array + python_complexgives acomplex128array.I think the real question is, what is the idea behind the rule "convert the scalar to zero-dimensional array with the same data type as that of the array"? If the idea is that "when adding a Python scalar to an array, the dtype doesn't change", then indeed real_array + complex_scalar shouldn't be supported. But if it's rather that "the precision doesn't change", then it should be supported.
rgommers commentedon Oct 3, 2024
This is quite clumsy indeed. Independent of the decision on allowing
x + 1j, we should also make this nicer by supporting dtype kinds inastypeI think, so the above becomes:Doing that would also address the problem for unsigned -> signed integers, which cannot be done with a Python scalar equivalent like
+ 0j.ev-br commentedon Nov 25, 2024
One rather annoying example I tripped over in scipy is
An "obvious" workaround,
is actually a footgun because if
x.dtype == xp.float32, the exponential ends up beingcomplex128(at least with numpy). So the correct incantation is something likewhich looks like something
1j * xshould do under the hood.feat: define complex scalar <op> fp array