¿Por qué (inf + 0j) * 1 se evalúa como inf + nanj?

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

¿Por qué? Esto causó un error desagradable en mi código.

¿Por qué 1la identidad multiplicativa no es dar (inf + 0j)?

marnix
fuente
1
Creo que la palabra clave que está buscando es " campo ". La suma y la multiplicación se definen de forma predeterminada dentro de un solo campo, y en este caso, el único campo estándar que puede acomodar su código es el campo de números complejos, por lo que ambos números deben tratarse como números complejos por defecto antes de que la operación esté bien. definido. Lo que no quiere decir que no pudieran extender estas definiciones, pero aparentemente simplemente se apegaron a lo estándar y no sintieron la necesidad de hacer todo lo posible para extender las definiciones.
user541686
1
Ah, y si encuentra estas idiosincrasias frustrantes y quiere golpear su computadora, tiene mi simpatía .
user541686
2
@Mehrdad una vez que agregas esos elementos no finitos deja de ser un campo. De hecho, como ya no existe un neutral multiplicativo, por definición no puede ser un campo.
Paul Panzer
@PaulPanzer: Sí, creo que después introdujeron esos elementos.
user541686
1
los números de punto flotante (incluso si excluye infinito y NaN) no son un campo. La mayoría de las identidades que se aplican a los campos no se aplican a los números de coma flotante.
Plugwash

Respuestas:

95

El 1se convierte en un número complejo primero, 1 + 0j, que entonces lleva a una inf * 0multiplicación, lo que resulta en 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
fuente
8
Para responder a la pregunta "¿por qué ...?", Probablemente el paso más importante es el primero, hacia dónde 1se lanza 1 + 0j.
Warren Weckesser
5
Tenga en cuenta que C99 especifica que los tipos de punto flotante reales no se promueven a complejos cuando se multiplican por un tipo complejo (sección 6.3.1.8 del borrador del estándar), y hasta donde yo sé, lo mismo ocurre con std :: complex de C ++. Esto puede deberse en parte a motivos de rendimiento, pero también evita NaN innecesarios.
benrg
@benrg En NumPy, array([inf+0j])*1también se evalúa como array([inf+nanj]). Suponiendo que la multiplicación real ocurre en algún lugar del código C / C ++, ¿significaría esto que escribieron código personalizado para emular el comportamiento de CPython, en lugar de usar _Complex o std :: complex?
marnix
1
@marnix es más complicado que eso. numpytiene una clase central ufuncde la que derivan casi todos los operadores y funciones. ufuncse encarga de la transmisión de la gestión de todos los pasos de ese administrador complicado que hace que trabajar con matrices sea tan conveniente. Más precisamente, la división del trabajo entre un operador específico y la maquinaria general es que el operador específico implementa un conjunto de "bucles más internos" para cada combinación de tipos de elementos de entrada y salida que desea manejar. La maquinaria general se encarga de cualquier bucle externo y selecciona el bucle más interno que mejor coincida ...
Paul Panzer
1
... promocionando cualquier tipo que no coincida exactamente como se requiere. Podemos acceder a la lista de bucles internos proporcionados a través del typesatributo para np.multiplyeste rendimiento ['??->?', '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']. Podemos ver que casi no hay tipos mixtos, en particular, ninguno que mezcle float "efdg"con complex "FDG".
Paul Panzer
32

Mecánicamente, la respuesta aceptada es, por supuesto, correcta, pero yo diría que se puede dar una respuesta más profunda.

Primero, es útil aclarar la pregunta como lo hace @PeterCordes en un comentario: "¿Existe una identidad multiplicativa para números complejos que funcione en inf + 0j?"o, en otras palabras, es lo que OP ve una debilidad en la implementación informática de la multiplicación compleja o hay algo conceptualmente incorrecto coninf+0j

Respuesta corta:

