Aritmética finita y fuentes del error

Contenidos

Sistemas decimal y binario

En el sistema decimal el número $105.8125$ significa:

$$105.8125=1 \cdot 10^2+5\cdot 10^0+8\cdot10^{-1}+1\cdot10^{-2}+2\cdot10^{-3}+5\cdot10^{-4}$$

Es decir, es una suma cuyos términos son potencias de $10$ y utiliza $10$ dígitos $0,1,\ldots,9$.

Los ordenadores usan el sistema binario porque así los únicos dígitos que se almacenan son el $0$ y el $1$. En sistema binario los números se representan como sumas de potencias de $2$:

$$(105.8125)_{10}=2^6+2^5+2^3+2^0+2^{-1}+2^{-2}+2^{-4}=(1101001.1101)_2 \qquad (1)$$

Conversión decimal a binario

Para convertir la parte entera, dividimos por $2$ de forma repetida. Los restos de estas divisiones son los dígitos en base $2$, de menos a más significativos.

$$ \begin{array}{lrrrrrrrr} Cocientes & 105 & 52 & 26 & 13 & 6 & 3 & 1\\ Restos & 1 & 0 & 0 & 1 & 0 & 1 & \swarrow & \end{array} $$

El número $(105)_{10}$ en sistema binario es $(1101001)_2$.

Nota La función de python bin realiza esta conversión.

In [1]:
bin(105)
Out[1]:
'0b1101001'

Ejercicio 1

Escribir una función de2bi_a que transforme la parte entera de un número decimal a binario. Aplicarla al número x = 105.8125.

NOTA: pueden ser útiles las siguientes funciones

  • np.fix(x): redondea x hacia 0.
  • a//b calcula el cociente de la división entera.
  • a%b calcula el resto de la división entera.
In [1]:
%run Ejercicio1.py
x = 105.8125
binario1 = de2bi_a(x)
print ''.join(binario1)  # si binario1 está almacenado como una lista de cadenas
111000011

Para convertir a binario la parte decimal, multiplicamos por $2$, sustraemos la parte entera, que será nuestro dígito binario y repetimos el proceso.

$$ \begin{array}{lrrrr} Fraccionaria & 0.8125& 0.625 & 0.25 & 0.5 & 0.\\ Entera & & 1 & 1 & 0 & 1 \end{array} $$

Ejercicio 2

Escribir una función de2bi_b que transforme la parte fraccionaria de un número en base $10$ a base $2$. Aplicarla al número x = 105.8125.

In [3]:
%run Ejercicio2.py
x = 105.8125
binario2 = de2bi_b(x)
print ''.join(binario2)  # si binario2 está almacenado como una lista de cadenas
1101

Y el número 105.8125 en binario sería

In [4]:
print ''.join(binario1 + ['.'] + binario2)
1101001.1101

Conversión binario a decimal

De acuerdo con $(1)$ para convertir $(1101001.1101)_2$ a base $10$

In [5]:
1*(2**6)+1*(2**5)+0*(2**4)+1*(2**3)+0*(2**2)+0*(2**1)+1*(2**0)+1*(2**-1)+1*(2**-2)+0*(2**-3)+1*(2**-4)
Out[5]:
105.8125

Representación de enteros

Si tenemos $m$ dígitos o bits de memoria podemos almacenar $2^m$ números diferentes en binario.

Enteros positivos

Si sólo tomamos enteros positivos podríamos representar enteros con valores entre

$$ (00\ldots 00)_2=(0)_{10} \quad \mathrm{y} \quad (11\ldots 11)_2=(2^m-1)_{10}.$$

Por ejemplo, para $m=3$ bits podríamos representar los enteros del $0$ al $7$.

