3

I am using Python 3.7.7 and numpy 1.19.1. This is the code:

import numpy as np
a = 55.74947517067784019673 + 0j
print(f'{-a == -1 * a}, {np.angle(-a)}, {np.angle(-1 * a)}')

and this is the output:

True, -3.141592653589793, 3.141592653589793

I have two questions:

  1. Why does the angle function give different outputs for the same input?
  2. According to the documentation, the angle output range is (-pi, pi], so why is one of the outputs -np.pi?
Ehsan
  • 12,072
  • 2
  • 20
  • 33

2 Answers2

2

If you look at the source of the np.angle, it uses the function np.arctan2. Now, according to the numpy docs, np.arctan2 uses the underlying C library, which has the following rule:

Note that +0 and -0 are distinct floating point numbers, as are +inf and -inf.

which results in different behavior when calculating using +/-0. So, in this case, the rule is:

y: +/- 0
x: <0
angle: +/- pi

Now, if you try:

a = 55.74947517067784019673
print(f'{-a == -1 * a}, {np.angle(-a)}, {np.angle(-1 * a)}')
#True, 3.141592653589793, 3.141592653589793

and if you try:

a = 55.74947517067784019673 + 0j
print(-a)
#(-55.74947517067784-0j)
print(-1*a)
#(-55.74947517067784+0j)
print(f'{-a == -1 * a}, {np.angle(-a)}, {np.angle(-1 * a)}')
#True, -3.141592653589793, 3.141592653589793

Which is inline with the library protocol.

As for your second question, I guess it is a typo/mistake since the np.arctan2 doc says:

Array of angles in radians, in the range [-pi, pi]. This is a scalar if both x1 and x2 are scalars.

Explanation of -a vs. -1*a:

To start with, 55.74947517067784019673 + 0j is NOT construction of a complex number and merely addition of a float to a complex number (to construct a complex number explicitly use complex(55.74947517067784019673, 0.0) and beware that integers do not have signed zeros and only floats have). -a is simply reverting the sign and quite self explanatory. Lets see what happens when we calculate -1*a:

For simplicity assume a = 55.5 + 0j

  • First a = 55.5+0j converts to complex(55.5, 0.0)
  • Second -1 equals to complex(-1.0, 0.0)
  • Then complex(-1.0, 0.0)*complex(55.5, 0.0) equals to complex((-1.0*55.5 - 0.0*0.0), (-1.0*0.0 + 0.0*55.5)) equals to complex((-55.5 - 0.0), (-0.0 + 0.0)) which then equals to complex(-55.5, 0.0).

Note that -0.0+0.0 equals to 0.0 and the sign rule only applies to multiplication and division as mentioned in this link and quoted in comments below. To better understand it, see this:

print(complex(-1.0, -0.0)*complex(55.5, 0.0))
#(-55.5-0j)

where the imaginary part is (-0.0*55.5 - 1.0*0.0) = (-0.0 - 0.0) = -0.0

Ehsan
  • 12,072
  • 2
  • 20
  • 33
  • BTW any idea why `-a` and `-1 * a` are not identically equal in the first place? – Julien Aug 24 '20 at 10:55
  • @Julien They explain similar situation in here: https://stackoverflow.com/questions/4083401/negative-zero-in-python – Ehsan Aug 24 '20 at 21:22
  • @Ehsan In the answer in your link it it says "When a multiplication or division involves a signed zero, the usual sign rules apply in computing the sign of the answer." So the question that Julian asked is not answered. I would expect that -1 * (0+0j) results in (-0-0j), so why is it (-0+0j)? – Wim van der Meer Aug 24 '20 at 23:08
  • @WimvanderMeer It exactly explains the issue you are addressing. Your misconception comes from the understanding of complex numbers. I will add an edit to explain it more. – Ehsan Aug 24 '20 at 23:34
  • @WimvanderMeer I tried my best to explain clearly what is going on in the edit on the post. Feel free to accept/upvote the answer and close the question if your question is resolved. Thank you. – Ehsan Aug 25 '20 at 00:00
  • 1
    Nice and sneaky. I naively thought real * complex would merely distribute like scalar * vector multiplication... As usual always question your assumptions. :) +1 – Julien Aug 25 '20 at 01:53
1

For 1) print -a and -1*a, you'll see they are different.

-a
Out[4]: (-55.74947517067784-0j)

-1*a
Out[5]: (-55.74947517067784+0j) # note +0j not -0j

Without knowing the details of the numpy implementation, the sign of the imaginary part is probably used to compute the angle... which could explain why this degenerate case gives different results.

For 2) this looks like a bug or a doco mistake to me then...

Julien
  • 13,986
  • 5
  • 29
  • 53
  • 1
    Not sure if 2) technically counts as a mistake in the docs. `pi = -3.141592653589793..... <-3.141592653589793` in a mathematical sense. So -3.141592653589793 is in fact in `(-pi, pi]`. – Emil Vatai Aug 24 '20 at 02:40
  • 1
    I think it's fair to assume that when the docs says `pi` they mean its numerical representation `np.pi` not its true transcendental value. This is the only thing any programmer will care about, and in that sense the doc would be wrong... At the very least it looks like a nasty wart. – Julien Aug 24 '20 at 02:53
  • Thanks for the comment. I modified the question to indicate that I mean -np.pi. – Wim van der Meer Aug 24 '20 at 02:55