Usando coordenadas polares podemos ver la multiplicación compleja como una escala y una rotación. Al girar un "brazo" infinito incluso en 0 grados, como en el caso de multiplicar por uno, no podemos esperar colocar su punta con precisión finita. Entonces, de hecho, hay algo fundamentalmente que no está bien con inf+0j, a saber, que tan pronto como estamos en el infinito, un desplazamiento finito deja de tener sentido.

Respuesta larga:

Antecedentes: El "gran elemento" en torno al cual gira esta pregunta es la cuestión de extender un sistema de números (piense en números reales o complejos). Una de las razones por las que uno podría querer hacer eso es agregar algún concepto de infinito, o "compactar" si uno es matemático. También hay otras razones ( https://en.wikipedia.org/wiki/Galois_theory , https://en.wikipedia.org/wiki/Non-standard_analysis ), pero aquí no nos interesan.

Compactación de un punto

Lo complicado de esta extensión es, por supuesto, que queremos que estos nuevos números encajen en la aritmética existente. La forma más sencilla es agregar un solo elemento en el infinito ( https://en.wikipedia.org/wiki/Alexandroff_extension ) y hacer que sea igual a todo menos cero dividido por cero. Esto funciona para los reales ( https://en.wikipedia.org/wiki/Projectively_extended_real_line ) y los números complejos ( https://en.wikipedia.org/wiki/Riemann_sphere ).

Otras extensiones ...

Si bien la compactación de un punto es simple y matemáticamente sólida, se han buscado extensiones "más ricas" que comprendan múltiples infintos. El estándar IEEE 754 para números de coma flotante reales tiene + inf y -inf ( https://en.wikipedia.org/wiki/Extended_real_number_line ). Parece natural y sencillo, pero ya nos obliga a saltar e inventar cosas como -0 https://en.wikipedia.org/wiki/Signed_zero

... del plano complejo

¿Qué pasa con las extensiones de más de una inf del plano complejo?

En las computadoras, los números complejos generalmente se implementan uniendo dos fp reales, uno para el real y otro para la parte imaginaria. Eso está perfectamente bien siempre que todo sea finito. Sin embargo, tan pronto como se consideran infinitos, las cosas se vuelven complicadas.

El plano complejo tiene una simetría rotacional natural, que se relaciona muy bien con la aritmética compleja, ya que multiplicar el plano completo por e ^ phij es lo mismo que una rotación phi radianes alrededor 0.

Esa cosa del anexo G

Ahora, para mantener las cosas simples, fp complejo simplemente usa las extensiones (+/- inf, nan, etc.) de la implementación del número real subyacente. Esta elección puede parecer tan natural que ni siquiera se percibe como una elección, pero echemos un vistazo más de cerca a lo que implica. Una visualización simple de esta extensión del plano complejo se ve así (I = infinito, f = finito, 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

Pero dado que un verdadero plano complejo es aquel que respeta la multiplicación compleja, una proyección más informativa sería

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

En esta proyección vemos la "distribución desigual" de infinitos que no solo es fea sino también la raíz de problemas del tipo que ha sufrido OP: La mayoría de los infinitos (los de las formas (+/- inf, finito) y (finito, + / -inf) se agrupan en las cuatro direcciones principales, todas las demás direcciones están representadas por solo cuatro infinitos (+/- inf, + -inf). No debería sorprender que extender la multiplicación compleja a esta geometría sea una pesadilla .

El anexo G de la especificación C99 hace todo lo posible para que funcione, incluida la flexión de las reglas sobre cómo infe naninteractuar (esencialmente inftriunfa nan). El problema de OP se evita al no promover los reales y un tipo puramente imaginario propuesto a complejo, pero hacer que el 1 real se comporte de manera diferente al complejo 1 no me parece una solución. Es revelador que el Anexo G no llegue a especificar completamente cuál debería ser el producto de dos infinitos.

¿Podemos hacerlo mejor?

Es tentador intentar solucionar estos problemas eligiendo una mejor geometría de infinitos. En analogía con la línea real extendida, podríamos agregar un infinito para cada dirección. Esta construcción es similar al plano proyectivo, pero no agrupa direcciones opuestas. Los infinitos se representarían en coordenadas polares inf xe ^ {2 omega pi i}, la definición de productos sería sencilla. En particular, el problema de OP se resolvería de forma bastante natural.

Pero aquí es donde terminan las buenas noticias. En cierto modo, podemos ser arrojados de nuevo al punto de partida, no sin razón, requiriendo que nuestros infinitos de estilo nuevo admitan funciones que extraigan sus partes reales o imaginarias. La adición es otro problema; agregando dos infinitos no antipodales, tendríamos que establecer el ángulo en indefinido, es decir nan(se podría argumentar que el ángulo debe estar entre los dos ángulos de entrada, pero no hay una forma simple de representar esa "nan-nidad parcial")

Riemann al rescate

En vista de todo esto, tal vez la compactación de un solo punto sea lo más seguro. Quizás los autores del Anexo G sintieron lo mismo al imponer una función cprojque agrupa todos los infinitos.


Aquí hay una pregunta relacionada respondida por personas más competentes en el tema que yo.

Paul Panzer
fuente
5
Sí, porque nan != nan. Entiendo que esta respuesta es medio en broma, pero no veo por qué debería ser útil para el OP de la forma en que está escrito.
cmaster - reinstalar a monica
Dado que el código en el cuerpo de la pregunta no se estaba usando en realidad ==(y dado que aceptaron la otra respuesta), parece que fue solo un problema de cómo el OP expresó el título. Reformulé el título para corregir esa inconsistencia. (Invalidar intencionalmente la primera mitad de esta respuesta porque estoy de acuerdo con @cmaster: eso no es lo que preguntaba esta pregunta).
Peter Cordes
3
@PeterCordes eso sería preocupante porque usando coordenadas polares podemos ver la multiplicación compleja como una escala y una rotación. Al girar un "brazo" infinito incluso en 0 grados, como en el caso de multiplicar por uno, no podemos esperar colocar su punta con precisión finita. En mi opinión, esta es una explicación más profunda que la aceptada, y también con ecos en la regla nan! = Nan.
Paul Panzer
3
C99 especifica que los tipos de punto flotante reales no se promueven a complejos cuando se multiplican por un tipo complejo (sección 6.3.1.8 del borrador del estándar), y hasta donde yo sé, lo mismo ocurre con el std :: complex de C ++. Esto significa que 1 es una identidad multiplicativa para esos tipos en esos idiomas. Python debería hacer lo mismo. Yo llamaría a su comportamiento actual simplemente un error.
benrg
2
@PaulPanzer: No lo sé, pero el concepto básico sería que un cero (que llamaré Z) siempre mantendría x + Z = x y x * Z = Z, y 1 / Z = NaN, uno (positivo infinitesimal) mantendría 1 / P = + INF, uno (infinitesimal negativo) mantendría 1 / N = -INF, y (infinitesimal sin signo) produciría 1 / U = NaN. En general, xx sería U a menos que x sea un entero verdadero, en cuyo caso daría Z.
supercat
6

Este es un detalle de implementación de cómo se implementa la multiplicación compleja en CPython. A diferencia de otros lenguajes (por ejemplo, C o C ++), CPython adopta un enfoque algo simplista:

  1. los ints / floats se promueven a números complejos en la multiplicación
  2. Se usa la fórmula escolar simple , que no proporciona los resultados deseados / esperados tan pronto como se involucran números infinitos:
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 caso problemático con el código anterior sería:

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

Sin embargo, a uno le gustaría tener -inf + inf*jcomo resultado.

En este sentido, otros lenguajes no están muy por delante: la multiplicación de números complejos no fue durante mucho tiempo parte del estándar C, incluido solo en C99 como apéndice G, que describe cómo se debe realizar una multiplicación compleja, y no es tan simple como la fórmula de la escuela anterior! El estándar C ++ no especifica cómo debería funcionar la multiplicación compleja, por lo que la mayoría de las implementaciones de compiladores están recurriendo a la implementación de C, que puede ser conforme a C99 (gcc, clang) o no (MSVC).

Para el ejemplo "problemático" anterior, las implementaciones compatibles con C99 (que son más complicadas que la fórmula de la escuela) darían ( ver en vivo ) el resultado esperado:

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

Incluso con el estándar C99, no se define un resultado inequívoco para todas las entradas y podría ser diferente incluso para las versiones compatibles con C99.

Otro efecto secundario de floatno ser promocionado complexen C99 es que multiplicar inf+0.0jcon 1.0o 1.0+0.0jpuede conducir a resultados diferentes (ver aquí en vivo):

  • (inf+0.0j)*1.0 = inf+0.0j
  • (inf+0.0j)*(1.0+0.0j) = inf-nanj, ser parte imaginaria -nany no nan(como para CPython) no juega un papel aquí, porque todos los nans silenciosos son equivalentes (ver esto ), incluso algunos de ellos tienen un bit de signo establecido (y por lo tanto se imprimen como "-", ver esto ) y otros no.

Lo cual es al menos contrario a la intuición.


Mi conclusión clave es: no hay nada simple en la multiplicación (o división) de números complejos "simples" y cuando se cambia entre lenguajes o incluso compiladores uno debe prepararse para errores / diferencias sutiles.

ead
fuente
Sé que hay muchos patrones de nan bits. Sin embargo, no sabía la cosa de la señal. Pero quise decir semánticamente ¿En qué se diferencia -nan de nan? ¿O debería decir más diferente de lo que nan es de nan?
Paul Panzer
@PaulPanzer Esto es solo un detalle de implementación de cómo printfy similar funciona con double: miran el bit de signo para decidir si "-" debe imprimirse o no (sin importar si es nan o no). Entonces tiene razón, no hay una diferencia significativa entre "nan" y "-nan", arreglando esta parte de la respuesta pronto.
ead
Ah bueno. Estuve preocupado por un mes que todo lo que pensé que sabía sobre fp no era realmente correcto ...
Paul Panzer
Perdón por ser molesto, pero estás seguro de que "no hay 1.0 imaginario, es decir, 1.0j que no es lo mismo que 0.0 + 1.0j con respecto a la multiplicación". ¿es correcto? Ese anexo G parece especificar un tipo puramente imaginario (G.2) y también prescribir cómo debe multiplicarse, etc. (G.5.1)
Paul Panzer
@PaulPanzer No, gracias por señalar los problemas. Como codificador de c ++, veo sobre todo el estándar C99 a través de C ++, glases, se me pasó por la cabeza, que C está un paso adelante aquí, obviamente tienes razón, una vez más.
ead
3

Definición divertida de Python. Si estamos resolviendo esto con lápiz y papel, diría que el resultado esperado sería el expected: (inf + 0j)que usted señaló porque sabemos que nos referimos a la norma de 1eso (float('inf')+0j)*1 =should= ('inf'+0j):

Pero ese no es el caso como puede ver ... cuando lo ejecutamos obtenemos:

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

Python entiende esto *1como un número complejo y no como la norma, por 1lo que interpreta como *(1+0j)y el error aparece cuando intentamos hacer lo inf * 0j = nanjque inf*0no se puede resolver.

Lo que realmente quiere hacer (asumiendo que 1 es la norma de 1):

Recuerde que si z = x + iyes un número complejo con parte real xy parte imaginaria y, el conjugado complejo de zse define como z* = x − iy, y el valor absoluto, también llamado norm of zse define como:

ingrese la descripción de la imagen aquí

Suponiendo que 1es la norma 1, deberíamos hacer algo como:

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

no muy intuitivo, lo sé ... pero a veces los lenguajes de codificación se definen de una forma diferente a la que usamos en nuestro día a día.

costargc
fuente