$$ \begin{array}{|c|c|} \hline Base \; 10 & Base \; 2\\ \hline \mathtt{0} & \mathtt{000}\\ \mathtt{1} & \mathtt{001}\\ \mathtt{2} & \mathtt{010}\\ \mathtt{3} & \mathtt{011}\\ \mathtt{4} & \mathtt{100}\\ \mathtt{5} & \mathtt{101}\\ \mathtt{6} & \mathtt{110}\\ \mathtt{7} & \mathtt{111}\\ \hline \end{array} $$

Enteros con signo

Si queremos representar enteros con signo reservaremos el primer bit para el signo y entonces tendremos espacio para $m-1$ cifras significativas. Es decir $2^{m-1}-1$ números positivos, otros tantos negativos y dos ceros, uno con signo positivo y otro negativo. El rango de números representados sería de $[-2^{m-1}+1, 2^{m-1}-1]$. Si $m=3$ podremos representar números de $-3$ a $3$.

$$ \begin{array}{|c|c|} \hline Base \; 10 & Base \; 2\\ \hline \mathtt{-3} & \mathtt{111}\\ \mathtt{-2} & \mathtt{110}\\ \mathtt{-1} & \mathtt{101}\\ \mathtt{-0} & \mathtt{100}\\ \mathtt{+0} & \mathtt{000}\\ \mathtt{+1} & \mathtt{001}\\ \mathtt{+2} & \mathtt{010}\\ \mathtt{+3} & \mathtt{011}\\ \hline \end{array} $$

El primer bit es el signo, $0$ para los positivos y $1$ para los negativos.

Ejemplo

Calcular cómo se almacenará $(-80)_{10}$ si usamos representación binaria con una precisión de 8-bits con signo.

Solución: como es un número negativo, la representación es la misma que en el caso anterior pero el primer bit es el signo y como es negativo, es $1$. Es decir, $(-80)_{10}=(11010000)_2$

Representación al complemento de dos

Una forma de evitar la doble representación del cero es usar la representación al complemento de dos: los números negativos se construyen tomando los dígitos de los positivos y cambiando los $0$ a $1$ y los $1$ a $0$ y sumándole $1$. De esta manera la suma de un número y su opuesto es siempre $0$.

$$ \begin{array}{|c|c|} \hline Base \; 10 & Base \; 2\\ \hline \mathtt{-4} & \mathtt{100}\\ \mathtt{-3} & \mathtt{101}\\ \mathtt{-2} & \mathtt{110}\\ \mathtt{-1} & \mathtt{111}\\ \mathtt{0} & \mathtt{000}\\ \mathtt{1} & \mathtt{001}\\ \mathtt{2} & \mathtt{010}\\ \mathtt{3} & \mathtt{011}\\ \hline \end{array} $$

Ejemplo

Para representar $(-2)_{10}$ empezamos convirtiendo $(2)_{10}$ a binario $(010)_2$

  • invertimos sus dígitos $\rightarrow (101)_2$
  • le sumamos $001$ (tener en cuenta que en binario $0+0=0 \quad 0+1=1 \quad 1+1=10$) $\rightarrow (110)_2$

Si sumamos $(010)_2+(110)_2=(000)_2$

En este caso, el primer bit de los negativos es $1$ y el de los positivos es $0$. Hemos representado enteros en el rango de $[-2^{m-1},2^{m-1}-1]$.

Ejemplo

Calcular cómo se almacenará $(-80)_{10}$ si usamos representación binaria representación al complemento de dos con una precisión de 8-bits.

Solución: como $(80)_{10}=(01010000)_2$

  • en el número anterior cambiamos los $0$ por $1$ y viceversa $\rightarrow 10101111 $
  • y sumándole $1$ ya tenemos la representación $(-80)_{10}=(1011000)_2$

Representación sesgada

Otra forma es la representación sesgada. Los números negativos se representan con valores positivos consecutivos, empezando por el menor negativo, y los positivos toman los valores siguientes. La representación de los números la obtenemos añadiendo el sesgo $2^{m-1}$ al número $x_r=x+2^{m-1}\in[0,2^m-1]$.

