97
>>> (float('inf')+0j)*1
(inf+nanj)

Why? This caused a nasty bug in my code.

Why isn't 1 the multiplicative identity, giving (inf + 0j)?

Francisco
  • 10,918
  • 6
  • 34
  • 45
marnix
  • 1,210
  • 8
  • 12
  • 1
    I think the keyword you're looking for is "[**field**](https://en.wikipedia.org/wiki/Field_(mathematics))". Addition and multiplication are by default defined within a single field, and in this case the only standard field that can accommodate your code is the field of complex numbers, so both numbers need to be treated as complex numbers by default before the operation is well-defined. Which is not to say they *couldn't* extend these definitions, but apparently they just went with the standard thing and didn't feel an urge to go out of their way to extend the definitions. – user541686 Sep 22 '19 at 08:33
  • 1
    Oh, and if you find these idiosyncrasies frustrating and want to punch your computer, [you have my sympathy](/q/19371340/541686). – user541686 Sep 22 '19 at 08:36
  • 2
    @Mehrdad once you add those non finite elements it ceases to be a field. Indeed, as there isn't a multiplicative neutral anymore it cannot by definition be a field. – Paul Panzer Sep 22 '19 at 09:37
  • @PaulPanzer: Yeah I think they just shoved those elements in afterward. – user541686 Sep 22 '19 at 10:03
  • 1
    floating point numbers (even if you exclude infinity and NaN) are not a field. Most of the identities that hold for fields do not hold for floating point numbers. – plugwash Sep 23 '19 at 14:54
  • @plugwash: they are obviously intended as approximations to such. – user541686 Sep 23 '19 at 19:01

4 Answers4

95

The 1 is converted to a complex number first, 1 + 0j, which then leads to an inf * 0 multiplication, resulting in a nan.

(inf + 0j) * 1
(inf + 0j) * (1 + 0j)
inf * 1  + inf * 0j  + 0j * 1 + 0j * 0j
#          ^ this is where it comes from
inf  + nan j  + 0j - 0
inf  + nan j
Marat
  • 15,215
  • 2
  • 39
  • 48
  • 9
    For answering the question "why...?", probably the important most step is the first one, where `1` is cast to `1 + 0j`. – Warren Weckesser Sep 20 '19 at 16:28
  • 5
    Note that C99 specifies that real floating-point types are *not* promoted to complex when multiplying by a complex type (section 6.3.1.8 of the draft standard), and as far as I know the same is true of C++'s std::complex. This may partly be for performance reasons but it also avoids unnecessary NaNs. – benrg Sep 21 '19 at 19:31
  • @benrg In NumPy, `array([inf+0j])*1` also evaluates to `array([inf+nanj])`. Assuming that the actual multiplication happens somewhere in C/C++ code, would this mean that they wrote custom code to emulate the CPython behavior, rather than using _Complex or std::complex? – marnix Sep 22 '19 at 09:22
  • 1
    @marnix it is more involved than that. `numpy` has one central class `ufunc` from which almost every operator and function derives. `ufunc` takes care of broadcasting managing strides all that tricky admin that makes working with arrays so convenient. More precisely the split of labor between a specific operator and the general machinery is that the specific operator implements a set of "innermost loops" for each combination of input and output element types it wants to handle. The general machinery takes care of any outer loops and selects the best match innermost loop ... – Paul Panzer Sep 22 '19 at 09:55
  • 1
    ...promoting any not exactly matching types as required. We can access the list of provided inner loops via the `types` attribute for `np.multiply` this yields `['??->?', 'bb->b', 'BB->B', 'hh->h', 'HH->H', 'ii->i', 'II->I', 'll->l', 'LL->L', 'qq->q', 'QQ->Q', 'ee->e', 'ff->f', 'dd->d', 'gg->g', 'FF->F', 'DD->D', 'GG->G', 'mq->m', 'qm->m', 'md->m', 'dm->m', 'OO->O']` we can see that there are almost no mixed types, in particular, none that mix float `"efdg"` with complex `"FDG"`. – Paul Panzer Sep 22 '19 at 10:02
  • @PaulPanzer OK, I see, the conversion is imposed externally. Thanks – marnix Sep 23 '19 at 07:53
  • Python don't have a very clean way to handle `norm of a value` in this case `1` when doing complex operations... best way is to split the num in real and imag parts using `num.real` and `num.imag` multiplying each by `norm of the value` in this case is `1` and rebuild it as a complex number with `complex(realPart,imagPart)` ... I've added this as an answers for you to see. – costargc Oct 04 '19 at 18:38
