Change definition of complex sign (and use it in copysign)
Hi All, A longstanding, small wart in numpy has been that the definition of sign for complex numbers is really useless (`np.sign(z)` gives the sign of the real component, unless that is zero, in which case it gives the sign of the imaginary component, in both cases as a complex number with zero imaginary part). Useless enough, in fact, that in the Array API it is suggested [1] that sign should return `z / z` (a definition consistent with those of reals, giving the direction in the complex plane). The question then becomes what to do. My suggestion  see https://github.com/numpy/numpy/pull/25441  is to adapt the Array API definition for numpy 2.0, with the logic that if we don't change it in 2.0, when will we? Implementing it, I found no real test failures except one for `np.geomspace`, where it turned out that to correct the failure, the new definition substantially simplified the implementation. Furthermore, with the redefinition, it has become possible to extend ``np.copysign(x1, x2)`` to complex numbers, since it can now generally return ``x1 * sign(x2)`` with the sign as defined above (with no special treatment for zero). Anyway, to me the main question would be whether this would break any workflows (though it is hard to see how it could, given that the previous definition was really rather useless...). Thanks, https://github.com/dataapis/arrayapi/pull/556, Marten [1] https://dataapis.org/arrayapi/latest/API_specification/generated/array_api... (and https://github.com/dataapis/arrayapi/pull/556, which has links to previous discussion)
In my opinion, with the caveat that anyone that asks for the sign of a complex number gets what they deserve, this seems about as useful a definition as any. On Fri, Dec 22, 2023 at 8:23 AM <mhvk@astro.utoronto.ca> wrote:
Hi All,
A longstanding, small wart in numpy has been that the definition of sign for complex numbers is really useless (`np.sign(z)` gives the sign of the real component, unless that is zero, in which case it gives the sign of the imaginary component, in both cases as a complex number with zero imaginary part). Useless enough, in fact, that in the Array API it is suggested [1] that sign should return `z / z` (a definition consistent with those of reals, giving the direction in the complex plane).
The question then becomes what to do. My suggestion  see https://github.com/numpy/numpy/pull/25441  is to adapt the Array API definition for numpy 2.0, with the logic that if we don't change it in 2.0, when will we?
Implementing it, I found no real test failures except one for `np.geomspace`, where it turned out that to correct the failure, the new definition substantially simplified the implementation.
Furthermore, with the redefinition, it has become possible to extend ``np.copysign(x1, x2)`` to complex numbers, since it can now generally return ``x1 * sign(x2)`` with the sign as defined above (with no special treatment for zero).
Anyway, to me the main question would be whether this would break any workflows (though it is hard to see how it could, given that the previous definition was really rather useless...).
Thanks, https://github.com/dataapis/arrayapi/pull/556, Marten
[1] https://dataapis.org/arrayapi/latest/API_specification/generated/array_api... (and https://github.com/dataapis/arrayapi/pull/556, which has links to previous discussion) _______________________________________________ NumPyDiscussion mailing list  numpydiscussion@python.org To unsubscribe send an email to numpydiscussionleave@python.org https://mail.python.org/mailman3/lists/numpydiscussion.python.org/ Member address: ndbecker2@gmail.com
 *Those who don't understand recursion are doomed to repeat it*