$$ \begin{array}{|c|c|} \hline Base \; 10 & Base \; 2\\ \hline \mathtt{-4} & \mathtt{000}\\ \mathtt{-3} & \mathtt{001}\\ \mathtt{-2} & \mathtt{010}\\ \mathtt{-1} & \mathtt{011}\\ \mathtt{0} & \mathtt{100}\\ \mathtt{1} & \mathtt{101}\\ \mathtt{2} & \mathtt{110}\\ \mathtt{3} & \mathtt{111}\\ \hline \end{array} $$

De nuevo vemos que hemos representado enteros en el rango de $[-2^{m-1},2^{m-1}-1]$.

¿De cual de las formas descritas se almacenan los enteros? La mayor parte de las máquinas almacenan los enteros con la representación del complemento al dos y los exponentes de los números en punto flotante, que son enteros, con representación sesgada. ¿Por qué? Hay varios motivos. Uno de ellos es que los números con representación sesgada son más fáciles de comparar. Los códigos comparan frecuentemente números reales, y se empieza comparando el exponente y sólo si son iguales se compara la mantisa.

Representación de enteros en Python

Las variables numéricas en Python pueden ser int, float, long y complex. Las variables booleanas son un subtipo de los enteros.

Los enteros sencillos o int tienen al menos 32 bits de precisión, normalmente 32 o 64 bits. Esta depende del ordenador y sistema operativo que estemos utilizando.

El valor máximo que puede tomar un entero sencillo en nuestro ordenador nos lo da

In [6]:
import sys
sys.maxint
Out[6]:
9223372036854775807

En representación sesgada el mayor entero viene dado por $2^{m-1}-1$. Si $m=64$ es el número de bits el máximo entero es

In [7]:
2**(64-1)-1
Out[7]:
9223372036854775807L

Y para los valores negativos por $-2^{m-1}$

In [8]:
-sys.maxint-1
Out[8]:
-9223372036854775808
In [9]:
-2**(64-1)
Out[9]:
-9223372036854775808L

Así que, en este caso, el entero sencillo tiene una precisión de 64 bits.

Si necesitamos representar un entero mayor, Python utiliza enteros long. Su precisión solo está limitada por la memoria disponible. Se reconocen porque acaban en L.

In [10]:
sys.maxint+1
Out[10]:
9223372036854775808L
In [11]:
-sys.maxint-2
Out[11]:
-9223372036854775809L

Representación de los números reales

En el caso de los números reales se utiliza una representación en punto flotante en base $\beta=2$.

$$x_r=(-1)^s \cdot m \cdot \beta^e=(-1)^s \cdot (a_0.a_1a_2\ldots a_t) \cdot \beta^e$$

donde:

  • $s$ es el signo. Se almacena un $1$ si el número es negativo y $0$ si es positivo.
  • $m$ es la mantisa que si está normalizada tiene un valor $1\leq m < \beta$ y el primer dígito ha de ser distinto de $0$. Este dígito en base dos solo puede ser $1$ y por lo tanto, en una mantisa normalizada siempre es $a_0=1$, no es necesario almacenarlo y ganamos un bit. Esto se llama técnica del bit escondido.
  • $e$ es el exponente que es un entero con signo y utiliza representación binaria sesgada.

Los números se almacenan en "palabras" de 32 bits (precisión sencilla), 64 bits (precisión doble), o incluso 128 bits (precisión cuádruple). En casi todas las máquinas, la precisión en Python para el tipo float es la precisión doble. En el caso de precisión doble los bits se reparten:

  • 1 bit para el signo.
  • 11 bits para el exponente.
  • 52 bits para la mantisa.

Si tenemos $11$ bits para el exponente con signo, significa que tenemos espacio para $2^{11}=2048$ números en formato binario. Si empezamos en el cero, el $ 0 \le E \le 2047 $.

El primer valor $00000000000$ lo reservamos para cero y números desnormalizados y el último $11111111111$ para Inf y NaN. Así que el rango de valores que puede tomar el exponente queda reducido a $ 1 \le E \le 2046 $.

