29.1 Verificar antes de confiar
Una animación suave solo demuestra que el navegador dibuja imágenes coherentes. No demuestra que las unidades, ecuaciones, signos, integrador o eventos sean correctos. Los errores pequeños pueden pasar inadvertidos y los grandes pueden parecer plausibles.
Verificar consiste en reunir evidencia de que el programa resuelve correctamente el modelo elegido. La evidencia debe poder repetirse después de modificar el código.
29.2 Verificación y validación
Un proyectil ideal puede estar perfectamente verificado y no reproducir una pelota real si ignora el aire. A la inversa, ajustar datos no demuestra que el integrador esté bien implementado. En este tema nos concentraremos en la verificación.
29.3 La solución analítica como oráculo
Un oráculo de prueba proporciona el resultado esperado. Para aceleración constante conocemos:
v(t)=v₀+at
Podemos ejecutar el integrador con las mismas condiciones iniciales y comparar cada estado. Aunque el proyecto final use aceleraciones complejas, verificar primero casos sencillos descubre errores básicos de actualización.
29.4 Construir un oráculo reutilizable
function estadoExactoMUA(inicial, aceleracionMps2, tiempoS) {
const dt = tiempoS - inicial.tiempoS;
return {
tiempoS,
posicionM: inicial.posicionM + inicial.velocidadMps * dt
+ 0.5 * aceleracionMps2 * dt ** 2,
velocidadMps: inicial.velocidadMps + aceleracionMps2 * dt
};
}
console.log(estadoExactoMUA(
{ tiempoS: 0, posicionM: 2, velocidadMps: 3 },
-4,
1.5
));El oráculo debe ser independiente del código probado. Copiar exactamente el mismo algoritmo a la prueba puede repetir el mismo error y producir una confirmación falsa.
29.5 Comparar en los mismos instantes
La comparación solo es válida si ambos estados pertenecen al mismo tiempo, usan las mismas unidades y referencia. Si el integrador produce muestras en otros instantes, debemos solicitar al oráculo esos tiempos o interpolar de manera declarada.
Comparar el estado n con t=nΔt falla si la simulación comenzó en otro t₀ o si el último paso fue más corto. Conviene utilizar la propiedad estado.tiempoS de cada muestra.
29.6 Tolerancias absolutas y relativas
Los números de coma flotante y los métodos aproximados rara vez coinciden mediante ===. Una comparación robusta combina tolerancia absoluta y relativa:
function casiIguales(a, b, absTol = 1e-9, relTol = 1e-7) {
if (!Number.isFinite(a) || !Number.isFinite(b)) return a === b;
const diferencia = Math.abs(a - b);
const escala = Math.max(Math.abs(a), Math.abs(b));
return diferencia <= absTol + relTol * escala;
}
console.log(casiIguales(0.1 + 0.2, 0.3));
console.log(casiIguales(1_000_000.01, 1_000_000));La tolerancia absoluta protege valores cercanos a cero; la relativa se adapta a valores grandes. Ambas deben surgir de los requisitos y del error esperado del método.
29.7 Una aserción mínima
Una prueba automática ejecuta una situación, compara el resultado y falla de manera visible si no se cumple la expectativa:
function afirmarCasiIgual(actual, esperado, tolerancia, mensaje) {
const error = Math.abs(actual - esperado);
if (error > tolerancia) {
throw new Error(`${mensaje}: esperado ${esperado}, obtenido ${actual}`);
}
}
function pasoEuler(estado, aceleracion, dt) {
return {
posicion: estado.posicion + estado.velocidad * dt,
velocidad: estado.velocidad + aceleracion * dt
};
}
const resultado = pasoEuler({ posicion: 2, velocidad: 3 }, -4, 0.5);
afirmarCasiIgual(resultado.posicion, 3.5, 1e-12, "posición");
afirmarCasiIgual(resultado.velocidad, 1, 1e-12, "velocidad");
console.log("Prueba superada");Esta prueba verifica la fórmula de un paso de Euler, no que Euler coincida con la solución exacta acelerada. La expectativa debe corresponder al contrato específico del código probado.
29.8 Casos exactos especialmente útiles
- Reposo: posición constante con velocidad y aceleración nulas.
- MRU: todos los integradores presentados deben reproducir la posición lineal.
- Aceleración constante: permite comprobar velocidad y medir el error esperado de posición.
- Componentes independientes: acelerar y sin alterar x revela mezclas de ejes.
- Transformación inversa: cambiar de referencia y volver debe recuperar el estado.
- Paso cero en la función matemática: no cambia el estado, aunque el bucle debe rechazarlo.
Los casos simples aíslan una propiedad. Una prueba enorme que combina todo puede fallar sin indicar cuál fue la causa.
29.9 Invariantes
Un invariante es una propiedad que debe mantenerse durante una familia de estados. No siempre necesitamos conocer la trayectoria exacta:
Los invariantes detectan clases completas de errores y son especialmente valiosos cuando no existe una solución analítica cerrada.
29.10 Verificar una trayectoria completa
function verificarHistoria(historia) {
if (historia.length === 0) throw new Error("Historia vacía");
for (let i = 0; i < historia.length; i++) {
const estado = historia[i];
const valores = [estado.tiempoS, estado.posicionM, estado.velocidadMps];
if (!valores.every(Number.isFinite)) {
throw new Error(`Valor no finito en el estado ${i}`);
}
if (i > 0 && estado.tiempoS <= historia[i - 1].tiempoS) {
throw new Error(`Tiempo no creciente en el estado ${i}`);
}
}
return true;
}
console.log(verificarHistoria([
{ tiempoS: 0, posicionM: 0, velocidadMps: 2 },
{ tiempoS: 0.1, posicionM: 0.2, velocidadMps: 2 }
]));29.11 Gráficos que comparan, no solo decoran
Superponer solución analítica y numérica revela deriva, desfase y crecimiento. Sin embargo, cuando las curvas están muy próximas pueden parecer idénticas. Conviene añadir una gráfica de residuo:
El residuo amplía la diferencia y muestra su signo. Los ejes deben incluir unidades, escalas y leyenda; una escala automática excesivamente amplia puede ocultar un error importante.
29.12 Resumir el error de una historia
function metricas(historia, referenciaEn) {
let maximo = 0;
let sumaCuadrados = 0;
for (const estado of historia) {
const error = estado.posicionM - referenciaEn(estado.tiempoS);
maximo = Math.max(maximo, Math.abs(error));
sumaCuadrados += error ** 2;
}
return {
errorMaximo: maximo,
errorRms: Math.sqrt(sumaCuadrados / historia.length),
errorFinal: historia.at(-1).posicionM
- referenciaEn(historia.at(-1).tiempoS)
};
}
const historia = [
{ tiempoS: 0, posicionM: 0 },
{ tiempoS: 1, posicionM: 2.9 },
{ tiempoS: 2, posicionM: 7.7 }
];
console.log(metricas(historia, t => t + 1.5 * t ** 2));Error máximo, RMS y final responden preguntas diferentes. Informar varias métricas reduce el riesgo de ocultar una trayectoria mala detrás de una coincidencia final.
29.13 Actividad interactiva: banco de verificación
Compará Euler explícito y Velocity Verlet para una aceleración constante. La parte superior muestra posiciones y la inferior el residuo. Las pruebas se recalculan al cambiar parámetros.
Banco de verificación numérica
Condición inicial: x₀=0 m. La tolerancia del error es un requisito elegido para este ensayo.
29.14 Pruebas de convergencia
Una prueba puede verificar no solo un valor, sino que el error disminuya con el orden esperado al refinar el paso:
function errorEulerMUA(aceleracion, duracionS, pasoS) {
let x = 0;
let v = 0;
let t = 0;
while (t < duracionS - 1e-12) {
const dt = Math.min(pasoS, duracionS - t);
x += v * dt;
v += aceleracion * dt;
t += dt;
}
const exacta = 0.5 * aceleracion * duracionS ** 2;
return Math.abs(x - exacta);
}
const errorGrueso = errorEulerMUA(2, 4, 0.2);
const errorFino = errorEulerMUA(2, 4, 0.1);
console.log({ errorGrueso, errorFino, razon: errorGrueso / errorFino });La razón debe aproximarse a 2 para Euler en este caso. Una tolerancia razonable admite redondeo y pasos finales distintos sin aceptar cualquier resultado.
29.15 Pruebas de propiedades
En lugar de comprobar un único ejemplo, podemos generar muchos valores dentro de un dominio y verificar una propiedad. Por ejemplo, en MRU avanzar dt y luego −dt debe recuperar la posición:
function avanzarMRU(posicion, velocidad, dt) {
return posicion + velocidad * dt;
}
let superadas = 0;
for (let i = 0; i < 100; i++) {
const x = -50 + Math.random() * 100;
const v = -20 + Math.random() * 40;
const dt = Math.random() * 2;
const ida = avanzarMRU(x, v, dt);
const vuelta = avanzarMRU(ida, v, -dt);
if (Math.abs(vuelta - x) > 1e-10) {
throw new Error("Falló la propiedad de ida y vuelta");
}
superadas++;
}
console.log(`${superadas} casos superados`);Para que una falla sea reproducible conviene usar un generador pseudoaleatorio con semilla y registrar el caso que la produjo.
29.16 Casos de borde y errores esperados
También debemos verificar el comportamiento frente a entradas límites o inválidas:
- Δt cero, negativo, infinito o NaN;
- duración menor que el paso y último paso fraccionario;
- velocidad o aceleración nulas;
- valores negativos y cambios de sentido;
- cantidades muy grandes o pequeñas;
- historia vacía, referencias diferentes o unidades incompatibles;
- pausa, reanudación e intervalos reales extraordinarios.
Una prueba debe comprobar que la función rechaza deliberadamente lo inválido, no solamente que “ocurre algún error”.
29.17 Pruebas de regresión
Cuando corregimos un defecto, agregamos una prueba que falle con la versión defectuosa y pase con la corrección. Así impedimos que el mismo problema reaparezca silenciosamente.
También podemos conservar métricas de referencia para escenarios representativos. Si cambian, la revisión debe determinar si se trata de una mejora deliberada, un cambio de modelo o una regresión.
Comparar archivos completos con decimales exactos suele ser frágil. Es preferible comparar magnitudes significativas con tolerancias y metadatos claros sobre método, paso y versión.
29.18 Separar física, dibujo y pruebas
La función de integración debe poder ejecutarse sin Canvas, reloj real ni interfaz. Entonces las pruebas pueden suministrar estados conocidos y obtener resultados deterministas.
El dibujo se prueba con estados preparados; el reloj se prueba con secuencias de marcas temporales; la física se prueba con Δt explícitos. Separar responsabilidades reduce la cantidad de causas posibles cuando algo falla.
29.19 Organizar una batería de pruebas
function ejecutarPruebas(pruebas) {
let superadas = 0;
for (const [nombre, prueba] of Object.entries(pruebas)) {
try {
prueba();
console.log(`✓ ${nombre}`);
superadas++;
} catch (error) {
console.log(`✕ ${nombre}: ${error.message}`);
}
}
return { superadas, total: Object.keys(pruebas).length };
}
const resultado = ejecutarPruebas({
"suma": () => { if (2 + 2 !== 4) throw new Error("resultado incorrecto"); },
"número finito": () => { if (!Number.isFinite(3)) throw new Error("no finito"); }
});
console.log(resultado);En un proyecto real, un entorno de pruebas aporta informes, aislamiento y automatización. La estructura mínima permite comprender el ciclo: preparar, ejecutar, comprobar e informar.
29.20 Evidencia y trazabilidad
Un informe de verificación útil registra:
- versión del modelo y del código;
- condiciones iniciales, unidades y referencia;
- integrador, Δt, duración y tolerancias;
- solución de referencia o invariante usado;
- métricas, gráficos y resultado de cada prueba;
- entorno y semilla aleatoria cuando sean relevantes.
Esta información permite reproducir una conclusión. Un gráfico sin parámetros o un mensaje “todo funciona” no constituye evidencia suficiente.
29.21 Errores frecuentes
- Probar solo mirando: complementar la inspección visual con métricas y aserciones.
- Usar el mismo algoritmo como oráculo: buscar una derivación independiente.
- Comparar con igualdad estricta: definir tolerancias absolutas y relativas.
- Elegir tolerancias enormes: deben detectar errores relevantes, no hacer pasar cualquier resultado.
- Comprobar solo un caso feliz: incluir bordes, signos, ceros y entradas inválidas.
- Medir solo el estado final: observar residuo, máximo, RMS e invariantes de la historia.
- Confundir verificación con validación: resolver bien un modelo no garantiza que represente la realidad.
- Depender del reloj o del dibujo: probar la física con funciones deterministas y Δt explícito.
29.22 Ejercicios propuestos
- Calculá el estado exacto a 2 s para x₀=1 m, v₀=3 m/s y a=−2 m/s².
- Diseñá una prueba de reposo para una función de integración.
- Explicá cuándo usarías tolerancia absoluta, relativa o ambas.
- Una trayectoria tiene errores 0; −0,1; 0,2 y −0,1 m. Calculá error máximo, final y RMS.
- Proponé un invariante para MRU y otro para un movimiento bidimensional con aₓ=0.
- ¿Por qué una captura de pantalla no basta como prueba de regresión numérica?
- Diseñá tres casos de borde para una función que simula hasta una duración.
- Indicá qué información incluirías al informar una prueba de convergencia.
Ver soluciones y explicaciones
x=1+3·2+½(−2)·2²=3 myv=3−2·2=−1 m/s.- Crear x arbitraria con v=0 y a=0, avanzar varios pasos y afirmar que posición y velocidad se conservan dentro de tolerancia.
- Absoluta cerca de cero o con una precisión fija; relativa para escalas variables; ambas para cubrir correctamente los dos regímenes.
- Máximo 0,2 m; final −0,1 m; RMS=
√[(0²+0,1²+0,2²+0,1²)/4]≈0,122 m. - En MRU la velocidad permanece constante. Si aₓ=0, la componente vₓ debe permanecer constante aunque cambie y.
- No contiene necesariamente valores, tolerancias, parámetros ni historia, y una diferencia pequeña puede ser invisible.
- Duración cero; duración menor que Δt; duración no múltiplo de Δt. También conviene probar duración negativa y paso inválido.
- Modelo, condiciones iniciales, integrador, secuencia de pasos, duración, oráculo o referencia, métrica, errores obtenidos y orden observado.
29.23 Ideas para recordar
- Verificar comprueba la implementación; validar evalúa la representación del fenómeno.
- Las soluciones analíticas proporcionan oráculos independientes.
- Las comparaciones numéricas necesitan tolerancias justificadas.
- Los invariantes permiten probar incluso sin una solución cerrada.
- Los gráficos de residuo revelan diferencias ocultas entre curvas superpuestas.
- Error máximo, RMS y final describen aspectos diferentes.
- Las pruebas de convergencia verifican el comportamiento al reducir Δt.
- Una batería reproducible protege contra regresiones y documenta evidencia.
En el próximo tema integraremos el curso en un simulador y analizador de trayectorias en JavaScript.