Uso básico

Los elementos básicos del paquete son los intervalos, i.e. "conjuntos" de números reales (posiblemente incluyendo $\pm \infty$) de la forma

$$ [a,b] := a \leq x \leq b \subseteq \mathbb{R} $$

Creando intervalos

Los intervalos son creados utilizando el macro @interval, que toma una o dos expresiones:


In [ ]:
using ValidatedNumerics
#Pkg.checkout("ValidatedNumerics")

In [ ]:
a = @interval(1)

In [ ]:
typeof(a)

In [ ]:
b = @interval(1, 2)

Esto retorna objetos del tipo parametrizado Interval, el objeto básico del paquete.

El constructor Interval puede ser usado directamente, pero no es recomendado en general por los autores del paquete, por la siguiente razón:


In [ ]:
a = Interval(0.1, 0.3)

In [ ]:
b = @interval(0.1, 0.3)

¿Qué está pasando aquí?

Debido a la forma en que trabaja la aritmética de punto flotante, el intervalo "a" creado directamente por el constructor no contiene el verdadero número real 0.1 ni el 0.3. El macro @interval, sin embargo, usa redondeo directo para garantizar que los verdaderos 0.1 y 0.3 estén incluidos en el resultado.

¿Por qué es necesario el redondeo?

Consideremos el siguiente código en Julia


In [ ]:
x = 0.1

Esto aparentemente almacena el valor 0.1 en una variable x del tipo Float64. De hecho, sin embargo, almacena un número un poco diferente de 0.1, debido a que 0.1 no puede ser representado en la aritmética de punto flotante binaria, a ninguna precisión.

Vemos internamente la representación en Float64 del número 0.1 (para convencernos de esto):


In [ ]:
bits(0.1)

Los últimos 53 bits de estos 64 bits corresponden a la expansión binaria de 0.1, que es

0.000110011001100110011001100110011001100...

Vemos que esta expresión es periódica; de hecho, la expansión binaria de 0.1 tiene una repetición infinita de la secuencia de dígitos 1100. Por lo tanto es imposible representar al decimal 0.1 en binario, a cualquier precisión.

El valor que en realidad es almacenado en la variable puede ser determinado convenientemente en Julia usando aritmética de precisión arbitraria (BigFloat):


In [ ]:
big(0.1) # con 256 bits de precisión

Entonces, de hecho, el valor es un poquito más grande que 0.1. Por defecto, estos cálculos se hacen en el modo de "round-to-nearest", redondeo al más cercano (RoundNearest); es decir, el número de punto flotante representable más cercano a 0.1 es utilizado.

Supongamos ahora que hemos creado un intervalo como


In [ ]:
II = Interval(0.1)

Pareciera como si el intervalo contiene el valor real 0.1, pero debido a la discusión que acabamos de hacer vemos que, de hecho, esto no es así. Para que contenga el valor real 0.1, los puntos finales del intervalo deben ser redondeados hacia afuera ("redondeo directo"): el límite inferior es redondeado hacia abajo, y el índice superior hacia arriba.

Este redondeo es manejado por el macro @interval, que genera intervalos correctamente redondeados:


In [ ]:
a = @interval(0.1)

El valor verdadero 0.1 está contenido correctamente ahora en los intervalos, por lo tanto cualquier cálculo en estos intevalos contendrá el resultado real calculando con 0.1. Por ejemplo si definimos


In [ ]:
f(x) = 2x + 0.2

In [ ]:
f(a)

El resultado contiene correctamente el verdadero 0.4.

Tras bambalinas, el macro @interval reescribe la(s) expresión(es) que se le pasan, reemplazando los literales (0.1, 1, etc.) con llamadas a crear intervalos con redondeo correcto, manejado internamente por la función make_interval.

Esto nos permite escribir, por ejemplo


In [ ]:
@interval sin(0.1) + cos(0.2)

y obtener un resultado que es equivalente a


In [ ]:
sin(@interval(0.1)) + cos(@interval(0.2))

Esto también puede ser usado para funciones que defina el usuario:


In [ ]:
f(x) = 2x