Y como el sesgo es $2^{m-1}-1=2^{10}-1=1024-1=1023$ estos valores representan a los exponentes $ -1022 \le E - 1023 \le 1023 $.

Por lo tanto el exponente máximo es $e_{max}=1023$, y el mínimo $e_{min}=-1022$. Si dividimos por el valor mínimo:

$$\frac{1}{x_{min}}=\frac{1}{m\beta^{e_{min}}}=\frac{1}{m\beta^{-1022}}=\frac{1}{m}\beta^{1022}<\frac{1}{m}\beta^{1023}$$

y no se supera el valor máximo (overflow).


Ejercicio 3

Escribir un programa Ejercicio3.py que calcule la representación de los números los números 105.8125, 120.875, 7.1 y -1.41 en precisión simple, según la norma IEEE 754, utilizando las funciones de los ejercicios 1 y 2. Considerar únicamente el caso en el que la parte entera es mayor que cero. Redondear por truncamiento.

Nota: Recordar que en precisión simple se utilizan 32 bits, de los cuales, uno es para el signo, 8 para el exponente y el resto para la mantisa. El sesgo del exponente es 127.

In [12]:
%run Ejercicio3.py
105.8125 --->  1101001.1101
[Signo]:  0  [Exponente]:  10000101  [Mantisa]:  10100111010000000000000
 
120.875  --->  1111000.111
[Signo]:  0  [Exponente]:  10000101  [Mantisa]:  11100011100000000000000
 
7.1      --->  111.00011001100110011001100
[Signo]:  0  [Exponente]:  10000001  [Mantisa]:  11000110011001100110011
 
-1.41    --->  1.01101000111101011100001
[Signo]:  1  [Exponente]:  01111111  [Mantisa]:  01101000111101011100001

Las características del tipo float en nuestra máquina nos las da la orden sys.float_info

In [13]:
sys.float_info
Out[13]:
sys.float_info(max=1.7976931348623157e+308, max_exp=1024, max_10_exp=308, min=2.2250738585072014e-308, min_exp=-1021, min_10_exp=-307, dig=15, mant_dig=53, epsilon=2.220446049250313e-16, radix=2, rounds=1)

Vemos que el número de dígitos de la mantisa, mant_dig es 53 (52 bits pero 53 dígitos debido al bit escondido). Es decir, el float que usa Python por defecto es el de 64 bits.

Pero si observamos esta salida dice

max_exp=1024 y min_exp=-1021

que no concuerda con lo dicho arriba.

Pero si ejecutamos

help(sys.float_info)

entre otras cosas obtenemos

| max_exp | DBL_MAX_EXP -- maximum int e such that radix**(e-1) is representable

| min_exp | DBL_MIN_EXP -- minimum int e such that radix**(e-1) is a normalized float


Ejercicio 4

(a) El mayor número normalizado que puede representar python en precisión doble será, en representación binaria

$$(+1)\cdot (1.11\ldots 11) \cdot 2^{1023}$$

Como habíamos dicho, el primer $1$ no hace falta guardarlo y nos quedan 52 bits donde almacenamos los unos de $0.11\ldots 11$. Por lo tanto, en decimal este número será:

$$(1+1\cdot 2^{-1}+1\cdot 2^{-2}+1\cdot 2^{-3}+\cdots+1\cdot 2^{-52})\cdot 2^{1023}$$

Hacer un programa Ejercicio4a.py que calcule esta suma. Como norma general, conviene sumar los elementos de la serie de menores a mayores (más adelante veremos por qué). Su valor tiene que coincidir con el valor obtenido con la orden python sys.float_info.max. Llamar sal al valor de salida.

In [14]:
import sys

%run Ejercicio4a.py
print '%.16e' % sal      # forma exponencial con 16 decimales
1.7976931348623157e+308
In [15]:
sys.float_info.max
Out[15]:
1.7976931348623157e+308