On Fri, 22 Dec 2023 at 13:25, <mhvk@astro.utoronto.ca> wrote:
Anyway, to me the main question would be whether this would break any workflows (though it is hard to see how it could, given that the previous definition was really rather useless...).
SymPy already defines sign(z) as z/abs(z) (with sign(0) = 0) as proposed here. Checking this I see that the current mismatch causes a bug when SymPy's lambdify function is used to evaluate the sign function with NumPy: In [12]: sign(z).subs(z, 1+1j) Out[12]: 0.707106781186548 + 0.707106781186548⋅ⅈ In [13]: lambdify(z, sign(z))(1+1j) # uses numpy Out[13]: (1+0j) The proposed change to NumPy's sign function would fix this bug.  Oscar
To me this sounds like a reasonable change. It does seem like there is a return value which is more sensible than alternatives. And the fact that sympy is already doing that indicates that same conclusion was reached more than once. I am not dealing much with complex numbers at the moment, but I see this being of nontrivial convenience when I need to. Regards, DG
On 22 Dec 2023, at 15:48, Oscar Benjamin <oscar.j.benjamin@gmail.com> wrote:
On Fri, 22 Dec 2023 at 13:25, <mhvk@astro.utoronto.ca> wrote:
Anyway, to me the main question would be whether this would break any workflows (though it is hard to see how it could, given that the previous definition was really rather useless...).
SymPy already defines sign(z) as z/abs(z) (with sign(0) = 0) as proposed here.
Checking this I see that the current mismatch causes a bug when SymPy's lambdify function is used to evaluate the sign function with NumPy:
In [12]: sign(z).subs(z, 1+1j) Out[12]: 0.707106781186548 + 0.707106781186548⋅ⅈ
In [13]: lambdify(z, sign(z))(1+1j) # uses numpy Out[13]: (1+0j)
The proposed change to NumPy's sign function would fix this bug.
 Oscar _______________________________________________ NumPyDiscussion mailing list  numpydiscussion@python.org To unsubscribe send an email to numpydiscussionleave@python.org https://mail.python.org/mailman3/lists/numpydiscussion.python.org/ Member address: dom.grigonis@gmail.com
sign(z) = z/z is a fairly standard definition. See https://oeis.org/wiki/Sign_function and https://en.wikipedia.org/wiki/Sign_function. It's also implemented this way in MATLAB and Mathematica (see https://www.mathworks.com/help/symbolic/sign.html and https://reference.wolfram.com/language/ref/Sign.html). The function z/z is useful because it represents a normalization of z as a vector in the complex plane onto the unit circle. With that being said, I'm not so sure about the suggestion about extending copysign(x1, x2) as x1*sign(x2). I generally think of copysign as a function to manipulate the floatingpoint representation of a number. It literally copies the sign *bit* from x2 into x1. It's useful because of things like 0.0, which is otherwise difficult to work with since it compares equal to 0.0. I would find it surprising for copysign to do a numeric calculation on complex numbers. Also, your suggested definition would be wrong for 0.0 and 0.0, since sign(0) is 0, and this is precisely where copysign matters. I suppose one could make sense of copysign(x1, x2) where x1 is complex and x2 is float, by copying the sign of x2 into both components of x1. Although I would suggest only adding such a thing if there's an actual need for it. I imagine presently if anyone does need this they just use copysign(x1.view(float64), x2).view(complex128). Aaron Meurer On Sat, Dec 23, 2023 at 8:36 AM Dom Grigonis <dom.grigonis@gmail.com> wrote:
To me this sounds like a reasonable change.
It does seem like there is a return value which is more sensible than alternatives.
And the fact that sympy is already doing that indicates that same conclusion was reached more than once.
I am not dealing much with complex numbers at the moment, but I see this being of nontrivial convenience when I need to.
Regards, DG
On 22 Dec 2023, at 15:48, Oscar Benjamin <oscar.j.benjamin@gmail.com> wrote:
On Fri, 22 Dec 2023 at 13:25, <mhvk@astro.utoronto.ca> wrote:
Anyway, to me the main question would be whether this would break any workflows (though it is hard to see how it could, given that the previous definition was really rather useless...).
SymPy already defines sign(z) as z/abs(z) (with sign(0) = 0) as proposed here.
Checking this I see that the current mismatch causes a bug when SymPy's lambdify function is used to evaluate the sign function with NumPy:
In [12]: sign(z).subs(z, 1+1j) Out[12]: 0.707106781186548 + 0.707106781186548⋅ⅈ
In [13]: lambdify(z, sign(z))(1+1j) # uses numpy Out[13]: (1+0j)
The proposed change to NumPy's sign function would fix this bug.
 Oscar _______________________________________________ NumPyDiscussion mailing list  numpydiscussion@python.org To unsubscribe send an email to numpydiscussionleave@python.org https://mail.python.org/mailman3/lists/numpydiscussion.python.org/ Member address: dom.grigonis@gmail.com