In [ ]:
f(@interval(0.1))

que es equivalente a


In [ ]:
@interval f(0.1)

Ejemplos

$\pi$

Se pueden crear intervalos correctamente redondeados que contengan a $\pi$:


In [ ]:
@interval(pi)

e introducirlos en expresiones:


In [ ]:
@interval(3*pi/2 + 1)

In [ ]:
@interval(3*π/2 + 1) # la belleza de Julia

Formas de crear intervalos

Los intervalos pueden construirse usando racionales:


In [ ]:
@interval(1//10)

Los reales son convertidos a racionales:


In [ ]:
@interval(0.1)

Pueden usarse Strings:


In [ ]:
@interval("0.1"*"2")

Pueden usarse Strings en forma de intervalos también:


In [ ]:
@interval "[1.2, 3.4]"

Los intervalos pueden ser creados desde variables:


In [ ]:
a = 3.6

In [ ]:
b = @interval(a)

Los límites superiores e inferiores del intervalon pueden ser accesados usando los campos lo y hi:


In [ ]:
b.lo

In [ ]:
b.hi

El diámetro (longitud) de un intervalo es obtenido usando diam(b); para números que no pueden ser representados en binario, el diámetro de los recien creados intervalos pequeños corresponde al epsilon (eps) de la máquina, en el modo de redondeo :narrow:


In [ ]:
diam(b)

Aritmética

Las operaciones aritmética básicas (+,-,*,/,^) están definidas para pares de intervalos en la forma estándar: el resultado es el intervalo más pequeño que contiene el resultado de la operación con cada elemento en cada intervalo. Esto eso, para dos intervalos $X$ y $Y$ y una operación $\circ$, definimos la operación sobre los dos intervalos por

$$ X \circ Y := x \circ y: x \in X \quad \text{y} \quad y \in Y $$

De nuevo, redondeo directo es usado si se necesita. Por ejemplo:


In [ ]:
a = @interval(0.1, 0.3)

In [ ]:
b = @interval(0.3, 0.6)

In [ ]:
a + b

Pequeña aplicación en la física

Esta aritmética es muy útil para hacer cálculos científicos, ya que introduce el estudio de errores de manera natural. Hagamos un simple ejemplo de esto.

Supongamos que tenemos un circuito eléctrico muy simple, con una fuente de voltaje $V$ , una corriente $i$ y una resistencia $R$. Alguna medición experimental arrojó el siguiente resultado:

$$ i = 2 \pm 0.1, $$$$ R = 7 \pm 0.5, $$

y queremos, utilizando la ley de Ohm $V = iR$, saber cuanto debe ser el voltaje del circuito.

Utilizando aritmética de intervalos, podemos introducir directamente los errores en nuestro cálculo y obtener un valor para el voltaje que toma en cuenta los mismos, y además que pese el error asociado a cada medición dando un resultado en forma de intervalo, donde sabemos que está garantizado que se encuentre el valor para el voltaje.


In [ ]:
i = @interval(1.9,2.1)

In [ ]:
R = @interval(6.5,7.5)

In [ ]:
V = i * R

El cual es el resultado correcto, porque el voltaje debe estar incluido en ese intervalo. Si quieremos expresarlo de una forma parecida a como definimos la corriente y la resistencia hacemos lo siguiente:


In [ ]:
mid = (V.lo + V.hi)/2
print("V = ",mid,"±",mid-V.lo)

Funciones elementales

Las funciones elementales principales están definidas, actuando sobre argumentos de intervalos. Actualmente están implementadas las funciones: exp, log, sin, cos, tan, así como sus inversas; en la última versión (0.2) lanzada el 20 de noviembre de 2015, fueron agregadas algunas funciones hiperbólicas como sinh, cosh, tanh y sus inversas. Ejemplo:


In [ ]:
sin(@interval(1))

In [ ]:
cos(@interval(10))

In [ ]:
tan(@interval(π))

De nuevo, el resultado debe contener el resultado de aplicar la función a cada número real contenido en el intervalo. Actualmente los creadores del paquete están trabajando para implementar redondeo directo a las funciones elementales.