Comprobamos que, efectivamente, son iguales

In [16]:
print sal == sys.float_info.max
True

(b) El valor real positivo mínimo representable en punto flotante de forma normalizada es

$$(+1)\cdot (1.00\ldots 00) \cdot 2^{-1022}$$

Calcularlo y llamarlo sal. Su valor tiene que coincidir con orden el valor de salida de python sys.float_info.min.

In [17]:
%run Ejercicio4b.py
print '%.16e' % sal
2.2250738585072014e-308
In [18]:
sys.float_info.min
Out[18]:
2.2250738585072014e-308

Mayor entero representable exactamente de forma normalizada tal que todos los enteros positivos menores se pueden almacenar de forma exacta en forma normalizada

El mayor entero consecutivo, utilizando el formato en punto flotante, del que podemos almacenar todos los dígitos significativos y que, por tanto, podemos almacenar de forma exacta en binario podría ser:

$$(+1)\cdot (1.11\ldots 11) \cdot 2^{52}$$

Es decir, tenemos $53$ cifras de precisión en binario. El entero siguiente:

$$(+1)\cdot (1.00\ldots 00) \cdot 2^{53}$$

también podemos almacenarlo de forma exacta. Pero el siguiente a este ya no porque necesitaríamos un bit más a la derecha en la mantisa. Este entero es

In [19]:
print(2**53)
9007199254740992

Todos los enteros menores se pueden almacenar de forma exacta. Si contamos, vemos que tiene 16 dígitos. Por lo tanto, podemos almacenar todos los enteros con 15 dígitos decimales y casi todos los enteros con 16 dígitos de forma exacta.

Pero, podría decirse, el número

$$(+1)\cdot (1.11\ldots 11) \cdot 2^{62}$$

es más grande y también es entero. Sin embargo sus $10$ últimas cifras serían forzosamente $0$ porque no las podemos almacenar y desde el punto de vista de la precisión no cuentan porque no les podemos dar el valor que queramos.

Valores desnormalizados

¿Qué sucede con los valores de los exponentes fuera del rango $[-1022,1023]$?

Si el exponente es menor que $-1022$ , el número es desnormalizado o cero y estamos usando para los bits correspondientes al exponente el valor especial $00000000000$. Entonces ya no se asume que el bit escondido es $1$ sino $0$.

In [20]:
x = 0.5*(2**-1023)   
print x               # desnormalizado
print 1/x             # su inverso
5.56268464627e-309
inf
In [21]:
print 2**-1080        # Underflow
0.0

El menor valor desnormalizado es

$$(+1)(0.000\ldots 01)\times 2^{-1022}$$

es decir, $2^{-1022-52}$

In [22]:
num_min = 2.**(-1022-52)
print num_min
4.94065645841e-324

Si el valor es menor, devuelve es Underflow y devuelve cero.

In [23]:
2.**(-1022-53)    # Underflow
Out[23]:
0.0

Si el exponente es mayor que $1023$ devuelve OverflowError.

In [24]:
2.**1024          # Overflow
---------------------------------------------------------------------------
OverflowError                             Traceback (most recent call last)
<ipython-input-24-38590efc1dcd> in <module>()
----> 1 2.**1024          # Overflow

OverflowError: (34, 'Result too large')

Precisión de la máquina

Vamos a calcular el número más pequeño que se puede representar después de $1$ utilizando la representación binaria en punto flotante con doble precisión:

El número real $1$ en representación en punto flotante de doble precisión normalizada (con $52$ ceros en la mantisa) es

$$(+1)\cdot (1.00\ldots 00) \cdot 2^{0}$$

y no podemos representar, de forma exacta en punto flotante, un número real entre los dos. La diferencia entre $1$ y $1+\epsilon$ es

$$\epsilon=(+1)\cdot (0.00\ldots 01) \cdot 2^{0}$$

que pasado a decimal es

In [25]:
1*2.**(-52)
Out[25]:
2.220446049250313e-16