_______________________________________________ NumPyDiscussion mailing list  numpydiscussion@python.org To unsubscribe send an email to numpydiscussionleave@python.org https://mail.python.org/mailman3/lists/numpydiscussion.python.org/ Member address: asmeurer@gmail.com
On Wed, Jan 3, 2024 at 4:09 PM Aaron Meurer <asmeurer@gmail.com> wrote:
sign(z) = z/z is a fairly standard definition. See https://oeis.org/wiki/Sign_function and https://en.wikipedia.org/wiki/Sign_function. It's also implemented this way in MATLAB and Mathematica (see https://www.mathworks.com/help/symbolic/sign.html and https://reference.wolfram.com/language/ref/Sign.html). The function z/z is useful because it represents a normalization of z as a vector in the complex plane onto the unit circle.
With that being said, I'm not so sure about the suggestion about extending copysign(x1, x2) as x1*sign(x2). I generally think of copysign as a function to manipulate the floatingpoint representation of a number. It literally copies the sign *bit* from x2 into x1. It's useful because of things like 0.0, which is otherwise difficult to work with since it compares equal to 0.0. I would find it surprising for copysign to do a numeric calculation on complex numbers. Also, your suggested definition would be wrong for 0.0 and 0.0, since sign(0) is 0, and this is precisely where copysign matters.
Agreed on all points.  Robert Kern
I think this suggestion regarding sign is solid. From both theoretical and practical points of view. And agree with all of Aaron’s points as well. Regards, DG
On 4 Jan 2024, at 22:58, Robert Kern <robert.kern@gmail.com> wrote:
On Wed, Jan 3, 2024 at 4:09 PM Aaron Meurer <asmeurer@gmail.com <mailto:asmeurer@gmail.com>> wrote: sign(z) = z/z is a fairly standard definition. See https://oeis.org/wiki/Sign_function <https://oeis.org/wiki/Sign_function> and https://en.wikipedia.org/wiki/Sign_function <https://en.wikipedia.org/wiki/Sign_function>. It's also implemented this way in MATLAB and Mathematica (see https://www.mathworks.com/help/symbolic/sign.html <https://www.mathworks.com/help/symbolic/sign.html> and https://reference.wolfram.com/language/ref/Sign.html <https://reference.wolfram.com/language/ref/Sign.html>). The function z/z is useful because it represents a normalization of z as a vector in the complex plane onto the unit circle.
With that being said, I'm not so sure about the suggestion about extending copysign(x1, x2) as x1*sign(x2). I generally think of copysign as a function to manipulate the floatingpoint representation of a number. It literally copies the sign *bit* from x2 into x1. It's useful because of things like 0.0, which is otherwise difficult to work with since it compares equal to 0.0. I would find it surprising for copysign to do a numeric calculation on complex numbers. Also, your suggested definition would be wrong for 0.0 and 0.0, since sign(0) is 0, and this is precisely where copysign matters.
Agreed on all points.
 Robert Kern _______________________________________________ NumPyDiscussion mailing list  numpydiscussion@python.org To unsubscribe send an email to numpydiscussionleave@python.org https://mail.python.org/mailman3/lists/numpydiscussion.python.org/ Member address: dom.grigonis@gmail.com
Hi All, Thanks for the comments on complex sign  it seems there is good support for it. On copysign, currently it is not supported for complex values at all. I think given the responses so far, it looks like we should just keep it like that; although my extension was fairly logical, I cannot say that I know that I can think of a clear use case! Anyway, I'm fine with removing it from the PR. All the best, Marten
participants (7)

Aaron Meurer

Dom Grigonis

Marten van Kerkwijk

mhvk＠astro.utoronto.ca

Neal Becker

Oscar Benjamin

Robert Kern