21.1 El cálculo exacto y el cálculo digital
En matemática, 0,1 + 0,2 = 0,3. En JavaScript, los números se almacenan normalmente con el formato binario IEEE 754 de doble precisión. Muchos decimales finitos no tienen una representación binaria finita y deben redondearse.
const suma = 0.1 + 0.2;
console.log(suma);
console.log(suma === 0.3);
console.log(suma - 0.3);El resultado no implica que JavaScript calcule al azar. Es la aproximación binaria más cercana dentro de la precisión disponible. El error suele ser diminuto, pero una comparación exacta puede hacerlo visible.
21.2 Cómo se representa un número
Un número de coma flotante combina signo, significando y exponente. Esto permite representar valores extremadamente grandes y pequeños, pero la separación entre números representables aumenta con su magnitud.
Number.EPSILON es la distancia entre 1 y el siguiente número representable mayor que 1. No es una tolerancia universal.
console.log("Number.EPSILON:", Number.EPSILON);
console.log("1 + EPSILON:", 1 + Number.EPSILON);
console.log("1e16 + 1:", 1e16 + 1);
console.log("¿Se conservó la unidad?", 1e16 + 1 !== 1e16);21.3 Igualdad aproximada absoluta
Dos valores son aproximadamente iguales si su diferencia absoluta no supera una tolerancia acordada:
function casiIgualesAbs(a, b, epsilon = 1e-10) {
return Math.abs(a - b) <= epsilon;
}
const resultado = 0.1 + 0.2;
console.log("Igualdad exacta:", resultado === 0.3);
console.log("Igualdad aproximada:", casiIgualesAbs(resultado, 0.3));La tolerancia absoluta funciona bien cerca de cero y cuando conocemos la escala física del problema. Por ejemplo, en una interfaz gráfica puede ser razonable considerar iguales posiciones separadas por menos de una milésima de píxel.
21.4 Tolerancia relativa
Una tolerancia absoluta fija puede ser demasiado estricta para valores grandes y demasiado permisiva para valores pequeños. La tolerancia relativa escala con la magnitud:
function casiIgualesRel(a, b, rel = 1e-9) {
const escala = Math.max(Math.abs(a), Math.abs(b));
return Math.abs(a - b) <= rel * escala;
}
console.log(casiIgualesRel(1_000_000_000, 1_000_000_000.5));
console.log(casiIgualesRel(0, 1e-12)); // cerca de cero falla21.5 Combinar tolerancias absoluta y relativa
Una comparación robusta utiliza un umbral absoluto cerca de cero y uno relativo para magnitudes grandes:
function casiIguales(a, b, absTol = 1e-12, relTol = 1e-9) {
if (a === b) return true;
if (!Number.isFinite(a) || !Number.isFinite(b)) return false;
const diferencia = Math.abs(a - b);
const escala = Math.max(Math.abs(a), Math.abs(b));
return diferencia <= Math.max(absTol, relTol * escala);
}
console.log(casiIguales(0.1 + 0.2, 0.3));
console.log(casiIguales(0, 5e-13));
console.log(casiIguales(Infinity, Infinity));La comprobación exacta inicial hace que dos infinitos del mismo signo sean iguales. Los demás valores no finitos se rechazan antes de restarlos.
21.6 Elegir una tolerancia con significado
La tolerancia depende del dominio, las unidades y el propósito. No se elige solamente mirando Number.EPSILON.
| Situación | Escala relevante | Criterio posible |
|---|---|---|
| Coordenadas en pantalla | Píxeles | Una fracción de píxel |
| Robot móvil | Metros y precisión del sensor | Mayor que el ruido medido |
| Vector unitario | Módulo cercano a 1 | Error adimensional pequeño |
| Tiempo de simulación | Segundos y paso temporal | Fracción del paso dt |
21.7 Comparar vectores
Podemos comparar componentes o medir la distancia entre extremos. Ambos criterios responden preguntas ligeramente diferentes.
const distancia = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
function vectoresCercanos(a, b, tolerancia = 1e-9) {
return distancia(a, b) <= tolerancia;
}
const a = { x: 0.1 + 0.2, y: Math.sqrt(4) };
const b = { x: 0.3, y: 2 };
console.log("Exactamente iguales:", a.x === b.x && a.y === b.y);
console.log("Geométricamente cercanos:", vectoresCercanos(a, b));La distancia euclídea produce un umbral circular. Comparar cada componente produce una región cuadrada. La geometría del problema indica cuál conviene.
21.8 ¿Cuándo un vector es nulo?
Después de varias operaciones, un vector que matemáticamente debería ser nulo puede conservar residuos diminutos. En vez de comprobar x === 0 && y === 0, comparamos su módulo con un umbral.
const modulo2 = v => v.x * v.x + v.y * v.y;
function esCasiNulo(v, tolerancia = 1e-9) {
return modulo2(v) <= tolerancia * tolerancia;
}
console.log(esCasiNulo({ x: 1e-12, y: -2e-12 }));
console.log(esCasiNulo({ x: 1e-4, y: 0 }));Comparar módulos al cuadrado evita calcular una raíz cuadrada. Es imprescindible elevar también la tolerancia al cuadrado.
21.9 Normalización segura
Dividir por un módulo extremadamente pequeño amplifica ruido numérico. Una normalización segura acepta un umbral relacionado con la escala de la aplicación.
function normalizarSeguro(v, tolerancia = 1e-12) {
const modulo = Math.hypot(v.x, v.y);
if (!Number.isFinite(modulo) || modulo <= tolerancia) return null;
return { x: v.x / modulo, y: v.y / modulo };
}
console.log(normalizarSeguro({ x: 3, y: 4 }));
console.log(normalizarSeguro({ x: 1e-15, y: 0 }));21.10 Acumulación de errores
Un error pequeño puede acumularse en un bucle largo. Sumar 0.1 diez veces no necesariamente llega exactamente a 1.
let tiempo = 0;
for (let i = 0; i < 10; i++) tiempo += 0.1;
console.log("Tiempo acumulado:", tiempo);
console.log("¿Es exactamente 1?", tiempo === 1);
console.log("Error:", tiempo - 1);Cuando sea posible, conviene calcular tiempo = paso * cantidadDePasos o mantener un contador entero. En una simulación, el estado igualmente acumula aproximaciones, por lo que hay que observar invariantes como energía, longitud o velocidad.
21.11 Cancelación y orden de las operaciones
Restar números grandes muy cercanos puede eliminar dígitos significativos. También el orden de una suma cambia cuánto se pierde.
const grande = 1e16;
console.log((grande + 1) - grande);
console.log((grande - grande) + 1);
const posicionGlobal = 1e12 + 0.001;
console.log("Detalle recuperado:", posicionGlobal - 1e12);En mundos virtuales extensos se usa a veces un origen local cercano al observador. Así las posiciones que se restan tienen escalas moderadas y conservan más detalle.
21.12 Suma compensada
El algoritmo de Kahan conserva una compensación para parte de los bits perdidos al sumar muchos valores. No vuelve exacta toda operación, pero puede reducir el error acumulado.
function sumaKahan(valores) {
let suma = 0, compensacion = 0;
for (const valor of valores) {
const y = valor - compensacion;
const t = suma + y;
compensacion = (t - suma) - y;
suma = t;
}
return suma;
}
const datos = [1e16, 1, 1, 1, 1, -1e16];
console.log("Suma común:", datos.reduce((a, b) => a + b, 0));
console.log("Suma compensada:", sumaKahan(datos));21.13 NaN, Infinity y valores finitos
NaN representa un resultado numérico inválido; Infinity puede aparecer por desbordamiento o división por cero. Ambos se propagan y pueden contaminar toda una simulación.
const valores = [3, NaN, Infinity, -Infinity, "3"];
for (const valor of valores) {
console.log(String(valor), "finito:", Number.isFinite(valor));
}
console.log("NaN es igual a sí mismo:", NaN === NaN);
console.log("Detectar NaN:", Number.isNaN(NaN));Preferimos Number.isFinite y Number.isNaN porque no convierten textos automáticamente.
21.14 Casos límite geométricos
Una biblioteca vectorial debe decidir qué hace ante situaciones degeneradas:
- Normalizar un vector nulo.
- Proyectar sobre un vector nulo.
- Calcular el ángulo si uno de los vectores no tiene dirección.
- Construir una base con ejes paralelos.
- Intersectar rectas paralelas o coincidentes.
- Recibir componentes no finitas.
Devolver null, lanzar una excepción o entregar un objeto resultado con estado son contratos posibles. Devolver silenciosamente un número arbitrario oculta el problema.
21.15 Limitar dominios matemáticos
Por redondeo, un coseno calculado puede ser 1.0000000000000002. Como acos solo acepta valores entre −1 y 1, debemos limitar el dato antes de usarlo.
const limitar = (n, minimo, maximo) =>
Math.max(minimo, Math.min(maximo, n));
const cosenoAproximado = 1.0000000000000002;
console.log("Sin limitar:", Math.acos(cosenoAproximado));
console.log("Limitado:", Math.acos(limitar(cosenoAproximado, -1, 1)));Limitar es correcto cuando el valor excede el dominio solo por error numérico y la teoría garantiza que debería pertenecer a él. No debe usarse para esconder entradas físicamente inválidas.
21.16 Laboratorio de tolerancias
Elegí un experimento y cambiá la tolerancia. El gráfico amplifica el error para hacerlo visible; la barra amarilla representa su magnitud y la marca rosa, el umbral elegido.
La igualdad exacta falla, pero la tolerancia reconoce valores equivalentes para esta escala.
21.17 Estrategia para una simulación estable
- Definir unidades y escalas antes de elegir tolerancias.
- Validar entradas externas con
Number.isFinite. - Tratar explícitamente operaciones degeneradas.
- Comparar resultados calculados con tolerancias justificadas.
- Evitar coordenadas desproporcionadamente grandes si se necesita detalle local.
- Medir invariantes durante ejecuciones largas.
- Registrar el primer valor no finito cerca de su origen.
Una tolerancia no corrige por sí sola un algoritmo inestable. Si el error crece en cada paso, también puede ser necesario reducir dt, cambiar el integrador o reformular el cálculo.
21.18 Errores frecuentes
- Comparar con
===un resultado obtenido mediante varios cálculos decimales. - Usar
Number.EPSILONcomo tolerancia universal. - Aplicar solo tolerancia relativa cerca de cero.
- Elegir una tolerancia sin considerar unidades ni escala.
- Normalizar vectores diminutos y amplificar el ruido.
- Ocultar entradas inválidas mediante
clamp. - Permitir que
NaNavance por toda la simulación. - Redondear el estado interno en cada cuadro solo para mejorar su presentación.
- Confundir cantidad de decimales mostrados con precisión almacenada.
21.19 Ejercicio integrador
Implementá anguloEntreSeguro(a, b, tolerancia). Debe validar componentes finitas, devolver null si algún vector es casi nulo y limitar el coseno antes de llamar a Math.acos.
Ver solución y pruebas
function anguloEntreSeguro(a, b, tolerancia = 1e-12) {
const componentes = [a.x, a.y, b.x, b.y];
if (!componentes.every(Number.isFinite)) {
throw new TypeError("Las componentes deben ser finitas");
}
const ma = Math.hypot(a.x, a.y);
const mb = Math.hypot(b.x, b.y);
if (ma <= tolerancia || mb <= tolerancia) return null;
const coseno = (a.x * b.x + a.y * b.y) / (ma * mb);
return Math.acos(Math.max(-1, Math.min(1, coseno)));
}
const recto = anguloEntreSeguro({x:1,y:0}, {x:0,y:1});
console.log("Ángulo recto en grados:", recto * 180 / Math.PI);
console.log("Con vector casi nulo:", anguloEntreSeguro(
{x:1e-15,y:0}, {x:1,y:0}
));21.20 Ideas para recordar
- Los números de coma flotante aproximan muchos valores reales.
Number.EPSILONdescribe el espaciado cerca de 1, no el error permitido por toda aplicación.- Una comparación general combina tolerancias absoluta y relativa.
- Las tolerancias deben derivarse de unidades, escalas y requisitos físicos.
- Los vectores casi nulos requieren tratamiento antes de normalizar o dividir.
- El orden de las operaciones puede afectar la precisión.
NaNe infinitos deben detectarse cerca de su origen.- Una API confiable documenta cada caso degenerado.
En el próximo tema llevaremos los vectores a una visualización interactiva con Canvas, transformando coordenadas físicas en píxeles y dibujando ejes, flechas y animaciones.