32

Mechanistically, the accepted answer is, of course, correct, but I would argue that a deeper ansswer can be given.

First, it is useful to clarify the question as @PeterCordes does in a comment: "Is there a multiplicative identity for complex numbers that does work on inf + 0j?" or in other words is what OP sees a weakness in the computer implementation of complex multiplication or is there something conceptually unsound with inf+0j

Short answer:

Using polar coordinates we can view complex multiplication as a scaling and a rotation. Rotating an infinite "arm" even by 0 degrees as in the case of multiplying by one we cannot expect to place its tip with finite precision. So indeed, there is something fundamentally not right with inf+0j, namely, that as soon as we are at infinity a finite offset becomes meaningless.

Long answer:

Background: The "big thing" around which this question revolves is the matter of extending a system of numbers (think reals or complex numbers). One reason one might want to do that is to add some concept of infinity, or to "compactify" if one happens to be a mathematician. There are other reasons, too (https://en.wikipedia.org/wiki/Galois_theory, https://en.wikipedia.org/wiki/Non-standard_analysis), but we are not interested in those here.

One point compactification

The tricky bit about such an extension is, of course, that we want these new numbers to fit into the existing arithmetic. The simplest way is to add a single element at infinity (https://en.wikipedia.org/wiki/Alexandroff_extension) and make it equal anything but zero divided by zero. This works for the reals (https://en.wikipedia.org/wiki/Projectively_extended_real_line) and the complex numbers (https://en.wikipedia.org/wiki/Riemann_sphere).

Other extensions ...

While the one point compactification is simple and mathematically sound, "richer" extensions comprising multiple infinties have been sought. The IEEE 754 standard for real floating point numbers has +inf and -inf (https://en.wikipedia.org/wiki/Extended_real_number_line). Looks natural and straightforward but already forces us to jump through hoops and invent stuff like -0 https://en.wikipedia.org/wiki/Signed_zero

... of the complex plane

What about more-than-one-inf extensions of the complex plane?

In computers, complex numbers are typically implemented by sticking two fp reals together one for the real and one for the imaginary part. That is perfectly fine as long as everything is finite. As soon, however, as infinities are considered things become tricky.

The complex plane has a natural rotational symmetry, which ties in nicely with complex arithmetic as multiplying the entire plane by e^phij is the same as a phi radian rotation around 0.

That annex G thing

Now, to keep things simple, complex fp simply uses the extensions (+/-inf, nan etc.) of the underlying real number implementation. This choice may seem so natural it isn't even perceived as a choice, but let's take a closer look at what it implies. A simple visualization of this extension of the complex plane looks like (I = infinite, f = finite, 0 = 0)

I IIIIIIIII I
             
I fffffffff I
I fffffffff I
I fffffffff I
I fffffffff I
I ffff0ffff I
I fffffffff I
I fffffffff I
I fffffffff I
I fffffffff I
             
I IIIIIIIII I

But since a true complex plane is one that respects complex multiplication, a more informative projection would be

     III    
 I         I  
    fffff    
   fffffff   
  fffffffff  
I fffffffff I
I ffff0ffff I
I fffffffff I
  fffffffff  
   fffffff   
    fffff    
 I         I 
     III    

In this projection we see the "uneven distribution" of infinities that is not only ugly but also the root of problems of the kind OP has suffered: Most infinities (those of the forms (+/-inf, finite) and (finite, +/-inf) are lumped together at the four principal directions all other directions are represented by just four infinities (+/-inf, +-inf). It shouldn't come as a surprise that extending complex multiplication to this geometry is a nightmare.

Annex G of the C99 spec tries its best to make it work, including bending the rules on how inf and nan interact (essentially inf trumps nan). OP's problem is sidestepped by not promoting reals and a proposed purely imaginary type to complex, but having the real 1 behave differently from the complex 1 doesn't strike me as a solution. Tellingly, Annex G stops short of fully specifying what the product of two infinities should be.

Can we do better?

It is tempting to try and fix these problems by choosing a better geometry of infinities. In analogy to the extended real line we could add one infinity for each direction. This construction is similar to the projective plane but doesn't lump together opposite directions. Infinities would be represented in polar coordinates inf x e^{2 omega pi i}, defining products would be straightforward. In particular, OP's problem would be solved quite naturally.

But this is where the good news ends. In a way we can be hurled back to square one by---not unreasonably---requiring that our newstyle infinities support functions that extract their real or imaginary parts. Addition is another problem; adding two nonantipodal infinities we'd have to set the angle to undefined i.e. nan (one could argue the angle must lie between the two input angles but there is no simple way of representing that "partial nan-ness")

Riemann to the rescue

In view of all this maybe the good old one point compactification is the safest thing to do. Maybe the authors of Annex G felt the same when mandating a function cproj that lumps all the infinities together.


Here is a related question answered by people more competent on the subject matter than I am.

Community
  • 1
  • 1
Paul Panzer
  • 51,835
  • 3
  • 54
  • 99
  • 5
    Yeah, because `nan != nan`. I understand that this answer is half-joking, but I fail to see why it should be helpful to the OP the way it's written. – cmaster - reinstate monica Sep 21 '19 at 00:37
  • Given that the code in the question body wasn't actually using `==` (and given they accepted the other answer), it seems it was just a problem of how the OP expressed the title. I reworded the title to fix that inconsistency. (Intentionally invaliding the first half of this answer because I agree with @cmaster: that isn't what this question was asking about). – Peter Cordes Sep 21 '19 at 03:02
  • Is there a multiplicative identity for complex numbers that does work on `inf + 0j`? – Peter Cordes Sep 21 '19 at 03:06
  • 3
    @PeterCordes that would be troubling because using polar coordinates we can view complex multiplication as a scaling and a rotation. Rotating an infinite "arm" even by 0 degrees as in the case of multiplying by one we cannot expect to place its tip with finite precision. This is in my opinion a deeper explanation than the accepted one, and also one with echos in the nan != nan rule. – Paul Panzer Sep 21 '19 at 03:22
  • @cmaster It was all there all the time between the lines ;-D – Paul Panzer Sep 21 '19 at 10:46
  • @PaulPanzer: I wonder how much it would have cost to make IEEE-754 include four kinds of zeroes, as well as a range of "true integers"? The four zeroes would be integer zero, positive infinitesimal, negative infinitesimal, and unsigned infinitesimal. Corner cases involving those would be symmetric, and would be cleaner than those using IEEE-754 zeroes. – supercat Sep 21 '19 at 19:26
  • 3
    C99 specifies that real floating-point types are not promoted to complex when multiplying by a complex type (section 6.3.1.8 of the draft standard), and as far as I know the same is true of C++'s std::complex. This means that 1 is a multiplicative identity for those types in those languages. Python should do the same. I would call its current behavior simply a bug. – benrg Sep 21 '19 at 19:28
  • @supercat Would you have a link where this idea is spelt out in detail? I must admit I find it difficult to keep track of all the permutations without aid. – Paul Panzer Sep 21 '19 at 19:47
  • @benrg Just so I understand correctly, does this mean 1 + 0j doesn't exist or does it mean it is distinct from 1? Or do they both exist and are equal? Doesn't this invite a whole lot of other awkwardness? What's the sum or product of complex conjugates? Is it real real or complex real? – Paul Panzer Sep 21 '19 at 20:30
  • @benrg Btw. my confusion notwithstanding, I believe that your point is relevant to the question and should be more visible. Maybe make it an aswer? – Paul Panzer Sep 21 '19 at 20:48
  • 2
    @PaulPanzer: I don't, but the basic concept would be that one zero (which I'll call Z) would always uphold x+Z=x and x*Z=Z, and 1/Z=NaN, one (positive infinitesimal) would uphold 1/P=+INF, one (negative infinitesimal) would uphold 1/N=-INF, and (unsigned infinitesimal) would yield 1/U=NaN. In general, x-x would be U unless x is a true integer, in which case it would yield Z. – supercat Sep 21 '19 at 20:48
  • @supercat thanks! Sounds interesting, though it's probably impossible to fully assess its merits without actually field testing it. – Paul Panzer Sep 21 '19 at 20:56
  • @supercat Are you also distinguishing true integers from integral floats then? (Is this a python thing, or are you proposing to extend IEEE-like float representation with a "true integer" subset?) In either case, it seems you essentially get a disjoint union "int + float" where Z lies in the "int" part, so it's only one new element U relative to IEEE floats. – Mario Carneiro Sep 21 '19 at 21:31
  • @MarioCarneiro: The idea would be that a special exponent bit pattern would distinguish integers. I'm not sure how much that or the four-zeroes model would cost to implement, but they'd make many things semantically cleaner. My guess would be that the cost of adding the two additional kinds of zeroes would have been slight, but integers would have cost more. On the other hand, for software implementations (which were a major consideration when IEEE-754 was designed), being able to process integers without having to bit-shift and normalizing would have been useful. – supercat Sep 21 '19 at 21:35
  • @supercat Why stop at integers? What about rationals? Algebraic numbers? Hell, I mean even some transcendentals. Basically anything that may occur in an expression that evaluates to something exact? I'm obviously only half serious, and half testing whether I understand correctly your design, but is there a principal reason to draw the line at integers? – Paul Panzer Sep 22 '19 at 00:31
  • @PaulPanzer: When using software emulation, having a recognizable range of integers that could be processed directly, rather than having to shift before computations and normalize after, would improve performance. From a semantic perspective, I think having a recognized value for the multiplicative identity, and having ONE-ONE yield ZERO would be the most relevant case, though even if ONE-ONE yielded unsigned infinitesimal that wouldn't be terrible. The main problem with IEEE-754's zeroes is that too many things get lumped together as "positive zero". – supercat Sep 22 '19 at 14:06
  • "Rotating an infinite "arm" even by 0 degrees as in the case of multiplying by one we cannot expect to place its tip with finite precision" Why not? (0, inf) rotated 0 degrees - i.e., having nothing done to it, would intuitively fall on (0, inf), right? 90 degrees would swap x and y, 180 degrees would negate x and y, and 270 would swap and negate x and y. Is there something I'm conceptually misunderstanding? – Adam Barnes Sep 22 '19 at 21:32
  • @AdamBarnes (Second attempt, sorry I was a bit tired earlier) But are these really rotations?Is it meaningful to call (0,inf) 90 degrees when in this coordinate system 89 degrees literally doesn't exist? And as soon as we reparameterize to make the full circle available using for example polar coordinates we will lose the ability to resolve finite displacements. This carries over from 1D (inf + 1 = inf) to more dims if these dims are isotropic which they must be to have a consistent concept of rotations. – Paul Panzer Sep 23 '19 at 01:58
6

This is an implementation detail of how complex multiplication is implemented in CPython. Unlike other languages (e.g. C or C++), CPython takes a somewhat simplistic approach:

  1. ints/floats are promoted to complex numbers in multiplication
  2. the simple school-formula is used, which doesn't provide desired/expected results as soon as infinite numbers are involved:
Py_complex
_Py_c_prod(Py_complex a, Py_complex b)
{
    Py_complex r;
    r.real = a.real*b.real - a.imag*b.imag;
    r.imag = a.real*b.imag + a.imag*b.real;
    return r;
}

One problematic case with the above code would be:

(0.0+1.0*j)*(inf+inf*j) = (0.0*inf-1*inf)+(0.0*inf+1.0*inf)j
                        =  nan + nan*j

However, one would like to have -inf + inf*j as result.

In this respect other languages are not far ahead: complex number multiplication was for long a time not part of the C standard, included only in C99 as appendix G, which describes how a complex multiplication should be performed - and it is not as simple as the school formula above! The C++ standard doesn't specify how complex multiplication should work, thus most compiler implementations are falling back to C-implementation, which might be C99 conforming (gcc, clang) or not (MSVC).

For the above "problematic" example, C99-compliant implementations (which are more complicated than the school formula) would give (see live) the expected result:

(0.0+1.0*j)*(inf+inf*j) = -inf + inf*j 

Even with C99 standard, an unambiguous result is not defined for all inputs and it might be different even for C99-compliant versions.

Another side effect of float not being promoted to complex in C99 is that multiplyinginf+0.0j with 1.0 or 1.0+0.0j can lead to different results (see here live):

  • (inf+0.0j)*1.0 = inf+0.0j
  • (inf+0.0j)*(1.0+0.0j) = inf-nanj, imaginary part being -nan and not nan (as for CPython) doesn't play a role here, because all quiet nans are equivalent (see this), even some of them have sign-bit set (and thus printed as "-", see this) and some not.

Which is at least counter-intuitive.


My key take-away from it is: there is nothing simple about "simple" complex number multiplication (or division) and when switching between languages or even compilers one must brace oneself for subtle bugs/differences.

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
ead
  • 32,758
  • 6
  • 90
  • 153
  • I know there are lots of nan bit patterns. Didn't know the sign bit thing, though. But I meant semantically How is -nan different from nan? Or should I say more different than nan is from nan? – Paul Panzer Sep 22 '19 at 08:29
  • @PaulPanzer This is just an implementation detail of how `printf` and similar works with double: they look at the sign-bit to decide whether "-" should be printed or not (no matter whether it is nan or not). So you are right, there is no meaningful difference between "nan" and "-nan", fixing this part of the answer soon. – ead Sep 22 '19 at 08:55
  • Ah, good. Was worrying for a mo that everything I thought I knew about fp was not actually correct... – Paul Panzer Sep 22 '19 at 08:58
  • Sorry for being annoying but are you sure this _"there is no imaginary 1.0, i.e. 1.0j which is not the same as 0.0+1.0j with respect to multiplication."_ is correct? That annex G seems to specify a purely imaginary type (G.2) and also to prescribe how it should be multiplied etc. (G.5.1) – Paul Panzer Sep 22 '19 at 09:14
  • @PaulPanzer No, thank you for pointing out the issues! As c++-coder, I mostly see C99-standard through C++-glases - it slipped my mind, that C is a step ahead here - you are right obviously, once again. – ead Sep 22 '19 at 09:39
  • @PaulPanzer Now I remember, what the issue with `_Imaginary` was: neither gcc nor clang support it, see for example "GCC does not support the Annex G imaginary types, but this support is optional.." https://gcc.gnu.org/c99status.html I'm not aware of any compiler which does. – ead Sep 22 '19 at 14:35
3

Funny definition from Python. If we are solving this with a pen and paper I would say that expected result would be expected: (inf + 0j) as you pointed out because we know that we mean the norm of 1 so (float('inf')+0j)*1 =should= ('inf'+0j):

But that is not the case as you can see... when we run it we get:

>>> Complex( float('inf') , 0j ) * 1
result: (inf + nanj)

Python understands this *1 as a complex number and not the norm of 1 so it interprets as *(1+0j) and the error appears when we try to do inf * 0j = nanj as inf*0 can't be resolved.

What you actually want to do (assuming 1 is the norm of 1):

Recall that if z = x + iy is a complex number with real part x and imaginary part y, the complex conjugate of z is defined as z* = x − iy, and the absolute value, also called the norm of z is defined as:

enter image description here

Assuming 1 is the norm of 1 we should do something like:

>>> c_num = complex(float('inf'),0)
>>> value = 1
>>> realPart=(c_num.real)*value
>>> imagPart=(c_num.imag)*value
>>> complex(realPart,imagPart)
result: (inf+0j)

not very intuitive I know... but sometimes coding languages get defined in a different way from what we are used in our day to day.

costargc
  • 516
  • 5
  • 14