o lo que es lo mismo

In [26]:
sys.float_info.epsilon
Out[26]:
2.220446049250313e-16

Este valor, el menor número $\epsilon$ tal que $1+\epsilon>1$ se llama precisión de la máquina y es una cota del error relativo que cometemos al almacenar cuanlquier número real en punto flotante, en este caso, en doble precisión.

Como, en doble precisión, $\epsilon\approx 2.22\cdot 10^{-16}$ se puede decir que corresponde, aproximadamente, a $16$ cifras decimales.

Si queremos el número más pequeño comparable con $x$ hay que sumarle un valor eps

In [27]:
x = 1.
eps = np.spacing(x)
print(eps)
2.22044604925e-16
In [28]:
x = 10.
eps = np.spacing(x)
print(eps)
1.7763568394e-15
In [29]:
x = 100.
eps = np.spacing(x)
print(eps)
1.42108547152e-14

Y vemos que la diferencia entre dos números consecutivos representables de forma exacta en punto flotante, aumenta conforme nos alejamos del cero. Por ello, la densidad de números representados en punto flotante no es uniforme. Es mayor cerca del cero y menor a medida que nos alejamos.

Resultados especiales

Algunas operaciones dan resultados especiales

In [30]:
x = 1.e200
y = x*x
print(y)
inf
In [31]:
y/y
Out[31]:
nan

nan significa "not a number". Su aparición significa, en general, que algo ha ido mal con el programa y/o que se ha realizado una operación no válida.

Overflow. Ocurre cuando el resultado de la operación realizada es finita pero el resultado supera el rango de reales que pueden ser representados.

In [32]:
x = 1.e300
x**2
---------------------------------------------------------------------------
OverflowError                             Traceback (most recent call last)
<ipython-input-32-35043931f28e> in <module>()
      1 x = 1.e300
----> 2 x**2

OverflowError: (34, 'Result too large')

Aproximación y error de redondeo

Redondeo

A menudo, para un número real $x$, no exite una representación exacta en punto flotante y cae entre dos números racionales consecutivos $x^- \lt x \lt x^+$. Como representación de $x$ elegiremos uno de los dos dependiendo del método de redondeo usado. IEEE propone 5 sistemas de redondeo:

  • Hacia arriba.
  • Hacia abajo.
  • Hacia cero ("truncamiento").
  • Hacia el más alejado de cero más cercano.
  • Hacia el par más cercano.

Este último método de redondeo es el más utilizado.

Pérdida de dígitos

Supongamos que estamos usando un sistema de representación decimal que almacena 3 cifras significativas y queremos sumar los números $a=123$ y $b=1.25$. El resultado es $s=124.25$ pero como solo podemos almacenar tres cifras redondeamos, por ejemplo, a $s_r=124$. Hemos perdido cifras y estamos cometiendo un error. En general, cualquier operación aritmética conlleva errores de redondeo.

Ejemplo

Si realizamos la operación $3\times 0.1 \times 100$:

In [33]:
3*0.1*100
Out[33]:
30.000000000000004

El resultado no es $30$ debido al error de redondeo. Estos se pueden propagar y afectar hasta invalidar los resultados. En el caso anterior el error se debe a que, como vimos al principio, el número $x=0.1$ en base dos es periódico y por ello su representación en el ordenador no es exacta.

Ejemplo

Si calculamos $$\sum_{k=1}^{10000} 0.1$$

In [34]:
suma = 0.
for i in range(10000):
    suma += 0.1
print '%.16f'% suma      # En forma decimal con 16 decimales
1000.0000000001588205

y el error absoluto es

In [35]:
import numpy as np
abs(suma-1000.)
Out[35]:
1.588205122970976e-10

Ejemplo

Si sumamos o restamos dos números muy diferentes en magnitud se pierde exactitud debido a los errores de redondeo. Por ejemplo

In [36]:
a = 1.e+9
epsilon = 1.e-8

