Pourquoi (inf + 0j) * 1 est-il évalué à inf + nanj?

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

Pourquoi? Cela a causé un vilain bogue dans mon code.

Pourquoi 1l'identité multiplicative ne donne- (inf + 0j)t-elle pas?

Marnix
la source
1
Je pense que le mot clé que vous recherchez est " champ ". L'addition et la multiplication sont définies par défaut dans un seul champ, et dans ce cas, le seul champ standard qui peut accueillir votre code est le champ de nombres complexes, donc les deux nombres doivent être traités comme des nombres complexes par défaut avant que l'opération ne se passe bien. défini. Ce qui ne veut pas dire qu'ils ne pouvaient pas étendre ces définitions, mais apparemment, ils ont simplement opté pour la norme et n'ont pas ressenti le besoin de faire tout leur possible pour étendre les définitions.
user541686
1
Oh, et si vous trouvez ces idiosyncrasies frustrantes et que vous voulez frapper votre ordinateur, vous avez ma sympathie .
user541686
2
@Mehrdad une fois que vous ajoutez ces éléments non finis, il cesse d'être un champ. En effet, comme il n'y a plus de neutre multiplicatif, il ne peut par définition être un champ.
Paul Panzer
@PaulPanzer: Ouais, je pense qu'ils ont juste poussé ces éléments par la suite.
user541686
1
les nombres à virgule flottante (même si vous excluez l'infini et NaN) ne sont pas un champ. La plupart des identités des champs ne sont pas valables pour les nombres à virgule flottante.
plugwash

Réponses:

95

Le 1est d'abord converti en un nombre complexe 1 + 0j, ce qui conduit ensuite à une inf * 0multiplication, ce qui donne un 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
la source
8
Pour répondre à la question "pourquoi ...?", L'étape la plus importante est probablement la première, vers où 1est jeté 1 + 0j.
Warren Weckesser
5
Notez que C99 spécifie que les vrais types à virgule flottante ne sont pas promus en complexes lors de la multiplication par un type complexe (section 6.3.1.8 du projet de norme), et pour autant que je sache, il en va de même pour std :: complex de C ++. Cela peut être en partie pour des raisons de performances, mais cela évite également les NaN inutiles.
benrg le
@benrg Dans NumPy, array([inf+0j])*1évalue également à array([inf+nanj]). En supposant que la multiplication réelle se produit quelque part dans le code C / C ++, cela signifierait-il qu'ils ont écrit du code personnalisé pour émuler le comportement CPython, plutôt que d'utiliser _Complex ou std :: complex?
marnix
1
@marnix c'est plus impliqué que ça. numpya une classe centrale ufuncdont dérivent presque tous les opérateurs et fonctions. ufuncs'occupe de la diffusion de la gestion des enjambées tout cet administrateur délicat qui rend le travail avec des tableaux si pratique. Plus précisément, la répartition du travail entre un opérateur spécifique et la machine générale est que l'opérateur spécifique implémente un ensemble de "boucles les plus internes" pour chaque combinaison de types d'éléments d'entrée et de sortie qu'il souhaite gérer. La machinerie générale s'occupe de toutes les boucles extérieures et sélectionne la boucle la plus intérieure qui correspond le mieux ...
Paul Panzer
1
... la promotion de tous les types qui ne correspondent pas exactement au besoin. Nous pouvons accéder à la liste des boucles internes fournies via l' typesattribut pour np.multiplyce rendement, ['??->?', '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']nous pouvons voir qu'il n'y a presque pas de types mixtes, en particulier, aucun qui mélange float "efdg"avec complex "FDG".
Paul Panzer
32

D'un point de vue mécanique, la réponse acceptée est, bien sûr, correcte, mais je dirais qu'une réponse plus profonde peut être donnée.

Tout d'abord, il est utile de clarifier la question comme le fait @PeterCordes dans un commentaire: "Y a-t-il une identité multiplicative pour les nombres complexes qui fonctionne sur inf + 0j?" ou en d'autres termes, est-ce que OP voit une faiblesse dans la mise en œuvre informatique de la multiplication complexe ou y a-t-il quelque chose de conceptuellement incohérent avecinf+0j

Réponse courte:

En utilisant les coordonnées polaires, nous pouvons voir la multiplication complexe comme une mise à l'échelle et une rotation. En tournant un "bras" infini même de 0 degré comme dans le cas de la multiplication par un, nous ne pouvons pas nous attendre à placer sa pointe avec une précision finie. Donc, en effet, il y a quelque chose de fondamentalement faux avec inf+0j, à savoir que dès que nous sommes à l'infini, un décalage fini devient dénué de sens.

Longue réponse:

Contexte: La "grande chose" autour de laquelle cette question tourne est la question d'étendre un système de nombres (pensez aux nombres réels ou complexes). Une des raisons pour lesquelles on pourrait vouloir faire cela est d'ajouter un concept d'infini, ou de "compacter" si l'on se trouve être un mathématicien. Il y a aussi d'autres raisons ( https://en.wikipedia.org/wiki/Galois_theory , https://en.wikipedia.org/wiki/Non-standard_analysis ), mais celles-ci ne nous intéressent pas ici.

Compactification en un point

La difficulté à propos d'une telle extension est, bien sûr, que nous voulons que ces nouveaux nombres s'inscrivent dans l'arithmétique existante. Le moyen le plus simple est d'ajouter un seul élément à l'infini ( https://en.wikipedia.org/wiki/Alexandroff_extension ) et de le rendre égal à tout sauf à zéro divisé par zéro. Cela fonctionne pour les réels ( https://en.wikipedia.org/wiki/Projectively_extended_real_line ) et les nombres complexes ( https://en.wikipedia.org/wiki/Riemann_sphere ).

Autres extensions ...

Alors que la compactification en un point est simple et mathématiquement valable, des extensions "plus riches" comprenant de multiples infinties ont été recherchées. La norme IEEE 754 pour les nombres à virgule flottante réels a + inf et -inf ( https://en.wikipedia.org/wiki/Extended_real_number_line ). Cela semble naturel et simple, mais nous oblige déjà à sauter à travers des cerceaux et à inventer des trucs comme-0 https://en.wikipedia.org/wiki/Signed_zero

... du plan complexe

Qu'en est-il des extensions plus d'un inf du plan complexe?

Dans les ordinateurs, les nombres complexes sont généralement implémentés en collant deux réels fp ensemble, un pour le réel et un pour la partie imaginaire. C'est parfaitement bien tant que tout est fini. Mais dès que l'on considère l'infini, les choses deviennent délicates.

Le plan complexe a une symétrie de rotation naturelle, qui s'accorde bien avec l'arithmétique complexe, car multiplier le plan entier par e ^ phij équivaut à une rotation phi radian autour 0.

Cette chose annexe G

Maintenant, pour garder les choses simples, le complexe fp utilise simplement les extensions (+/- inf, nan, etc.) de l'implémentation de nombres réels sous-jacente. Ce choix peut sembler si naturel qu'il n'est même pas perçu comme un choix, mais regardons de plus près ce que cela implique. Une simple visualisation de cette extension du plan complexe ressemble à (I = infini, f = fini, 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

Mais comme un vrai plan complexe est celui qui respecte la multiplication complexe, une projection plus informative serait

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

Dans cette projection, nous voyons la "distribution inégale" des infinis qui est non seulement laide mais aussi la racine des problèmes du genre OP a souffert: la plupart des infinis (ceux des formes (+/- inf, finies) et (finies, + / -inf) sont regroupés dans les quatre directions principales toutes les autres directions sont représentées par seulement quatre infinis (+/- inf, + -inf). Il n'est pas surprenant que l'extension de la multiplication complexe à cette géométrie soit un cauchemar .

L'annexe G de la spécification C99 fait de son mieux pour le faire fonctionner, y compris en contournant les règles sur la façon dont infet naninteragir (essentiellement des infatouts nan). Le problème d'OP est contourné en ne faisant pas la promotion de réels et d'un type purement imaginaire proposé en complexe, mais le fait que le réel 1 se comporte différemment du complexe 1 ne me semble pas être une solution. Fait révélateur, l'annexe G ne précise pas complètement ce que devrait être le produit de deux infinis.

Pouvons-nous faire mieux?

Il est tentant d'essayer de résoudre ces problèmes en choisissant une meilleure géométrie d'infinis. Par analogie avec la ligne réelle étendue, nous pourrions ajouter une infinité pour chaque direction. Cette construction est similaire au plan projectif mais ne regroupe pas les directions opposées. Les infinis seraient représentés en coordonnées polaires inf xe ^ {2 omega pi i}, la définition des produits serait simple. En particulier, le problème d'OP serait résolu tout naturellement.

Mais c'est là que s'arrête la bonne nouvelle. D'une certaine manière, nous pouvons être renvoyés à la case départ en exigeant - pas sans raison - que nos infinis de style nouveau prennent en charge des fonctions qui extraient leurs parties réelles ou imaginaires. L'addition est un autre problème; en ajoutant deux infinis non antipodaux, nous devrons définir l'angle sur indéfini, c'est-à-dire nan(on pourrait dire que l'angle doit se trouver entre les deux angles d'entrée mais il n'y a pas de moyen simple de représenter cette "nanitude partielle")

Riemann à la rescousse

Au vu de tout cela, peut-être que la bonne vieille compactification en un point est la chose la plus sûre à faire. Peut-être que les auteurs de l'annexe G ont ressenti la même chose lorsqu'ils ont mandaté une fonction cprojqui regroupe tous les infinis ensemble.


Voici une question connexe à laquelle ont répondu des personnes plus compétentes que moi sur le sujet.

Paul Panzer
la source
5
Ouais, parce que nan != nan. Je comprends que cette réponse est à moitié plaisante, mais je ne vois pas pourquoi elle devrait être utile au PO tel qu'il est écrit.
cmaster - réintégrer monica le
Étant donné que le code dans le corps de la question n'utilisait pas réellement ==(et étant donné qu'ils ont accepté l'autre réponse), il semble que c'était juste un problème de la façon dont le PO a exprimé le titre. J'ai reformulé le titre pour corriger cette incohérence. (Invalider intentionnellement la première moitié de cette réponse parce que je suis d'accord avec @cmaster: ce n'est pas le sujet de cette question).
Peter Cordes
3
@PeterCordes ce serait dérangeant car en utilisant des coordonnées polaires, nous pouvons voir une multiplication complexe comme une mise à l'échelle et une rotation. En tournant un "bras" infini même de 0 degré comme dans le cas de la multiplication par un, nous ne pouvons pas nous attendre à placer sa pointe avec une précision finie. C'est à mon avis une explication plus profonde que celle acceptée, et aussi une avec des échos dans la règle nan! = Nan.
Paul Panzer
3
C99 spécifie que les vrais types à virgule flottante ne sont pas promus en complexes lors de la multiplication par un type complexe (section 6.3.1.8 du projet de norme), et pour autant que je sache, il en va de même pour std :: complex de C ++. Cela signifie que 1 est une identité multiplicative pour ces types dans ces langues. Python devrait faire de même. J'appellerais son comportement actuel simplement un bogue.
benrg le
2
@PaulPanzer: Je ne le fais pas, mais le concept de base serait qu'un zéro (que j'appellerai Z) maintiendrait toujours x + Z = x et x * Z = Z, et 1 / Z = NaN, un (positif infinitésimal) maintiendrait 1 / P = + INF, un (infinitésimal négatif) maintiendrait 1 / N = -INF, et (infinitésimal non signé) donnerait 1 / U = NaN. En général, xx serait U sauf si x est un vrai entier, auquel cas il donnerait Z.
supercat
6

Ceci est un détail d'implémentation de la façon dont la multiplication complexe est implémentée dans CPython. Contrairement à d'autres langages (par exemple C ou C ++), CPython adopte une approche quelque peu simpliste:

  1. les entiers / flottants sont promus en nombres complexes lors de la multiplication
  2. la formule école simple est utilisée , qui ne fournit pas les résultats souhaités / attendus dès que des nombres infinis sont impliqués:
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;
}

Un cas problématique avec le code ci-dessus serait:

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

Cependant, on aimerait avoir -inf + inf*jcomme résultat.

À cet égard, les autres langages ne sont pas loin: la multiplication de nombres complexes n'a pas été pendant longtemps une partie de la norme C, incluse uniquement dans C99 en annexe G, qui décrit comment une multiplication complexe doit être effectuée - et ce n'est pas aussi simple que la formule école ci-dessus! Le standard C ++ ne spécifie pas comment la multiplication complexe devrait fonctionner, donc la plupart des implémentations du compilateur reviennent à l'implémentation C, qui peut être conforme à C99 (gcc, clang) ou non (MSVC).

Pour l'exemple "problématique" ci-dessus, les implémentations conformes à C99 (qui sont plus compliquées que la formule scolaire) donneraient ( voir en direct ) le résultat attendu:

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

Même avec la norme C99, un résultat sans ambiguïté n'est pas défini pour toutes les entrées et il peut être différent même pour les versions compatibles C99.

Un autre effet secondaire de floatne pas être promu complexen C99 est que la multiplication inf+0.0javec 1.0ou 1.0+0.0jpeut conduire à des résultats différents (voir ici en direct):

  • (inf+0.0j)*1.0 = inf+0.0j
  • (inf+0.0j)*(1.0+0.0j) = inf-nanj, la partie imaginaire étant -nanet non nan(comme pour CPython) ne joue pas de rôle ici, car tous les nans tranquilles sont équivalents (voir ceci ), même certains d'entre eux ont un bit de signe défini (et donc imprimé comme "-", voir ceci ) et d'autres non.

Ce qui est au moins contre-intuitif.


Ce que j'en retire, c'est qu'il n'y a rien de simple dans la multiplication (ou division) de nombres complexes "simples" et lors du passage d'un langage à l'autre ou même d'un compilateur, il faut se préparer à des bogues / différences subtiles.

ead
la source
Je sais qu'il existe de nombreux modèles de bits nanométriques. Je ne connaissais pas le truc du signe, cependant. Mais je voulais dire sémantiquement En quoi -nan est-il différent de nan? Ou devrais-je dire plus différent que nan est de nan?
Paul Panzer
@PaulPanzer Ceci est juste un détail d'implémentation de la façon dont fonctionne printfet similaire avec double: ils regardent le bit de signe pour décider si "-" doit être imprimé ou non (peu importe si c'est nan ou non). Donc vous avez raison, il n'y a pas de différence significative entre "nan" et "-nan", corrigeant bientôt cette partie de la réponse.
ead le
Ah bien. Était inquiétant pendant un mois que tout ce que je pensais savoir sur fp n'était pas vraiment correct ...
Paul Panzer
Désolé d'être ennuyeux mais êtes-vous sûr que "il n'y a pas de 1.0 imaginaire, c'est-à-dire 1.0j qui n'est pas la même chose que 0.0 + 1.0j en ce qui concerne la multiplication". est correct? Cette annexe G semble spécifier un type purement imaginaire (G.2) et aussi prescrire comment il devrait être multiplié, etc. (G.5.1)
Paul Panzer
@PaulPanzer Non, merci d'avoir signalé les problèmes! En tant que codeur c ++, je vois principalement le standard C99 à C ++ - glases - cela m'a échappé, que C est une longueur d'avance ici - vous avez évidemment raison, encore une fois.
ead le
3

Drôle de définition de Python. Si nous réglons cela avec un stylo et du papier , je dirais que ce résultat attendu serait expected: (inf + 0j)comme vous l' avez dit , parce que nous savons que nous entendons la norme de 1façon(float('inf')+0j)*1 =should= ('inf'+0j) :

Mais ce n'est pas le cas comme vous pouvez le voir ... lorsque nous l'exécutons, nous obtenons:

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

Python comprend cela *1comme un nombre complexe et non comme la norme de 1donc il interprète comme *(1+0j)et l'erreur apparaît lorsque nous essayons de faire inf * 0j = nanjcomme ne inf*0peut pas être résolu.

Ce que vous voulez réellement faire (en supposant que 1 est la norme de 1):

Rappelons que si z = x + iyest un nombre complexe avec une partie réelle x et une partie imaginaire y, le conjugué complexe de zest défini comme z* = x − iy, et la valeur absolue, également appelée le, norm of zest définie comme:

entrez la description de l'image ici

En supposant que 1la norme 1est de faire quelque chose comme:

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

pas très intuitif je sais ... mais parfois les langages de codage sont définis d'une manière différente de ce que nous utilisons dans notre vie quotidienne.

costargc
la source