suma = a
for i in range(10000):
    suma += epsilon
print '%.16e'% suma       # En forma exponencial con 16 decimales
1.0000000000000000e+09

Sin embargo el resultado debería haber sido

In [37]:
suma = a + 10000*epsilon
print '%.16e'% suma
1.0000000000001000e+09

Ejemplo

Vamos a sumar los $N$ primeros términos de la serie armónica $$\sum_{n=1}^N \frac{1}{n}$$

El resultado teórico para 1000 términos sería

In [38]:
import numpy as np
from scipy import special
N = 10000
suma = special.polygamma(0,N+1)-special.polygamma(0,1)
print '%.20f'% suma
9.78760603604438372827

Empezando la suma por el término $n=1$

In [39]:
suma1 = 0.;
for n in range(1,N+1):
    suma1 += 1./n
print '%.20f'% suma1
9.78760603604434820113
In [40]:
error1 = abs(suma1 - suma)
print error1
3.5527136788e-14

Empezando por el término final

In [41]:
suma2 = 0.;
for n in range(N,0,-1):
    suma2 += 1./n
print '%.20f'% suma2
9.78760603604438550462

La diferencia entre ambos resultados es

In [42]:
error2 = abs(suma2 - suma)
print error2
1.7763568394e-15

Es mayor el error en el caso primero porque los términos de la sucesión son cada vez más pequeños mientras que la suma es cada vez más grande. Por lo tanto, conforme avanzamos en la suma se producirá pérdida de dígitos por sumar números de magnitud muy distinta.

Error de cancelación

Aparece al restar números de valores muy parecidos. Por ejemplo

$$\sqrt{x^2+\epsilon^2}-x$$
In [43]:
import numpy as np
x  = 1000000.
ep = 0.01
a  = np.sqrt(x**2 + ep**2) - x
print a
1.16415321827e-10

En este caso el error de calcelación se puede evitar utilizando una expresión matemática equivalente

$$\sqrt{x^2+\epsilon^2}-x=\frac{(\sqrt{x^2+\epsilon^2}-x)(\sqrt{x^2+\epsilon^2}+x)}{\sqrt{x^2+\epsilon^2}+x}=\frac{\epsilon^2}{\sqrt{x^2+\epsilon^2}+x}\approx \frac{\epsilon^2}{\sqrt{x^2}+x}=\frac{\epsilon^2}{2x} $$
In [44]:
b = ep**2 / (np.sqrt(x**2 + ep**2) + x)
print b
5e-11

O bien

In [45]:
b = ep**2 / (2*x)
print b
5e-11

El error relativo cometido al calcularlo con la primera expresión es muy grande

In [46]:
Er = abs((a - b)/b)
print Er
1.32830643654

Además, este ejemplo muestra que expresiones matemáticamente equivalentes no son, necesariamente, equivalentes desde el punto de vista computacional. Y viceversa, que expresiones matemáticas que no son iguales, pueden tener el mismo resultado en el ordenador y ser, por tanto, computacionalmente equivalentes.


Ejercicio 5

(a) Al resolver la ecuación de segundo grado

$$x^2+10^{8}x+1=0$$

utilizando la fórmula

$$x_1=\frac{-b+\sqrt{b^2-4ac}}{2a}$$

una de las soluciones es:

In [46]:
a = 1.; b = 10.**8; c = 1.
x1 = (-b + np.sqrt(b**2 - 4*a*c)) / (2*a)
print x1
-7.45058059692e-09

Pero al sustituir en el polinomio para calcular el residuo

In [47]:
residuo1 = a*x1**2 + b*x1 + c
print residuo1
0.254941940308

es relativamente grande. Encontrar una expresión matemática equivalente que dé una solución con menor residuo y calcular de nuevo la solución y su residual. Escribir para ello un programa llamado Ejercicio5a.py

In [48]:
%run Ejercicio5a.py
print x2
print residuo2
-1e-08
1.11022302463e-16

(b) Al resolver la ecuación de segundo grado

$$x^2-10^{8}x+1=0$$

utilizando la fórmula

$$x_2=\frac{-b-\sqrt{b^2-4ac}}{2a}$$

una de las soluciones es:

In [49]:
a = 1.; b = -10.**8; c = 1.
x3 = (- b - np.sqrt(b**2 - 4*a*c)) / (2*a)
print x3
7.45058059692e-09

Pero al sustituir en el polinomio

In [50]:
residuo3 = a*x3**2 + b*x3 + c
print residuo3
0.254941940308

es relativamente grande. Encontrar una expresión matemática equivalente que dé una solución con menor residual y calcular de nuevo la solución y su residual. Escribir para ello un programa llamado Ejercicio5b.py.

In [51]:
%run Ejercicio5b.py
print x4
print residuo4
1e-08
1.11022302463e-16

Ejemplo

Tenemos que

$$e^x=\sum_{n=0}^{\infty}\frac{x^n}{n!}$$

Aproximemos el valor de $e$, usando $x=1$, con un número finito de términos de la suma, por ejemplo, $50$

In [22]:
import math

suma = 0.
x = 1.

for n in range(100):
    suma += x**n / math.factorial(n)
    if n in np.array([5,10,15,20,40,60,80,100])-1:
        error   = abs(np.exp(1.) - suma)
        print u"Número de términos        %i" % (n+1)
        print "Error                     %.16e" % error
        print ""
Número de términos        5
Error                     9.9484951257120535e-03

Número de términos        10
Error                     3.0288585284310443e-07

Número de términos        15
Error                     8.1490370007486490e-13

Número de términos        20
Error                     4.4408920985006262e-16

Número de términos        40
Error                     4.4408920985006262e-16

Número de términos        60
Error                     4.4408920985006262e-16

Número de términos        80
Error                     4.4408920985006262e-16

Número de términos        100
Error                     4.4408920985006262e-16

Vemos que primero el error disminuye y luego se estanca. ¿Por qué? Si estuviéramos usando aritmética infinita el error disminuiría con las iteraciones. Pero como estamos usando aritmética finita, el error, en este caso, está limitado por el espaciado entre números representables en la zona de la solución $e$ que es

In [53]:
Ea = np.spacing(np.exp(1.))
print Ea
4.4408920985e-16

Calculemos el menor número de términos necesarios para obtener el número $e$ con el menor error posible.

In [28]:
import math

x = 1.
Ea = np.spacing(np.exp(x))
n = 0
suma = 1.


itermax = 100
error   = abs(np.exp(x) - suma)

while (error > Ea and n < itermax):
    n += 1
    suma += x**n / math.factorial(n)
    error = abs(np.exp(x) - suma)
    
print u"Número de iteraciones     %i" % n
print "Error                     %.16e" % error
print "Espaciado alrededor de e  %.16e" % Ea
Número de iteraciones     17
Error                     4.4408920985006262e-16
Espaciado alrededor de e  4.4408920985006262e-16

Y aunque sabemos que con $17$ iteraciones todavía cometemos error, este ya no es apreciable por la máquina y aunque realicemos más iteraciones este error computado no disminuye.


Ejercicio 6

Teniendo en cuenta que

$$\log (1+x) = \dfrac{x^1}{1}-\dfrac{x^2}{2}+\dfrac{x^3}{3}-\dfrac{x^4}{4}+\dfrac{x^5}{5}-\cdots \quad \mathrm{para} \quad |x|\lt 1$$

calcular el número de términos necesarios para calcular $\log 1.5$ con el menor error posible.

In [29]:
%run Ejercicio6.py
print "Número de iteraciones            %i" % n
print "Error                            %.16e" % error
print "Espaciado alrededor de ln(1.5)   %.16e" % Ea
Número de iteraciones            45
Error                            5.5511151231257827e-17
Espaciado alrededor de ln(1.5)   5.5511151231257827e-17

Referencias